Compare commits

..
137 Commits
Author SHA1 Message Date
BZLZHH 8025745fa3 [Chore] (Version): bump version to 26.8 for android plugin 2026-08-01 23:46:58 +08:00
BZLZHH 4a533a215a [Chore] (MG_Backend): fix compiling error. 2026-08-01 23:21:35 +08:00
BZLZHH 0a5d7ceb6c [Feat] (DirectGLES): implement GL_ANY_SAMPLES_PASSED occlusion queries
DirectGLES never registered BeginOcclusionQuery/EndOcclusionQuery, so
the frontend rejected the occlusion query targets entirely; the CTS
tests that use them (e.g. packed_depth_stencil.verify_partial/mixed_
attachments) left a stray GL_INVALID_ENUM that a later, unrelated
glGetError() check would report as its own failure
("Uploading buffer data failed" at gluDrawUtil.cpp:363).

Occlusion queries are core ES3 (glGenQueries/glBeginQuery(GL_ANY_
SAMPLES_PASSED, ...)/glEndQuery/glGetQueryObjectuiv), unlike the timer
queries which need GL_EXT_disjoint_timer_query, so they're wired up
unconditionally (independent of MOBILEGL_DISABLE_TIMERQUERY) using the
same handle-based GetQueryResult64/DeleteBackendQuery plumbing already
shared with timer queries. GetQueryResult64 now reads the 0/1 result
through the core 32-bit glGetQueryObjectuiv getter for occlusion
handles instead of the timer-only 64-bit GL_EXT_disjoint_timer_query
getter, since a driver can fully support core occlusion queries while
lacking that extension entirely.
2026-08-01 11:20:38 -04:00
BZLZHH ac81185968 [Fix] (DirectGLES): implement GL_DEPTH_STENCIL readback for ReadPixels/GetTexImage
Neither ReadPixels nor GetTexImage recognized format=GL_DEPTH_STENCIL
(type GL_UNSIGNED_INT_24_8 / GL_FLOAT_32_UNSIGNED_INT_24_8_REV): it
matched none of the native-passthrough gates nor the color-channel
conversion mapping, so both silently no-op'd (logging a compiled-out
MGLOG_E) and left the caller's buffer untouched. Real GLES/GL drivers
already implement this readback natively, so widen the native-pair
gates to include it.

GetTexImage additionally always attached the source texture to its
scratch FBO as GL_COLOR_ATTACHMENT0, which a depth-stencil texture
cannot be (framebuffer-incomplete) - route it through the existing
EnsureDepthAttachment2D(..., withStencil=true) path instead and skip
the color-only glReadBuffer call for that format.

Fixes KHR-GL3{2,3}.packed_depth_stencil.verify_read_pixels,
verify_get_tex_image, and verify_copy_tex_image (which depends on
GetTexImage internally) for both depth24_stencil8 and
depth32f_stencil8.
2026-08-01 11:14:33 -04:00
BZLZHH 9e861f3f7a [Test] (tools/cts): extend the fbo-harness waiver to the Espryt renderer
Same test-methodology artifact as Magma (dEQP's fbo-surface-type
wrapper FBO being mistaken for the true default framebuffer by
ApiCoverageTestCase's ReadBuffer coverage sub-test) - the waiver's
renderer_list only matched "Magma*", so KHR-GL3{0,1,2}.api.coverage
still reported Fail under the DirectGLES (Espryt) backend. Add
"Espryt*" to the same waiver entry.
2026-08-01 10:48:11 -04:00
BZLZHH da6f75dbd1 [Fix] (DirectGLES): cap advertised GL_MAX_SAMPLE_MASK_WORDS to 1
MobileGL's sample-mask state is a single 32-bit word (RenderState::
SampleMaskValue) and SampleMaski_State() hard-rejects any maskNumber
other than 0. DirectGLES forwarded the real underlying driver's
GL_MAX_SAMPLE_MASK_WORDS unmodified (NVIDIA's GLES driver reports 2),
so dEQP's per-test-case gluStateReset - which always calls
glSampleMaski up to that reported word count - hit GL_INVALID_VALUE on
word 1 after every single case and aborted the whole glcts process.
Each restart only got through one more case before repeating, which
run_cts_local.py recorded as a wall of per-case crashes (63 in
packed_pixels.rectangle alone) and tripped its "many empty chunks"
abort heuristic partway through the GL32 suite. 1 is the spec-required
minimum and is what MobileGL actually implements, so cap to it instead
of forwarding the raw driver limit.
2026-08-01 09:59:26 -04:00
BZLZHH 951da362f7 [Fix] (submodules): point include/FastSTL at the fork's already-pushed fix
Our local-only commit f8567f6 for the erase(iterator) bug was never
pushed to MobileGL-Dev/FastSTL and broke CI's submodule checkout
("not our ref"). The fork's own main branch already carries an
equivalent fix (022211c, same root cause) plus a perf improvement on
erase(key) (34f55f9), so switch to that instead of pushing a redundant
duplicate that would diverge from it.
2026-08-01 09:25:01 -04:00
BZLZHH 4c929b9b3f Merge branch 'dev' of github.com:MobileGL-Dev/MobileGL into dev 2026-08-01 21:03:14 +08:00
BZLZHH 8d5072543a [Test] (tools/cts): waive api.coverage's fbo-surface-type wrapper-FBO artifact
KHR-GL3{0,1,2,3}.api.coverage's ReadBuffer coverage sub-test captures
GL_READ_BUFFER while dEQP's own fbo-surface-type wrapper FBO is bound
(a real, non-zero-named FBO, not framebuffer 0), then later deletes an
unrelated FBO of its own. Per the GL spec, deleting a bound FBO
implicitly rebinds framebuffer target 0 - the true default framebuffer
this time, not the wrapper - and restoring the captured
GL_COLOR_ATTACHMENTn value against it correctly raises GL_INVALID_ENUM
(only FRONT/BACK-style tokens are valid there). This is a spec-correct
response to a --deqp-surface-type=fbo-only test-methodology artifact,
not a MobileGL conformance defect, and cannot occur on a real
window/pbuffer-backed run where framebuffer 0 is genuinely bound
throughout. Add a waiver (dEQP's own mechanism for exactly this kind of
known non-defect) instead of weakening the (correct) validation, and
wire --waiver-file through run_cts_local.py.
2026-08-01 08:53:35 -04:00
BZLZHH 5de2b9e3e9 [Chore] (Version): bump version to 26.8 2026-08-01 20:35:31 +08:00
BZLZHH 9192d156d1 [Fix] (DirectVulkan): ReadPixels materializes pending clears before resolving the blit binding
ResolveColorBlitBinding cached a RenderbufferResource*/TextureResource*
(trackedLayout) before the pending-clear materialization step ran. For an
attachment that had never been part of any render pass yet (e.g. a
GL_NONE draw buffer slot read back via an explicit glReadBuffer), the
materialize call was the first thing to touch its resource, and creating
that entry in the UnorderedMap (FastSTL, open-addressing) can rehash and
invalidate every previously-taken pointer into the map - including the
one just cached. The read then saw a stale VK_IMAGE_LAYOUT_UNDEFINED and
silently bailed (via a compiled-out MGLOG_E in release builds), leaving
the client buffer untouched. Reordering so the clear is materialized
first, then the binding resolved, guarantees the pointer reflects the
final resource state. Fixes KHR-GL3{0,1,2,3}.draw_buffers.draw_buffers_1.
2026-08-01 08:09:49 -04:00
BZLZHH 27cdfbc0ca [Fix] (DirectVulkan): advertise GL_ARB_explicit_attrib_location unconditionally
layout(location=N) out qualifiers are fully supported (glslang parses them,
SPIR-V expresses them natively), but the extension string was never
advertised. KHR-GL3{0,1,2}.draw_buffers.draw_buffers_1 builds its MRT
fragment shader with per-attachment layout(location=i) outputs only when
GL_ARB_explicit_attrib_location is reported or the context is >=3.3; below
that it fell back to a single non-indexed `out vec4`, which only ever
targets location 0, leaving every draw buffer past slot 0 unwritten.
2026-08-01 08:09:35 -04:00
BZLZHH 4567c3b468 [Fix] (DirectVulkan): advertise GL_ARB_texture_multisample unconditionally
GL_ARB_texture_multisample was implemented (glTexImage2D/3DMultisample,
GL_TEXTURE_2D_MULTISAMPLE) but never listed in BuildAdvertisedExtensions.
dEQP's GL 3.1 context loader only binds non-core-until-3.2 entry points
when the extension string is present, so glTexImage2DMultisample stayed
a null function pointer and KHR-GL31.texture_size_promotion.functional
crashed on the null call. GL 3.2+ contexts treat it as core and were
unaffected.
2026-08-01 08:09:20 -04:00
BZLZHH d18c6a1bae [Fix] (DirectVulkan): Xlib surface fallback for ICDs without VK_EXT_headless_surface
Real drivers (NVIDIA proprietary Linux) don't implement VK_EXT_headless_surface,
which the pbuffer path required unconditionally, hard-aborting at CreateInstance.
CreateInstance now detects instance-extension support and requests
VK_KHR_xlib_surface instead when headless is unavailable; CreateSurface creates
an unmapped Xlib window purely to obtain a VkSurfaceKHR, then proceeds through
the existing swapchain path unchanged. Shutdown destroys the window it owns.
Lavapipe and other headless-capable ICDs are unaffected.
2026-08-01 06:38:30 -04:00
BZLZHH dd745d7547 [Fix] (MG_State, MG_Impl): GL-order capture for geometry triangle strips
Vulkan transform feedback captures odd strip triangles as (i, i+2, i+1)
while GL table 10.1 decomposes them as (i+1, i, i+2). When the capture
stage is a triangle-strip geometry shader whose EmitVertex/EndPrimitive
sequence is statically knowable (no emission under control flow), link
time extracts the per-invocation strip lengths from the glslang AST, and
EndTransformFeedback rotates each odd triangle's captured vertex records
into GL order in place (bounded by the binding ranges' whole-triangle
capacity; raw input primitives tracked per Begin/End).
KHR-GL33.transform_feedback.geometry passes - the family is 21/21.
2026-08-01 03:17:30 -04:00
BZLZHH 4407be89cd [Fix] (MG_Impl): count multisample texture attachments in GL_SAMPLE_BUFFERS
The draw-framebuffer sample resolver only looked at renderbuffer
attachments, so framebuffers with multisample texture attachments
reported GL_SAMPLE_BUFFERS == 0 and callers took single-sampled paths
(the CTS blit helpers read multisampled attachments based on it).
2026-08-01 02:50:29 -04:00
BZLZHH 0c1a433af6 [Fix] (DirectVulkan): multisample resolve blits; per-buffer blit skip; RGBA-widened renderbuffers
Color blits from a multisampled source now use vkCmdResolveImage (both
blit resolvers carry the image sample count); a buffer named in the blit
mask but absent from either framebuffer skips just that buffer instead
of cancelling the whole blit (GL 4.6 18.3.1); and three-channel color
renderbuffers widen to their RGBA twin exactly like textures, so
renderbuffer<->texture blits of the same GL format see one VkFormat.
framebuffer_blit.multisampled_to_singlesampled_blit_color_config_test
passes - the whole framebuffer_blit family is green.
2026-08-01 02:46:16 -04:00
BZLZHH a2f3efe22c [Feat] (DirectVulkan): combined and scissored blits; cross-format depth-stencil blits
BlitFramebuffer now serves any GL_COLOR/DEPTH/STENCIL mask combination:
the depth/stencil aspects run as per-aspect image copies before the color
path, renderbuffer attachments materialize their pending clears like
texture ones, and the scissor test clips blit writes (destination rect
intersected, source shrunk proportionally). Depth copies between images
of different depth formats (a D24S8 renderbuffer into a
DEPTH_COMPONENT24 texture riding the D32_SFLOAT fallback) round-trip
through the host with a per-texel re-encode; stencil aspects pass
through raw since every packed format encodes S8. scissor_blit and
packed_depth_stencil.blit.* now pass.
2026-08-01 02:19:07 -04:00
BZLZHH b9a15aed61 [Feat] (DirectVulkan, MG_Impl): GPU transform feedback primitive queries
The TF primitive queries now ride VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT
pools when the device reports transformFeedbackQueries: each captured draw
is wrapped in a slot (shared between both GL targets when active
together), and results sum the (written, needed) pairs -
GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN from the first,
GL_PRIMITIVES_GENERATED from the second. This is exact through geometry
shaders, so KHR-GL33.transform_feedback.query_geometry_* pass; the CPU
accounting delta remains the fallback for backends without the feature.
2026-08-01 02:02:35 -04:00
BZLZHH f748a06632 [Fix] (3rdparty): glslang evaluates defined() expanded from macros
Pulls the glslang change that downgrades the 'defined in macro
expansion' diagnostic to a portability warning with normal evaluation.
KHR-GL33.shaders.preprocessor.conditional_inclusion.basic_2_* pass; the
whole preprocessor family (482 cases, including every negative
invalid_defined_* case) stays green.
2026-08-01 01:54:48 -04:00
BZLZHH 4532cae175 [Feat] (MG_Impl, MG_Util): STENCIL_INDEX8 renderbuffers; report distinct D/S renderbuffers unsupported
GL_STENCIL_INDEX8 becomes a first-class internal format (VK_FORMAT_S8_UINT
backing, metrics, classifiers, converters), so glRenderbufferStorage
accepts it instead of leaving GL_INVALID_ENUM behind. Framebuffer
completeness now also mirrors the renderer's gate for renderbuffers:
distinct depth/stencil renderbuffer attachments (or a renderbuffer
paired with a texture) report GL_FRAMEBUFFER_UNSUPPORTED - the spec only
requires the same-image case - instead of passing completeness and then
failing at draw/clear (verify_mixed_attachments.* now passes).
2026-08-01 01:50:34 -04:00
BZLZHH 107b56d603 [Feat] (DirectVulkan, MG_Impl): occlusion queries via Vulkan query pools
GL_SAMPLES_PASSED / GL_ANY_SAMPLES_PASSED(_CONSERVATIVE) now work: every
app draw between Begin/EndQuery is wrapped in a slot of a host-reset
occlusion query pool (precise counts when occlusionQueryPrecise is
granted), and the result flush ends any active render pass before
submitting, waits, sums the slots and recycles them. ANY_* targets
report the boolean form; GL_QUERY_COUNTER_BITS and GL_CURRENT_QUERY
answer for the occlusion targets, and deleting an active query releases
its slot. Draw-time depth/stencil state also honors attachment absence:
a framebuffer without a depth (stencil) attachment behaves as if that
test always passes, even when a packed depth-stencil image is attached
through only one half (verify_partial_attachments.*).
2026-08-01 01:50:34 -04:00
BZLZHH 22b749dd37 [Fix] (MG_Util, DirectVulkan): canonical depth shadows with upload conversion
Depth textures previously raw-copied whatever the client handed over
into the Vulkan image, so any client format other than the image's exact
texel layout uploaded garbage (float DEPTH_COMPONENT data read as
16-bit words, GL_TEXTURE_1D/2D alike).

The shadow now has a defined canonical layout - unorm16 for
DEPTH_COMPONENT16, a full-scale unorm32 word for the 24/32-bit fixed
depths, float for DEPTH_COMPONENT32F - produced by the pixel-store
unpack converter (new DepthComponent channel mapping + UNorm32
component). GL_DEPTH_COMPONENT client data may also fill packed
depth-stencil internals (stencil half zero). The Vulkan uploader
converts shadow words to the image texel layout per aspect, and
X8_D24_UNORM falls back to D32_SFLOAT where optimal tiling lacks
support (lavapipe). texture_size_promotion.functional and
packed_depth_stencil.verify_copy_tex_image.* now pass.
2026-08-01 01:15:58 -04:00
BZLZHH f0c0211767 [Fix] (DirectVulkan): raw sRGB attachment writes while FRAMEBUFFER_SRGB is off
GL renders into sRGB color attachments RAW when GL_FRAMEBUFFER_SRGB is
disabled (the core-profile default), but Vulkan sRGB attachments always
encode on write - one decode went missing whenever a rendered-into sRGB
texture was sampled again (multisampled sRGB targets in
texture_size_promotion and texture_swizzle idx27/28 ms cases).

Attachment views (textures and renderbuffers) now reinterpret sRGB
images through their UNORM twin while the capability is off, switching
back when enabled: images get MUTABLE_FORMAT, the attachment-view cache
keys the view format, renderbuffers carry a second view, and the render
pass hash includes the capability state. Sampled views keep decoding.
The VkTextureManager.cpp half of this rides with the next commit.
2026-08-01 01:15:57 -04:00
BZLZHH 9ebbb76df1 [Fix] (DirectVulkan): clamp fixed-point ReadPixels to [0,1]
glReadPixels final conversion honors GL_CLAMP_READ_COLOR (default
GL_FIXED_ONLY): fixed-point normalized color buffers clamp to [0,1] on
read - visible for SNORM attachments, whose negative values previously
leaked through (texture_size_promotion SNORM cases). True float formats
stay unclamped unless the mode is GL_TRUE; GetTexImage is unaffected.
2026-08-01 01:15:57 -04:00
BZLZHH 95547ab9ce [Fix] (MG_Util): map GL adjacency primitives to their Vulkan topologies
GL_LINES_ADJACENCY / GL_LINE_STRIP_ADJACENCY / GL_TRIANGLES_ADJACENCY /
GL_TRIANGLE_STRIP_ADJACENCY fell through to the TRIANGLE_LIST default,
so adjacency draws assembled garbage. They now map to the matching
*_WITH_ADJACENCY topologies (adjacency vertices are discarded by Vulkan
when no geometry shader is active, matching GL semantics);
KHR-GL33.primitive_restart.restart_mode passes.
2026-08-01 00:30:01 -04:00
BZLZHH 8eaf2d0069 [Fix] (MG_Impl): create sampler objects at Gen; guard combined-format CopyTexImage
glGenSamplers creates the sampler objects themselves (unlike texture and
buffer names), so glIsSampler must answer GL_TRUE before any bind - the
names now get their state vectors at Gen time (KHR-GL33.api.coverage).

glCopyTexImage2D with a combined DEPTH_STENCIL internalformat now
requires both halves in the read framebuffer and reports
GL_INVALID_OPERATION when only the depth or only the stencil attachment
point is populated (packed_depth_stencil.validate_errors.*).
2026-08-01 00:27:05 -04:00
BZLZHH 54a8609c64 [Feat] (DirectVulkan): depth-stencil GetTexImage
The depth-stencil ReadPixels core (per-aspect copies + CPU repack) is
now shared, and glGetTexImage serves GL_DEPTH_COMPONENT /
GL_DEPTH_STENCIL / GL_STENCIL_INDEX queries of depth textures with it
instead of rejecting every non-color aspect
(packed_depth_stencil.verify_get_tex_image.* now passes).
2026-07-31 18:45:52 -04:00
BZLZHH 1fb0eb0737 [Fix] (MG_Util): GL_FLOAT_32_UNSIGNED_INT_24_8_REV is 8 bytes per pixel
The packed-type size table listed the D32F+S8 client format at 4 bytes,
so every GL_DEPTH32F_STENCIL8 upload copied only half its client data -
the top half of such textures stayed zero (packed_depth_stencil
verify_read_pixels/clear_buffer.depth32f_stencil8 now pass).
2026-07-31 18:43:30 -04:00
BZLZHH 282dd69230 [Feat] (DirectVulkan, MG_Impl): depth-stencil ReadPixels and combined-attachment queries
glReadPixels now serves GL_DEPTH_COMPONENT, GL_DEPTH_STENCIL and
GL_STENCIL_INDEX from the read framebuffer's depth/stencil attachment:
per-aspect vkCmdCopyImageToBuffer copies (4-byte-aligned stencil region)
with CPU repacking into GL_FLOAT / GL_UNSIGNED_SHORT / GL_UNSIGNED_INT /
GL_UNSIGNED_INT_24_8 / GL_FLOAT_32_UNSIGNED_INT_24_8_REV /
GL_UNSIGNED_BYTE layouts, honoring pack state and pixel-pack buffers.

GL_DEPTH_STENCIL_ATTACHMENT parameter queries follow the spec's combined
rules: differing depth/stencil attachment images (or a lone half) fail
with GL_INVALID_OPERATION, as does GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE
on the combined name. packed_depth_stencil.verify_parameters.* and
verify_read_pixels.depth24_stencil8 now pass.
2026-07-31 18:41:08 -04:00
BZLZHH e6ebe7078d [Fix] (MG_Util, MG_State): reject reserved GLSL identifiers glslang accepts
glslang parses "packed" and "row_major" as plain identifiers outside a
layout(...) list and accepts the reserved image*Shadow names outright.
A comment/preprocessor-aware pre-scan in the compile path now fails such
shaders with a proper info log, while layout(packed)/layout(row_major)
qualifier lists stay legal (uniform_block family still passes).
KHR-GL31/32/33.CommonBugs.CommonBug_ReservedNames now pass.
2026-07-31 18:27:11 -04:00
BZLZHH 30a91023f6 [Chore] (tools/cts): pin local runner surface size to 256x256
Without an explicit size dEQP's FboRenderContext sizes the wrapper FBO
to GL_MAX_RENDERBUFFER_SIZE (16384^2 here) and size-derived test
allocations explode - the multisampled depth blit config test alone
needs a 4 GiB depth texture on such a surface.
2026-07-31 18:21:28 -04:00
BZLZHH 47dd8cdc05 [Feat] (MG_Impl): answer format-derived framebuffer attachment queries
glGetFramebufferAttachmentParameteriv (and the DSA variant) now answer
GL_FRAMEBUFFER_ATTACHMENT_RED/GREEN/BLUE/ALPHA/DEPTH/STENCIL_SIZE,
COMPONENT_TYPE and COLOR_ENCODING from the attached image's internal
format, and accept the default-framebuffer attachment names (GL_DEPTH,
GL_STENCIL, GL_FRONT/GL_BACK variants). Querying them with no image
attached reports GL_INVALID_OPERATION per spec instead of
GL_INVALID_ENUM.
2026-07-31 18:21:28 -04:00
BZLZHH 8b36a15fb3 [Fix] (DirectVulkan): repair VK_VERIFY varargs and soften image-creation OOM
VK_VERIFY appended the caller's context format string to the base format
while the context ARGUMENTS expanded before the base arguments, so any
failing VK_VERIFY with context args formatted every conversion from the
wrong slot - the %s for VkResultToString dereferenced an integer arg and
crashed inside the logger. The context line is now its own log call
(XXHASH_VERIFY had the same defect).

vmaCreateImage failure in SyncTextureResource is now a soft failure like
the unsupported-sample-count path: a driver may pass the
vkGetPhysicalDeviceImageFormatProperties pre-check yet still refuse
creation (a 4-sample 16K depth texture on lavapipe is 4 GiB), and a GL
implementation must not abort on that.
2026-07-31 18:21:28 -04:00
BZLZHH 07fa84fb8d [Fix] (MG_State, MG_Impl): defer deletion of the program in use
glDeleteProgram on the current program now only flags it: the name (and
every glGetProgram* query) stays valid until the program stops being
current, at which point UseProgram frees the slot and releases orphaned
attached shaders. Previously the name died immediately, so a second
glDeleteProgram - as issued by common CTS utility teardown - recorded
GL_INVALID_VALUE that poisoned the next test iteration's build
(KHR-GL33.clip_distance.functional now passes its build phase).

glIsProgram/glIsShader piggyback on the same rule: a flagged name is
still a program/shader while it stays GL-visible, which resolves the
long-standing FIXMEs there.
2026-07-31 18:21:14 -04:00
BZLZHH a03817b4ee [Fix] (FastSTL): bump submodule for erase(iterator) out-of-bounds fix
Pulls the FastSTL fix for erase() iterator advancement: erase loops
(pending-clear GC, render-pass eviction, frame-transient drains) no
longer skip elements or walk past the bucket array. Root cause of the
order-dependent CTS batch segfaults (texture_lod_bias_all,
clip_distance.functional after ReadPixels, batch-order aborts).
2026-07-31 18:21:05 -04:00
BZLZHH 641bc0cdd9 [Feat] (MG_Impl): transform feedback primitive queries
glBeginQuery/glEndQuery now accept GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN
and GL_PRIMITIVES_GENERATED. The result comes from CPU-side accounting:
every captured draw adds the primitives it assembles, clamped by the
capture buffers' remaining capacity in whole primitives (a full buffer
stops recording, which is exactly what PRIMITIVES_WRITTEN reports), with
the captured-vertex cursor resetting on glBeginTransformFeedback.

Draws without a geometry stage write exactly what they assemble, so this
is precise for them (KHR-GL33.transform_feedback.query_vertex_* now pass);
geometry amplification is not modelled yet and the query_geometry_*
variants still fail.
2026-07-31 17:40:42 -04:00
BZLZHH c069890ac7 [Feat] (DirectVulkan): GPU transform feedback capture via VK_EXT_transform_feedback
Second stage of GL 3.0 transform feedback: captured draws now write real
data.

- Device setup enables the VK_EXT_transform_feedback feature when present
  and loads the bind/begin/end entry points.
- Captured draws compile an XfbCapture program variant whose last
  vertex-processing stage gets XfbBuffer/XfbStride/Offset decorations from
  the program's resolved varyings (a new spirv-opt pass). A captured
  gl_Position is mirrored into a dedicated output written before every
  OpReturn - or before every OpEmitVertex in a geometry stage - ahead of
  the position fixup, so the captured value is the shader's own pre-remap
  position.
- DrawArrays/DrawElements wrap the draw in Begin/EndTransformFeedbackEXT;
  a small counter buffer resumes the append position across draws within
  one glBeginTransformFeedback (fresh Begin starts at the bound offsets).
- Capture targets are promoted to persistently-mapped host-coherent GPU
  storage (persistent-map storage now also carries the transform feedback
  usage), so MapBuffer/GetBufferSubData read the captured bytes after the
  fence wait glEndTransformFeedback now performs.
- Draw-mode/feedback-mode validation defers to the geometry shader's
  output primitive when one is present, and glGetBooleanv reports
  GL_TRANSFORM_FEEDBACK_ACTIVE/PAUSED so dEQP's per-case state reset can
  unwind an active capture.

KHR-GL33: transform_feedback capture_vertex_*/capture_geometry_*/
discard_*/draw_xfb and clip_distance.coverage now pass; queries
(PRIMITIVES_WRITTEN) and gl_ClipDistance capture remain.
2026-07-31 17:31:41 -04:00
BZLZHH 48dd1c5956 [Feat] (MG_State, MG_Impl): transform feedback state, validation and reflection
First stage of GL 3.0 transform feedback: glTransformFeedbackVaryings /
glGetTransformFeedbackVarying / glBeginTransformFeedback /
glEndTransformFeedback were unimplemented stubs. This adds

- per-program capture state: requested varyings apply on the next link and
  resolve against the last vertex-processing stage's linker objects (with
  gl_Position/gl_PointSize handled as builtins), failing the link on
  unknown or duplicate names or exceeded interleaved/separate limits, with
  offsets and strides computed per GL rules;
- context Begin/End state with the GL 3.3 error semantics: invalid
  primitive modes, redundant Begin/End, missing program or capture-buffer
  bindings, primitive-mode compatibility at draw time, and the
  while-active prohibitions on rebinding capture buffers, switching
  programs, and relinking the captured program;
- GetProgramiv TRANSFORM_FEEDBACK_* queries and a 4-slot bound on indexed
  GL_TRANSFORM_FEEDBACK_BUFFER binding points.

KHR-GL33.transform_feedback api_errors/linking_errors/get_xfb_varying now
pass; GPU-side capture is the remaining stage.
2026-07-31 17:08:37 -04:00
BZLZHH a389477f78 [Fix] (MG_Impl): report INVALID_OPERATION for shader names in program APIs
Program entry points answered GL_INVALID_VALUE whenever the name did not
resolve to a program, including names that exist but belong to a shader
object. Programs and shaders share one name space, so the spec (and
KHR-GL33.get_uniform_tests.get_uniform) requires GL_INVALID_OPERATION for
the shader-name case and GL_INVALID_VALUE only for names GL never handed
out, matching the interface-query helper's existing behavior.
2026-07-31 16:47:45 -04:00
BZLZHH 94a8f1e3f3 [Fix] (ShaderTranspiler): keep declared modern GLSL versions strict
Normalization rewrote every desktop core #version below 400 to 330 (and
400+ to 460), and a failed parse was retried at 460. Together these erased
the declared version's rules: KHR-GL33 negative-compile cases (reserved
names, parenthesized layout-qualifier values in a declared-420 shader,
GLSL 4.5 mix() overloads at 330, precise in struct members) all compiled.

Explicitly declared core versions >= 330 now keep their number, and the
460 retry only fires for sources whose directive carries the normalizer's
own legacy marker - i.e. shaders that declared 110-150 (or nothing), which
is the shader-pack compatibility case the retry exists for. Replaces the
narrower arrays-of-arrays special case.
2026-07-31 16:43:27 -04:00
BZLZHH 45d506545e [Fix] (MG_Util, DirectVulkan): tolerate storage-less attachments in component-size queries
GetComponentSizesForInternalFormat asserted on TextureInternalFormat::Unknown,
which framebuffer-parameter queries legitimately reach for attachments that
have no storage yet (KHR-GL33.packed_depth_stencil.validate_errors.initial_state
aborted there). Answer with all-zero sizes and keep a warning for genuinely
unhandled formats. Also include the image dimensions in the texture
vmaCreateImage failure report.
2026-07-31 16:35:03 -04:00
BZLZHH d9d63c9496 [Fix] (MG_Impl): answer ARB_transform_feedback3 limit queries
The GL CTS queries GL_MAX_TRANSFORM_FEEDBACK_BUFFERS and
GL_MAX_VERTEX_STREAMS before checking whether the extension is advertised
and requires no GL error (desktop drivers all accept these enums). Answer
with the separate-attrib capacity and a single vertex stream; the
transform_feedback3 tests then report NotSupported instead of failing on
GL_INVALID_ENUM.
2026-07-31 16:25:46 -04:00
BZLZHH 92140405c1 [Feat] (DirectVulkan): emulate GL_LINE_LOOP with closed indexed line strips
Vulkan has no LINE_LOOP topology and the frontend used to reject the mode
with GL_INVALID_OPERATION, which is itself non-conformant (several KHR-GL33
transform_feedback tests draw line loops and expect no error). DrawArrays,
DrawElements and DrawElementsBaseVertex now rewrite the draw into an
indexed GL_LINE_STRIP whose synthesized uint32 index list revisits the
first vertex, delivered through the client-memory index path (a new
forceClientMemory flag keeps a bound element-array buffer from hijacking
the synthesized pointer). Entry points without the rewrite degrade to an
open line strip instead of a triangle list.
2026-07-31 16:25:46 -04:00
BZLZHH b9ecfef0b6 [Fix] (DirectVulkan): handle renderbuffer attachments in color blit clears
BlitFramebuffer's color path asserted that the read framebuffer's source
attachment is a texture; a renderbuffer source (packed_depth_stencil.blit
color checks) aborted the process. Materialize pending clears through the
renderbuffer path for both source and destination, as ReadPixels already
does.
2026-07-31 16:15:50 -04:00
BZLZHH fba26ea169 [Fix] (DirectVulkan): round renderbuffer MSAA requests to supported counts
glRenderbufferStorageMultisample accepts any sample count up to MAX_SAMPLES
(including non-powers-of-two like 3) and promises at-least allocation, but
the renderbuffer path required an exact Vulkan sample-count match and failed
on devices like llvmpipe that expose 1x/4x only. Round the request up to a
power of two and then to the nearest count the device supports for the
format, cached per format so per-draw resolution does not re-query the
physical device.

Un-crashes KHR-GL33.packed_depth_stencil.blit.* (2x/3x MSAA renderbuffers).
2026-07-31 16:15:50 -04:00
BZLZHH 93a3b55907 [Feat] (DirectVulkan): upload combined depth-stencil texture data
UploadDirtyMipLevels used to skip D24S8/D32FS8 textures outright, leaving
glTexImage-supplied depth-stencil data unuploaded (KHR-GL33
texture_repeat_mode depth24_stencil8 and texture_swizzle depth-stencil
cases all sampled zeros). De-interleave the shadow's GL wire format into a
depth plane (X8_D24 word / float) and a stencil byte plane and record one
copy per aspect, with cross-conversion when the device backs the texture
with the other depth-stencil format.

Depth32FStencil8's shadow byte size also claimed 16 bytes/texel while the
stored wire format (GL_FLOAT_32_UNSIGNED_INT_24_8_REV) is 8; that mismatch
truncated every upload of it.

Also route a multisample-texture sample-count request through the device's
supported counts (round up, GL promises at-least semantics).
2026-07-31 16:15:50 -04:00
BZLZHH d1487bedf0 [Fix] (DirectVulkan): address array layers in mip upload copies
UploadDirtyMipLevels encoded a texture's GL depth into VkBufferImageCopy
imageExtent.depth with layerCount = 1. For array textures the layers live in
the image's arrayLayers, and extent.depth > 1 is invalid for 2D images - in
practice every layer past the first never received its data.

Route the third dimension into layerCount for 1D/2D/cube array images and
keep imageExtent.depth for genuine 3D images.

Fixes the KHR-GL33.pixelstoragemodes.teximage3d.* failures (110 cases) on
lavapipe.
2026-07-31 16:01:41 -04:00
BZLZHH 1bb736c57e [Fix] (ShaderTranspiler): keep arrays-of-arrays illegal below GLSL 430
The legacy-shader retry that retargets a failed parse to #version 460 also
re-legalized multidimensional arrays, which every desktop driver rejects
below 430 and KHR-GL33.shaders.arrays.invalid.* requires to fail. Skip the
retry when the original failure is glslang's arrays-of-arrays error; other
legacy rescues (e.g. layout(binding=...)) keep working.

KHR-GL33.shaders.arrays.invalid.multidimensional_array* now report the
required compile failure (4 cases).
2026-07-31 15:51:40 -04:00
BZLZHH eb76686c1e [Fix] (DirectVulkan): stop replaying consumed renderbuffer clears mid-pass
A cached RenderPassEntry bakes renderbuffer clear payloads inline into its
pendingClearAttachments, and that list outlives the clear's consumption at
pass begin (loadOp CLEAR). Every subsequent draw that reused the entry while
its pass was still active replayed the stale clear through
vkCmdClearAttachments, wiping the color and depth of everything drawn so far
in the pass.

Texture-keyed clears already re-checked the clear manager before clearing;
do the same for inline renderbuffer payloads: only clear while the
renderbuffer clear is still actually pending, and take the live payload so a
newer glClear's values win.

On lavapipe this takes KHR-GL33.shaders.fragdepth.* from 0/18 to 18/18; the
same defect hit any renderbuffer-FBO case with several draws per pass.
2026-07-31 15:51:40 -04:00
BZLZHH 1a04fb8c0c [Fix] (DirectVulkan): support client-memory indices in DrawElements
With no GL_ELEMENT_ARRAY_BUFFER bound, the IndexBufferView byte offset is a
raw client pointer (desktop drivers accept client-memory indices and the GL
CTS relies on this even in core contexts). UploadAndBindIndexBuffer used to
assert-crash the process there; it now snapshots the client index data into
a transient per-frame slice and binds that, matching how client-memory
vertex attributes are already streamed.

Fixes the process abort in KHR-GL33.transform_feedback.capture_* and every
other mustpass case that draws with client-side index arrays.
2026-07-31 15:10:01 -04:00
BZLZHH 306790ee7c [Feat] (tools/cts): add crash-resuming local-host glcts runner
Local counterpart of run_cts.py for desktop Linux runs: re-invokes glcts
with the not-yet-measured cases after a crash, quarantines timed-out cases
with the dEQP watchdog enabled, and records crashed/hung/unrun lists so a
partial run cannot read as a complete one.
2026-07-31 14:52:38 -04:00
BZLZHH 170ccda3e7 [Feat] (tools/cts): port dEQP MobileGL platform to desktop Linux
Guard the AImageReader window path behind __ANDROID__ and add a
mobilegl-desktop DEQP target so glcts can run against libMobileGL.so on a
Linux host via pbuffer surfaces (VK_EXT_headless_surface).
2026-07-31 14:50:18 -04:00
BZLZHH e86a9bbec5 [Chore] (MG_Backend): restore target GL version to 3.3 2026-07-31 16:45:19 +08:00
swung0x48 c5569e71b3 [Feat] (tools/cts): automate Windows WGL conformance runs 2026-07-30 23:00:13 -04:00
swung0x48 7e8c32a063 [Feat] (MG_Backend, MG_Impl): expose experimental GL 4.6 CTS limits 2026-07-30 23:00:13 -04:00
swung0x48 77bd03d962 [Chore]: remove unnecessary doc 2026-07-30 21:56:17 -04:00
swung0x48 37111ae992 [Perf] (DirectVulkan): snapshot-gated consecutive-draw fast path skips SetupDraw re-resolution 2026-07-30 09:40:54 -04:00
swung0x48 6b0c2a15ab [Perf] (DirectVulkan): reuse unchanged global-UBO slices and skip identical descriptor binds 2026-07-30 08:18:21 -04:00
swung0x48 e9ffd99313 [Perf] (DirectVulkan): bake the attribute location mask and memoize the explicit-LOD eligibility probe 2026-07-30 08:18:20 -04:00
swung0x48 7c01ddea0c [Perf] (DirectVulkan): drop per-draw weak-ptr locks, re-resolves and rebuilt masks from the sampled-texture and vertex paths 2026-07-30 08:01:07 -04:00
swung0x48 2d4d6e9cfb [Perf] (DirectVulkan): skip pending-clear probes through a lock-free empty check 2026-07-30 07:02:03 -04:00
swung0x48 76b8957b99 [Perf] (DirectVulkan): memoize sampled-texture resources across draws 2026-07-30 07:02:02 -04:00
swung0x48 ec685b9fa7 [Perf] (DirectVulkan): memoize resolved vertex-input state on the VAO and dedupe vertex/index binds 2026-07-30 07:02:01 -04:00
swung0x48 a12068df52 [Perf] (DirectVulkan): reuse pipelines across per-chunk buffers and skip redundant pipeline binds 2026-07-30 05:01:46 -04:00
swung0x48 0b344792cc [Fix] (DirectVulkan): stop fence-waiting out-of-band texture uploads; reclaim transients asynchronously 2026-07-30 05:01:46 -04:00
swung0x48 9fa32bdad0 [Fix] (DirectVulkan): declare only the used colour attachment span per subpass
- every render pass declared colorAttachmentCount=8 (the full GL draw-buffer
  slot span) with trailing VK_ATTACHMENT_UNUSED references, and Adreno
  configures its per-pixel render-backend/export path from the DECLARED
  count - so every fragment of every pass paid an 8-render-target export
  cost; this was the bulk of the 1.5x per-pixel gap against
  MobileGlues+ANGLE on the same Qualcomm driver (their subpasses declare
  exactly the used span)
- measured on Adreno 650 / MC 26.2 / 1440x3044: total GPU frame time
  11.9 -> 7.5 ms (-37%, now below ANGLE's 7.87 ms), the single-quad
  swapchain blit pass alone 1.26 -> 0.40 ms, steady in-world FPS 82.8 -> 123
  under the standard cooled-start protocol, matching the
  MobileGlues+ANGLE+system-Vulkan benchmark of 123.8
- trailing UNUSED references are popped before the subpass is built (the
  entry's colorAttachmentCount and every pipeline's colour-blend span follow
  it); interior GL_NONE holes keep their slots so fragment-output locations
  still line up
- the pipeline-side fragmentOutputMask check downgrades from assert to a
  debug log: an output at a location past the trimmed span is discarded,
  which is GL's defined behaviour for a draw buffer set to GL_NONE
2026-07-30 02:45:39 -04:00
swung0x48 a4980f2b56 [Fix] (DirectVulkan): skip redundant per-draw dynamic-state commands
- viewport, scissor, blend constants, depth bias, line width and the six
  stencil parameters were re-emitted unconditionally for EVERY draw (~1500
  vkCmdSet* per frame in MC 26.2, where ANGLE emits a handful), costing CPU
  record time and GPU command-processor work for values that almost never
  change between draws
- a recording-scoped shadow now drops any vkCmdSet* whose values match what
  the command buffer already holds; valid because every PipelineFactory
  pipeline declares the same eight dynamic states, so set values persist
  across those binds
- the shadow resets at every command-buffer (re)begin (dynamic state does
  not survive the boundary) and after binding the blit or depth-mipmap
  pipelines, whose narrower dynamic sets make the untouched states undefined
  and whose raw viewport/scissor writes bypass the shadow
2026-07-30 02:45:07 -04:00
swung0x48 8ca20e28ca [Fix] (DirectVulkan): size texture backings by their defined mip level count
- every non-MSAA texture was allocated with a full mip chain regardless of
  how many levels the GL texture actually defines, so MC's 3044x1440 main
  colour and depth render targets each carried 12 levels where ANGLE
  allocates one; a level-0-only texture now gets a single-level backing and
  upgrades to the full chain exactly once when a second level is first
  defined, through the existing preserve-copy recreation path
- saves a third of the memory of every mip-less texture and keeps
  single-level render targets off the multi-mip image layout entirely, which
  also removes the surface the Adreno 650 implicit-LOD overread workaround
  (ForceExplicitLod0SamplePass) exists to defend
- measured perf-neutral on Adreno 650 / MC 26.2 (the driver keeps full UBWC
  on multi-mip render targets), so this is a memory/robustness fix, not a
  speed one
2026-07-30 02:43:32 -04:00
swung0x48 c353a2055f [Feat] (DirectVulkan): pre-pass command stream for reorderable out-of-pass work
- a draw whose sampled texture needs out-of-pass work (deferred clear
  materialization or a sampled-layout transition) used to end the active
  render pass - a full-target store+reload on a tiler - even when the only
  ordering the work needs is 'before this draw'; MC 26.2 clears an overlay
  texture every frame and samples it mid-pass, splitting the main scene pass
  once per frame for nothing
- every frame slot now carries a second primary command buffer, submitted
  strictly AHEAD of the frame command buffer in the same vkQueueSubmit; when
  the open recording has not referenced the image yet (tracked via a
  recording-generation stamp on the texture resource, advanced on every
  frame-command-buffer begin and stamped at every recorded reference:
  attachments at BeginRenderPass/attachment-write, sampled reads per draw,
  layout transitions), the clear/transition is recorded there and the active
  pass stays open - ANGLE's outside-render-pass command stream, restricted
  to the provably reorderable case
- mid-frame flushes and readback submits close and carry the pre stream with
  the frame buffer (it must never be submitted later than the recording it
  was paired with), retiring both under the same submit index; dropped
  recordings (present suspension, swapchain recreation) abandon it
- MaterializePendingClearForTexture's no-active-render-pass assert now
  applies only to the frame command buffer, since the pre stream records
  while a pass is open on the frame buffer by design
2026-07-30 02:43:13 -04:00
swung0x48 421c20984e [Fix] (DirectVulkan): stop loading and carrying dead default-framebuffer content
- EGL swap semantics make the presented colour buffer's content undefined at
  its next acquire (EGL_BUFFER_DESTROYED, the implementation default) and
  every ancillary depth/stencil buffer's content undefined after ANY swap,
  yet the default-FBO render pass reloaded both with LOAD_OP_LOAD every
  frame; SwapchainObject now tracks per-image content validity (defined when
  a pass stores into the attachment, invalidated at present) and the
  render-pass manager turns an undefined attachment's tile load into
  LOAD_OP_DONT_CARE with initialLayout=UNDEFINED, keyed into both hashes so
  the cached LOAD variants cannot be hit by mistake
- the default framebuffer's depth attachment is now attached ON DEMAND: a
  draw with depth test and stencil test both disabled (GL: a disabled test
  neither reads nor writes its buffer), and no pending depth/stencil clear,
  resolves to a depth-less pass flavour, dropping the D24S8 tile load AND
  store outright - MC 26.2 renders its GUI into its own FBO and only ever
  blits colour to the default framebuffer, so its swapchain pass carried a
  full-screen depth round-trip for nothing
- the flavour only escalates: an active depth-full pass absorbs depth-less
  draws unchanged, while a depth-using draw against a depth-less pass
  resolves to an incompatible entry and splits, its depth loading DONT_CARE
  (the content was undefined all along); the depth-less flavour is folded
  into ComputeHash and the per-draw fast-path memo so the two flavours can
  never alias
2026-07-30 02:40:47 -04:00
swung0x48 fc4cd980f2 [Fix] (DirectVulkan): bound image mutability so Adreno keeps UBWC compression
- Every storage-capable colour texture was created MUTABLE_FORMAT, and Adreno
  gives up bandwidth compression on an image that may be viewed as any format in
  its compatibility class. MC's main render target therefore ran uncompressed;
  in a fill-bound scene that is the whole frame budget. Measured on Adreno 650,
  MC 26.2, same scene and camera, device cooled to 38-40C before each run:
  65.3 -> 80.9 fps (+23.9%), GPU busy ~93% in both.
- VK_KHR_image_format_list (enabled when present) fixes it without giving up
  mutability: VkImageFormatListCreateInfo names the exact formats a view may
  use, so the driver can keep the image compressed. The set must be exhaustive
  or the result is undefined - for sampled views it is exactly what
  ResolveSampledImageViewFormat can return over the three numeric domains.
- glBindImageTexture may name any compatible format, which cannot be enumerated
  ahead of time, so a texture bound to an image unit gets no format list. That
  is what VK_IMAGE_USAGE_STORAGE_BIT becoming on-demand is for: it makes
  "unmarked" mean "will never receive an arbitrary-format storage view", which
  is what makes the list sound. Removing STORAGE is worth nothing on its own
  (65.4 fps, measured) - only the mutability bound pays.
- MarkStorageImageTexture runs over every collected image-unit texture before
  the probe loop in PrepareStorageImageTextures, because that loop stops at the
  first texture needing work and would leave the rest unmarked. The mark makes
  NeedsStorageImagePreparation report true, which is what ends the render pass,
  so the recreate lands outside it.
- storageUsageResolved separates "not upgraded yet" from "this format can never
  carry STORAGE", so a format whose optimalTilingFeatures lack STORAGE_IMAGE
  cannot ask for a recreate that will never happen. SyncTexture's cross-draw
  early-out also has to break on a pending upgrade or the recreate never runs.
- An upgrade recreates the image and carries its contents forward through
  PreserveTextureContentsOnRecreate, which submits its own command buffer and
  waits. Whatever the frame already recorded into the old image is still
  unsubmitted, so that copy would read pre-frame content and this frame's
  rendering into the texture would be lost - exactly the render-target-then-
  image-unit case. PrepareStorageImageTextures now flushes first; it takes the
  FrameData rather than a command buffer because the flush retires the current
  one, and drops the sampled-descriptor-set memo that described it.
2026-07-29 07:07:50 -04:00
swung0x48 992d16267c [Fix] (DirectVulkan): rewrite implicit-LOD fragment samples to explicit LOD 0 when every bound sampler is pinned to a single mip level - Adreno 650 (driver 512.502) reads outside a full-screen colour render target's allocation on its implicit-LOD sampling path and faults the GPU, which killed MC 26.2 on its own blit shader (texture(InSampler, texCoord)) between frames 344-421 on every run; this is the same driver defect the default-framebuffer blit shader already works around with textureLod, but an application's shader cannot be edited, so ForceExplicitLod0SamplePass converts OpImageSample*ImplicitLod to the explicit form at the SPIR-V level under a new CompileOptionBit that is only requested when the rewrite provably cannot move a texel (every sampler binding on a single-level view, no anisotropy, and either a LOD clamp that already pins lambda at 0 or min and mag filters that agree - an explicit LOD 0 always takes the magnification side of the min/mag decision); a single-level view now also clamps its sampler to mipmapMode NEAREST with maxLod min(maxLod, 0.25) rather than 0, since collapsing the clamp would make every fragment magnify and quietly retire the min filter; and the program's backend hash memo grows from one slot to four so a program resolved under two compile-flag sets in the same frame stops re-hashing every stage's SPIR-V once per draw 2026-07-29 03:26:57 -04:00
swung0x48 0ea9e6de5f [Fix] (DirectVulkan): follow surface resizes instead of rebuilding the swapchain on VK_SUBOPTIMAL_KHR - a per-frame surface-capabilities comparison (ANGLE's model) is now the only thing that schedules a rebuild, so a launcher-side resolution change reaches the swapchain and the compositor scales the smaller image up to the view, while a driver that merely reports the surface as suboptimal can no longer rebuild every frame (each rebuild destroys every pipeline, resets the render-pass manager and reallocates the default framebuffer, which showed as flicker, then corruption, then a crash); the comparison runs in SURFACE space against the extent the live swapchain was created from, since comparing against the swapchain's own quarter-turn-swapped extent reports a difference on every rotated frame 2026-07-28 21:06:57 -04:00
swung0x48 241ed377b4 [Fix] (macOS): harden Cocoa context setup and isolate embedded glslang 2026-07-28 11:54:50 -04:00
swung0x48 bf312a4b67 [Fix] (DirectVulkan): explicit-LOD blit sampling and present-path hardening - the default-framebuffer blit shader now samples with textureLod 0 (a blit reads exactly the selected level; Adreno 650's implicit-LOD path reads past a single-mip UBWC render target's allocation despite maxLod=0, page-faulting the GPU on MC 26.2's second startup frame once the neighbouring startup staging memory is returned - the invalidated context then failed the next Present submit with EDEADLK/DEVICE_LOST), TransitionToPresent appends the present barrier into the frame's open recording instead of silently dropping it whenever anything was recorded (frames without a default-FBO render pass presented images stuck in their acquired layout), VK_SUBOPTIMAL_KHR acquires are treated as the success they are (image acquired, semaphore signal armed - the early return skipped the fence reset and consumed-flag clear, and callers re-acquired on the same binary semaphore; rebuilds now defer to after the signal is consumed), and validation builds report through VK_EXT_debug_report when VK_EXT_debug_utils is absent instead of aborting instance creation 2026-07-28 06:00:20 -04:00
swung0x48 56b31a9587 [Fix] (FastSTL): bump submodule for the erase(iterator) double-advance fix and add erase-while-iterating regression tests - the old semantics skipped one live element per erase and ran past end() when erasing the highest occupied bucket, sending the new mass pipeline-cache eviction sweeps off the bucket array (device crash on first eviction during world load: garbage handles fed to vkDestroyPipeline) 2026-07-27 23:50:25 -04:00
swung0x48 8a0a8a0274 [Fix] (DirectVulkan): harden the leak-fix round after adversarial review - pipeline memo now drops at every command-buffer boundary (a flush-loop-memoized pipeline could age out and be destroyed while its submission was in flight), mid-frame drains no longer rewind the arena or advance the cache-aging clocks in presenting apps (gated to every 8th drain since the last Present, so readback/fence-heavy frames neither churn conversions nor shrink the 1024-boundary retire window), render-pass eviction notifies the pipeline cache once per sweep batch instead of once per dying pass, descriptor pools use FREE_DESCRIPTOR_SET_BIT so a destroyed layout's cached sets are freed back and credited instead of abandoning pool slots (the live-layout age sweep that could orphan slots is removed - layout destruction is the sole purge path), and renderbuffer respecify parks the old backing for aged destruction instead of destroying it while possibly in flight 2026-07-27 22:51:26 -04:00
swung0x48 d076c29146 [Fix] (DirectVulkan): bound the vertex-input and sampler caches and sweep undeleted GL syncs - both caches age out entries idle >1024 frame boundaries (animated LOD bias no longer mints a VkSampler per float value, buffer/VAO churn no longer grows the vertex-input map for the whole session), and library teardown drains the live-sync registry exactly as glDeleteSync would since GL requires syncs to die with their context 2026-07-27 22:16:16 -04:00
swung0x48 930a607bdf [Fix] (DirectVulkan): make texture/renderbuffer GC reach every dead resource - name-deleted textures register via weak_from_this so first-sync-after-delete can no longer orphan a TextureResource, an orphan sweep makes GC authoritative over the resource map, dead-texture pruning moves to a frame-boundary gate (64 frames) so churn through clears/readbacks reclaims without draws, and dead renderbuffers age past frames-in-flight before their VkImage/view is destroyed instead of leaking until shutdown (or being freed while in flight) 2026-07-27 22:16:15 -04:00
swung0x48 34685b4bb0 [Fix] (DirectVulkan): age-based eviction for the content-addressed cache family - ProgramFactory entries (shader modules/layouts), PipelineFactory graphics pipelines, compute pipelines and per-layout descriptor-set tracking now retire after ~1024 idle frame boundaries (render-pass-manager sweep precedent), render-pass eviction purges pipelines hashed on the dying handle (closes a handle-recycling stale-pipeline hazard), and the program reflection cache is lifetime-id-keyed and cleared at EGL teardown - shader/program churn no longer grows Vulkan objects without bound 2026-07-27 22:05:25 -04:00
swung0x48 c540fb88ee [Fix] (DirectVulkan): drain frame transients on present-less paths - readback waits, suspended presentation, blocking sync waits and flush completion polls now run Present's per-frame drains (deferred buffer/texture releases, transient arena rewind, descriptor cursors, retired command buffers, conversion caches) whenever every submission is provably complete, so offscreen/minimized workloads stay bounded; never blocks, frames-in-flight overlap untouched 2026-07-27 21:45:21 -04:00
swung0x48 6ae3245a0d [Test] (CTS): raise the no-output abort threshold - consecutive instant-crash cases are real progress once device liveness is confirmed 2026-07-26 19:23:05 -04:00
swung0x48 7e048fc2bf [Fix] (DirectVulkan): map RGB10_A2(UI) to A2B10G10R10 - GL 2_10_10_10_REV puts R in bits 0-9 so the A2R10G10B10 mapping silently swapped R/B on upload; also decode both 1010102 variants in readback 2026-07-26 19:13:46 -04:00
swung0x48 83cdfd6bdd [Fix] (DirectVulkan): GetTexImage reads all 3D slices/array layers with PACK_IMAGE_HEIGHT/SKIP_IMAGES semantics, and sRGB readback returns raw sRGB-encoded bytes instead of linearizing 2026-07-26 18:30:43 -04:00
swung0x48 1c76f886cf [Fix] (DirectVulkan): back legacy low-bit formats (RGB565/RGB5A1/RGBA4/R3G3B2/RGB4/RGBA2/RGB10/12) with their UNorm8/16 canonical shadow layouts and add capability fallbacks - they mapped to VK_FORMAT_UNDEFINED and crashed or wedged the GPU on upload; also admit 2DMSArray/CubeMap/3D color attachment targets in the render pass 2026-07-26 18:30:42 -04:00
swung0x48 a2e109beff [Fix] (DirectVulkan): general (format,type) readback conversion - hoist the CTS-verified StoreWideRowsToClient into shared ReadbackImpl and decode any color VkFormat to wide RGBA rows; readback previously supported only RGB/BGR/RGBA/BGRA x UNSIGNED_BYTE/FLOAT and silently returned zeros for everything else 2026-07-26 18:30:41 -04:00
swung0x48 63f0756644 [Fix] (DirectVulkan): support UBO instance arrays as arrayed descriptors - uniform Block{...}b[N] reflected as one binding with descriptorCount=N, per-element GL block mapping, per-element buffer infos and dynamic offsets; non-UBO descriptor arrays now fail program creation cleanly instead of continuing corrupt 2026-07-26 18:30:41 -04:00
swung0x48 450215d12c [Fix] (DirectVulkan): implement color renderbuffer attachments - render pass/pipeline/blit/copy/readback/clear paths treated color renderbuffers as absent (writes masked to VK_ATTACHMENT_UNUSED, glClear dropped, readback zeros) 2026-07-26 18:30:40 -04:00
swung0x48 3a9e520170 [Test] (CTS): isolate the DirectVulkan renderbuffer-FBO readback defect so the rest of KHR-GL33 can be measured 2026-07-26 18:30:39 -04:00
swung0x48 d2996ba1cf [Test] (CTS): run VK-GL-CTS KHR-GL33 against MobileGL on Android via a standalone glcts binary 2026-07-26 18:30:39 -04:00
swung0x48 c8632dfefe [Test] (piglit-android): add on-device piglit harness for MobileGL - patched waffle (WAFFLE_EGL_LIBRARY/WAFFLE_GL_LIBRARY overrides so waffle drives libMobileGL.so directly, AImageReader-backed windows for DirectVulkan since Android ICDs lack VK_EXT_headless_surface, WAFFLE_FORCE_GL_CONTEXT_VERSION to upgrade piglit's low compat context requests to 3.3 core, meson cross fixes) and patched piglit (Android platform support, EGL support decoupled from the X11-dependent EGL tests, and a dispatch-init fix: the waffle resolvers were never installed because gl_fw is NULL during framework construction, so gl* silently bound to the system driver via the DT_NEEDED libEGL's eglGetProcAddress), plus the adb chunked runner with PIGLIT-result parsing, a results comparator, cross-file examples, and the piglit-on-android skill 2026-07-26 11:57:11 -04:00
swung0x48 b8a8a660e1 [Refactor] (Lifecycle): own MobileGL's lifecycle from the EGL layer instead of ELF static ctor/dtor - the first EGL/WGL entry point lazily initializes via a thread-safe, re-init-capable EnsureInitialized (AutoInit is gone), the last eglTerminate with no initialized display and nothing current tears the whole library down deterministically inside the EGL lifecycle, and the global singletons move to leak-at-exit heap storage so process exit runs no backend destructors at all (AutoDestroy and the Windows DllMain abandon hook are gone); fixes the exit-time SIGABRT from undefined static-destruction order - the DirectGLES buffer-pool mutex abort on Android clean exits, and the pre-existing macOS QueryTest/ProgramTest 'Subprocess aborted' gtest failures now pass (ctest 411/411) 2026-07-26 10:59:14 -04:00
swung0x48 7ab83861ca [Fix] (DirectVulkan): suspend presentation while the window is zero-area - a minimized window's out-of-date swapchain used to keep Present submitting on a signaled fence and presenting never-acquired images (adversarial review); also drop logging from the process-detach abandon path 2026-07-26 07:59:40 -04:00
swung0x48 eaeba556a3 [Test] (WGL): add manual Windows smoke tests - hand-rolled WGL bootstrap and GLFW-driven variant covering the zero-area helper-window path 2026-07-26 07:21:36 -04:00
swung0x48 72fa1221a5 [Fix] (DirectVulkan): survive zero-area windows at renderer init - skip the eager first acquire when RecreateSwapchain's minimize guard left no swapchain (GLFW's hidden helper window), and let Present bring the swapchain up once the window has real size 2026-07-26 07:16:11 -04:00
swung0x48 c4254c4bbd [Feat] (WGL): add Windows host layer - drop-in opengl32.dll with WGL over EGLImpl, Win32 window backend plumbing for both backends, ANGLE loader path, and leak-at-exit process teardown 2026-07-26 06:46:29 -04:00
swung0x48 199164c2e0 [Perf] (DirectGLES): route the per-upload GL_PIXEL_UNPACK_BUFFER unbinds through the unpack binding cache - the six texture-upload sites re-issued glBindBuffer(UNPACK, 0) on every upload; with the resting-0 shadow they now no-op after the first 2026-07-21 19:54:55 -04:00
swung0x48 e87063e90c [Fix] (DirectGLES): route default-framebuffer binds through the FBO-binding shadow - the raw glBindFramebuffer(0) in BindCurrentFBO/SyncAndBindFramebufferObject left the shadow claiming the previous user FBO, false-skipping its next re-bind and letting scoped guards restore a stale binding (caught by adversarial review); also scrub buffer-binding shadows when VAO client-attribute staging buffers are deleted, include cube-map arrays in the pack-image-params gate, and make the delete-recording test hook assertion-unwind safe 2026-07-21 19:54:54 -04:00
swung0x48 122da27249 [Fix] (DirectGLES): overhaul readback/copy/blit driver-state handling with shadow-backed RAII guards - pixel-PACK/UNPACK PBO binding caches resting at 0 (the old bind-then-query 'restore' left the user PBO bound forever, capturing later client-memory readbacks), a PACK pixel-store shadow replacing per-readback glGetIntegerv syncs, a driver FBO-binding shadow behind all scoped binders (per-instance prev slots, nest-safe), scratch-FBO attachment shadows that detach cross-aspect residue exactly when present (depth CopyTex* attachments used to wedge later GetTexImage color reads and vice versa), scissor guards around emulation blits (app scissor clipped depth copies), stale-driver-error drains before single-shot glGetError consumers (fallbacks silently dropped readbacks / GenerateMipmap raised phantom app errors in production builds), ClearBufferiv missing BindCurrentFBO(Draw), cube-face glBindTexture INVALID_ENUM cache poisoning, per-slice GL_PACK_IMAGE_HEIGHT/SKIP_IMAGES semantics on 3D GetTexImage, backend texture ids deleted on wrapper destruction with cache/scratch-FBO scrubs (ids used to leak for the context lifetime and dangling cache pointers could false-skip binds), and context-death/MakeCurrent invalidation for all new shadows; regression tests drive the shadows against a recording mock GLES table 2026-07-21 19:54:54 -04:00
swung0x48 bc2d698b3e [Fix] (DirectGLES): apply the read buffer when one FBO is bound as both draw and read
- SyncCurrentFBO skips the READ-target pass when the same GL FBO is bound as
  both draw and read (the common GL_FRAMEBUFFER case), but the read buffer
  (glReadBuffer) is only applied inside SyncToBackend's READ path — so the skip
  silently dropped every glReadBuffer change, leaving the backend read buffer
  stuck at COLOR_ATTACHMENT0.
- Extract the read-buffer application into BackendFramebufferObject::
  SyncReadBufferToBackend and invoke it from the skip branch (target == Read)
  as well as from SyncToBackend, so reads always target the right attachment.
- Bind the backend FBO as READ inside the helper before glReadBuffer, since the
  skip path only bound it as DRAW.
- Fixes KHR-GL3x.draw_buffers.draw_buffers_1 (reading COLOR_ATTACHMENT1 while
  the FBO stays GL_FRAMEBUFFER-bound returned attachment 0's value); the render
  was already correct, only the readback resolved the wrong attachment.
2026-07-21 12:18:55 -04:00
swung0x48 3049c4b82b [Fix] (ShaderTranspiler): stop blanking block comments in the source handed to glslang
- BlankBlockComments replaced comment chars with spaces but preserved interior newlines, so a block comment spanning a newline inside a #define truncated the macro body (VALUE became empty)
- glslang has a conformant preprocessor and collapses a block comment to one space across newlines, so the delivered source now keeps comments intact and lets glslang handle them
- Fixes KHR-GL3x.shaders.preprocessor multiline_comment_define / redefine_object_multiline_comment / function_redefinition_3 (6 cases, both devices)
- FilterUnsupportedGpuShaderInt64 relied on the blanking to skip commented-out #extension lines; it now masks comments locally (MaskCommentsAndQuotedText) like the sibling passes, collecting edits and applying them back-to-front
- conditional_inclusion.basic_2 (defined() via macro expansion) stays failing by design: glslang rejects it as UB and working around it would mean re-running preprocessing MobileGL defers to glslang
2026-07-21 05:27:01 -04:00
swung0x48 2b3850b76b [Fix] (DirectGLES): upload RGB565/RGB5_A1 shadow data as packed 16-bit types
- The 8-bit unorm shadow was uploaded as GL_UNSIGNED_BYTE, leaving the 8->5/6-bit requantization to the driver
- That rounding direction is implementation-defined: Adreno rounds to nearest (lossless round trip), Mali floors
- On Mali mid-range texels drifted one 5-bit step down, failing all 20 KHR-GL33.pixelstoragemodes.teximage3d rgb565/rgb5a1 cases (eps 1/32); layers with exact values (0.125/0.25/1.0-ish) passed, matching the observed 0,1,7-valid pattern
- PreparePackedNormUpload repacks shadow rows to GL_UNSIGNED_SHORT_5_6_5 / 5_5_5_1 with round-to-nearest, which exactly recovers the original 5/6-bit values (the shadow expansion is injective), so the driver stores them verbatim
- Idempotent across each region's level loop (glType is shared); RGBA4 exempt since its 8-bit expansion (v*17) is exact under either rounding
- Wired at all four SyncMipmapsToBackend upload regions (append-mipmaps, immutable TexSubImage, mutable full, dirty-level update)
2026-07-21 04:30:46 -04:00
swung0x48 2a0ae743a0 [Fix] (DirectGLES): glFinish before the glGetTexImage temp-FBO readback
- Mali (tile-based) does not resolve a texture's render into memory when it is read back through a different (temp) FBO than the one it was rendered with
- The cross-FBO glReadPixels raced the deferred tile resolve and returned pre-render clear contents
- Distinct render targets read back byte-identical, so KHR-GLxx.glsl_noperspective failed on Mali-G715 (all four programs read as the clear colour)
- glGetTexImage is already a CPU/GPU sync point so the extra drain is negligible; Adreno resolves eagerly and was unaffected
2026-07-21 03:34:33 -04:00
swung0x48 6839219c10 [Fix] (GLImpl): report GL_NO_ERROR from glGetGraphicsResetStatus
- The generic export stub returned (GLenum)1; dEQP reads any non-zero status as a lost device
- It is polled after every case (gl3cTestPackages.cpp:121) and sets QP_TEST_RESULT_DEVICE_LOST
- Under the default --deqp-terminate-on-device-lost=enable that tears the whole CTS run down
- MobileGL tracks no GPU resets, so GL_NO_ERROR ("no reset detected") is the honest, spec-correct answer
- Routed through GLImpl::GetGraphicsResetStatus like every other entry point, no inline body in Definitions.cpp
2026-07-21 02:40:15 -04:00
swung0x48 52ddb440ca [Feat] (SelfTest/DriverPost): add a noperspective correctness check to the GLES POST - render a strong-perspective quad and read the centre texel to verify the varying interpolates screen-linear (not perspective-correct), carried through the native GL_NV_shader_noperspective_interpolation path when present or MobileGL's exact gl_Position.w/gl_FragCoord.w emulation when absent. PASS = native and correct; WARN = emulated and correct (the fallback path shipping packs hit on such devices); FAIL = interpolation wrong/perspective-correct, or the program will not build. The ESSL header matches the device version because noperspective is rejected at #version 300 es on some drivers even with the extension enabled 2026-07-21 02:09:17 -04:00
swung0x48 79feeffd25 [Feat] (ShaderTranspiler, DirectGLES): emulate noperspective on GLES devices lacking GL_NV_shader_noperspective_interpolation - EmulateNoPerspectivePass pre-multiplies each NoPerspective output by gl_Position.w in the vertex stage and recovers each input via gl_FragCoord.w in the fragment stage (exact screen-linear L = P(a*w)*gl_FragCoord.w, handling whole-variable and component/access-chain reads, scalar and vector varyings), forces highp on emulated varyings, and strips what it cannot emulate; replaces the smooth-strip fallback so no NV extension is ever required. Restricts the vertex pre-multiply to the entry function so a non-inlined helper cannot double-scale 2026-07-20 23:48:48 -04:00
swung0x48 202037b5a3 [Fix] (DirectVulkan): shrink the blended depth-write quirk to MIN/MAX extremum blends only - a fixture-wide trace sweep showed the additive ONE+ONE arm never fires on the 26.3 OIT chain (its accumulation passes disable depth writes themselves) and only hit unrelated additive glow content; also fail reflection toward the gl_FragDepth exemption and zero phantom default-FBO blend slots so stale indexed state cannot trigger the strip 2026-07-20 23:39:28 -04:00
swung0x48 bce9c48c8e [Feat] (ShaderTranspiler, DirectGLES): support noperspective conformantly instead of stripping it - let the qualifier reach glslang as the core SPIR-V NoPerspective decoration (native on DirectVulkan; SPIRV-Cross emits ESSL noperspective + GL_NV_shader_noperspective_interpolation on DirectGLES), and for GLES devices lacking that extension add StripNoPerspectivePass to drop the decoration and fall back to smooth; the old naked substring erase discarded the interpolation shader packs need and mangled identifiers containing the word 2026-07-20 22:59:43 -04:00
swung0x48 b6a7807a3a [Fix] (MG_Util/ShaderTranspiler): reject malformed #version directives instead of legalizing them - an unrecognized version number (329/331), a bad profile keyword, a float or trailing token used to be rewritten to "#version 330 core" (or rescued to 460 by the retry); now InspectShaderLanguage marks such directives invalid so NormalizeVersionDirective and RetargetLegacyVersionDirectiveTo460 leave them for glslang to reject, while every valid version still normalizes as before 2026-07-20 22:03:36 -04:00
swung0x48 48ba622387 [Fix] (MG_Util/ShaderTranspiler): keep #line directives instead of deleting them, dropping only the GLSL-illegal quoted filename and the ones that precede #version, so __LINE__ and compiler diagnostics follow the application's own numbering 2026-07-20 21:06:38 -04:00
swung0x48 05260d1262 [Fix] (MG_Util/ShaderTranspiler): blank block comments lexically instead of erasing them - a '//*** banner ***' line opened a comment the old scanner never closed, so it deleted the rest of the shader, and a commented-out builtin definition renamed every genuine call to a name nothing defines 2026-07-20 21:06:37 -04:00
swung0x48 6eb5ff51c5 [Fix] (MG_Impl/GLImpl): reject the RGTC internal formats on 3D texture targets - RGTC compresses 4x4 blocks of a 2D image and has no 3D form, and the check must run on the raw enum because RGTC now resolves to plain R8/RG8 storage 2026-07-20 21:05:57 -04:00
swung0x48 e526f8e8ac [Fix] (MG_Util/Converters): resolve the GL_COMPRESSED_* internal formats to the uncompressed storage that backs them instead of rejecting them as unknown - GL prescribes this base-format fallback for the six generic formats, and RGTC stores uncompressed because ES exposes no compressor 2026-07-20 21:05:57 -04:00
swung0x48 e724e88eec [Fix] (MG_State, MG_Impl/GLImpl): allocating a mipmap level no longer truncates the chain above it - AllocateLevel now only grows and the callers that genuinely redefine the whole level set (glTexStorage*, mip regeneration, multisample storage, level-0 respecification) drop the tail explicitly 2026-07-20 21:02:54 -04:00
swung0x48 3b175fb88a [Fix] (MG_Impl/GLImpl): a multisample sample count above the format's maximum is INVALID_OPERATION, not INVALID_VALUE - matching both the spec and the native Adreno driver 2026-07-20 21:02:53 -04:00
swung0x48 b5a4e7075a [Fix] (MG_Impl/GLImpl): record a GL error from the unimplemented compressed texture entry points instead of throwing - a C++ exception unwinding through the C GL ABI hard-crashes any caller, and glGetCompressedTexImage reported success while writing nothing 2026-07-20 21:02:53 -04:00
swung0x48 520c2b6750 [Fix] (MG_Backend/DirectGLES): sync GL_TEXTURE_SWIZZLE_* on multisample targets - the early return meant to skip the sampler-only parameters dropped every swizzle write, which the frontend already treats as legal on those targets 2026-07-20 21:02:52 -04:00
swung0x48 57cc652b1d [Fix] (MG_Impl/GLImpl): glIsTransformFeedback reports GL_FALSE instead of claiming every name it is handed is a live transform feedback object 2026-07-20 21:02:52 -04:00
swung0x48 65ea54da9e [Refactor] (ShaderTranspiler, DirectVulkan): replace the hand-rolled SPIR-V word walkers with a DecoratePositionInvariantPass and SPIRV-Reflect-based InstanceIndex detection 2026-07-20 08:35:00 -04:00
swung0x48 c81dd04f08 [Test] (CI, trace_replay): force the blended depth-write quirk on the Linux DirectVulkan OIT retrace lane and tighten that case's SSIM threshold to 0.995 2026-07-20 07:39:25 -04:00
swung0x48 c158bfa584 [Fix] (DirectVulkan): narrow the blended depth-write quirk to order-independent accumulation blends, exempting sorted-transparency, gl_FragDepth writers and fully masked attachments 2026-07-20 07:39:25 -04:00
swung0x48 f9f455144c [Refactor] (MG_Config, DirectVulkan): route the blended depth-write quirk through the FeaturesTable as MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE instead of an ad-hoc getenv 2026-07-20 04:37:23 -04:00
swung0x48 64e4840de2 [Fix] (DirectVulkan): fall back to immutable images when the mutable probe fails, gate robustBufferAccess behind MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS, decode R32/RG32/R16-class readback formats, and warn when shaderStorageImage*WithoutFormat is unavailable 2026-07-20 02:36:26 -04:00
swung0x48 bf7b5755cc [Refactor] (ShaderTranspiler, MG_Backend, MG_Util): gate the subgroup prefix-scan rewrite behind a generic device-quirk registry with GPU vendor detection and MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN override, warn on template mismatch, and block ARB/NV subgroup spellings 2026-07-20 02:36:26 -04:00
swung0x48 293f64b3c2 [Fix] (MG_Impl, DirectGLES): correct ARB_clear_texture error codes, reject cube maps in CopyTextureSubImage2D, advertise the extension on Espryt, and pin the error contracts with tests 2026-07-20 02:36:25 -04:00
swung0x48 f0cc07c937 [Perf] (DirectVulkan): bound vertex-stream conversion by the draw's real fetch range, reuse cached prefixes, pin cached source buffers, and stop repacking client arrays for pointer alignment 2026-07-20 02:36:25 -04:00
swung0x48 4658536652 [Perf] (DirectVulkan): keep the render pass alive for steady-state storage-image draws and skip storage-image collection for programs without them 2026-07-20 02:36:24 -04:00
swung0x48 04b4627c65 [Fix] (DirectVulkan): pass depth/stencil formats through sampled-view resolution so D24S8/D32FS8 samplers stop dropping draws, and memoize per-binding view-format resolution 2026-07-20 02:36:24 -04:00
swung0x48 68e13705c8 [Fix] (MG_Backend/DirectVulkan, ShaderTranspiler, MG_Test, TraceReplay): make iterationRP retrace pass on 64-lane Vulkan devices with subgroup-width emulation and format-aware readback 2026-07-20 02:36:23 -04:00
swung0x48 e5388c0e7e [Fix] (MG_Backend, MG_Impl, ShaderTranspiler, MG_Test): support iterationRP custom images and storage format reinterpretation 2026-07-20 02:35:30 -04:00
swung0x48 8bc4808b1a [Test] (TraceReplay): match the iterationRP fixture name published on the mirror 2026-07-19 23:09:29 -04:00
swung0x48 5d8a5387e2 [Test] (TraceReplay): try the hit.moe mirror before miawa and Git LFS 2026-07-19 22:29:55 -04:00
swung0x48 aa2184e47a [Test] (TraceReplay): register iterationRP in-world fixture (non-CI, fixture files pending LFS) 2026-07-19 21:39:06 -04:00
swung0x48 9152a4a4bc [Test] (TraceReplay): enable improved-transparency fixture in CI and drop unneeded coherent_as_flush 2026-07-19 07:35:34 -04:00
swung0x48 1963b427db [Refactor] (TraceReplay): reorganize trace skills into uniform packages with bundled scripts 2026-07-19 07:00:56 -04:00
swung0x48 f3def150e7 [Fix] (DirectVulkan): suppress blended depth writes on Qualcomm and mark gl_Position invariant to fix MC 26.3 OIT cloud flicker 2026-07-19 05:47:23 -04:00
193 changed files with 24085 additions and 1415 deletions
+38 -7
View File
@@ -9,7 +9,23 @@ fi
case_name="$1"
fixture_dir="${2:-tools/trace_replay/fixtures}"
python_bin="${PYTHON:-python3}"
mirror_base="${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE:-https://repo.miawa.cn/mgl/tools/trace_replay/fixtures}"
# Fixture mirrors, tried in order before falling back to Git LFS. Override the
# whole list with MOBILEGL_TRACE_FIXTURE_MIRROR_BASES (whitespace separated);
# MOBILEGL_TRACE_FIXTURE_MIRROR_BASE still works and is tried first.
default_mirror_bases=(
"https://git.hit.moe/swung0x48/MobileGL/media/branch/dev/tools/trace_replay/fixtures"
"https://repo.miawa.cn/mgl/tools/trace_replay/fixtures"
)
if [ -n "${MOBILEGL_TRACE_FIXTURE_MIRROR_BASES:-}" ]; then
read -r -a mirror_bases <<< "${MOBILEGL_TRACE_FIXTURE_MIRROR_BASES}"
else
mirror_bases=("${default_mirror_bases[@]}")
fi
if [ -n "${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE:-}" ]; then
mirror_bases=("${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE}" "${mirror_bases[@]}")
fi
# Optional bearer token for mirrors that require authentication (private Gitea).
mirror_token="${MOBILEGL_TRACE_FIXTURE_MIRROR_TOKEN:-}"
download_attempts="${MOBILEGL_TRACE_FIXTURE_DOWNLOAD_ATTEMPTS:-5}"
retry_delay="${MOBILEGL_TRACE_FIXTURE_RETRY_DELAY:-2}"
@@ -30,7 +46,8 @@ fixture_list="$("${python_bin}" tools/trace_replay/trace_cases.py \
--format fixture-files \
--case "${case_name}" \
--fixture-root "${fixture_dir}")"
mapfile -t files <<< "${fixture_list}"
# Strip CR so the script also works when python emits CRLF (Git Bash on Windows).
mapfile -t files < <(printf '%s\n' "${fixture_list}" | tr -d '\r')
include="$(IFS=,; echo "${files[*]}")"
if [ "${case_name}" = "OpenRA" ]; then
@@ -106,6 +123,7 @@ fetch_file_from_mirror() {
local attempt
local partial_size
local curl_status
local curl_auth
metadata="$(get_lfs_metadata "${file}")" || return 1
read -r expected_oid expected_size <<< "${metadata}"
@@ -136,7 +154,11 @@ fetch_file_from_mirror() {
echo "Starting mirror download for ${file} (attempt ${attempt}/${download_attempts})"
fi
if curl -L --fail --show-error --continue-at - --output "${tmp_file}" "${url}"; then
curl_auth=()
if [ -n "${mirror_token}" ]; then
curl_auth=(--header "Authorization: token ${mirror_token}")
fi
if curl -L --fail --show-error --continue-at - "${curl_auth[@]}" --output "${tmp_file}" "${url}"; then
if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then
mv "${tmp_file}" "${file}"
return 0
@@ -184,10 +206,19 @@ fetch_from_mirror() {
for file in "${files[@]}"; do
local name
local url
local base
local fetched=0
name="$(basename "${file}")"
url="${mirror_base%/}/${name}"
echo "Fetching trace fixture from mirror: ${url}"
if ! fetch_file_from_mirror "${file}" "${url}"; then
for base in "${mirror_bases[@]}"; do
url="${base%/}/${name}"
echo "Fetching trace fixture from mirror: ${url}"
if fetch_file_from_mirror "${file}" "${url}"; then
fetched=1
break
fi
echo "Mirror did not serve ${name}; trying the next mirror" >&2
done
if [ "${fetched}" -ne 1 ]; then
return 1
fi
done
@@ -196,7 +227,7 @@ fetch_from_mirror() {
if fetch_from_mirror; then
echo "Fetched trace fixture files for ${case_name} from mirror: ${include}"
else
echo "Mirror fetch failed for ${case_name}; falling back to Git LFS: ${include}"
echo "All mirrors failed for ${case_name}; falling back to Git LFS: ${include}"
git lfs install --local
git lfs pull --include="${include}" --exclude=""
fi
+9
View File
@@ -455,6 +455,15 @@ jobs:
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
fi
# The blended depth-write quirk auto-enables only on Qualcomm, which no CI
# runner has, so force it on for the OIT case it exists to fix. ForceOn
# bypasses only the vendor gate, so this exercises the real strip on
# lavapipe. The Android AVD lane deliberately leaves it off, keeping the
# unstripped path covered for the same trace.
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
&& [ '${{ matrix.case }}' = 'improved-transparency-minecraft-26.3' ]; then
export MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE=1
fi
ctest -V --no-tests=error -R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
- name: Upload actual image
+2
View File
@@ -25,3 +25,5 @@ MobileGL/MG*/cmake-build*
/android-plugin/app/src/trace/jniLibs
/android-plugin/local.properties
tools/trace_replay/work/
__pycache__/
*.py[cod]
+45 -1
View File
@@ -188,9 +188,12 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
@@ -300,6 +303,13 @@ if (ANDROID)
)
endif()
if (WIN32)
list(APPEND SOURCE_FILES
MobileGL/MG_Impl/WGLImpl/WGLImpl.cpp
MobileGL/MG_Impl/WGLImpl/Exporting/Definitions.cpp
)
endif()
set(MOBILEGL_LINK_LIBRARIES
glslang::glslang
spirv-cross-c
@@ -328,10 +338,18 @@ set(MOBILEGL_INCLUDE_DIR
${SPIRV-Headers_SOURCE_DIR}/include
)
add_library(${CMAKE_PROJECT_NAME} SHARED
add_library(${CMAKE_PROJECT_NAME} SHARED
${SOURCE_FILES}
)
if (WIN32)
# The wgl* entry points are exported via .def (see the comment in wgl.def);
# only the shared library links it.
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
MobileGL/MG_Impl/WGLImpl/Exporting/wgl.def
)
endif()
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
C_VISIBILITY_PRESET default
@@ -374,6 +392,18 @@ if(UNIX AND NOT APPLE AND NOT ANDROID)
endforeach()
endif()
if(WIN32)
# Drop-in for the classic GL loader path: a copy named opengl32.dll placed
# next to a host executable is what LoadLibrary("opengl32.dll") and gdi32's
# pixel-format forwarding will resolve.
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:${CMAKE_PROJECT_NAME}>"
"$<TARGET_FILE_DIR:${CMAKE_PROJECT_NAME}>/opengl32.dll"
COMMENT "Creating opengl32.dll drop-in copy"
)
endif()
if(NOT ANDROID)
add_library(${CMAKE_PROJECT_NAME}_s STATIC
${SOURCE_FILES}
@@ -425,8 +455,21 @@ if (ANDROID)
endif()
if (APPLE AND NOT MOBILEGL_IOS)
# MobileGL statically embeds glslang, SPIRV-Tools, and SPIRV-Cross. When
# this dylib is injected with DYLD_INSERT_LIBRARIES, exporting those C++
# symbols interposes incompatible copies embedded by host libraries such
# as shaderc. Keep only the public GL/EGL/CGL loader surface globally
# visible; GetProcAddress can still return pointers to hidden internals.
set(MOBILEGL_MACOS_EXPORTED_SYMBOLS
"${CMAKE_CURRENT_SOURCE_DIR}/MobileGL/MG_Impl/DyldInterpose/ExportedSymbols.txt")
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE
"LINKER:-exported_symbols_list,${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
set_property(TARGET ${CMAKE_PROJECT_NAME} APPEND PROPERTY
LINK_DEPENDS "${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
"-framework Cocoa"
"-framework CoreVideo"
"-framework QuartzCore"
"-framework Foundation"
"-framework OpenGL"
@@ -434,6 +477,7 @@ if (APPLE AND NOT MOBILEGL_IOS)
if(TARGET ${CMAKE_PROJECT_NAME}_s)
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC
"-framework Cocoa"
"-framework CoreVideo"
"-framework QuartzCore"
"-framework Foundation"
"-framework OpenGL"
+25 -1
View File
@@ -14,12 +14,21 @@ namespace MobileGL::MG_Config {
inline const String ProjectName = "MobileGL";
inline const String CoreName = "MobileGL Core";
inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)";
inline const Version CoreVersion = {26, 7, 0, "-dev", VersionType::Development};
inline const Version CoreVersion = {26, 8, 0, "-dev", VersionType::Development};
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
inline const Uint64 CacheVersion = 0;
extern BackendType ActiveBackendType;
// Tri-state override for device-specific quirks: Auto lets the detected device decide,
// ForceOn/ForceOff bypass the detection in either direction. ForceOn only bypasses the
// device gate - each quirk keeps its structural safety checks.
enum class QuirkOverride : Uint8 {
Auto = 0,
ForceOn,
ForceOff,
};
// Feature toggles parsed once from environment variables in MG_ConfigLoader::Init()
// (ConfigLoader.cpp), before the accepted-env map is destroyed. All Bool fields share
// one truthy rule: the variable is set, non-empty, not "0", and not "false"
@@ -67,6 +76,21 @@ namespace MobileGL::MG_Config {
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
// version request.
Bool RelaxedSemantics = false;
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN: overrides the shader-source quirk that
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
// cross-pipeline vertex position invariance. Sorted-transparency "over" blends,
// gl_FragDepth writers, and fully color-masked attachments are exempt (see
// PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm.
QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto;
// MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
// feature off. It is enabled by default to match GL's defined out-of-range fetch
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
Bool DisableRobustBufferAccess = false;
};
extern FeaturesTable Features;
} // namespace MobileGL::MG_Config
+15
View File
@@ -86,6 +86,17 @@ namespace MobileGL::MG_ConfigLoader {
return it != acceptedEnvVariablesMap->end() && IsTruthyValue(it->second);
}
// Quirk overrides are tri-state: an unset variable keeps device auto-detection, a truthy
// value forces the quirk on, anything else set ("0", "false", "") forces it off.
inline MG_Config::QuirkOverride QueryEnvQuirkOverride(const String& key) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
return MG_Config::QuirkOverride::Auto;
}
return IsTruthyValue(it->second) ? MG_Config::QuirkOverride::ForceOn
: MG_Config::QuirkOverride::ForceOff;
}
inline Uint32 QueryEnvUint32(const String& key, Uint32 defaultValue, Uint32 minValue, Uint32 maxValue) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
@@ -123,6 +134,10 @@ namespace MobileGL::MG_ConfigLoader {
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
features.MagmaDisableBlendedDepthWriteQuirk =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
}
inline void InitBackendType() {
+1
View File
@@ -34,6 +34,7 @@
#define MOBILEGL_EGL_API MOBILEGL_API
#define MOBILEGL_CGL_API MOBILEGL_API
#define MOBILEGL_NSOPENGL_API MOBILEGL_API
#define MOBILEGL_WGL_API MOBILEGL_API
// ====================== MobileGL configurations ======================= //
#ifndef MOBILEGL_LOG_ACTIVE_LEVEL
+7 -1
View File
@@ -14,7 +14,13 @@ namespace MobileGL {
} // namespace MG_Config
namespace MG_Backend {
UniquePtr<BackendObject> pActiveBackendObject;
// Leak-at-exit storage: the UniquePtr itself lives on the heap and is
// never destroyed by the runtime, so process exit runs no backend
// destructors (static destruction order across TUs is undefined).
// Deterministic teardown happens inside the EGL lifecycle instead:
// the last eglTerminate calls MobileGL::Destroy(), which .reset()s
// these singletons while the process is still healthy.
UniquePtr<BackendObject>& pActiveBackendObject = *new UniquePtr<BackendObject>();
GlobalBackendFunctionsTable gBackendFunctionsTable;
} // namespace MG_Backend
} // namespace MobileGL
+47 -33
View File
@@ -9,14 +9,25 @@
#include "Init.h"
#include "Config.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_Backend/DirectVulkan/DirectVulkan.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/EGLState/Core.h>
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
#include <atomic>
#include <mutex>
namespace MobileGL {
namespace {
Bool g_isInitialized = false;
std::atomic<Bool> g_isInitialized = false;
thread_local Bool tl_initializing = false;
std::mutex& InitMutex() {
static std::mutex mutex;
return mutex;
}
void DestroyImpl(Bool logLifecycle) {
if (!g_isInitialized) {
@@ -27,6 +38,12 @@ namespace MobileGL {
MGLOG_I("MobileGL closing...");
}
glslang::FinalizeProcess();
// GL syncs die with their contexts, and every context is gone by the
// time full teardown runs: drain the live-sync registry while the
// backend function table can still release the backend handles (and
// before a re-initialized library could pair them with the wrong
// backend's DeleteSync).
MG_Impl::GLImpl::DestroyAllSyncObjects();
MG_Backend::pActiveBackendObject.reset();
MG_State::pGLContext.reset();
MG_State::pEGLContext.reset();
@@ -64,40 +81,37 @@ namespace MobileGL {
MGLOG_I("MobileGL initialized");
}
void EnsureInitialized() {
if (g_isInitialized.load(std::memory_order_acquire)) {
return;
}
// Re-entrant call while this thread is already inside Initialize()
// (e.g. an init step routing back through a public entry point).
if (tl_initializing) {
return;
}
const std::lock_guard<std::mutex> lock(InitMutex());
if (g_isInitialized.load(std::memory_order_acquire)) {
return;
}
tl_initializing = true;
Initialize();
tl_initializing = false;
}
void Destroy() {
DestroyImpl(true);
}
#if defined(__linux__) || defined(__APPLE__)
__attribute__((constructor)) static void AutoInit() {
Initialize();
}
__attribute__((destructor)) static void AutoDestroy() {
if (MG_Config::Features.TraceSkipAutodestroy) {
return;
}
#if defined(__APPLE__)
// macOS injected dylibs can run destructors after logging/backend static state is already torn down.
return;
#else
DestroyImpl(false);
#endif
}
#endif
#ifdef _WIN32
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
Initialize();
break;
case DLL_PROCESS_DETACH:
Destroy();
break;
}
return TRUE;
}
#endif
// MobileGL's lifecycle is owned entirely by the host-API layers
// (EGL/WGL/CGL): initialization happens lazily on the first entry point
// via EnsureInitialized(), and full teardown happens deterministically
// when the last EGL display is terminated with nothing current (EGLImpl
// calls Destroy()). There is intentionally no backend-initializing static
// constructor, no static destructor, and no DllMain: the global singletons
// use leak-at-exit storage (see GlobalObjects.cpp), so a process that exits
// without eglTerminate simply leaks them to the OS instead of running
// backend destructors during static teardown. macOS has a lightweight
// dyld constructor that installs NSOpenGL dispatch hooks only; full backend
// initialization still enters here from the first hooked CGL context.
} // namespace MobileGL
+7
View File
@@ -11,6 +11,13 @@
namespace MobileGL {
void Initialize();
// Thread-safe, idempotent, and re-entrant wrapper around Initialize().
// Host layers (EGL/WGL/CGL entry points) call this lazily on first use so
// full backend initialization never depends on ELF/DLL static constructors,
// and so a fresh init can follow a full Destroy() (e.g. after the last
// eglTerminate). The macOS dyld bootstrap installs only lightweight
// NSOpenGL method hooks.
void EnsureInitialized();
void Destroy();
namespace MG_Util::Debug {
+34 -1
View File
@@ -220,6 +220,15 @@ namespace MobileGL {
// and leave the query readable later.
Bool (*GetQueryResult64)(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
void (*DeleteBackendQuery)(BackendQueryHandle query);
// GL_SAMPLES_PASSED occlusion queries (optional; null = unsupported,
// the frontend then rejects the target). Results/deletion flow through
// GetQueryResult64 / DeleteBackendQuery like timer queries.
BackendQueryHandle (*BeginOcclusionQuery)();
void (*EndOcclusionQuery)(BackendQueryHandle query);
// Transform feedback primitive queries backed by real GPU query pools
// (optional; null = frontend falls back to CPU accounting).
BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated);
void (*EndXfbPrimitivesQuery)(BackendQueryHandle query);
Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported
};
struct GlobalBackendFunctionsTable {
@@ -230,6 +239,21 @@ namespace MobileGL {
void (*SetSwapInterval)(Int interval);
};
// Coarse GPU vendor identity for gating device-specific quirks. Detected from the
// Vulkan physical-device vendorID or the GLES GL_VENDOR/GL_RENDERER strings; stays
// Unknown when detection is inconclusive, in which case auto-gated quirks stay off.
enum class GpuVendorKind : Uint8 {
Unknown = 0,
Qualcomm,
Arm,
Nvidia,
Amd,
Intel,
ImgTec,
// Software rasterizers (llvmpipe/lavapipe, SwiftShader).
Software,
};
struct DynamicBackendParameters {
SizeT UniformBufferOffsetAlignment = 256;
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically,
@@ -285,19 +309,28 @@ namespace MobileGL {
Float ViewportBoundsRangeMin = 0.0f;
Float ViewportBoundsRangeMax = 0.0f;
Int ViewportSubpixelBits = 0;
// GL 4.x fragment-interpolation offset limits. These defaults are the
// core minimums and are replaced by live GLES/Vulkan device limits.
Float MinFragmentInterpolationOffset = -0.5f;
// For four fractional bits the greatest required legal offset is
// 0.5 - 2^-4 = 0.4375 (GL 4.6 table 23.70).
Float MaxFragmentInterpolationOffset = 0.4375f;
Int FragmentInterpolationOffsetBits = 4;
Bool SupportsWideLines = false;
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
Uint32 SubgroupSize = 0;
Uint32 SubgroupSupportedStages = 0;
Uint32 SubgroupSupportedFeatures = 0;
Bool SubgroupQuadOperationsInAllStages = false;
GpuVendorKind GpuVendor = GpuVendorKind::Unknown;
};
enum class WindowBackend {
Android,
X11,
MetalLayer,
// TODO: Wayland, Windows, etc.
Win32, // Handle is an HWND
// TODO: Wayland, etc.
WindowBackendCount,
Unknown = -1
};
+1 -1
View File
@@ -13,6 +13,6 @@
#include "DirectVulkan/BackendObject_DirectVulkan.h"
namespace MobileGL::MG_Backend {
extern UniquePtr<BackendObject> pActiveBackendObject;
extern UniquePtr<BackendObject>& pActiveBackendObject;
extern GlobalBackendFunctionsTable gBackendFunctionsTable;
} // namespace MobileGL::MG_Backend
@@ -20,6 +20,7 @@
#include <MG_Util/Texture/TextureFormatProcessor.h>
#include <Config.h>
#include <algorithm>
#include <cmath>
#include <format>
namespace MobileGL::MG_Backend::DirectGLES {
@@ -603,7 +604,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
.ExtraVendor = Nullopt, // Extra vendor
.RendererGLInfo =
{
.TargetGLVersion = {3, 3, 0}, // Target OpenGL Version
.TargetGLVersion = {3, 3, 0}, // GL target version
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
@@ -701,9 +702,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
if ((handle.Backend != WindowBackend::Android &&
handle.Backend != WindowBackend::X11 &&
handle.Backend != WindowBackend::MetalLayer) ||
handle.Backend != WindowBackend::MetalLayer &&
handle.Backend != WindowBackend::Win32) ||
!handle.Handle) {
MGLOG_E("DirectGLES backend only supports Android, X11, and CAMetalLayer native windows");
MGLOG_E("DirectGLES backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
return false;
}
@@ -826,7 +828,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object,
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample,
E_GL_ARB_direct_state_access,
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding,
@@ -933,11 +935,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
funcsTable.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
funcsTable.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
funcsTable.GL.QueryCounterTimestamp = QueryCounterTimestamp;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
}
// Occlusion queries are core ES3 (independent of MOBILEGL_DISABLE_TIMERQUERY)
// and share the handle-based result/delete entries, which must exist even
// when the timer-query group above is disabled.
funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTableInitialized = true;
}
return funcsTable;
@@ -1033,8 +1040,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
m_dynamicParameters.ViewportBoundsRangeMax = m_GLESCapabilities.ViewportBoundsRangeMax;
m_dynamicParameters.ViewportSubpixelBits = m_GLESCapabilities.ViewportSubpixelBits;
m_dynamicParameters.MinFragmentInterpolationOffset =
std::isfinite(m_GLESCapabilities.MinFragmentInterpolationOffset) &&
m_GLESCapabilities.MinFragmentInterpolationOffset <= -0.5f
? m_GLESCapabilities.MinFragmentInterpolationOffset
: -0.5f;
m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f;
m_dynamicParameters.FragmentInterpolationOffsetBits = 4;
if (m_GLESCapabilities.FragmentInterpolationOffsetBits >= 4 &&
std::isfinite(m_GLESCapabilities.MaxFragmentInterpolationOffset)) {
const Float requiredMaxOffset =
0.5f - std::ldexp(1.0f, -m_GLESCapabilities.FragmentInterpolationOffsetBits);
if (m_GLESCapabilities.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
m_dynamicParameters.MaxFragmentInterpolationOffset =
m_GLESCapabilities.MaxFragmentInterpolationOffset;
m_dynamicParameters.FragmentInterpolationOffsetBits =
m_GLESCapabilities.FragmentInterpolationOffsetBits;
}
}
m_dynamicParameters.SupportsWideLines =
m_GLESCapabilities.AliasedLineWidthRangeMax > 1.0f || m_GLESCapabilities.SmoothLineWidthRangeMax > 1.0f;
const auto containsAny = [](const String& haystack, std::initializer_list<const char*> needles) {
return std::any_of(needles.begin(), needles.end(), [&](const char* needle) {
return haystack.find(needle) != String::npos;
});
};
const String vendorAndRenderer =
m_GLESCapabilities.GLESVendorString + " " + m_GLESCapabilities.GLESRendererString;
if (containsAny(vendorAndRenderer, {"llvmpipe", "SwiftShader", "softpipe"})) {
// Check software rasterizers first: ANGLE-on-llvmpipe reports both.
m_dynamicParameters.GpuVendor = GpuVendorKind::Software;
} else if (containsAny(vendorAndRenderer, {"Qualcomm", "Adreno"})) {
m_dynamicParameters.GpuVendor = GpuVendorKind::Qualcomm;
} else if (containsAny(vendorAndRenderer, {"Mali", "ARM"})) {
m_dynamicParameters.GpuVendor = GpuVendorKind::Arm;
} else if (containsAny(vendorAndRenderer, {"NVIDIA"})) {
m_dynamicParameters.GpuVendor = GpuVendorKind::Nvidia;
} else if (containsAny(vendorAndRenderer, {"AMD", "Radeon"})) {
m_dynamicParameters.GpuVendor = GpuVendorKind::Amd;
} else if (containsAny(vendorAndRenderer, {"Intel"})) {
m_dynamicParameters.GpuVendor = GpuVendorKind::Intel;
} else if (containsAny(vendorAndRenderer, {"Imagination", "PowerVR"})) {
m_dynamicParameters.GpuVendor = GpuVendorKind::ImgTec;
} else {
m_dynamicParameters.GpuVendor = GpuVendorKind::Unknown;
}
}
const MG_External::GLESFunctionsTable& BackendObject_DirectGLES::GetGLESFunctions() const {
File diff suppressed because it is too large Load Diff
@@ -130,6 +130,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendQueryHandle BeginTimeElapsedQuery();
void EndTimeElapsedQuery(BackendQueryHandle query);
BackendQueryHandle QueryCounterTimestamp();
// GL_ANY_SAMPLES_PASSED(_CONSERVATIVE) occlusion queries: core ES3, independent of
// GL_EXT_disjoint_timer_query and of MOBILEGL_DISABLE_TIMERQUERY. Results/deletion
// flow through GetQueryResult64/DeleteBackendQuery like the timer queries above.
BackendQueryHandle BeginOcclusionQuery();
void EndOcclusionQuery(BackendQueryHandle query);
Bool IsQueryResultAvailable(BackendQueryHandle query);
// Returns true when a final value landed in *outNanoseconds (a zero for
// null or stale-generation handles IS final: the frontend may cache it
+575 -43
View File
@@ -267,16 +267,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint g_boundArrayBufferId = 0;
Bool g_boundArrayBufferKnown = false;
// Driver-level GL_PIXEL_PACK/UNPACK_BUFFER binding shadows (see
// Managers.h). Resting state between operations is 0; scopes in the
// readback/upload paths bind what they need through the cache and
// return to 0, so a stale user PBO can never capture a later
// readback that meant to target client memory.
Uint g_boundPixelPackBufferId = 0;
Bool g_boundPixelPackBufferKnown = false;
Uint g_boundPixelUnpackBufferId = 0;
Bool g_boundPixelUnpackBufferKnown = false;
// Bumped whenever the backend ES context is destroyed; resources with
// an older generation hold ids from a dead context.
Uint g_bufferContextGeneration = 1;
// Defined next to the indexed-binding shadow below; forward-declared so
// every glDeleteBuffers site in this namespace can scrub stale shadow
// entries (GL resets a deleted buffer's indexed bindings to 0, and a
// recycled name matching a stale shadow entry would otherwise
// false-skip the rebind).
void ScrubIndexedBufferBindingShadowForId(Uint id);
// entries (GL resets a deleted buffer's bindings - indexed and pixel
// pack/unpack alike - to 0, and a recycled name matching a stale shadow
// entry would otherwise false-skip the rebind).
void ScrubBufferBindingShadowsForId(Uint id);
// Resources whose owning BufferObject died; ids deleted at the next
// sync point with a current ES context.
@@ -318,10 +328,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (g_boundArrayBufferKnown && g_boundArrayBufferId == r.id) {
InvalidateArrayBufferBindingCache();
}
// Pooling keeps the id alive (and thus any driver binding of it);
// drop to unknown rather than claiming the post-delete 0 state.
if ((g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == r.id) ||
(g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == r.id)) {
InvalidatePixelBufferBindingCaches();
}
const std::lock_guard<std::mutex> lock(g_poolMutex);
auto& bucket = g_bufferPool[r.storageSize];
if (bucket.size() >= kMaxEntriesPerBucket || g_pooledBytes + r.storageSize > kMaxPoolBytes) {
ScrubIndexedBufferBindingShadowForId(r.id);
ScrubBufferBindingShadowsForId(r.id);
g_GLESFuncs.glDeleteBuffers(1, &r.id); // over budget: don't pool
r.id = 0;
return;
@@ -502,7 +518,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Need a fresh id: glBufferStorage fails on a buffer that already has
// immutable storage, and any prior mutable store is replaced anyway.
if (resource->id != 0) {
ScrubIndexedBufferBindingShadowForId(resource->id);
ScrubBufferBindingShadowsForId(resource->id);
g_GLESFuncs.glDeleteBuffers(1, &resource->id);
resource->id = 0;
}
@@ -630,7 +646,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
InvalidateArrayBufferBindingCache();
}
ScrubIndexedBufferBindingShadowForId(glesResource->id);
ScrubBufferBindingShadowsForId(glesResource->id);
g_GLESFuncs.glDeleteBuffers(1, &glesResource->id);
glesResource->id = 0;
}
@@ -668,6 +684,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
void OnBackendContextDestroyed() {
UnregisterBufferBackendOps();
++g_bufferContextGeneration;
InvalidateArrayBufferBindingCache();
InvalidateIndexedBufferBindingCache();
InvalidatePixelBufferBindingCaches();
// The global-UBO ring's id and persistent map died with the context;
// drop the handles (no GL) and let the next draw recreate the ring.
ResetUboRingForNewContext();
@@ -694,7 +713,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (g_boundArrayBufferKnown && g_boundArrayBufferId == glesResource->id) {
InvalidateArrayBufferBindingCache();
}
ScrubIndexedBufferBindingShadowForId(glesResource->id);
ScrubBufferBindingShadowsForId(glesResource->id);
g_GLESFuncs.glDeleteBuffers(1, &glesResource->id);
glesResource->id = 0;
}
@@ -819,6 +838,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_boundArrayBufferKnown = false;
}
void BindPixelPackBufferId(Uint id) {
if (g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == id) {
return;
}
g_GLESFuncs.glBindBuffer(GL_PIXEL_PACK_BUFFER, id);
g_boundPixelPackBufferId = id;
g_boundPixelPackBufferKnown = true;
}
void BindPixelUnpackBufferId(Uint id) {
if (g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == id) {
return;
}
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, id);
g_boundPixelUnpackBufferId = id;
g_boundPixelUnpackBufferKnown = true;
}
void InvalidatePixelBufferBindingCaches() {
g_boundPixelPackBufferId = 0;
g_boundPixelPackBufferKnown = false;
g_boundPixelUnpackBufferId = 0;
g_boundPixelUnpackBufferKnown = false;
}
void NoteBufferIdDeleted(Uint id) {
if (id == 0) {
return;
}
if (g_boundArrayBufferKnown && g_boundArrayBufferId == id) {
InvalidateArrayBufferBindingCache();
}
ScrubBufferBindingShadowsForId(id);
}
namespace {
// Shadow of the GL indexed buffer bindings so redundant glBindBufferBase/Range
// (same index + id + range) are skipped. isBase distinguishes a whole-buffer
@@ -840,12 +894,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
return nullptr;
}
// glDeleteBuffers resets the deleted buffer's bindings (indexed ones
// included) to 0 in the current context; mirror that in the shadow, or a
// later buffer recycling the same name with a matching range would
// false-skip its rebind. Default IndexedBufferBinding{} == base(0) ==
// the post-delete GL state.
void ScrubIndexedBufferBindingShadowForId(Uint id) {
// glDeleteBuffers resets the deleted buffer's bindings (indexed and
// pixel pack/unpack ones included) to 0 in the current context; mirror
// that in the shadows, or a later buffer recycling the same name with a
// matching shadow entry would false-skip its rebind. Default
// IndexedBufferBinding{} == base(0) == the post-delete GL state.
void ScrubBufferBindingShadowsForId(Uint id) {
if (id == 0) return;
for (auto& binding : g_indexedUBOBindings) {
if (binding.id == id) binding = {};
@@ -853,6 +907,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
for (auto& binding : g_indexedSSBOBindings) {
if (binding.id == id) binding = {};
}
if (g_boundPixelPackBufferKnown && g_boundPixelPackBufferId == id) {
g_boundPixelPackBufferId = 0;
}
if (g_boundPixelUnpackBufferKnown && g_boundPixelUnpackBufferId == id) {
g_boundPixelUnpackBufferId = 0;
}
}
} // namespace
@@ -897,7 +957,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto& bucket = g_bufferPool[oldestKey];
PooledBuffer& e = bucket[oldestIdx];
if (e.contextGeneration == g_bufferContextGeneration && e.id != 0) {
ScrubIndexedBufferBindingShadowForId(e.id);
ScrubBufferBindingShadowsForId(e.id);
g_GLESFuncs.glDeleteBuffers(1, &e.id);
}
g_pooledBytes -= e.size;
@@ -1064,7 +1124,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool staleContext = entry.contextGeneration != g_bufferContextGeneration;
if (!staleContext && entry.retireSerial > completed) continue;
if (!staleContext && entry.id != 0) {
ScrubIndexedBufferBindingShadowForId(entry.id);
ScrubBufferBindingShadowsForId(entry.id);
g_GLESFuncs.glDeleteBuffers(1, &entry.id);
}
g_retiredUboRings[i] = g_retiredUboRings.back();
@@ -1152,6 +1212,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
for (auto& bufferId : m_clientAttributeBufferIds) {
if (bufferId != 0) {
BufferImpl::NoteBufferIdDeleted(bufferId);
g_GLESFuncs.glDeleteBuffers(1, &bufferId);
bufferId = 0;
}
@@ -1324,6 +1385,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
m_contextGeneration = g_textureContextGeneration;
if (m_backendTextureId == 0) {
MGLOG_E("Failed to generate texture object.");
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
@@ -1332,6 +1394,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
BackendTextureObject::~BackendTextureObject() {
if (m_backendTextureId == 0) {
return;
}
// Scrub every driver-state shadow that could false-skip when the name
// or this heap address is recycled - regardless of whether the id can
// still be deleted.
ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId);
for (auto& unitCache : g_boundTexturesCache) {
for (auto& boundTexture : unitCache) {
if (boundTexture == this) {
boundTexture = nullptr;
}
}
}
if (m_contextGeneration == g_textureContextGeneration && g_GLESFuncs.glDeleteTextures) {
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
}
m_backendTextureId = 0;
}
void BackendTextureObject::Bind(GLenum target, Uint unit) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
@@ -1364,7 +1447,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BackendTextureObject::RecreateBackendTexture() {
if (m_backendTextureId != 0) {
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId);
if (m_contextGeneration == g_textureContextGeneration) {
g_GLESFuncs.glDeleteTextures(1, &m_backendTextureId);
}
for (auto& unitCache : g_boundTexturesCache) {
for (auto& boundTexture : unitCache) {
if (boundTexture == this) {
@@ -1375,6 +1461,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
m_contextGeneration = g_textureContextGeneration;
if (m_backendTextureId == 0) {
MGLOG_E("Failed to regenerate texture object.");
MGLOG_E("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
@@ -1391,7 +1478,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// glGetIntegerv - that query forces a driver pipeline sync and, because texture
// uploads run it per dirty texture per frame, it dominated the DirectGLES draw
// path. The backend unpack state is set ONLY by MobileGL's own save/restore
// helpers (this class, TempPixelStoreParameterSync, the R32F copy path), all of
// helpers (this class and, historically, the R32F copy path), all of
// which restore to the resting default, so the shadow stays accurate; a one-time
// forced sync pins the backend to that known default up front. Apply() is
// compare-and-set, so the (now redundant) glPixelStorei calls also usually no-op.
@@ -1563,6 +1650,56 @@ namespace MobileGL::MG_Backend::DirectGLES {
return convertedData.data();
}
// RGB565/RGB5_A1 shadow data is stored as 8-bit unorm; uploading it as GL_UNSIGNED_BYTE
// leaves the 8-bit -> 5/6-bit requantization to the driver, whose rounding direction is
// implementation-defined: Adreno rounds to nearest (lossless round trip) but Mali floors,
// drifting mid-range texels one 5-bit step down and failing the KHR-GL3x
// pixelstoragemodes.teximage3d rgb565/rgb5a1 1/32-eps checks. Repack the shadow rows into
// the packed 16-bit client type with round-to-nearest instead - that recovers the original
// 5/6-bit values exactly (the shadow expansion round(v * 255 / max) is injective), so the
// driver stores them verbatim with no requantization left to its discretion. 4-bit formats
// (RGBA4) are exempt: their 8-bit expansion (v * 17) is exact under either rounding.
// Always retargets *inOutType for these formats (even for null data) so every upload of a
// level uses the same client type.
static const void* PreparePackedNormUpload(TextureInternalFormat format, const IntVec3& texelSize,
const void* data, SizeT byteSize, GLenum* inOutType,
Vector<Uint8>& packedData) {
if (format != TextureInternalFormat::RGB5 && format != TextureInternalFormat::RGB5A1) {
return data;
}
const Bool hasAlpha = format == TextureInternalFormat::RGB5A1;
const GLenum packedType = hasAlpha ? GL_UNSIGNED_SHORT_5_5_5_1 : GL_UNSIGNED_SHORT_5_6_5;
// Idempotent across a region's level loop: glType is shared, so later levels arrive with
// the already-retargeted packed type and must still be converted.
if (*inOutType != GL_UNSIGNED_BYTE && *inOutType != packedType) {
return data;
}
*inOutType = packedType;
if (data == nullptr || byteSize == 0) {
return data;
}
const SizeT srcPixelBytes = hasAlpha ? 4 : 3;
const SizeT texelCount = std::min(static_cast<SizeT>(std::max(texelSize.x(), 0)) *
static_cast<SizeT>(std::max(texelSize.y(), 0)) *
static_cast<SizeT>(std::max(texelSize.z(), 1)),
byteSize / srcPixelBytes);
packedData.resize(texelCount * sizeof(Uint16));
const Uint8* src = static_cast<const Uint8*>(data);
auto* dst = reinterpret_cast<Uint16*>(packedData.data());
for (SizeT i = 0; i < texelCount; ++i, src += srcPixelBytes) {
const Uint32 r = (static_cast<Uint32>(src[0]) * 31u + 127u) / 255u;
const Uint32 b = (static_cast<Uint32>(src[2]) * 31u + 127u) / 255u;
if (hasAlpha) {
const Uint32 g = (static_cast<Uint32>(src[1]) * 31u + 127u) / 255u;
dst[i] = static_cast<Uint16>((r << 11) | (g << 6) | (b << 1) | (src[3] >= 128 ? 1u : 0u));
} else {
const Uint32 g = (static_cast<Uint32>(src[1]) * 63u + 127u) / 255u;
dst[i] = static_cast<Uint16>((r << 11) | (g << 5) | b);
}
}
return packedData.data();
}
void BackendTextureObject::SyncMipmapsToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
if (!stateTextureObject) {
@@ -1706,9 +1843,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
const void* uploadData = PrepareNormFloatFallbackUpload(
textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType,
convertedUploadData);
Vector<Uint8> packedUploadData;
uploadData = PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize,
uploadData, levelByteSize, &glType, packedUploadData);
DebugImpl::ErrorLopper::Clear();
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
const IntVec3 uploadSize =
GetBackendUploadSize(stateTextureObject->GetTarget(), levelTexelSize);
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
@@ -1760,7 +1900,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const auto& uploadTargets = textureMipmapObject->GetUploadTargets();
if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) {
DebugImpl::ErrorLopper::Clear();
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
switch (targetInternal) {
case TextureTarget::Texture2DMultisample:
g_GLESFuncs.glTexStorage2DMultisample(
@@ -1787,7 +1927,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
} else if (stateTextureObject->IsImmutable() || m_imageBindableStorageRequired) {
DebugImpl::ErrorLopper::Clear();
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
const IntVec3 storageSize = GetBackendUploadSize(targetInternal, baseSize);
switch (MapToBackendTextureTarget(targetInternal)) {
case TextureTarget::Texture2D:
@@ -1831,9 +1971,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
const void* uploadData = PrepareNormFloatFallbackUpload(
textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType,
convertedUploadData);
Vector<Uint8> packedUploadData;
uploadData =
PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize,
uploadData, levelByteSize, &glType, packedUploadData);
DebugImpl::ErrorLopper::Clear();
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
const IntVec3 uploadSize =
GetBackendUploadSize(targetInternal, levelTexelSize);
switch (MapToBackendTextureTarget(targetInternal)) {
@@ -1885,6 +2029,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
const void* uploadData = PrepareNormFloatFallbackUpload(
textureMipmapObject->GetFormat(), levelTexelSize, pData, levelByteSize, glType,
convertedUploadData);
Vector<Uint8> packedUploadData;
uploadData =
PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize,
uploadData, levelByteSize, &glType, packedUploadData);
MGLOG_D("%s: target: %s: syncing mip %d: %dx%dx%d, byteSize = %d, pData = %p, "
"levelDirty = %s",
__func__, MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
@@ -1892,7 +2040,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
levelByteSize, pData, levelDirty ? "true" : "false");
DebugImpl::ErrorLopper::Clear();
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
auto textureTarget = stateTextureObject->GetTarget();
const IntVec3 uploadSize = GetBackendUploadSize(textureTarget, levelTexelSize);
switch (MapToBackendTextureTarget(textureTarget)) {
@@ -1978,7 +2126,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
textureMipmapObject->GetMipmapTexelSize(uploadTarget, level).y(), byteSize);
auto glUploadTarget = ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
g_GLESFuncs.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned
DebugImpl::ErrorLopper::Loop(
[file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
MGLOG_D("%s(%s:%d) ES error: %s", func, file, line,
@@ -1990,6 +2138,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
const void* uploadData = PrepareNormFloatFallbackUpload(
textureMipmapObject->GetFormat(), texelSize, mipData, byteSize, glType,
convertedUploadData);
Vector<Uint8> packedUploadData;
uploadData = PreparePackedNormUpload(textureMipmapObject->GetFormat(), texelSize,
uploadData, byteSize, &glType, packedUploadData);
const IntVec3 uploadSize =
GetBackendUploadSize(stateTextureObject->GetTarget(), texelSize);
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
@@ -2216,11 +2367,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
return;
}
if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) {
// Multisample targets reject the *sampler* parameters (LOD range, border color) but
// GL_TEXTURE_SWIZZLE_* is texture state, not sampler state, and ES accepts it on them.
// Bailing out entirely used to drop every swizzle write on the floor, which is what the
// frontend already assumes is legal (see GL_Texture.cpp's MS-invalid pname list, which
// deliberately omits the swizzle enums). Note the caches for the skipped parameters are
// still refreshed so they never look stale, but m_cacheSwizzleParams must NOT be, or the
// change detection below would swallow the very writes we came here to emit.
const Bool isMultisampleTarget = TextureImpl::IsMultisampleTextureTarget(targetInternal);
if (isMultisampleTarget) {
m_cacheLodRange = stateTextureObject->GetLevelRange();
m_cacheSwizzleParams = stateTextureObject->GetAllSwizzleParams();
m_cacheBorderColor = stateTextureObject->GetBorderColor();
return;
}
Bind(target);
@@ -2233,14 +2390,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
const auto& levelRange = stateTextureObject->GetLevelRange();
if (m_cacheLodRange.x() != levelRange.x()) {
if (!isMultisampleTarget && m_cacheLodRange.x() != levelRange.x()) {
g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_BASE_LEVEL, static_cast<GLint>(levelRange.x()));
m_cacheLodRange.x() = levelRange.x();
}
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
if (m_cacheLodRange.y() != levelRange.y()) {
if (!isMultisampleTarget && m_cacheLodRange.y() != levelRange.y()) {
g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, static_cast<GLint>(levelRange.y()));
m_cacheLodRange.y() = levelRange.y();
}
@@ -2266,7 +2423,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
});
}
if (m_cacheBorderColor != stateTextureObject->GetBorderColor()) {
if (!isMultisampleTarget && m_cacheBorderColor != stateTextureObject->GetBorderColor()) {
const auto& borderColor = stateTextureObject->GetBorderColor();
GLfloat borderColorArray[4] = {borderColor.x(), borderColor.y(), borderColor.z(), borderColor.w()};
g_GLESFuncs.glTexParameterfv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray);
@@ -2295,6 +2452,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
Uint g_activeTextureUnit = 0;
Uint g_textureContextGeneration = 1;
Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
g_boundTexturesCache;
@@ -2320,9 +2478,59 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (target == FramebufferTarget::Read)
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, m_backendFBOId);
BindFramebufferId(GL_READ_FRAMEBUFFER, m_backendFBOId);
else
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_backendFBOId);
BindFramebufferId(GL_DRAW_FRAMEBUFFER, m_backendFBOId);
}
namespace {
// Driver-level framebuffer-binding shadow (see Managers.h). Indexed by
// FramebufferTarget {Draw, Read}.
Array<Uint, SizeT(FramebufferTarget::FramebufferTargetCount)> g_driverFBOBindings = {0, 0};
Array<Bool, SizeT(FramebufferTarget::FramebufferTargetCount)> g_driverFBOBindingKnown = {false, false};
} // namespace
void BindFramebufferId(GLenum fbTarget, Uint id) {
const Bool bindsDraw = fbTarget == GL_DRAW_FRAMEBUFFER || fbTarget == GL_FRAMEBUFFER;
const Bool bindsRead = fbTarget == GL_READ_FRAMEBUFFER || fbTarget == GL_FRAMEBUFFER;
const SizeT drawIdx = SizeT(FramebufferTarget::Draw);
const SizeT readIdx = SizeT(FramebufferTarget::Read);
const Bool drawMatches =
!bindsDraw || (g_driverFBOBindingKnown[drawIdx] && g_driverFBOBindings[drawIdx] == id);
const Bool readMatches =
!bindsRead || (g_driverFBOBindingKnown[readIdx] && g_driverFBOBindings[readIdx] == id);
if (drawMatches && readMatches) {
return;
}
g_GLESFuncs.glBindFramebuffer(fbTarget, id);
if (bindsDraw) {
g_driverFBOBindings[drawIdx] = id;
g_driverFBOBindingKnown[drawIdx] = true;
}
if (bindsRead) {
g_driverFBOBindings[readIdx] = id;
g_driverFBOBindingKnown[readIdx] = true;
}
}
Uint CurrentFramebufferBinding(FramebufferTarget target) {
const SizeT idx = SizeT(target);
if (!g_driverFBOBindingKnown[idx]) {
// Cold path: pin the shadow from the driver once (init probes and
// pre-shadow code bind raw but restore what they found).
GLint binding = 0;
g_GLESFuncs.glGetIntegerv(
target == FramebufferTarget::Read ? GL_READ_FRAMEBUFFER_BINDING : GL_DRAW_FRAMEBUFFER_BINDING,
&binding);
g_driverFBOBindings[idx] = static_cast<Uint>(binding);
g_driverFBOBindingKnown[idx] = true;
}
return g_driverFBOBindings[idx];
}
void InvalidateFramebufferBindingCache() {
g_driverFBOBindings = {0, 0};
g_driverFBOBindingKnown = {false, false};
}
void BackendFramebufferObject::InvalidateSyncedState() {
@@ -2376,7 +2584,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (glTextureTarget == GL_UNKNOWN_MGL) {
glTextureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(textureObject->GetTarget());
}
backendTextureObject->Bind(glTextureTarget);
// glBindTexture rejects cube-face enums (INVALID_ENUM with no
// bind, while Bind() would still record the cube-map cache slot
// as bound): bind via the owning cube target; the attach below
// keeps the face target.
const Bool isCubeFace = glTextureTarget >= GL_TEXTURE_CUBE_MAP_POSITIVE_X &&
glTextureTarget <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;
backendTextureObject->Bind(isCubeFace ? GL_TEXTURE_CUBE_MAP : glTextureTarget);
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
backendTextureObject->GetBackendTextureId(),
static_cast<GLint>(attachmentObject.GetTextureLevel()));
@@ -2465,6 +2679,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false;
}
void BackendFramebufferObject::SyncReadBufferToBackend(
const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject) {
if (!stateFBOObject) {
return;
}
auto frontendReadBuf = stateFBOObject->GetReadBuffer();
if (frontendReadBuf == m_frontendReadBuffer) {
return;
}
m_frontendReadBuffer = frontendReadBuf;
GLenum glBackendReadBuffer = GetBackendAttachmentType(frontendReadBuf);
if (m_backendReadBuffer != glBackendReadBuffer) {
m_backendReadBuffer = glBackendReadBuffer;
// glReadBuffer targets whatever FBO is bound to GL_READ_FRAMEBUFFER. When this is
// reached from SyncCurrentFBO's "same FBO as draw" skip path the backend FBO was
// only bound as DRAW, so bind it as READ first to route the read buffer correctly.
Bind(FramebufferTarget::Read);
g_GLESFuncs.glReadBuffer(glBackendReadBuffer);
}
}
void BackendFramebufferObject::SyncToBackend(
const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject, FramebufferTarget asTarget) {
#ifdef TRACY_ENABLE
@@ -2546,16 +2782,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 2. Remap read buffer. glReadBuffer writes the READ-bound FBO's state, so
// only apply (and stamp the memo) when this object is bound as READ.
auto frontendReadBuf = stateFBOObject->GetReadBuffer();
if (frontendReadBuf != m_frontendReadBuffer && asTarget == FramebufferTarget::Read) {
m_frontendReadBuffer = frontendReadBuf;
GLenum glBackendReadBuffer = GetBackendAttachmentType(frontendReadBuf);
if (m_backendReadBuffer != glBackendReadBuffer) {
m_backendReadBuffer = glBackendReadBuffer;
g_GLESFuncs.glReadBuffer(glBackendReadBuffer);
}
if (asTarget == FramebufferTarget::Read) {
SyncReadBufferToBackend(stateFBOObject);
}
// -------------------- Attach texture to backend FBO -----------------------
@@ -2662,6 +2890,295 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_fboSyncedObjects = {};
} // namespace FramebufferImpl
namespace ScratchFBOImpl {
namespace {
ScratchFramebuffer g_tempFramebuffer;
ScratchFramebuffer g_blitReadFramebuffer;
ScratchFramebuffer g_blitDrawFramebuffer;
Uint g_completeTinyFBOId = 0;
Uint g_completeTinyRBOId = 0;
// Detach every point the shadow no longer vouches for. Used when the
// shadow is unknown (context reset, texture id deleted while attached).
void ScrubAllAttachments(ScratchFramebuffer& fb, GLenum fbTarget) {
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
fb.colorTex = 0;
fb.colorTarget = 0;
fb.colorLevel = 0;
fb.colorLayer = -1;
fb.depthTex = 0;
fb.depthTarget = 0;
fb.depthLevel = 0;
fb.depthHasStencil = false;
fb.attachmentsKnown = true;
}
void PrepareForUse(ScratchFramebuffer& fb, GLenum fbTarget) {
if (!fb.attachmentsKnown) {
ScrubAllAttachments(fb, fbTarget);
}
}
// The post-attach glGetError probe below must not misread an error some
// earlier operation left queued; drain before attaching (rare path -
// only runs when the attachment actually changes).
void DrainPendingGLErrors() {
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
}
}
// Record the color point as detached when the shadow said something was
// there; the actual detach call is the caller's (it may be replaced by
// the new attach directly when the point is being overwritten).
void RecordNoColor(ScratchFramebuffer& fb) {
fb.colorTex = 0;
fb.colorTarget = 0;
fb.colorLevel = 0;
fb.colorLayer = -1;
}
void RecordNoDepth(ScratchFramebuffer& fb) {
fb.depthTex = 0;
fb.depthTarget = 0;
fb.depthLevel = 0;
fb.depthHasStencil = false;
}
} // namespace
ScratchFramebuffer& TempFramebuffer() {
return g_tempFramebuffer;
}
ScratchFramebuffer& BlitReadFramebuffer() {
return g_blitReadFramebuffer;
}
ScratchFramebuffer& BlitDrawFramebuffer() {
return g_blitDrawFramebuffer;
}
Uint EnsureId(ScratchFramebuffer& fb) {
if (fb.id == 0) {
g_GLESFuncs.glGenFramebuffers(1, &fb.id);
// A fresh FBO has nothing attached and COLOR_ATTACHMENT0 read/draw
// buffers (the ES defaults for a non-default framebuffer).
fb.attachmentsKnown = true;
RecordNoColor(fb);
RecordNoDepth(fb);
fb.readBuffer = GL_COLOR_ATTACHMENT0;
fb.drawBuffer = GL_COLOR_ATTACHMENT0;
}
return fb.id;
}
void EnsureColorAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget,
GLint level) {
PrepareForUse(fb, fbTarget);
if (fb.depthTex != 0) {
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
RecordNoDepth(fb);
}
if (fb.colorTex == tex && fb.colorTarget == texTarget && fb.colorLevel == level && fb.colorLayer < 0) {
return;
}
if (fb.colorTex != 0) {
// Detach first: if the new attach fails, the point must read as
// missing (incomplete FBO), not silently keep the old texture.
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
}
DrainPendingGLErrors();
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, texTarget, tex, level);
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
RecordNoColor(fb);
return;
}
fb.colorTex = tex;
fb.colorTarget = texTarget;
fb.colorLevel = level;
fb.colorLayer = -1;
}
void EnsureColorAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer) {
PrepareForUse(fb, fbTarget);
if (fb.depthTex != 0) {
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
RecordNoDepth(fb);
}
if (fb.colorTex == tex && fb.colorTarget == 0 && fb.colorLevel == level && fb.colorLayer == layer) {
return;
}
if (fb.colorTex != 0) {
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
}
DrainPendingGLErrors();
g_GLESFuncs.glFramebufferTextureLayer(fbTarget, GL_COLOR_ATTACHMENT0, tex, level, layer);
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
RecordNoColor(fb);
return;
}
fb.colorTex = tex;
fb.colorTarget = 0;
fb.colorLevel = level;
fb.colorLayer = layer;
}
void EnsureDepthAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level,
Bool withStencil) {
PrepareForUse(fb, fbTarget);
if (fb.colorTex != 0) {
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
RecordNoColor(fb);
}
if (fb.depthTex == tex && fb.depthTarget == texTarget && fb.depthLevel == level &&
fb.depthHasStencil == withStencil) {
return;
}
if (fb.depthTex != 0) {
// One call clears both depth and stencil points regardless of how
// the previous attachment was made.
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
}
DrainPendingGLErrors();
g_GLESFuncs.glFramebufferTexture2D(fbTarget,
withStencil ? GL_DEPTH_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT,
texTarget, tex, level);
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
RecordNoDepth(fb);
return;
}
fb.depthTex = tex;
fb.depthTarget = texTarget;
fb.depthLevel = level;
fb.depthHasStencil = withStencil;
}
void EnsureNoColorAttachment(ScratchFramebuffer& fb, GLenum fbTarget) {
PrepareForUse(fb, fbTarget);
if (fb.colorTex != 0) {
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
RecordNoColor(fb);
}
}
void EnsureNoDepthAttachment(ScratchFramebuffer& fb, GLenum fbTarget) {
PrepareForUse(fb, fbTarget);
if (fb.depthTex != 0) {
g_GLESFuncs.glFramebufferTexture2D(fbTarget, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
RecordNoDepth(fb);
}
}
void EnsureReadBuffer(ScratchFramebuffer& fb, GLenum readBuffer) {
if (fb.readBuffer == readBuffer) {
return;
}
g_GLESFuncs.glReadBuffer(readBuffer);
fb.readBuffer = readBuffer;
}
void EnsureDrawBuffer(ScratchFramebuffer& fb, GLenum drawBuffer) {
if (fb.drawBuffer == drawBuffer) {
return;
}
g_GLESFuncs.glDrawBuffers(1, &drawBuffer);
fb.drawBuffer = drawBuffer;
}
Uint EnsureCompleteTinyFramebufferId() {
if (g_completeTinyFBOId != 0) {
return g_completeTinyFBOId;
}
// One-time creation: the renderbuffer binding is context state with no
// shadow, so save/restore it by query here (cold path only).
GLint prevRenderbuffer = 0;
g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer);
g_GLESFuncs.glGenFramebuffers(1, &g_completeTinyFBOId);
g_GLESFuncs.glGenRenderbuffers(1, &g_completeTinyRBOId);
FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, g_completeTinyFBOId);
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, g_completeTinyRBOId);
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1);
g_GLESFuncs.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
g_completeTinyRBOId);
const GLenum drawBuffer = GL_COLOR_ATTACHMENT0;
g_GLESFuncs.glDrawBuffers(1, &drawBuffer);
g_GLESFuncs.glReadBuffer(GL_COLOR_ATTACHMENT0);
MOBILEGL_ASSERT(g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE,
"Scratch 1x1 framebuffer is incomplete.");
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<Uint>(prevRenderbuffer));
return g_completeTinyFBOId;
}
void NoteTextureIdDeleted(Uint textureId) {
if (textureId == 0) {
return;
}
for (ScratchFramebuffer* fb : {&g_tempFramebuffer, &g_blitReadFramebuffer, &g_blitDrawFramebuffer}) {
if (fb->colorTex == textureId || fb->depthTex == textureId) {
fb->attachmentsKnown = false;
}
}
}
void OnBackendContextDestroyed() {
g_tempFramebuffer = {};
g_blitReadFramebuffer = {};
g_blitDrawFramebuffer = {};
g_completeTinyFBOId = 0;
g_completeTinyRBOId = 0;
}
} // namespace ScratchFBOImpl
namespace PixelStoreImpl {
namespace {
PackState g_packState;
Bool g_packStateKnown = false;
void PinPackState(const PackState& value) {
g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, value.Alignment);
g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, value.RowLength);
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, value.SkipRows);
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, value.SkipPixels);
g_packState = value;
g_packStateKnown = true;
}
} // namespace
void ApplyPackState(const PackState& desired) {
if (!g_packStateKnown) {
PinPackState(desired);
return;
}
if (desired.Alignment != g_packState.Alignment) {
g_GLESFuncs.glPixelStorei(GL_PACK_ALIGNMENT, desired.Alignment);
g_packState.Alignment = desired.Alignment;
}
if (desired.RowLength != g_packState.RowLength) {
g_GLESFuncs.glPixelStorei(GL_PACK_ROW_LENGTH, desired.RowLength);
g_packState.RowLength = desired.RowLength;
}
if (desired.SkipRows != g_packState.SkipRows) {
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_ROWS, desired.SkipRows);
g_packState.SkipRows = desired.SkipRows;
}
if (desired.SkipPixels != g_packState.SkipPixels) {
g_GLESFuncs.glPixelStorei(GL_PACK_SKIP_PIXELS, desired.SkipPixels);
g_packState.SkipPixels = desired.SkipPixels;
}
}
PackState CurrentPackState() {
if (!g_packStateKnown) {
// Fresh/unknown context: pin to the GL defaults (what a new context
// starts with; writing them makes the shadow authoritative either way).
PinPackState(PackState{});
}
return g_packState;
}
void InvalidatePackStateCache() {
g_packStateKnown = false;
}
} // namespace PixelStoreImpl
namespace PrgramImpl {
Uint32 g_snormFallbackClampOutputMask = 0;
Uint32 g_unormFallbackClampOutputMask = 0;
@@ -2784,6 +3301,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
effectiveSpirv = &uboPrecisionSpirv;
}
// noperspective is core desktop GLSL and reaches here as the SPIR-V NoPerspective
// decoration. SPIRV-Cross renders it as ESSL `noperspective` + `#extension
// GL_NV_shader_noperspective_interpolation : require`; a driver without that extension
// rejects the require. So on such devices emulate screen-linear interpolation instead
// (pre-multiply outputs by gl_Position.w, recover inputs via gl_FragCoord.w) and drop
// the decoration - exact, extension-free. Devices that have the extension keep the
// decoration and let the hardware do it natively.
Vector<unsigned int> noperspectiveSpirv;
if (!g_GLESCapabilities.SupportsNoperspectiveInterpolation &&
MG_Util::ShaderTranspiler::ShaderCompiler::EmulateNoPerspectiveForEssl(
*effectiveSpirv, noperspectiveSpirv) &&
!noperspectiveSpirv.empty()) {
effectiveSpirv = &noperspectiveSpirv;
}
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
+122
View File
@@ -178,6 +178,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
// glBindBuffer with a redundant-bind cache for GL_ARRAY_BUFFER.
void BindBufferId(GLenum target, Uint id);
void InvalidateArrayBufferBindingCache();
// Redundant-bind caches for the driver-level GL_PIXEL_PACK/UNPACK_BUFFER
// bindings. Every backend readback (glReadPixels / pack-PBO map) and pixel
// upload site routes its binding through these so the shadow always matches
// the driver; the resting state between operations is 0, which keeps any
// path that implicitly assumes "no PBO bound" correct. Scrubbed when a
// buffer id is deleted/pooled (GL resets a deleted buffer's bindings to 0,
// and a recycled name matching the shadow would false-skip the rebind) and
// invalidated on MakeCurrent (context may reset).
void BindPixelPackBufferId(Uint id);
void BindPixelUnpackBufferId(Uint id);
void InvalidatePixelBufferBindingCaches();
// A GL buffer id is being deleted by code outside BufferImpl (e.g. the VAO
// client-attribute staging buffers): scrub every buffer-binding shadow that
// could false-skip when the name is recycled.
void NoteBufferIdDeleted(Uint id);
// Redundant-bind cache for INDEXED buffer bindings (glBindBufferBase/Range on
// GL_UNIFORM_BUFFER / GL_SHADER_STORAGE_BUFFER): skips the GL call when the
// (id, range) already at that index matches, like the array-buffer/texture/
@@ -332,6 +347,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
class BackendTextureObject {
public:
BackendTextureObject();
// Deletes the GL texture (frontend glDeleteTextures used to leak every
// backend id for the context lifetime) and scrubs the binding/scratch-FBO
// shadows so a recycled name or heap address cannot false-skip a rebind.
~BackendTextureObject();
BackendTextureObject(const BackendTextureObject&) = delete;
BackendTextureObject& operator=(const BackendTextureObject&) = delete;
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void SyncBuiltinSamplerToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
void SyncTextureParamsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
@@ -343,6 +364,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
void RecreateBackendTexture();
Uint m_backendTextureId = 0;
// ES context generation the id was created under; a dtor running after
// that context died must not delete a foreign (recycled) name.
Uint m_contextGeneration = 0;
Bool m_isInitialized = false;
Bool m_imageBindableStorageRequired = false;
Bool m_backendStorageImmutable = false;
@@ -367,6 +391,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
g_boundTexturesCache;
extern Uint g_activeTextureUnit;
// Bumped when the backend ES context is destroyed; texture ids stamped with
// an older generation belong to a dead context and must not be deleted.
extern Uint g_textureContextGeneration;
} // namespace TextureImpl
namespace FramebufferImpl {
@@ -375,6 +402,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendFramebufferObject();
void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,
FramebufferTarget asTarget);
// Apply only this FBO's read buffer (glReadBuffer) to the backend. Split out so it can
// still run when SyncCurrentFBO skips the READ-target sync because the same GL FBO is
// bound as both draw and read (otherwise glReadBuffer changes would be silently dropped).
void SyncReadBufferToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject);
void InvalidateSyncedState();
Uint GetBackendFramebufferId() const { return m_backendFBOId; }
void Bind(FramebufferTarget target) const;
@@ -414,8 +445,99 @@ namespace MobileGL::MG_Backend::DirectGLES {
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedObjectVersions;
extern Array<MG_State::GLState::FramebufferObject*, SizeT(FramebufferTarget::FramebufferTargetCount)>
g_fboSyncedObjects;
// Driver-level READ/DRAW framebuffer-binding shadow. Every backend
// glBindFramebuffer routes through BindFramebufferId so scoped helpers can
// save/restore the current binding without a glGetIntegerv round-trip (that
// query forces a driver pipeline sync) and so redundant rebinds no-op.
// Starts unknown; the first CurrentFramebufferBinding() query pins it from
// the driver once. Invalidated on MakeCurrent (context may reset).
// GL_FRAMEBUFFER binds both targets.
void BindFramebufferId(GLenum fbTarget, Uint id);
Uint CurrentFramebufferBinding(FramebufferTarget target);
void InvalidateFramebufferBindingCache();
} // namespace FramebufferImpl
// Shared scratch framebuffers for the readback/copy/blit emulation paths, with a
// driver-side attachment shadow: repeated uses skip redundant detach/attach GL
// calls, and an attachment left by one use (e.g. a depth copy's DEPTH_STENCIL
// texture) is detached exactly when a later use of another aspect would
// otherwise inherit it (stale cross-aspect attachments made the shared temp FBO
// incomplete and silently degraded later readbacks).
namespace ScratchFBOImpl {
struct ScratchFramebuffer {
Uint id = 0;
// false => attachment state unknown; scrub every point on next use.
// A fresh FBO starts with nothing attached, so creation sets it true.
Bool attachmentsKnown = false;
Uint colorTex = 0;
GLenum colorTarget = 0;
GLint colorLevel = 0;
GLint colorLayer = -1; // >= 0 => attached via glFramebufferTextureLayer
Uint depthTex = 0;
GLenum depthTarget = 0;
GLint depthLevel = 0;
Bool depthHasStencil = false;
// Per-FBO read/draw buffer state (0 = unknown, set on first use).
GLenum readBuffer = 0;
GLenum drawBuffer = 0;
};
ScratchFramebuffer& TempFramebuffer(); // GetTexImage READ / CopyTex*Image2D depth DRAW
ScratchFramebuffer& BlitReadFramebuffer(); // texture-to-texture blit source
ScratchFramebuffer& BlitDrawFramebuffer(); // texture-to-texture blit destination
// Returns the GL id, generating it if needed (requires a current ES context).
Uint EnsureId(ScratchFramebuffer& fb);
// The fb must currently be bound at fbTarget (glReadBuffer/glDrawBuffers
// target the READ/DRAW binding respectively). Each Ensure* performs the
// minimal detach/attach set and keeps the shadow in sync; a failed attach
// records the point as detached so the completeness check fails instead of
// silently reading a stale attachment.
void EnsureColorAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level);
void EnsureColorAttachmentLayer(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLint level, GLint layer);
void EnsureDepthAttachment2D(ScratchFramebuffer& fb, GLenum fbTarget, Uint tex, GLenum texTarget, GLint level,
Bool withStencil);
void EnsureNoColorAttachment(ScratchFramebuffer& fb, GLenum fbTarget);
void EnsureNoDepthAttachment(ScratchFramebuffer& fb, GLenum fbTarget);
void EnsureReadBuffer(ScratchFramebuffer& fb, GLenum readBuffer);
void EnsureDrawBuffer(ScratchFramebuffer& fb, GLenum drawBuffer);
// A 1x1 RGBA8-renderbuffer-complete FBO (GenerateMipmap needs a complete
// binding while respecifying texture storage). Attachment is set once at
// creation and never changes.
Uint EnsureCompleteTinyFramebufferId();
// A backend texture id is being deleted or respecified: a scratch FBO still
// referencing it would hold a dangling attachment (ES only auto-detaches
// from the *bound* framebuffer), and a recycled name could false-skip a
// re-attach; force a full scrub on next use.
void NoteTextureIdDeleted(Uint textureId);
// The ES context (and the scratch FBO ids with it) is going away.
void OnBackendContextDestroyed();
} // namespace ScratchFBOImpl
// Driver-level GL_PACK_* pixel-store shadow, the readback-side sibling of the
// upload path's ScopedDefaultUnpackState (Managers.cpp): the backend PACK state
// is written ONLY through ApplyPackState, so scoped helpers can save/restore it
// from the shadow instead of glGetIntegerv (which forces a driver pipeline
// sync), and redundant glPixelStorei calls no-op. The first Apply/Current call
// pins the driver to the shadow by writing all fields once. Invalidated on
// MakeCurrent (context may reset). PACK_IMAGE_HEIGHT/SKIP_IMAGES/SWAP_BYTES/
// LSB_FIRST have no ES equivalents; readbacks honor them on the CPU from the
// frontend context state instead.
namespace PixelStoreImpl {
struct PackState {
GLint Alignment = 4;
GLint RowLength = 0;
GLint SkipRows = 0;
GLint SkipPixels = 0;
Bool operator==(const PackState& o) const {
return Alignment == o.Alignment && RowLength == o.RowLength && SkipRows == o.SkipRows &&
SkipPixels == o.SkipPixels;
}
};
void ApplyPackState(const PackState& desired);
PackState CurrentPackState();
void InvalidatePackStateCache();
} // namespace PixelStoreImpl
// Image uniforms take their unit from the layout(binding=N) qualifier baked into
// the transpiled ESSL; unlike samplers they must not (and in ES cannot) be
// assigned through glUniform1i.
+90
View File
@@ -764,5 +764,95 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
}
static SizeT AlignReadbackRow(SizeT rowBytes, Int alignment) {
const SizeT align = alignment > 0 ? static_cast<SizeT>(alignment) : 1;
return (rowBytes + align - 1) / align * align;
}
// Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the
// client-side PACK parameters and the bound pixel-pack buffer. `wide` holds
// `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked),
// 4 components x GetReadbackComponentSize(wideType) bytes each.
// applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage
// of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4).
// Per the GL addressing rules, slice k row j lands at
// SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes
// + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride.
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams) {
const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type);
if (dstPixelBytes == 0) {
return false;
}
PackedReadbackLayout packedLayout{};
const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout);
const SizeT dstComponentSize = GetReadbackComponentSize(type);
const auto& pixelPackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
// Destination layout is computed from the client-side PACK parameters; only the actual pixel
// rows are written so skip regions of the destination stay untouched.
const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
const SizeT rowPixels = static_cast<SizeT>(packParams.RowLength > 0 ? packParams.RowLength : width);
const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment);
const SizeT imageRows =
applyPackImageParams && packParams.ImageHeight > 0
? static_cast<SizeT>(packParams.ImageHeight)
: static_cast<SizeT>(sliceHeight);
const SizeT dstImageStride = imageRows * dstRowStride;
const SizeT skipImages =
applyPackImageParams ? static_cast<SizeT>(std::max(packParams.SkipImages, 0)) : SizeT{0};
const SizeT dstSkipOffset = skipImages * dstImageStride +
static_cast<SizeT>(std::max(packParams.SkipRows, 0)) * dstRowStride +
static_cast<SizeT>(std::max(packParams.SkipPixels, 0)) * dstPixelBytes;
const SizeT dstRowBytes = static_cast<SizeT>(width) * dstPixelBytes;
const SizeT pboBaseOffset = reinterpret_cast<SizeT>(pixels); // with a PBO, `pixels` is an offset
if (pixelPackBufferObject) {
const SizeT requiredSize = pboBaseOffset + dstSkipOffset +
static_cast<SizeT>(sliceCount - 1) * dstImageStride +
static_cast<SizeT>(sliceHeight - 1) * dstRowStride + dstRowBytes;
if (requiredSize > pixelPackBufferObject->GetSize()) {
MGLOG_E("Readback conversion: pixel pack buffer is too small");
return true;
}
}
const SizeT srcComponentSize = GetReadbackComponentSize(wideType);
const SizeT srcPixelBytes = 4 * srcComponentSize;
Vector<Uint8> convertedRow(dstRowBytes);
for (GLsizei slice = 0; slice < sliceCount; ++slice) {
for (GLsizei row = 0; row < sliceHeight; ++row) {
const SizeT flatRow = static_cast<SizeT>(slice) * static_cast<SizeT>(sliceHeight) +
static_cast<SizeT>(row);
const Uint8* srcRow = wide + flatRow * static_cast<SizeT>(width) * srcPixelBytes;
ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast<SizeT>(width), wideType,
mapping, type);
if (packParams.SwapBytes) {
const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize;
if (groupSize > 1) {
for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) {
std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize);
}
}
}
const SizeT dstOffset = dstSkipOffset + static_cast<SizeT>(slice) * dstImageStride +
static_cast<SizeT>(row) * dstRowStride;
if (pixelPackBufferObject) {
pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes},
pboBaseOffset + dstOffset);
} else {
Memcpy(static_cast<Uint8*>(pixels) + dstOffset, convertedRow.data(), dstRowBytes);
}
}
}
return true;
}
} // namespace ReadbackImpl
} // namespace MobileGL::MG_Backend::DirectGLES
+8
View File
@@ -88,6 +88,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// bytes, dst receives width * GetReadbackDstPixelSize(mapping, type) bytes.
void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType,
const ReadbackChannelMapping& mapping, GLenum type);
// Stores wide RGBA(_INTEGER) rows into the client pointer or the bound PACK pixel buffer,
// honoring the client-side PACK pixel-store parameters (row length, alignment, skips,
// swap-bytes, and - when applyPackImageParams - image height/skip images). Shared by the
// DirectGLES and DirectVulkan readback conversion paths.
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams);
} // namespace ReadbackImpl
namespace PrgramImpl {
@@ -18,6 +18,7 @@
#include "MG_Util/Texture/TextureFormatProcessor.h"
#include <Config.h>
#include <cmath>
#include <cstdlib>
#include <cstring>
@@ -140,6 +141,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case TextureInternalFormat::RGB:
case TextureInternalFormat::RGB8:
return TextureInternalFormat::RGBA8;
// Legacy low-bit-depth formats with no (or rarely supported) native Vulkan
// encoding; a wider normalized fallback keeps at least the required precision.
case TextureInternalFormat::R3G3B2:
case TextureInternalFormat::RGB4:
case TextureInternalFormat::RGB5:
case TextureInternalFormat::RGBA2:
case TextureInternalFormat::RGBA4:
case TextureInternalFormat::RGB5A1:
return TextureInternalFormat::RGBA8;
case TextureInternalFormat::RGB10:
return TextureInternalFormat::RGB10A2;
case TextureInternalFormat::RGB12:
case TextureInternalFormat::RGBA12:
return TextureInternalFormat::RGBA16;
case TextureInternalFormat::SRGB8:
return TextureInternalFormat::SRGB8Alpha8;
case TextureInternalFormat::RGB8Snorm:
@@ -397,8 +412,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (!handle.Handle || (handle.Backend != WindowBackend::Android &&
handle.Backend != WindowBackend::X11 &&
handle.Backend != WindowBackend::MetalLayer)) {
MGLOG_E("DirectVulkan backend only supports Android, X11, and CAMetalLayer native windows");
handle.Backend != WindowBackend::MetalLayer &&
handle.Backend != WindowBackend::Win32)) {
MGLOG_E("DirectVulkan backend only supports Android, X11, CAMetalLayer, and Win32 native windows");
return false;
}
@@ -454,6 +470,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// treat them as signaled/available with zero results from here on.
BumpRendererGeneration();
pVulkanRenderer.reset();
// The reflection cache is file-scope, not renderer-owned; without this the
// deleted programs' reflection strings survive full context teardown.
ClearProgramResourceCaches();
BackendObject::ReleaseEGLResources();
}
@@ -463,6 +482,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// treat them as signaled/available with zero results from here on.
BumpRendererGeneration();
pVulkanRenderer.reset();
// The reflection cache is file-scope, not renderer-owned; without this the
// deleted programs' reflection strings survive full context teardown.
ClearProgramResourceCaches();
}
const RendererInfo& BackendObject_DirectVulkan::GetRendererInfo() const {
@@ -504,10 +526,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample,
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
E_GL_ARB_texture_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug,
E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size};
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size,
E_GL_ARB_explicit_attrib_location};
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
extensions.push_back(E_GL_KHR_shader_subgroup);
}
@@ -613,6 +636,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
}
// Occlusion queries share the handle-based result/delete entries, which must
// exist even when timer queries are disabled.
funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTableInitialized = true;
}
return funcsTable;
@@ -775,6 +807,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
m_dynamicParameters.ViewportBoundsRangeMax = m_vulkanCaps.ViewportBoundsRangeMax;
m_dynamicParameters.ViewportSubpixelBits = m_vulkanCaps.ViewportSubpixelBits;
m_dynamicParameters.MinFragmentInterpolationOffset =
std::isfinite(m_vulkanCaps.MinFragmentInterpolationOffset) &&
m_vulkanCaps.MinFragmentInterpolationOffset <= -0.5f
? m_vulkanCaps.MinFragmentInterpolationOffset
: -0.5f;
m_dynamicParameters.MaxFragmentInterpolationOffset = 0.4375f;
m_dynamicParameters.FragmentInterpolationOffsetBits = 4;
if (m_vulkanCaps.FragmentInterpolationOffsetBits >= 4 &&
std::isfinite(m_vulkanCaps.MaxFragmentInterpolationOffset)) {
const Float requiredMaxOffset =
0.5f - std::ldexp(1.0f, -m_vulkanCaps.FragmentInterpolationOffsetBits);
if (m_vulkanCaps.MaxFragmentInterpolationOffset >= requiredMaxOffset) {
m_dynamicParameters.MaxFragmentInterpolationOffset = m_vulkanCaps.MaxFragmentInterpolationOffset;
m_dynamicParameters.FragmentInterpolationOffsetBits =
m_vulkanCaps.FragmentInterpolationOffsetBits;
}
}
m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines;
m_dynamicParameters.MaxShaderStorageBlockSize =
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
@@ -794,5 +843,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_vulkanCaps.MaxShaderStorageBlockSize,
m_dynamicParameters.MaxShaderStorageBlockSize);
}
switch (m_vulkanCaps.VendorId) {
case 0x5143u: // VK_VENDOR_ID: Qualcomm
m_dynamicParameters.GpuVendor = GpuVendorKind::Qualcomm;
break;
case 0x13B5u: // ARM
m_dynamicParameters.GpuVendor = GpuVendorKind::Arm;
break;
case 0x10DEu: // NVIDIA
m_dynamicParameters.GpuVendor = GpuVendorKind::Nvidia;
break;
case 0x1002u: // AMD
m_dynamicParameters.GpuVendor = GpuVendorKind::Amd;
break;
case 0x8086u: // Intel
m_dynamicParameters.GpuVendor = GpuVendorKind::Intel;
break;
case 0x1010u: // Imagination
m_dynamicParameters.GpuVendor = GpuVendorKind::ImgTec;
break;
case 0x10005u: // Mesa software (lavapipe)
case 0x1AE0u: // Google (SwiftShader)
m_dynamicParameters.GpuVendor = GpuVendorKind::Software;
break;
default:
m_dynamicParameters.GpuVendor = GpuVendorKind::Unknown;
break;
}
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -20,7 +20,8 @@
#include <spirv_reflect.h>
namespace MobileGL::MG_Backend::DirectVulkan {
UniquePtr<VulkanRenderer> pVulkanRenderer = nullptr;
// Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<VulkanRenderer>& pVulkanRenderer = *new UniquePtr<VulkanRenderer>();
namespace {
// Generation of the live VulkanRenderer instance, mirroring
@@ -60,6 +61,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
};
struct ProgramResourceCache {
// Lifetime id of the program the cached reflection belongs to. GL names are
// recycled (IndexGenerator hands freed indices straight back), and a
// recreated program's backendStateVersion restarts at the same small values,
// so the version alone can collide; the never-reused lifetime id makes the
// slot's ownership unambiguous.
Uint64 programLifetimeId = 0;
Uint32 backendStateVersion = 0;
Vector<StorageBlockResource> storageBlocks;
Vector<BufferVariableResource> bufferVariables;
@@ -81,6 +88,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 baseInstance = 0;
};
// Keyed by GL program name so the freed-name reuse in IndexGenerator bounds the
// map at the peak-simultaneous-program high-water mark; each slot's ownership is
// checked against the program's lifetime id before it is served (see
// GetProgramResourceCache). Cleared wholesale at EGL teardown via
// ClearProgramResourceCaches.
UnorderedMap<GLuint, ProgramResourceCache> g_programResourceCaches;
void ClearReadPixelsOutput(GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
@@ -141,13 +153,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ProgramResourceCache& GetProgramResourceCache(const MG_State::GLState::ProgramObject& program) {
auto& cache = g_programResourceCaches[program.GetExternalIndex()];
const Uint64 programLifetimeId = program.GetLifetimeId();
const Uint32 backendStateVersion = program.GetBackendStateVersion();
if (cache.backendStateVersion == backendStateVersion &&
// The lifetime id must match too: a new program that reuses a deleted
// program's name and happens to land on the same backendStateVersion (both
// count from zero) would otherwise be served the dead program's reflection.
if (cache.programLifetimeId == programLifetimeId &&
cache.backendStateVersion == backendStateVersion &&
(!cache.storageBlocks.empty() || !cache.bufferVariables.empty())) {
return cache;
}
cache = {};
cache.programLifetimeId = programLifetimeId;
cache.backendStateVersion = backendStateVersion;
Vector<SpvReflectShaderModule> modules;
@@ -365,6 +383,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
} // namespace
void ClearProgramResourceCaches() {
// Called from EGL teardown while the backend's m_eglStateMutex is held; GL
// calls are serialized in this codebase (contexts migrate threads but never
// run concurrently), so no other thread can be inside the unsynchronized map.
// Live programs in another context self-heal: their entry rebuilds from the
// retained generated SPIR-V on the next resource query.
g_programResourceCaches.clear();
}
GLuint GetShaderStorageBlockIndex(const MG_State::GLState::ProgramObject& program, const String& name) {
auto& cache = GetProgramResourceCache(program);
const auto it = std::find_if(cache.storageBlocks.begin(), cache.storageBlocks.end(),
@@ -1223,10 +1250,76 @@ namespace MobileGL::MG_Backend::DirectVulkan {
pVulkanRenderer->Clear(mask);
}
// Vulkan has no LINE_LOOP topology; rewrite the draw as an indexed LINE_STRIP
// whose synthesized index list revisits the first vertex at the end.
static void DrawLineLoopAsIndexedStrip(const Vector<Uint32>& closedIndices, GLint basevertex) {
DrawIndexedCmd payload{};
payload.mode = GL_LINE_STRIP;
payload.indexBufferView.indexType = GL_UNSIGNED_INT;
payload.indexBufferView.indexByteOffset = reinterpret_cast<SizeT>(closedIndices.data());
payload.indexBufferView.indexByteSize = closedIndices.size() * sizeof(Uint32);
payload.indexBufferView.forceClientMemory = true;
payload.params.indexCount = static_cast<Uint32>(closedIndices.size());
payload.params.instanceCount = 1;
payload.params.vertexOffset = basevertex;
pVulkanRenderer->DrawElements(payload);
}
// Resolve a DrawElements index list (bound element-array buffer or client
// memory) into uint32 values with the loop-closing first index appended.
static Bool BuildClosedLineLoopIndices(GLsizei count, GLenum type, const void* indices,
Vector<Uint32>& outIndices) {
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
if (indexSize == 0 || count < 2) {
return false;
}
const Uint8* indexBytes = nullptr;
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
const auto& indexBufferShared = vao.GetIndexBufferBindingSlot().GetBoundObject();
if (indexBufferShared != nullptr) {
const SizeT offset = reinterpret_cast<SizeT>(indices);
const SizeT bufferSize = indexBufferShared->GetSize();
if (indexBufferShared->MappedData() == nullptr || offset > bufferSize ||
static_cast<SizeT>(count) * indexSize > bufferSize - offset) {
return false;
}
indexBufferShared->SyncPersistentMappedRange();
indexBytes = indexBufferShared->MappedData() + offset;
} else {
indexBytes = static_cast<const Uint8*>(indices);
if (indexBytes == nullptr) {
return false;
}
}
outIndices.resize(static_cast<SizeT>(count) + 1);
for (GLsizei i = 0; i < count; ++i) {
switch (indexSize) {
case 1: outIndices[i] = indexBytes[i]; break;
case 2: outIndices[i] = reinterpret_cast<const Uint16*>(indexBytes)[i]; break;
default: outIndices[i] = reinterpret_cast<const Uint32*>(indexBytes)[i]; break;
}
}
outIndices[count] = outIndices[0];
return true;
}
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawArrays called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawArrays called with null GL context");
if (mode == GL_LINE_LOOP) {
if (count < 2) {
return;
}
Vector<Uint32> closedIndices(static_cast<SizeT>(count) + 1);
for (GLsizei i = 0; i < count; ++i) {
closedIndices[i] = static_cast<Uint32>(first + i);
}
closedIndices[count] = static_cast<Uint32>(first);
DrawLineLoopAsIndexedStrip(closedIndices, 0);
return;
}
DrawCmd payload{};
payload.mode = mode;
payload.params.firstVertex = first;
@@ -1239,6 +1332,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElements called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElements called with null GL context");
if (mode == GL_LINE_LOOP) {
Vector<Uint32> closedIndices;
if (BuildClosedLineLoopIndices(count, type, indices, closedIndices)) {
DrawLineLoopAsIndexedStrip(closedIndices, 0);
}
return;
}
DrawIndexedCmd payload{};
payload.mode = mode;
payload.indexBufferView.indexType = type;
@@ -1307,6 +1408,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::DrawElementsBaseVertex called with null VulkanRenderer");
MOBILEGL_ASSERT(MG_State::pGLContext, "DirectVulkan::DrawElementsBaseVertex called with null GL context");
if (mode == GL_LINE_LOOP) {
Vector<Uint32> closedIndices;
if (BuildClosedLineLoopIndices(count, type, indices, closedIndices)) {
DrawLineLoopAsIndexedStrip(closedIndices, basevertex);
}
return;
}
DrawIndexedCmd payload{};
payload.mode = mode;
payload.indexBufferView.indexType = type;
@@ -1456,8 +1564,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// records are shared (SharedPtr) with the owning pool's pending list,
// so deleting the query while results are still in flight is safe.
struct VulkanTimerQuery {
enum class Kind : Uint8 { Timer, Occlusion, XfbWritten, XfbGenerated };
Kind kind = Kind::Timer;
SharedPtr<VkTimerQueryManager::TimestampRecord> begin;
SharedPtr<VkTimerQueryManager::TimestampRecord> end;
// Kind::Occlusion - pool slots recorded between Begin/End; summed at result time.
Vector<Uint32> occlusionSlots;
// Renderer generation the records were written under (see
// g_rendererGeneration). A stale generation resolves as available
// with a final zero result: the records' pool indices and frame
@@ -1547,6 +1659,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// ever be produced, so resolve with a final 0.
return true;
}
if (query->kind == VulkanTimerQuery::Kind::Occlusion) {
Uint64 samples = 0;
if (!pVulkanRenderer->ResolveOcclusionQueryResult(query->occlusionSlots, samples)) {
return false;
}
query->occlusionSlots.clear(); // slots are recycled by the resolve
*outNanoseconds = samples;
return true;
}
if (query->kind == VulkanTimerQuery::Kind::XfbWritten ||
query->kind == VulkanTimerQuery::Kind::XfbGenerated) {
Uint64 primitives = 0;
if (!pVulkanRenderer->ResolveXfbQueryResult(query->occlusionSlots,
query->kind == VulkanTimerQuery::Kind::XfbGenerated,
primitives)) {
return false;
}
*outNanoseconds = primitives;
return true;
}
// With wait, mirrors ClientWaitSync: a query ended this frame cannot
// complete until Present submits the commands, so the wait refuses to
// block on the current unsubmitted serial. Returning false keeps the
@@ -1579,6 +1711,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
delete static_cast<VulkanTimerQuery*>(handle);
}
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated) {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BeginXfbPrimitivesQuery called with null VulkanRenderer");
if (!pVulkanRenderer->StartXfbQueryCapture(generated ? 1u : 0u)) {
return nullptr;
}
auto* query = new VulkanTimerQuery{};
query->kind = generated ? VulkanTimerQuery::Kind::XfbGenerated : VulkanTimerQuery::Kind::XfbWritten;
query->rendererGeneration = GetRendererGeneration();
return query;
}
void EndXfbPrimitivesQuery(BackendQueryHandle handle) {
auto* query = static_cast<VulkanTimerQuery*>(handle);
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::EndXfbPrimitivesQuery called with null VulkanRenderer");
if (query == nullptr || query->rendererGeneration != GetRendererGeneration()) {
return;
}
pVulkanRenderer->StopXfbQueryCapture(
query->kind == VulkanTimerQuery::Kind::XfbGenerated ? 1u : 0u, query->occlusionSlots);
}
BackendQueryHandle BeginOcclusionQuery() {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::BeginOcclusionQuery called with null VulkanRenderer");
if (!pVulkanRenderer->StartOcclusionQueryCapture()) {
return nullptr;
}
auto* query = new VulkanTimerQuery{};
query->kind = VulkanTimerQuery::Kind::Occlusion;
query->rendererGeneration = GetRendererGeneration();
return query;
}
void EndOcclusionQuery(BackendQueryHandle handle) {
auto* query = static_cast<VulkanTimerQuery*>(handle);
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::EndOcclusionQuery called with null VulkanRenderer");
if (query == nullptr || query->rendererGeneration != GetRendererGeneration()) {
return;
}
pVulkanRenderer->StopOcclusionQueryCapture(query->occlusionSlots);
}
Int64 GetGpuTimestampNs() {
// Vulkan cannot synchronously sample the GPU clock: timestamps only
// exist as vkCmdWriteTimestamp results read back later, and
@@ -12,7 +12,7 @@
#include "Renderer/VulkanRenderer.h"
namespace MobileGL::MG_Backend::DirectVulkan {
extern UniquePtr<VulkanRenderer> pVulkanRenderer;
extern UniquePtr<VulkanRenderer>& pVulkanRenderer;
// Generation of the live VulkanRenderer instance, mirroring DirectGLES's
// g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever
@@ -23,6 +23,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 GetRendererGeneration();
void BumpRendererGeneration();
// Drops every cached program-resource reflection entry (CPU-side strings/vectors
// only, no Vulkan handles). Called at EGL teardown next to the renderer reset;
// safe because GL calls are serialized in this codebase, and any still-live
// program rebuilds its entry from the retained generated SPIR-V on demand.
void ClearProgramResourceCaches();
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
@@ -117,6 +123,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// only while a live renderer exists whose device can actually time.
Bool IsTimerQuerySupported();
BackendQueryHandle BeginTimeElapsedQuery();
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
void EndXfbPrimitivesQuery(BackendQueryHandle query);
BackendQueryHandle BeginOcclusionQuery();
void EndOcclusionQuery(BackendQueryHandle query);
void EndTimeElapsedQuery(BackendQueryHandle query);
BackendQueryHandle QueryCounterTimestamp();
Bool IsQueryResultAvailable(BackendQueryHandle query);
@@ -16,18 +16,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_device = device;
m_commandPool = commandPool;
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE);
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = frameCount;
allocInfo.commandBufferCount = frameCount * 2;
VkResult result = vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data());
if (result != VK_SUCCESS) {
return result;
}
for (Uint32 i = 0; i < frameCount; ++i) {
m_frames[i].commandBuffer = commandBuffers[i];
m_frames[i].preCommandBuffer = commandBuffers[frameCount + i];
}
VkSemaphoreCreateInfo semaphoreInfo{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
@@ -47,9 +48,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void FrameContext::Destroy(VkDevice device, VkCommandPool commandPool) {
const Uint32 frameCount = static_cast<Uint32>(m_frames.size());
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE);
for (Uint32 i = 0; i < frameCount; ++i) {
commandBuffers[i] = m_frames[i].commandBuffer;
commandBuffers[frameCount + i] = m_frames[i].preCommandBuffer;
}
for (Uint32 i = 0; i < frameCount; ++i) {
@@ -60,7 +62,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (auto& frame : m_frames) {
FreeRetiredCommandBuffers(frame);
}
vkFreeCommandBuffers(device, commandPool, frameCount, commandBuffers.data());
vkFreeCommandBuffers(device, commandPool, frameCount * 2, commandBuffers.data());
}
m_frames.clear();
currentFrameIndex = 0;
@@ -87,6 +89,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
currentFrameIndex = (currentFrameIndex + 1) % static_cast<Uint32>(m_frames.size());
GetCurrent().isCommandRecording = false;
GetCurrent().hasCommandBufferRecorded = false;
GetCurrent().isPreCommandRecording = false;
GetCurrent().hasPreCommandBufferRecorded = false;
}
VkCommandBuffer& FrameContext::BeginCommandRecording(VkCommandBufferUsageFlags flags,
@@ -118,6 +122,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
frame.hasCommandBufferRecorded = true;
}
VkCommandBuffer FrameContext::BeginPreCommandRecording() {
auto& frame = GetCurrent();
if (frame.isPreCommandRecording) {
return frame.preCommandBuffer;
}
MOBILEGL_ASSERT(!frame.hasPreCommandBufferRecorded,
"BeginPreCommandRecording: a recorded pre stream is still awaiting submission");
VK_VERIFY(vkResetCommandBuffer(frame.preCommandBuffer, 0), "BeginPreCommandRecording, vkResetCommandBuffer");
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
VK_VERIFY(vkBeginCommandBuffer(frame.preCommandBuffer, &beginInfo),
"BeginPreCommandRecording, vkBeginCommandBuffer");
frame.isPreCommandRecording = true;
return frame.preCommandBuffer;
}
void FrameContext::EndPreCommandRecordingIfOpen() {
auto& frame = GetCurrent();
if (!frame.isPreCommandRecording) {
return;
}
VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "EndPreCommandRecordingIfOpen, vkEndCommandBuffer");
frame.isPreCommandRecording = false;
frame.hasPreCommandBufferRecorded = true;
}
void FrameContext::AbandonPreCommandRecording() {
auto& frame = GetCurrent();
if (frame.isPreCommandRecording) {
VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "AbandonPreCommandRecording, vkEndCommandBuffer");
}
frame.isPreCommandRecording = false;
frame.hasPreCommandBufferRecorded = false;
}
VkResult FrameContext::InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount) {
DestroySwapchainSemaphores(device);
if (swapchainImageCount == 0) {
@@ -150,12 +189,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool FrameContext::TransitionToPresent(VkImage image, VkImageLayout oldLayout, VkImageLayout presentLayout) {
auto& frame = GetCurrent();
if (frame.hasCommandBufferRecorded || frame.isCommandRecording || oldLayout == presentLayout ||
oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) {
if (oldLayout == presentLayout || oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) {
return false;
}
auto& commandBuffer = BeginCommandRecording();
// The barrier belongs in the frame's own recording. Bailing out because
// something was already recorded (the previous behaviour) dropped the
// transition entirely for every frame that never ran a default-framebuffer
// render pass - the only other thing that carries the image to
// PRESENT_SRC_KHR, via that pass's finalLayout - so the swapchain image was
// handed to the WSI still in the layout it was acquired in.
// A closed-but-unsubmitted buffer can only come from a submit that already
// failed (SubmitPendingCommandBuffer leaves the flag set on error), and
// appending to it is illegal while reopening would reset the frame's own
// commands away. The device is gone on that path anyway - stay silent-safe
// rather than trade a lost device for a barrier into a closed buffer.
if (frame.hasCommandBufferRecorded) {
MGLOG_E("TransitionToPresent: command buffer already closed; skipping the present barrier");
return false;
}
// Reopening a recording here would vkResetCommandBuffer this frame's own
// commands away, so append to the open one and let the caller close it.
const Bool openedRecording = !frame.isCommandRecording;
VkCommandBuffer commandBuffer = openedRecording ? BeginCommandRecording() : frame.commandBuffer;
VkImageMemoryBarrier presentBarrier{};
presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
@@ -174,7 +231,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
nullptr, 0, nullptr, 1, &presentBarrier);
EndCommandRecording();
if (openedRecording) {
EndCommandRecording();
}
return true;
}
@@ -182,17 +241,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 swapchainImageIndex) const {
const auto& frame = GetCurrent();
MOBILEGL_ASSERT(!frame.isCommandRecording, "GetSubmitInfo called while command buffer recording is still active");
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
"GetSubmitInfo called while the pre-pass stream is still recording");
AssertValidSwapchainImageIndex(swapchainImageIndex);
SubmitInfoPacket packet{};
packet.waitSemaphore = frame.imageAvailableSemaphore;
packet.signalSemaphore = m_swapchainImageRenderFinishedSemaphores[swapchainImageIndex];
packet.commandBuffer = frame.commandBuffer;
Uint32 commandBufferCount = 0;
// The pre-pass stream executes strictly before the frame's commands.
if (frame.hasPreCommandBufferRecorded) {
packet.commandBuffers[commandBufferCount++] = frame.preCommandBuffer;
}
if (shouldSubmitCommandBuffer) {
packet.commandBuffers[commandBufferCount++] = frame.commandBuffer;
}
packet.submitInfo.waitSemaphoreCount = frame.imageAvailableSemaphoreConsumed ? 0U : 1U;
packet.submitInfo.pWaitSemaphores = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitSemaphore;
packet.submitInfo.pWaitDstStageMask = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitDstStageMask;
packet.submitInfo.commandBufferCount = shouldSubmitCommandBuffer ? 1U : 0U;
packet.submitInfo.pCommandBuffers = shouldSubmitCommandBuffer ? &packet.commandBuffer : nullptr;
packet.submitInfo.commandBufferCount = commandBufferCount;
packet.submitInfo.pCommandBuffers = commandBufferCount > 0 ? packet.commandBuffers : nullptr;
packet.submitInfo.signalSemaphoreCount = 1;
packet.submitInfo.pSignalSemaphores = &packet.signalSemaphore;
return packet;
@@ -227,12 +296,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
result = vkAcquireNextImageKHR(device, swapchain, timeout, frame.imageAvailableSemaphore, acquireFence,
&outImageIndex);
if (result != VK_SUCCESS) {
// VK_SUBOPTIMAL_KHR is a success code: an image *was* acquired and
// imageAvailableSemaphore *will* be signaled. Bailing out on it skipped both
// the consumed-flag reset (leaving a stale "already consumed", so the next
// submit never waited on the pending signal) and the fence reset (leaving
// the slot's fence signaled for the next submit to reuse). Only a genuine
// failure - VK_ERROR_OUT_OF_DATE_KHR and friends, where nothing is acquired
// and nothing is signaled - skips the bookkeeping.
if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
return result;
}
frame.imageAvailableSemaphoreConsumed = false;
return vkResetFences(device, 1, &frame.imageInFlightFence);
const VkResult resetResult = vkResetFences(device, 1, &frame.imageInFlightFence);
// Hand the acquire's own code back so the caller can schedule a rebuild.
return resetResult == VK_SUCCESS ? result : resetResult;
}
Uint32 FrameContext::GetCurrentFrameIndex() const {
@@ -247,12 +325,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_recordingObserver = observer;
}
VkResult FrameContext::RetireCurrentCommandBuffer() {
VkResult FrameContext::RetireCurrentCommandBuffer(Bool retirePreCommandBuffer) {
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE,
"RetireCurrentCommandBuffer requires an initialized FrameContext");
auto& frame = GetCurrent();
MOBILEGL_ASSERT(!frame.isCommandRecording,
"RetireCurrentCommandBuffer called while the command buffer is still recording");
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
"RetireCurrentCommandBuffer called while the pre-pass stream is still recording");
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
@@ -260,11 +340,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
VkCommandBuffer replacement = VK_NULL_HANDLE;
const VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
if (result != VK_SUCCESS) {
return result;
}
frame.retiredCommandBuffers.push_back(frame.commandBuffer);
if (retirePreCommandBuffer) {
VkCommandBuffer preReplacement = VK_NULL_HANDLE;
result = vkAllocateCommandBuffers(m_device, &allocInfo, &preReplacement);
if (result != VK_SUCCESS) {
vkFreeCommandBuffers(m_device, m_commandPool, 1, &replacement);
return result;
}
frame.retiredCommandBuffers.push_back({frame.preCommandBuffer, frame.lastSubmitIndex});
frame.preCommandBuffer = preReplacement;
}
// lastSubmitIndex was just written by the renderer for the submission
// that carried this command buffer.
frame.retiredCommandBuffers.push_back({frame.commandBuffer, frame.lastSubmitIndex});
frame.commandBuffer = replacement;
return VK_SUCCESS;
}
@@ -274,12 +366,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return;
}
if (m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE) {
vkFreeCommandBuffers(m_device, m_commandPool, static_cast<Uint32>(frame.retiredCommandBuffers.size()),
frame.retiredCommandBuffers.data());
for (const auto& retired : frame.retiredCommandBuffers) {
vkFreeCommandBuffers(m_device, m_commandPool, 1, &retired.commandBuffer);
}
}
frame.retiredCommandBuffers.clear();
}
void FrameContext::FreeRetiredCommandBuffersCompletedUpTo(Uint64 completedSubmitIndex) {
if (m_device == VK_NULL_HANDLE || m_commandPool == VK_NULL_HANDLE) {
return;
}
for (auto& frame : m_frames) {
// Retired buffers are appended in submit order, so the completed
// ones form a prefix.
SizeT completedCount = 0;
while (completedCount < frame.retiredCommandBuffers.size() &&
frame.retiredCommandBuffers[completedCount].submitIndex <= completedSubmitIndex) {
vkFreeCommandBuffers(m_device, m_commandPool, 1,
&frame.retiredCommandBuffers[completedCount].commandBuffer);
++completedCount;
}
if (completedCount > 0) {
frame.retiredCommandBuffers.erase(frame.retiredCommandBuffers.begin(),
frame.retiredCommandBuffers.begin() + completedCount);
}
}
}
void FrameContext::FreeAllRetiredCommandBuffers() {
for (auto& frame : m_frames) {
FreeRetiredCommandBuffers(frame);
}
}
void FrameContext::AssertValidFrameIndex(Uint32 frameIndex) const {
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "FrameContext index out of range");
}
@@ -29,7 +29,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
VkSemaphore signalSemaphore = VK_NULL_HANDLE;
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
// [0] = pre-pass command buffer (when recorded), then the frame
// command buffer; submitInfo.pCommandBuffers points here.
VkCommandBuffer commandBuffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO};
};
@@ -40,17 +42,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPresentInfoKHR presentInfo{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
};
// A command buffer submitted mid-frame (FlushPendingCommands), tagged
// with the submit-tracker index it was submitted under so it can be
// freed as soon as that submission is observed complete - without
// waiting for the slot's fence to be waited again (present-less flush
// loops never wait it).
struct RetiredCommandBuffer {
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
Uint64 submitIndex = 0;
};
struct FrameData {
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
// Pre-pass work stream: out-of-pass commands (deferred clear
// materialization, sampled-layout transitions) for resources the
// frame's recording has not touched yet. Submitted immediately
// BEFORE commandBuffer in the same vkQueueSubmit, so recording
// into it never has to split the frame's active render pass.
VkCommandBuffer preCommandBuffer = VK_NULL_HANDLE;
VkSemaphore imageAvailableSemaphore = VK_NULL_HANDLE;
VkFence imageInFlightFence = VK_NULL_HANDLE;
Bool isCommandRecording = false;
Bool hasCommandBufferRecorded = false;
Bool isPreCommandRecording = false;
Bool hasPreCommandBufferRecorded = false;
Bool imageAvailableSemaphoreConsumed = false;
// Command buffers submitted mid-frame (FlushPendingCommands) whose
// execution is only known complete once this slot's fence has been
// waited again; freed at that point.
Vector<VkCommandBuffer> retiredCommandBuffers;
// Command buffers submitted mid-frame (FlushPendingCommands),
// appended in submit order; freed once their submission is known
// complete (fence wait or completion poll).
Vector<RetiredCommandBuffer> retiredCommandBuffers;
// Submit-tracker index of this slot's most recent queue submission
// (written by the renderer at submit time).
Uint64 lastSubmitIndex = 0;
@@ -67,6 +87,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkCommandBuffer& BeginCommandRecording(VkCommandBufferUsageFlags flags = 0,
const VkCommandBufferInheritanceInfo* pInheritanceInfo = nullptr);
void EndCommandRecording();
// Lazily opens the pre-pass work stream (see FrameData::preCommandBuffer).
VkCommandBuffer BeginPreCommandRecording();
// Closes the pre stream if open, marking it for submission ahead of the
// frame command buffer. Safe to call when it never opened.
void EndPreCommandRecordingIfOpen();
// Drops an in-progress or recorded-but-unsubmitted pre stream (dropped
// frame recordings, swapchain recreation).
void AbandonPreCommandRecording();
VkResult InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount);
void DestroySwapchainSemaphores(VkDevice device);
Bool TransitionToPresent(VkImage image, VkImageLayout oldLayout,
@@ -79,8 +107,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Parks the current (already ended and submitted) command buffer on the
// slot's retired list and installs a freshly allocated one, so recording
// can restart while the submitted buffer is still executing. Retired
// buffers are freed after the slot's fence is next waited.
VkResult RetireCurrentCommandBuffer();
// buffers are freed after the slot's fence is next waited, or as soon
// as their submission is observed complete.
VkResult RetireCurrentCommandBuffer(Bool retirePreCommandBuffer = false);
// Frees every retired command buffer whose tagged submission index is
// known complete. Driven by the renderer's submit tracker on completion
// events (fence waits and non-blocking polls), so present-less flush
// loops reclaim their buffers without any extra wait.
void FreeRetiredCommandBuffersCompletedUpTo(Uint64 completedSubmitIndex);
// Frees every slot's retired command buffers. Only valid when the
// caller has proven every queue submission complete.
void FreeAllRetiredCommandBuffers();
Uint32 GetCurrentFrameIndex() const;
Uint32 GetFrameCount() const;
@@ -8,6 +8,8 @@
#include "PipelineFactory.h"
#include <algorithm>
namespace MobileGL::MG_Backend::DirectVulkan {
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
switch (topology) {
@@ -108,6 +110,81 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"vkCreatePipelineCache");
}
// Must be called once, before any pipeline is created: the flag is not part of the
// pipeline hash, so flipping it mid-life would serve cached pipelines built under the
// old value.
void PipelineFactory::SetSuppressBlendedDepthWrite(Bool enabled) {
s_suppressBlendedDepthWrite = enabled;
}
Bool PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(MG_Config::QuirkOverride quirkOverride,
Uint32 vendorId) {
static constexpr Uint32 kVendorIdQualcomm = 0x5143;
switch (quirkOverride) {
case MG_Config::QuirkOverride::ForceOn:
return true;
case MG_Config::QuirkOverride::ForceOff:
return false;
case MG_Config::QuirkOverride::Auto:
default:
return vendorId == kVendorIdQualcomm;
}
}
namespace {
// MIN/MAX extremum blending: the signature of a depth-bounds accumulation pass
// (MC 26.3 OIT writes vec4(-linD, linD, deviceZ, 0) under GL_MAX while writing
// depth for its equality chain). MIN/MAX ignore blend factors per the Vulkan spec.
//
// Deliberately the ONLY shape stripped. A quirk should touch as little unrelated
// content as possible, and a trace sweep of every fixture showed the wider
// alternatives all cost more than they fix:
// - additive ONE+ONE with a depth write matched zero draws of the 26.3 chain
// (its transmittance/accumulate passes disable depth writes themselves) - the
// only real content it caught was harmless additive glow effects (Create);
// - sorted-transparency "over" blends (SRC_ALPHA-style) are order-dependent,
// drawn once per surface, and rely on their depth writes for occlusion;
// - separate-alpha accumulation over an over-blending color channel has no
// known pairing with a depth-equality chain (color channel only, see tests).
// If a future workload pairs another blend shape with an equality chain, widen
// this with that evidence in hand rather than pre-emptively.
Bool IsAccumulationBlend(const VkPipelineColorBlendAttachmentState& attachment) {
return attachment.colorBlendOp == VK_BLEND_OP_MIN ||
attachment.colorBlendOp == VK_BLEND_OP_MAX;
}
} // namespace
Bool PipelineFactory::ShouldSuppressDepthWrite(const PipelineCreatePayload& payload) {
if (!payload.depthWriteEnable) {
return false;
}
// A shader that assigns gl_FragDepth supplies depth itself rather than taking the
// pipeline's interpolated Z, so a driver that varies the vertex position math
// between pipelines cannot desynchronize it. (A gl_FragDepth = gl_FragCoord.z
// passthrough is the exception that stays exposed; no known content pairs one with
// an equality chain, and 26.3's composite is a genuine computed-depth writer.)
if (payload.fragmentReplacesDepth) {
return false;
}
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
const VkPipelineColorBlendAttachmentState& attachment = payload.colorBlendAttachments[i];
if (attachment.blendEnable != VK_TRUE) {
continue;
}
// All color writes masked: blending is moot (depth-prepass pattern that left
// GL_BLEND enabled); stripping the depth write would delete the whole prepass.
if (attachment.colorWriteMask == 0) {
continue;
}
// Any attachment qualifies, not just attachment 0: the 26.3 transmittance pass
// accumulates into a 2-target MRT and must stay stripped.
if (IsAccumulationBlend(attachment)) {
return true;
}
}
return false;
}
PipelineFactory::~PipelineFactory() {
DestroyAll();
if (m_pipelineCache != VK_NULL_HANDLE) {
@@ -152,6 +229,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
if (payload.colorAttachmentCount > 0) {
XXHASH_VERIFY(XXH64_update(
m_hashState,
@@ -165,23 +244,108 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const HashType hash = ComputeHash(payload);
auto it = m_cache.find(hash);
if (it != m_cache.end()) {
return it->second;
it->second.lastUsedFrame = m_frameCounter;
return it->second.pipeline;
}
VkPipeline pipeline = CreatePipeline(payload);
m_cache.emplace(hash, pipeline);
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, payload.renderPass,
m_frameCounter});
return pipeline;
}
void PipelineFactory::DestroyAll() {
for (auto& pair : m_cache) {
if (pair.second != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, pair.second, nullptr);
if (pair.second.pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, pair.second.pipeline, nullptr);
}
}
m_cache.clear();
}
Uint32 PipelineFactory::OnFrameBoundary() {
++m_frameCounter;
// Sweep cadence and retire age mirror VkRenderPassManager::OnPresent: an entry
// idle for more than kRetireAgeFrames frame boundaries cannot be referenced by
// any in-flight command buffer (frames-in-flight <= MOBILEGL_MAGMA_FRAMESINFLIGHT),
// so immediate vkDestroyPipeline is safe. The caller must drop its "last
// pipeline" memo when this returns non-zero: the memo can return a cached
// handle without touching this cache, so an evicted pipeline may still be
// memoized (present-less flush loops never reset the memo per frame).
constexpr Uint64 kSweepInterval = 256;
constexpr Uint64 kRetireAgeFrames = 1024;
if ((m_frameCounter % kSweepInterval) != 0) {
return 0;
}
Uint32 evicted = 0;
for (auto it = m_cache.begin(); it != m_cache.end();) {
if (m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
if (it->second.pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
}
it = m_cache.erase(it);
++evicted;
} else {
++it;
}
}
if (evicted > 0) {
MGLOG_D("PipelineFactory::OnFrameBoundary: evicted %u idle pipelines (%zu remain)", evicted,
m_cache.size());
}
return evicted;
}
Uint32 PipelineFactory::EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses) {
if (renderPasses.empty() || m_cache.empty()) {
return 0;
}
// Sorted-batch membership test keeps a mass eviction (shader-pack switch,
// dimension exit) at one O(cache * log batch) scan instead of one full scan
// per dying pass.
Vector<VkRenderPass> sortedPasses = renderPasses;
std::sort(sortedPasses.begin(), sortedPasses.end());
Uint32 evicted = 0;
for (auto it = m_cache.begin(); it != m_cache.end();) {
if (std::binary_search(sortedPasses.begin(), sortedPasses.end(), it->second.renderPass)) {
if (it->second.pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
}
it = m_cache.erase(it);
++evicted;
} else {
++it;
}
}
if (evicted > 0) {
MGLOG_D("PipelineFactory::EvictByRenderPasses: evicted %u pipelines for %zu destroyed render passes",
evicted, sortedPasses.size());
}
return evicted;
}
Uint32 PipelineFactory::EvictByProgramHash(HashType programHash) {
Uint32 evicted = 0;
for (auto it = m_cache.begin(); it != m_cache.end();) {
if (it->second.programHash == programHash) {
if (it->second.pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
}
it = m_cache.erase(it);
++evicted;
} else {
++it;
}
}
if (evicted > 0) {
MGLOG_D("PipelineFactory::EvictByProgramHash: evicted %u pipelines for program hash 0x%llx",
evicted, static_cast<unsigned long long>(programHash));
}
return evicted;
}
VkPipeline PipelineFactory::CreatePipeline(const PipelineCreatePayload& payload) const {
MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty");
MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null");
@@ -258,6 +422,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
colorAttachments[i] = payload.colorBlendAttachments[i];
}
// Suppress depth writes on accumulation-blended pipelines when the active driver
// cannot keep vertex positions invariant across the pipelines of a multi-pass
// depth-equality chain (see SetSuppressBlendedDepthWrite). The decision is narrowed
// in ShouldSuppressDepthWrite: sorted-transparency "over" blends (vanilla MC water),
// gl_FragDepth writers, and masked-out attachments keep their depth writes.
// This bakes the decision into the pipeline, which only works because depth write is
// static state here - adding VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE to kDynamicStates
// would let the record-time value override it and silently disable the quirk.
if (s_suppressBlendedDepthWrite && ShouldSuppressDepthWrite(payload)) {
depthStencil.depthWriteEnable = VK_FALSE;
}
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
blend.logicOpEnable = payload.logicOpEnable ? VK_TRUE : VK_FALSE;
blend.logicOp = payload.logicOp;
@@ -49,6 +49,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkStencilOp backStencilPassOp = VK_STENCIL_OP_KEEP;
VkStencilOp backStencilDepthFailOp = VK_STENCIL_OP_KEEP;
VkCompareOp backStencilCompareOp = VK_COMPARE_OP_ALWAYS;
// The fragment module writes gl_FragDepth (SPIR-V DepthReplacing); exempts the
// pipeline from the blended depth-write quirk (see ShouldSuppressDepthWrite).
Bool fragmentReplacesDepth = false;
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
@@ -62,13 +65,69 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
void DestroyAll();
// Frame boundary hook: ages the pipeline cache and destroys long-unused entries
// (their command buffers retired many frames ago), mirroring
// VkRenderPassManager::OnPresent's sweep. Returns the number of pipelines
// destroyed so the caller can drop any memoized VkPipeline handle.
Uint32 OnFrameBoundary();
// Destroys every cached pipeline hashed on one of `renderPasses`. Only safe
// when the caller guarantees GPU idleness for them - the render-pass manager
// calls this (via the renderer) for passes its own >1024-boundary-idle sweep
// just evicted, and a pipeline hashed on those handles is only ever bound by
// draws that also hit the render-pass entries. Also closes the handle-recycling
// hazard: a recycled VkRenderPass value must never serve a stale pipeline.
// Batched: one cache scan regardless of how many passes died in the sweep.
// Returns the number destroyed (callers invalidate memos when non-zero).
Uint32 EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses);
// Destroys every cached pipeline built from the program with content hash
// `programHash`. Called from the ProgramFactory eviction path, which proves the
// same >1024-boundary idleness (the program's pipelines are only bound by draws
// that stamp its factory entry). Returns the number destroyed.
Uint32 EvictByProgramHash(HashType programHash);
// Driver quirk: suppress depth writes on accumulation-blended pipelines. Multi-pass
// depth-equality rendering (a blended prepass writes depth that later passes re-test
// with an equality-inclusive compare on the re-rasterized geometry) requires
// cross-pipeline position invariance that some mobile compilers do not provide, even
// with the SPIR-V Invariant decoration; whole primitives then drop out of the later
// passes. Only MIN/MAX extremum blends are stripped - the signature of such a
// chain's depth-bounds pass (MC 26.3 OIT), and per a fixture-wide trace sweep the
// only depth-writing shape the chain actually uses - so every other blend
// (sorted-transparency "over" like vanilla MC water, additive glows, ...) keeps
// its depth writes. Set at renderer initialization based on the active driver.
static void SetSuppressBlendedDepthWrite(Bool enabled);
static Bool IsSuppressBlendedDepthWriteEnabled() { return s_suppressBlendedDepthWrite; }
// Device gate for the quirk: ForceOn/ForceOff bypass detection, Auto enables it on
// the known-affected vendor (Qualcomm).
static Bool ShouldSuppressBlendedDepthWriteForDevice(MG_Config::QuirkOverride quirkOverride,
Uint32 vendorId);
// Pure per-pipeline strip decision (exempts gl_FragDepth writers, masked-out and
// non-accumulation blends); combined with the device flag in CreatePipeline. Static
// and payload-only so tests can pin the contract without a VkDevice.
static Bool ShouldSuppressDepthWrite(const PipelineCreatePayload& payload);
private:
struct PipelineCacheEntry {
VkPipeline pipeline = VK_NULL_HANDLE;
// The hashed inputs the eviction paths key on: programHash ties the entry to
// its ProgramFactory entry, renderPass records the exact handle the hash
// folded in (the hash is one-way, so targeted eviction needs them verbatim).
HashType programHash = 0;
VkRenderPass renderPass = VK_NULL_HANDLE;
// Frame-boundary counter value of the last GetOrCreatePipeline hit; drives
// cache eviction (see OnFrameBoundary).
Uint64 lastUsedFrame = 0;
};
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
VkDevice m_device = VK_NULL_HANDLE;
const VulkanRendererConfig& m_config;
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
UnorderedMap<HashType, VkPipeline> m_cache;
UnorderedMap<HashType, PipelineCacheEntry> m_cache;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameCounter = 0;
static inline XXH64_state_t* m_hashState = XXH64_createState();
static inline Bool s_suppressBlendedDepthWrite = false;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -312,34 +312,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return targetEnv;
}
// Cheap raw-word scan for an `OpDecorate <id> BuiltIn InstanceIndex` decoration. Used
// only to decide whether to warn when shaderDrawParameters is unavailable; a false
// negative merely suppresses a diagnostic.
Bool SpirvDeclaresInstanceIndexBuiltin(const Vector<Uint>& spirv) {
constexpr Uint32 kSpirvMagicNumber = 0x07230203u;
constexpr SizeT kHeaderWordCount = 5;
if (spirv.size() <= kHeaderWordCount || spirv[0] != kSpirvMagicNumber) {
return false;
}
SizeT wordIndex = kHeaderWordCount;
while (wordIndex < spirv.size()) {
const Uint32 firstWord = spirv[wordIndex];
const Uint32 wordCount = firstWord >> 16;
const auto opcode = static_cast<spv::Op>(firstWord & 0xffffu);
if (wordCount == 0 || wordIndex + wordCount > spirv.size()) {
break;
}
if (opcode == spv::Op::OpDecorate && wordCount >= 4 &&
static_cast<spv::Decoration>(spirv[wordIndex + 2]) == spv::Decoration::BuiltIn &&
static_cast<spv::BuiltIn>(spirv[wordIndex + 3]) == spv::BuiltIn::InstanceIndex) {
return true;
}
wordIndex += wordCount;
}
return false;
}
Bool IsInterfaceVariableStaticallyUsed(const Vector<Uint>& spirv, Uint32 spirvId) {
if (spirv.empty() || spirvId == 0) {
return false;
@@ -951,11 +923,403 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ProgramFactory::CompileOptionFlags m_transformFlags;
};
// Decorates the module's captured varyings for VK_EXT_transform_feedback:
// user outputs get XfbBuffer/XfbStride/Offset directly; a captured
// gl_Position (a gl_PerVertex member) is mirrored into a dedicated output
// variable copied before every OpReturn, BEFORE the position fixup runs,
// so the captured value is the shader's own (pre-remap) gl_Position.
class XfbCaptureDecoratePass final : public spvtools::opt::Pass {
public:
struct CapturedVarying {
std::string name;
Uint32 bufferIndex = 0;
Uint32 offsetBytes = 0;
};
const char* name() const override { return "mobilegl-xfb-capture-decorate"; }
XfbCaptureDecoratePass(Vector<CapturedVarying> varyings, Vector<Uint32> strides)
: m_varyings(Move(varyings)), m_strides(Move(strides)) {}
Status Process() override {
using namespace spvtools::opt;
if (m_varyings.empty()) return Status::SuccessWithoutChange;
auto entryPointIter = get_module()->entry_points().begin();
if (entryPointIter == get_module()->entry_points().end()) return Status::SuccessWithoutChange;
spvtools::opt::Instruction* entryPoint = &*entryPointIter;
const Uint32 entryFunctionId = entryPoint->GetSingleWordInOperand(1);
// Name -> result id map from the debug section.
std::unordered_map<std::string, Uint32> idsByName;
for (auto& debugInst : get_module()->debugs2()) {
if (debugInst.opcode() != spv::Op::OpName) continue;
idsByName[debugInst.GetInOperand(1).AsString()] = debugInst.GetSingleWordInOperand(0);
}
auto* decorationManager = context()->get_decoration_mgr();
const auto decorateForXfb = [&](Uint32 targetId, Uint32 bufferIndex, Uint32 offsetBytes) {
const Uint32 stride = bufferIndex < m_strides.size() ? m_strides[bufferIndex] : 0;
decorationManager->AddDecorationVal(targetId, static_cast<Uint32>(spv::Decoration::XfbBuffer),
bufferIndex);
decorationManager->AddDecorationVal(targetId, static_cast<Uint32>(spv::Decoration::XfbStride),
stride);
decorationManager->AddDecorationVal(targetId, static_cast<Uint32>(spv::Decoration::Offset),
offsetBytes);
};
Bool modified = false;
Bool needsPositionMirror = false;
Uint32 positionBufferIndex = 0;
Uint32 positionOffset = 0;
for (const auto& varying : m_varyings) {
if (varying.name == "gl_Position") {
needsPositionMirror = true;
positionBufferIndex = varying.bufferIndex;
positionOffset = varying.offsetBytes;
continue;
}
const auto idIt = idsByName.find(varying.name);
if (idIt == idsByName.end()) {
MGLOG_E("XfbCaptureDecoratePass: no SPIR-V variable named '%s'", varying.name.c_str());
continue;
}
decorateForXfb(idIt->second, varying.bufferIndex, varying.offsetBytes);
modified = true;
}
if (needsPositionMirror) {
modified |= MirrorPositionForCapture(entryFunctionId, *entryPoint, positionBufferIndex,
positionOffset, decorateForXfb);
}
if (!modified) return Status::SuccessWithoutChange;
context()->AddCapability(spv::Capability::TransformFeedback);
{
auto executionMode = MakeUnique<spvtools::opt::Instruction>(
context(), spv::Op::OpExecutionMode, 0, 0,
std::initializer_list<spvtools::opt::Operand>{
{SPV_OPERAND_TYPE_ID, {entryPoint->GetSingleWordInOperand(1)}},
{SPV_OPERAND_TYPE_EXECUTION_MODE, {static_cast<Uint32>(spv::ExecutionMode::Xfb)}}});
get_module()->AddExecutionMode(Move(executionMode));
}
context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
private:
template <typename DecorateFn>
Bool MirrorPositionForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint,
Uint32 bufferIndex, Uint32 offsetBytes, const DecorateFn& decorateForXfb) {
const Uint32 entryPointModel = entryPoint.GetSingleWordInOperand(0);
using namespace spvtools::opt;
PositionTargetInfo target{};
if (!FindPositionTarget(context(), &target)) {
MGLOG_E("XfbCaptureDecoratePass: gl_Position capture requested but no position output found");
return false;
}
if (!target.isMember) {
// Standalone gl_Position variable: decorate it directly.
decorateForXfb(target.variableId, bufferIndex, offsetBytes);
return true;
}
auto* typeManager = context()->get_type_mgr();
const Uint32 mirrorPointerTypeId =
typeManager->FindPointerToType(target.vectorTypeId, spv::StorageClass::Output);
if (mirrorPointerTypeId == 0) return false;
const Uint32 mirrorVariableId = context()->TakeNextId();
auto mirrorVariable = MakeUnique<Instruction>(
context(), spv::Op::OpVariable, mirrorPointerTypeId, mirrorVariableId,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast<Uint32>(spv::StorageClass::Output)}}});
get_module()->AddGlobalValue(Move(mirrorVariable));
// A free output location: past every explicitly decorated output.
Uint32 mirrorLocation = 0;
for (auto& annotation : get_module()->annotations()) {
if (annotation.opcode() != spv::Op::OpDecorate ||
annotation.GetSingleWordInOperand(1) != static_cast<Uint32>(spv::Decoration::Location)) {
continue;
}
mirrorLocation = std::max(mirrorLocation, annotation.GetSingleWordInOperand(2) + 1);
}
auto* decorationManager = context()->get_decoration_mgr();
decorationManager->AddDecorationVal(mirrorVariableId,
static_cast<Uint32>(spv::Decoration::Location), mirrorLocation);
decorateForXfb(mirrorVariableId, bufferIndex, offsetBytes);
entryPoint.AddOperand({SPV_OPERAND_TYPE_ID, {mirrorVariableId}});
auto* function = context()->GetFunction(entryFunctionId);
if (function == nullptr) return false;
const auto model = static_cast<spv::ExecutionModel>(entryPointModel);
Bool injected = false;
for (auto& block : *function) {
for (auto instIter = block.begin(); instIter != block.end(); ++instIter) {
// Geometry stages capture per emitted vertex; other stages at return.
const Bool isInjectionSite =
model == spv::ExecutionModel::Geometry
? instIter->opcode() == spv::Op::OpEmitVertex
: instIter->opcode() == spv::Op::OpReturn;
if (!isInjectionSite) continue;
InstructionBuilder builder(context(), &*instIter, IRContext::kAnalysisNone);
const Uint32 memberIndexId = builder.GetUintConstantId(target.memberIndex);
auto* access =
builder.AddAccessChain(target.vectorPtrTypeId, target.variableId, {memberIndexId});
if (access == nullptr) return injected;
auto* value = builder.AddLoad(target.vectorTypeId, access->result_id());
if (value == nullptr) return injected;
builder.AddStore(mirrorVariableId, value->result_id());
injected = true;
}
}
return injected;
}
Vector<CapturedVarying> m_varyings;
Vector<Uint32> m_strides;
};
// Adreno 650 (driver 512.502) faults the GPU on an implicit-LOD sample of a full-screen
// colour render target: the texture unit's derivative path reads outside the image's
// allocation even though the sampler clamps LOD to 0 and the mapping is 1:1. MobileGL's
// own default-framebuffer blit shader works around it with textureLod, but an
// application's shader (Minecraft's blit.fsh is `texture(InSampler, texCoord)`) cannot be
// edited - so rewrite the sample at the SPIR-V level instead.
//
// The rewrite is only requested for draws whose every sampler binding is clamped to one
// mip level, where explicit LOD 0 is exactly what the implicit form must already produce:
// lambda' = clamp(lambda + bias, minLod, maxLod) with minLod = maxLod = 0. Bias and MinLod
// operands are therefore dropped rather than translated.
class ForceExplicitLod0SamplePass final : public spvtools::opt::Pass {
public:
const char* name() const override { return "force-explicit-lod0-sample"; }
Status Process() override {
Bool isFragment = false;
for (auto& entryPoint : get_module()->entry_points()) {
if (entryPoint.opcode() != spv::Op::OpEntryPoint) continue;
if (static_cast<spv::ExecutionModel>(entryPoint.GetSingleWordInOperand(0)) ==
spv::ExecutionModel::Fragment) {
isFragment = true;
break;
}
}
if (!isFragment) return Status::SuccessWithoutChange;
// Plan first, mutate second. Materializing the LOD constant is itself a module
// change, so it must not happen unless at least one rewrite is going to follow -
// otherwise the pass would grow the binary while reporting SuccessWithoutChange.
Vector<RewritePlan> plans;
for (auto& function : *get_module()) {
for (auto& block : function) {
for (auto& inst : block) {
RewritePlan plan{};
if (PlanRewrite(&inst, plan)) plans.push_back(Move(plan));
}
}
}
if (plans.empty()) return Status::SuccessWithoutChange;
const Uint32 zeroId = GetFloatZeroId();
if (zeroId == 0) return Status::SuccessWithoutChange;
for (auto& plan : plans) {
plan.operands.push_back({SPV_OPERAND_TYPE_ID, {zeroId}});
for (auto& operand : plan.trailingOperands) {
plan.operands.push_back(operand);
}
plan.instruction->SetOpcode(plan.opcode);
plan.instruction->SetInOperands(Move(plan.operands));
}
// Opcodes and operand lists changed underneath every cached analysis.
context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
private:
struct RewritePlan {
spvtools::opt::Instruction* instruction = nullptr;
spv::Op opcode = spv::Op::OpNop;
// Everything up to and including the Image Operands mask; the Lod id and the
// trailing operand values are appended once the constant exists.
Vector<spvtools::opt::Operand> operands;
Vector<spvtools::opt::Operand> trailingOperands;
};
// Image Operands bits that may accompany an implicit-LOD sample, in the canonical
// ascending order SPIR-V requires the operand values to appear in.
static constexpr Uint32 kBias = 0x1;
static constexpr Uint32 kLod = 0x2;
static constexpr Uint32 kGrad = 0x4;
static constexpr Uint32 kConstOffset = 0x8;
static constexpr Uint32 kOffset = 0x10;
static constexpr Uint32 kConstOffsets = 0x20;
static constexpr Uint32 kSample = 0x40;
static constexpr Uint32 kMinLod = 0x80;
static constexpr Uint32 kKnownMask = 0xFF;
Uint32 GetFloatZeroId() {
// Reuse a 32-bit float type already in the module; a shader that samples always has
// one, and looking it up avoids depending on type-creation API details.
Uint32 floatTypeId = 0;
for (auto& inst : get_module()->types_values()) {
if (inst.opcode() == spv::Op::OpTypeFloat && inst.NumInOperands() >= 1 &&
inst.GetSingleWordInOperand(0) == 32) {
floatTypeId = inst.result_id();
break;
}
}
if (floatTypeId == 0) return 0;
const auto* floatType = context()->get_type_mgr()->GetType(floatTypeId);
if (floatType == nullptr) return 0;
const auto zeroBits = std::bit_cast<Uint32>(0.0f);
const auto* zeroConst = context()->get_constant_mgr()->GetConstant(floatType, {zeroBits});
if (zeroConst == nullptr) return 0;
auto* zeroInst = context()->get_constant_mgr()->GetDefiningInstruction(zeroConst);
return zeroInst != nullptr ? zeroInst->result_id() : 0;
}
static Bool MapOpcode(spv::Op op, spv::Op& outOpcode, Uint32& outFixedOperandCount) {
switch (op) {
case spv::Op::OpImageSampleImplicitLod:
outOpcode = spv::Op::OpImageSampleExplicitLod;
outFixedOperandCount = 2; // sampled image, coordinate
return true;
case spv::Op::OpImageSampleProjImplicitLod:
outOpcode = spv::Op::OpImageSampleProjExplicitLod;
outFixedOperandCount = 2;
return true;
case spv::Op::OpImageSampleDrefImplicitLod:
outOpcode = spv::Op::OpImageSampleDrefExplicitLod;
outFixedOperandCount = 3; // sampled image, coordinate, Dref
return true;
case spv::Op::OpImageSampleProjDrefImplicitLod:
outOpcode = spv::Op::OpImageSampleProjDrefExplicitLod;
outFixedOperandCount = 3;
return true;
default:
return false;
}
}
static Bool PlanRewrite(spvtools::opt::Instruction* inst, RewritePlan& outPlan) {
spv::Op newOpcode = spv::Op::OpNop;
Uint32 fixedCount = 0;
if (!MapOpcode(inst->opcode(), newOpcode, fixedCount)) return false;
if (inst->NumInOperands() < fixedCount) return false;
Uint32 mask = 0;
Uint32 next = fixedCount;
if (inst->NumInOperands() > fixedCount) {
mask = inst->GetSingleWordInOperand(fixedCount);
next = fixedCount + 1;
}
// An operand this pass does not model would be silently reordered or dropped, and
// Grad cannot legally accompany an implicit-LOD sample: leave such an instruction be.
if ((mask & ~kKnownMask) != 0 || (mask & kGrad) != 0) return false;
Vector<spvtools::opt::Operand> fixedOperands;
fixedOperands.reserve(fixedCount + 1);
for (Uint32 i = 0; i < fixedCount; ++i) {
fixedOperands.push_back(inst->GetInOperand(i));
}
// Collect the surviving operand values in the same ascending-bit order they were
// encoded in, so the rebuilt list stays canonical.
Uint32 keptMask = kLod;
Vector<spvtools::opt::Operand> keptOperands;
static constexpr Uint32 kOrderedBits[] = {kBias, kLod, kGrad, kConstOffset,
kOffset, kConstOffsets, kSample, kMinLod};
for (const Uint32 bit : kOrderedBits) {
if ((mask & bit) == 0) continue;
if (next >= inst->NumInOperands()) return false;
const spvtools::opt::Operand value = inst->GetInOperand(next++);
// Bias and MinLod only shift a lambda that is already clamped to 0, and any
// original Lod is replaced by the constant the caller appends.
if (bit == kBias || bit == kMinLod || bit == kLod) continue;
keptMask |= bit;
keptOperands.push_back(value);
}
fixedOperands.push_back({SPV_OPERAND_TYPE_IMAGE, {keptMask}});
outPlan.instruction = inst;
outPlan.opcode = newOpcode;
outPlan.operands = Move(fixedOperands);
outPlan.trailingOperands = Move(keptOperands);
return true;
}
};
spvtools::Optimizer::PassToken CreateForceExplicitLod0SamplePass() {
return spvtools::Optimizer::PassToken(MakeUnique<ForceExplicitLod0SamplePass>());
}
Bool TransformSpirvForExplicitLod0Sampling(const Vector<Uint>& input, Vector<Uint>& output) {
if (input.empty()) {
output.clear();
return true;
}
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
spvtools::OptimizerOptions options;
// Matches the position-fix pass: this build of spirv-tools asserts rather than
// reporting, so validation stays off in the shipping path.
options.set_run_validator(false);
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
const char* message) {
MGLOG_E("Vulkan: explicit-LOD0 pass: %s", message != nullptr ? message : "");
});
optimizer.RegisterPass(CreateForceExplicitLod0SamplePass());
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
if (!success) {
MGLOG_E("Vulkan: explicit-LOD0 sampling pass failed; keeping the original module");
output = input;
}
return success;
}
spvtools::Optimizer::PassToken CreateGlToVulkanPositionFixPass(
ProgramFactory::CompileOptionFlags transformFlags) {
return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags));
}
Bool TransformSpirvForXfbCapture(const Vector<Uint>& input, Vector<Uint>& output,
const MG_State::GLState::ProgramObject& program) {
if (input.empty()) {
output.clear();
return true;
}
Vector<XfbCaptureDecoratePass::CapturedVarying> varyings;
varyings.reserve(program.GetTransformFeedbackVaryingCount());
for (const auto& varying : program.GetTransformFeedbackVaryings()) {
varyings.push_back({varying.name, varying.bufferIndex, varying.offsetBytes});
}
Vector<Uint32> strides;
strides.reserve(program.GetTransformFeedbackBufferCount());
for (SizeT i = 0; i < program.GetTransformFeedbackBufferCount(); ++i) {
strides.push_back(program.GetTransformFeedbackStride(static_cast<Uint32>(i)));
}
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
spvtools::OptimizerOptions options;
options.set_run_validator(false);
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
const char* message) {
MGLOG_E("Vulkan: xfb capture pass: %s", message != nullptr ? message : "");
});
optimizer.RegisterPass(spvtools::Optimizer::PassToken(
MakeUnique<XfbCaptureDecoratePass>(Move(varyings), Move(strides))));
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
if (!success) {
MGLOG_E("Vulkan: xfb capture decoration pass failed; keeping the original module");
output = input;
}
return success;
}
Bool TransformSpirvForVulkanPositionFix(const Vector<Uint>& input, Vector<Uint>& output,
ProgramFactory::CompileOptionFlags transformFlags) {
if (input.empty()) {
@@ -1122,9 +1486,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (auto* binding : bindings) {
MOBILEGL_ASSERT(binding != nullptr, "ProgramFactory: null descriptor binding reflection record");
const auto kind = ReflectDescriptorTypeToBindingKind(binding->descriptor_type);
MOBILEGL_ASSERT(binding->count == 1,
"ProgramFactory: descriptor arrays are unsupported (name='%s' count=%u)",
binding->name ? binding->name : "<null>", binding->count);
// UBO instance arrays (uniform Block {...} b[N];) occupy one binding with
// descriptorCount = N; other descriptor arrays stay unsupported and must
// fail program creation cleanly rather than continue with corrupt state.
if (binding->count != 1 && kind != ProgramFactory::DescriptorBindingKind::UniformBufferDynamic) {
MGLOG_E("ProgramFactory: descriptor arrays are unsupported for this descriptor "
"kind (name='%s' count=%u type=%d)",
binding->name ? binding->name : "<null>", binding->count,
static_cast<Int>(binding->descriptor_type));
destroyReflectModules();
return false;
}
DescriptorKey key{};
key.kind = kind;
@@ -1218,6 +1590,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
} // namespace
// A shader that assigns gl_FragDepth (SPIR-V DepthReplacing) supplies depth itself
// instead of taking the pipeline's interpolated Z, so a driver that varies the vertex
// position math between pipelines cannot desynchronize it; the blended depth-write
// quirk therefore leaves it alone (see PipelineFactory::ShouldSuppressDepthWrite).
Bool ProgramFactory::ReflectedFragmentReplacesDepth(const SpvReflectShaderModule& reflectModule) {
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex];
for (Uint32 modeIndex = 0; modeIndex < entryPoint.execution_mode_count; ++modeIndex) {
if (entryPoint.execution_modes[modeIndex] == SpvExecutionModeDepthReplacing) {
return true;
}
}
}
return false;
}
// glslang's relaxed-Vulkan mode maps GL's gl_InstanceID onto the InstanceIndex builtin.
// Without shaderDrawParameters there is no gl_BaseInstance to subtract, so such a shader
// cannot be corrected and instanced draws with a non-zero baseInstance misrender; this
// detects the case so the user gets one warning instead of silent corruption.
Bool ProgramFactory::ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule) {
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex];
for (Uint32 variableIndex = 0; variableIndex < entryPoint.input_variable_count; ++variableIndex) {
const SpvReflectInterfaceVariable* variable = entryPoint.input_variables[variableIndex];
if (variable != nullptr &&
(variable->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0 &&
variable->built_in == SpvBuiltInInstanceIndex) {
return true;
}
}
}
return false;
}
VkShaderStageFlagBits ProgramFactory::ToVkStage(ShaderStage stage) {
switch (stage) {
case ShaderStage::Vertex:
@@ -1465,6 +1872,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue;
}
if (!m_shaderDrawParametersEnabled && ReflectedReadsInstanceIndexBuiltin(reflectModule)) {
static Bool s_warnedInstanceIndexUnsupported = false;
if (!s_warnedInstanceIndexUnsupported) {
s_warnedInstanceIndexUnsupported = true;
MGLOG_W("ProgramFactory: shaderDrawParameters is unavailable; gl_InstanceID cannot be "
"rebased and instanced draws with a non-zero baseInstance may render incorrectly");
}
}
uint32_t inputCount = 0;
SpvReflectResult reflectResult = spvReflectEnumerateInputVariables(&reflectModule, &inputCount, nullptr);
MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS,
@@ -1512,6 +1928,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkProgramObject& entry) const {
entry.activeFragmentOutputLocationMask = 0;
entry.fragmentOutputTypes.fill(0);
entry.fragmentReplacesDepth = false;
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Fragment) {
@@ -1530,9 +1947,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"ProgramFactory::ReflectFragmentOutputs: failed to create reflection module (result=%d)",
static_cast<Int>(createResult));
if (createResult != SPV_REFLECT_RESULT_SUCCESS) {
// Fail toward the exemption: stripping a genuine gl_FragDepth writer would
// corrupt its depth output outright, while wrongly exempting an accumulation
// pass merely reverts that one program to the pre-quirk behavior.
entry.fragmentReplacesDepth = true;
continue;
}
entry.fragmentReplacesDepth = ReflectedFragmentReplacesDepth(reflectModule);
uint32_t outputCount = 0;
SpvReflectResult reflectResult = spvReflectEnumerateOutputVariables(&reflectModule, &outputCount, nullptr);
MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS,
@@ -1590,6 +2013,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.storageBlockIndexByBinding.assign(m_maxBindings, -1);
entry.globalUboBinding = -1;
entry.dynamicBindings.clear();
entry.bindingDescriptorCounts.assign(m_maxBindings, 1);
entry.arrayedUniformBlockIndicesByBinding.clear();
// Use SpvcSession (Reflection mode) to reflect all SPIR-V modules in a single pass per module
for (const auto& module : spirv) {
@@ -1605,6 +2030,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"ProgramFactory::ReflectLayout: failed to create reflection module (result=%d)",
static_cast<Int>(createReflectResult));
// Descriptor counts per binding (UBO instance arrays reflect count > 1).
UnorderedMap<Uint32, Uint32> descriptorCountByBinding;
{
uint32_t countProbe = 0;
if (spvReflectEnumerateDescriptorBindings(&reflectModule, &countProbe, nullptr) ==
SPV_REFLECT_RESULT_SUCCESS &&
countProbe > 0) {
Vector<SpvReflectDescriptorBinding*> probeBindings(countProbe);
if (spvReflectEnumerateDescriptorBindings(&reflectModule, &countProbe,
probeBindings.data()) ==
SPV_REFLECT_RESULT_SUCCESS) {
for (const auto* probeBinding : probeBindings) {
if (probeBinding != nullptr) {
descriptorCountByBinding[probeBinding->binding] =
std::max<Uint32>(1, probeBinding->count);
}
}
}
}
}
// Reflect uniform buffers
auto ubos = session.GetShaderInterface(SPVC_RESOURCE_TYPE_UNIFORM_BUFFER);
for (const auto& ubo : ubos) {
@@ -1630,9 +2076,69 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue;
}
const Uint blockIndex = program.GetUniformBlockIndex(ubo.name.c_str());
if (blockIndex == 0xFFFFFFFFu) {
MGLOG_D("ProgramFactory::ReflectLayout: skipping inactive UBO '%s' at binding %u",
const auto countIt = descriptorCountByBinding.find(binding);
const Uint32 descriptorCount =
countIt != descriptorCountByBinding.end() ? countIt->second : 1u;
if (descriptorCount <= 1) {
const Uint blockIndex = program.GetUniformBlockIndex(ubo.name.c_str());
if (blockIndex == 0xFFFFFFFFu) {
MGLOG_D("ProgramFactory::ReflectLayout: skipping inactive UBO '%s' at binding %u",
ubo.name.c_str(), binding);
continue;
}
MOBILEGL_ASSERT(entry.bindingKinds[binding] == DescriptorBindingKind::None ||
entry.bindingKinds[binding] == DescriptorBindingKind::UniformBufferDynamic,
"ProgramFactory::ReflectLayout: descriptor binding %u has conflicting kinds for UBO '%s'",
binding, ubo.name.c_str());
entry.bindingKinds[binding] = DescriptorBindingKind::UniformBufferDynamic;
MOBILEGL_ASSERT(entry.globalUboBinding != static_cast<Int>(binding),
"ProgramFactory::ReflectLayout: regular UBO '%s' collides with global UBO binding %u",
ubo.name.c_str(), binding);
MOBILEGL_ASSERT(entry.uniformBlockIndexByBinding[binding] < 0 ||
entry.uniformBlockIndexByBinding[binding] == static_cast<Int>(blockIndex),
"ProgramFactory::ReflectLayout: descriptor binding %u maps to conflicting UBO blocks (%d vs %u)",
binding, entry.uniformBlockIndexByBinding[binding], blockIndex);
entry.uniformBlockIndexByBinding[binding] = static_cast<Int>(blockIndex);
continue;
}
// UBO instance array: one binding, descriptorCount elements. GL exposes each
// element as its own active block named "Name[i]"; map every element to its
// GL block index so the descriptor write can gather per-element buffer ranges.
if (descriptorCount > m_maxBindings) {
MGLOG_E("ProgramFactory::ReflectLayout: UBO array '%s' count %u exceeds maxBindings=%u; "
"leaving binding %u unmapped",
ubo.name.c_str(), descriptorCount, m_maxBindings, binding);
continue;
}
Vector<Int> elementBlockIndices;
elementBlockIndices.reserve(descriptorCount);
for (Uint32 element = 0; element < descriptorCount; ++element) {
String elementName = ubo.name + "[" + std::to_string(element) + "]";
Uint elementBlockIndex = program.GetUniformBlockIndex(elementName.c_str());
if (elementBlockIndex == 0xFFFFFFFFu && element == 0) {
// Some frontends report the first element under the bare block name.
elementBlockIndex = program.GetUniformBlockIndex(ubo.name.c_str());
}
if (elementBlockIndex == 0xFFFFFFFFu) {
// Degrade rather than corrupt: reuse element 0's block if we have one,
// otherwise give up on the binding (same observable behavior as an
// inactive block: wrong values, but no crash).
MGLOG_E("ProgramFactory::ReflectLayout: UBO array '%s' element %u has no active "
"GL uniform block",
ubo.name.c_str(), element);
if (!elementBlockIndices.empty()) {
elementBlockIndex = static_cast<Uint>(elementBlockIndices.front());
} else {
break;
}
}
elementBlockIndices.push_back(static_cast<Int>(elementBlockIndex));
}
if (elementBlockIndices.size() != descriptorCount) {
MGLOG_E("ProgramFactory::ReflectLayout: skipping unresolved UBO array '%s' at binding %u",
ubo.name.c_str(), binding);
continue;
}
@@ -1642,14 +2148,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"ProgramFactory::ReflectLayout: descriptor binding %u has conflicting kinds for UBO '%s'",
binding, ubo.name.c_str());
entry.bindingKinds[binding] = DescriptorBindingKind::UniformBufferDynamic;
MOBILEGL_ASSERT(entry.globalUboBinding != static_cast<Int>(binding),
"ProgramFactory::ReflectLayout: regular UBO '%s' collides with global UBO binding %u",
ubo.name.c_str(), binding);
MOBILEGL_ASSERT(entry.uniformBlockIndexByBinding[binding] < 0 ||
entry.uniformBlockIndexByBinding[binding] == static_cast<Int>(blockIndex),
"ProgramFactory::ReflectLayout: descriptor binding %u maps to conflicting UBO blocks (%d vs %u)",
binding, entry.uniformBlockIndexByBinding[binding], blockIndex);
entry.uniformBlockIndexByBinding[binding] = static_cast<Int>(blockIndex);
entry.bindingDescriptorCounts[binding] = static_cast<Uint16>(descriptorCount);
entry.uniformBlockIndexByBinding[binding] = elementBlockIndices[0];
entry.arrayedUniformBlockIndicesByBinding[binding] = Move(elementBlockIndices);
}
// Reflect sampled images, storage images, samplerBuffer uniforms, and SSBOs.
@@ -1796,7 +2297,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDescriptorSetLayoutBinding layoutBinding{};
layoutBinding.binding = binding;
layoutBinding.descriptorCount = 1;
layoutBinding.descriptorCount = entry.bindingDescriptorCounts[binding];
layoutBinding.stageFlags = VK_SHADER_STAGE_ALL;
layoutBinding.pImmutableSamplers = nullptr;
if (kind == DescriptorBindingKind::UniformBufferDynamic) {
@@ -1808,6 +2309,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
} else if (kind == DescriptorBindingKind::StorageImage) {
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
entry.hasStorageImages = true;
} else {
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
}
@@ -1840,11 +2342,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
auto it = m_cache.find(hash);
if (it != m_cache.end()) {
// Every draw/dispatch funnels through this lookup (the renderer memos only
// skip re-hashing, never the factory lookup), so an actively-used entry is
// stamped at least once per frame boundary and can never be aged out while
// any in-flight command buffer still references it.
it->second.lastUsedFrame = m_frameCounter;
return it->second;
}
auto& entry = m_cache[hash];
entry.hash = hash;
entry.lastUsedFrame = m_frameCounter;
auto& shaders = program.GetAttachedShaders();
auto& spirv = program.GetGeneratedSpirv();
Vector<Vector<Uint>> moduleSpirvs(spirv.size());
@@ -1857,33 +2365,66 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Apply position fixup if needed
if (fixupStage != ShaderStage::Unknown && shaders[i] && shaders[i]->GetShaderStage() == fixupStage) {
TransformSpirvForVulkanPositionFix(spv, moduleSpirvs[i], flags);
const Vector<Uint>* fixupInput = &spv;
Vector<Uint> xfbSpirv;
if ((flags & ProgramFactory::CompileOptionBit::XfbCapture) &&
program.GetTransformFeedbackVaryingCount() > 0) {
// Decorate BEFORE the position fixup so a captured gl_Position
// mirror copies the shader's own (pre-remap) value.
if (TransformSpirvForXfbCapture(spv, xfbSpirv, program)) {
fixupInput = &xfbSpirv;
}
}
TransformSpirvForVulkanPositionFix(*fixupInput, moduleSpirvs[i], flags);
} else {
moduleSpirvs[i] = spv;
}
if ((flags & ProgramFactory::CompileOptionBit::ExplicitLod0Sampling) && shaders[i] &&
shaders[i]->GetShaderStage() == ShaderStage::Fragment) {
Vector<Uint> explicitLodSpirv;
if (TransformSpirvForExplicitLod0Sampling(moduleSpirvs[i], explicitLodSpirv)) {
moduleSpirvs[i] = Move(explicitLodSpirv);
}
}
// GL apps depend on cross-program position invariance for multi-pass equality
// depth tests (MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the
// depth its own first pass wrote); decorate Position outputs Invariant so
// per-pipeline compilers cannot vary the position math between passes.
{
Vector<Uint> invariantSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::DecoratePositionInvariantForVulkan(
moduleSpirvs[i], invariantSpirv)) {
moduleSpirvs[i] = std::move(invariantSpirv);
} else {
// The pass round-trips through SPIRV-Tools IR, so an unparseable module
// fails open and keeps the undecorated words - which silently reinstates
// the multi-pass invariance bug rather than breaking anything loudly.
MGLOG_E("ProgramFactory: position-invariant decoration failed for program %u; "
"keeping the original module - multi-pass depth-equality chains "
"(e.g. MC 26.3 OIT clouds) may drop primitives on this device",
program.GetExternalIndex());
}
}
// glslang's relaxed-Vulkan mode aliases GL's zero-based gl_InstanceID to Vulkan's
// gl_InstanceIndex, which wrongly includes the draw's baseInstance. Rebase vertex-stage
// loads to (InstanceIndex - BaseInstance) so shaders observe GL semantics. Reflection
// below runs on the rebased words so the added BaseInstance builtin stays consistent.
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex) {
if (m_shaderDrawParametersEnabled) {
Vector<Uint> rebasedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i],
rebasedSpirv)) {
moduleSpirvs[i] = std::move(rebasedSpirv);
} else {
MGLOG_E("ProgramFactory: failed to rebase gl_InstanceID for program %u; "
"instanced draws with a non-zero baseInstance may render incorrectly",
program.GetExternalIndex());
}
} else if (SpirvDeclaresInstanceIndexBuiltin(moduleSpirvs[i])) {
static Bool s_warnedInstanceIndexUnsupported = false;
if (!s_warnedInstanceIndexUnsupported) {
s_warnedInstanceIndexUnsupported = true;
MGLOG_W("ProgramFactory: shaderDrawParameters is unavailable; gl_InstanceID cannot be "
"rebased and instanced draws with a non-zero baseInstance may render incorrectly");
}
// The unsupported-device counterpart of this rebase (warning when a shader reads
// the builtin but shaderDrawParameters is missing) rides along with
// ReflectVertexInputs, which already reflects this stage.
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex &&
m_shaderDrawParametersEnabled) {
Vector<Uint> rebasedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i],
rebasedSpirv)) {
moduleSpirvs[i] = std::move(rebasedSpirv);
} else {
MGLOG_E("ProgramFactory: failed to rebase gl_InstanceID for program %u; "
"instanced draws with a non-zero baseInstance may render incorrectly",
program.GetExternalIndex());
}
}
@@ -1943,4 +2484,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return entry;
}
void ProgramFactory::OnFrameBoundary() {
++m_frameCounter;
// Sweep cadence and retire age mirror VkRenderPassManager::OnPresent: an entry
// idle for more than kRetireAgeFrames frame boundaries cannot be referenced by
// any in-flight command buffer (frames-in-flight <= MOBILEGL_MAGMA_FRAMESINFLIGHT),
// so its shader modules and layouts are destroyed immediately - no deferred-
// destroy machinery needed. Eviction is content-based, never tied to
// glDeleteProgram: the cache is content-hash-shared across GL programs, so a
// delete-driven erase could free an entry another live program still resolves.
// An evicted entry self-heals - the frontend program keeps its generated
// SPIR-V, so the next GetOrCreateProgram rebuilds it (this also covers the
// renderer's internal blit/depth-mipmap programs).
constexpr Uint64 kSweepInterval = 256;
constexpr Uint64 kRetireAgeFrames = 1024;
if ((m_frameCounter % kSweepInterval) != 0) {
return;
}
for (auto it = m_cache.begin(); it != m_cache.end();) {
if (m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
const HashType hash = it->first;
const VkDescriptorSetLayout descriptorSetLayout = it->second.descriptorSetLayout;
MGLOG_D("ProgramFactory::OnFrameBoundary: evicting idle program entry hash=0x%llx",
static_cast<unsigned long long>(hash));
// erase runs ~VkProgramObject (modules/layouts destroyed); notify after
// so an observer never observes a half-destroyed entry through a lookup.
// Observers only need the handle values to purge their keyed caches.
it = m_cache.erase(it);
if (m_evictionObserver != nullptr) {
m_evictionObserver->OnProgramEvicted(hash, descriptorSetLayout);
}
} else {
++it;
}
}
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -42,6 +42,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
SurfaceRotate90 = 1 << 2,
SurfaceRotate180 = 1 << 3,
SurfaceRotate270 = 1 << 4,
// Rewrites the fragment stage's implicit-LOD image samples to explicit LOD 0.
// Only ever set for a draw whose every sampler binding is clamped to a single mip
// level, which makes the two forms produce identical texels (the implicit lambda is
// clamped into [minLod, maxLod] = [0, 0] regardless of derivatives or bias).
ExplicitLod0Sampling = 1 << 5,
// Decorates the last vertex-processing stage's captured varyings with
// XfbBuffer/XfbStride/Offset (VK_EXT_transform_feedback). Set only for draws
// recorded while GL transform feedback is active, so plain draws keep the
// undecorated variant.
XfbCapture = 1 << 6,
};
using CompileOptionFlags = Flags<CompileOptionBit>;
using HashType = Uint64;
@@ -59,6 +69,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<DescriptorBindingKind> bindingKinds;
Vector<Uint32> dynamicBindings;
Vector<Int> uniformBlockIndexByBinding;
// Descriptor count per binding (1 except for UBO instance arrays, which occupy one
// binding with descriptorCount = N).
Vector<Uint16> bindingDescriptorCounts;
// Per-element GL uniform block indices for arrayed UBO bindings (count > 1);
// element 0 of a non-arrayed binding stays in uniformBlockIndexByBinding.
UnorderedMap<Uint32, Vector<Int>> arrayedUniformBlockIndicesByBinding;
Vector<String> samplerNameByBinding;
Vector<Int> samplerUniformLocationByBinding;
Vector<TextureTarget> samplerTextureTargetByBinding;
@@ -67,6 +83,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<Bool> storageImageUsesBindingFormatByBinding;
Vector<String> storageBlockNameByBinding;
Vector<Int> storageBlockIndexByBinding;
// Set once during ReflectLayout so the per-draw path can skip the whole
// storage-image preparation for the overwhelming majority of programs.
Bool hasStorageImages = false;
Int globalUboBinding = -1;
Uint32 activeVertexInputLocationMask = 0;
Array<GLenum, kMaxVertexInputLocations> vertexInputTypes{};
@@ -75,6 +94,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ShaderStage rasterizationProducerStage = ShaderStage::Unknown;
Uint32 producerOutputComponentCount = 0;
Uint32 fragmentInputComponentCount = 0;
// The fragment module declares the DepthReplacing execution mode (writes
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
Bool fragmentReplacesDepth = false;
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
// cache eviction (see OnFrameBoundary).
Uint64 lastUsedFrame = 0;
static inline VkDevice s_device = VK_NULL_HANDLE;
@@ -90,6 +116,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
bindingKinds = std::move(other.bindingKinds);
dynamicBindings = std::move(other.dynamicBindings);
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
arrayedUniformBlockIndicesByBinding = std::move(other.arrayedUniformBlockIndicesByBinding);
samplerNameByBinding = std::move(other.samplerNameByBinding);
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
@@ -99,6 +127,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
std::move(other.storageImageUsesBindingFormatByBinding);
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
hasStorageImages = other.hasStorageImages;
globalUboBinding = other.globalUboBinding;
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
vertexInputTypes = other.vertexInputTypes;
@@ -107,15 +136,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
rasterizationProducerStage = other.rasterizationProducerStage;
producerOutputComponentCount = other.producerOutputComponentCount;
fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth;
lastUsedFrame = other.lastUsedFrame;
other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE;
other.pipelineLayout = VK_NULL_HANDLE;
other.hasStorageImages = false;
other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0;
other.rasterizationProducerStage = ShaderStage::Unknown;
other.producerOutputComponentCount = 0;
other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false;
other.lastUsedFrame = 0;
}
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
if (this == &other) {
@@ -130,6 +164,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
bindingKinds = std::move(other.bindingKinds);
dynamicBindings = std::move(other.dynamicBindings);
uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding);
bindingDescriptorCounts = std::move(other.bindingDescriptorCounts);
arrayedUniformBlockIndicesByBinding = std::move(other.arrayedUniformBlockIndicesByBinding);
samplerNameByBinding = std::move(other.samplerNameByBinding);
samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding);
samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding);
@@ -139,6 +175,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
std::move(other.storageImageUsesBindingFormatByBinding);
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
hasStorageImages = other.hasStorageImages;
globalUboBinding = other.globalUboBinding;
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
vertexInputTypes = other.vertexInputTypes;
@@ -147,15 +184,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
rasterizationProducerStage = other.rasterizationProducerStage;
producerOutputComponentCount = other.producerOutputComponentCount;
fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth;
lastUsedFrame = other.lastUsedFrame;
other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE;
other.pipelineLayout = VK_NULL_HANDLE;
other.hasStorageImages = false;
other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0;
other.rasterizationProducerStage = ShaderStage::Unknown;
other.producerOutputComponentCount = 0;
other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false;
other.lastUsedFrame = 0;
return *this;
}
@@ -185,6 +227,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
};
// Notified when the OnFrameBoundary sweep destroys an aged-out cache entry,
// carrying the entry's content hash and the VkDescriptorSetLayout it owned.
// Dependent caches (compute pipelines, PipelineFactory entries, UniformManager's
// per-layout descriptor sets) must purge in the same step: after vkDestroy the
// layout handle value may be recycled for an unrelated layout, and the program
// hash may be re-inserted by a later rebuild of the same content.
class IEvictionObserver {
public:
virtual ~IEvictionObserver() = default;
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
};
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
Bool shaderDrawParametersEnabled = false,
Bool unformattedFloatStorageImagesEnabled = false)
@@ -200,9 +254,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkProgramObject& GetOrCreateProgram(
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
// Observer may be null (no notifications). Not owned.
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
// Frame boundary hook: ages the program cache and evicts long-unused entries
// (their command buffers retired many frames ago), mirroring
// VkRenderPassManager::OnPresent's sweep.
void OnFrameBoundary();
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
// True when any entry point declares the DepthReplacing execution mode, i.e. the
// shader assigns gl_FragDepth. Exposed so the blended depth-write quirk's exemption
// can be pinned by tests. A false negative loses the exemption, so such a shader is
// stripped conservatively and forfeits its depth write.
static Bool ReflectedFragmentReplacesDepth(const SpvReflectShaderModule& reflectModule);
// True when an entry point reads the InstanceIndex builtin. Only gates a diagnostic:
// without shaderDrawParameters such a shader cannot have gl_InstanceID rebased.
static Bool ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule);
private:
struct ProgramLookupCache {
@@ -233,6 +302,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
Bool m_unformattedFloatStorageImagesEnabled = false;
mutable ProgramLookupCache m_lastLookup;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameCounter = 0;
IEvictionObserver* m_evictionObserver = nullptr;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -247,6 +247,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_surfaceFormat = {createInfo.imageFormat, createInfo.imageColorSpace};
m_extent = createInfo.imageExtent;
// The surface-space extent this swapchain was built from, i.e. before the
// quarter-turn swap above. Out-of-date checks must compare in THIS space: comparing a
// freshly queried currentExtent against the swapped m_extent flips axes every rotation
// and makes the comparison alternate forever.
m_surfaceExtent = defaultFramebufferExtent;
m_preTransform = createInfo.preTransform;
VK_VERIFY(vkCreateSwapchainKHR(device, &createInfo, nullptr, &m_swapchain));
@@ -257,6 +262,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_images.resize(imageCount, VK_NULL_HANDLE);
VK_VERIFY(vkGetSwapchainImagesKHR(device, m_swapchain, &imageCount, m_images.data()));
m_imageLayouts.assign(imageCount, VK_IMAGE_LAYOUT_UNDEFINED);
// Fresh swapchain images hold garbage until a render pass stores into them.
m_imageContentDefined.assign(imageCount, false);
m_depthStencilContentDefined.assign(imageCount, false);
CreateImageViews(device);
CreateDepthStencilResources(device, physicalDevice);
@@ -428,9 +436,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_images.clear();
m_imageLayouts.clear();
m_imageContentDefined.clear();
m_depthStencilContentDefined.clear();
m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
}
Bool SwapchainObject::IsImageContentDefined(Uint32 index) const {
MOBILEGL_ASSERT(index < m_imageContentDefined.size(), "Swapchain image content index out of range");
return m_imageContentDefined[index];
}
void SwapchainObject::SetImageContentDefined(Uint32 index, Bool defined) {
MOBILEGL_ASSERT(index < m_imageContentDefined.size(), "Swapchain image content index out of range");
m_imageContentDefined[index] = defined;
}
Bool SwapchainObject::IsDepthStencilContentDefined(Uint32 index) const {
MOBILEGL_ASSERT(index < m_depthStencilContentDefined.size(),
"Swapchain depth/stencil content index out of range");
return m_depthStencilContentDefined[index];
}
void SwapchainObject::SetDepthStencilContentDefined(Uint32 index, Bool defined) {
MOBILEGL_ASSERT(index < m_depthStencilContentDefined.size(),
"Swapchain depth/stencil content index out of range");
m_depthStencilContentDefined[index] = defined;
}
void SwapchainObject::SetAllDepthStencilContentUndefined() {
for (SizeT i = 0; i < m_depthStencilContentDefined.size(); ++i) {
m_depthStencilContentDefined[i] = false;
}
}
VkImage SwapchainObject::GetImage(Uint32 index) const {
MOBILEGL_ASSERT(index < m_images.size(), "Swapchain image index out of range");
return m_images[index];
@@ -35,6 +35,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSwapchainKHR GetHandle() const { return m_swapchain; }
const VkSurfaceFormatKHR& GetSurfaceFormat() const { return m_surfaceFormat; }
VkExtent2D GetExtent() const { return m_extent; }
// Surface-space extent (before the pre-rotation quarter-turn swap) this swapchain was
// created from - the value to compare a freshly queried currentExtent against.
VkExtent2D GetSurfaceExtent() const { return m_surfaceExtent; }
VkSurfaceTransformFlagBitsKHR GetPreTransform() const { return m_preTransform; }
const Vector<VkImage>& GetImages() const { return m_images; }
const Vector<VkImageView>& GetImageViews() const { return m_imageViews; }
@@ -49,6 +52,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void SetImageLayout(Uint32 index, VkImageLayout layout);
SizeT GetImageCount() const { return m_images.size(); }
// EGL content-validity tracking for the default framebuffer. A color
// buffer's content is undefined once its image has been presented
// (EGL_BUFFER_DESTROYED swap behaviour, the implementation default),
// and every ancillary (depth/stencil) buffer's content is undefined
// after ANY swap regardless of swap behaviour (EGL 1.5 §3.10.1). The
// render-pass manager turns an undefined attachment's tile load into
// LOAD_OP_DONT_CARE. Flags start false (a fresh swapchain image holds
// garbage) and a render pass storing into an attachment sets it back
// to defined.
Bool IsImageContentDefined(Uint32 index) const;
void SetImageContentDefined(Uint32 index, Bool defined);
Bool IsDepthStencilContentDefined(Uint32 index) const;
void SetDepthStencilContentDefined(Uint32 index, Bool defined);
void SetAllDepthStencilContentUndefined();
private:
void CreateImageViews(VkDevice device);
void CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice);
@@ -63,6 +81,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSwapchainKHR m_swapchain = VK_NULL_HANDLE;
VkSurfaceFormatKHR m_surfaceFormat{};
VkExtent2D m_extent{};
VkExtent2D m_surfaceExtent{};
VkSurfaceTransformFlagBitsKHR m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
Vector<VkImage> m_images;
Vector<VkImageView> m_imageViews;
@@ -73,5 +92,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<VkDeviceMemory> m_depthStencilImageMemories;
Vector<VkImageView> m_depthStencilImageViews;
Vector<VkImageLayout> m_depthStencilImageLayouts;
Vector<Bool> m_imageContentDefined;
Vector<Bool> m_depthStencilContentDefined;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -18,6 +18,7 @@
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include "MG_Util/Metrics/TextureMetrics.h"
#include <Config.h>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -204,6 +205,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// The frame's descriptor sets are recycled above, so last frame's reuse target
// is gone: start the per-draw descriptor-reuse cache fresh this frame.
m_hasLastDescriptor = false;
m_lastBindValid = false;
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
// reuse cannot outlive a single frame (see SamplerResolveMemo).
for (auto& memo : m_samplerResolveMemo) {
@@ -211,6 +213,40 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
void UniformManager::OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout) {
SizeT purgedSets = 0;
for (auto& frame : m_frames) {
const auto it = frame.descriptorSetCacheByLayout.find(descriptorSetLayout);
if (it == frame.descriptorSetCacheByLayout.end()) {
continue;
}
// Free the sets back to their pools and credit the bucket accounting, so
// program churn recycles pool capacity instead of abandoning the slots.
// GPU-safe: the layout only dies after >1024 idle frame boundaries, so no
// in-flight command buffer references these sets.
for (const auto& cached : it->second.sets) {
if (cached.set == VK_NULL_HANDLE) {
continue;
}
vkFreeDescriptorSets(m_device, cached.pool, 1, &cached.set);
const auto bucket = std::find_if(
frame.descriptorPools.begin(), frame.descriptorPools.end(),
[&cached](const DescriptorPoolBucket& candidate) { return candidate.handle == cached.pool; });
if (bucket != frame.descriptorPools.end() && bucket->allocatedSets > 0) {
--bucket->allocatedSets;
}
}
purgedSets += it->second.sets.size();
frame.descriptorSetCacheByLayout.erase(it);
}
if (purgedSets > 0) {
// The per-draw reuse memo folds the layout handle into its signature; drop
// it so a recycled handle value cannot revive a purged set mid-frame.
m_hasLastDescriptor = false;
MGLOG_D("UniformDescriptorBinder: freed %zu descriptor sets for destroyed layout", purgedSets);
}
}
Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
@@ -298,8 +334,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// descriptor valid.
const Bool forceNearestFiltering = numericDomain == SamplerNumericDomain::SignedInteger ||
numericDomain == SamplerNumericDomain::UnsignedInteger;
const VkFormat sampledViewFormat =
VkTextureManager::ResolveSampledImageViewFormat(resource->format, numericDomain);
SamplerResolveMemo* viewFormatMemo =
binding < m_samplerResolveMemo.size() ? &m_samplerResolveMemo[binding] : nullptr;
VkFormat sampledViewFormat;
if (viewFormatMemo != nullptr && viewFormatMemo->viewFormatValid &&
viewFormatMemo->viewFormatSource == resource->format &&
viewFormatMemo->viewFormatDomain == numericDomain) {
sampledViewFormat = viewFormatMemo->viewFormat;
} else {
sampledViewFormat =
VkTextureManager::ResolveSampledImageViewFormat(resource->format, numericDomain);
if (viewFormatMemo != nullptr) {
viewFormatMemo->viewFormatSource = resource->format;
viewFormatMemo->viewFormatDomain = numericDomain;
viewFormatMemo->viewFormat = sampledViewFormat;
viewFormatMemo->viewFormatValid = true;
}
}
if (sampledViewFormat == VK_FORMAT_UNDEFINED) {
MGLOG_E("ResolveSamplerDescriptor: no compatible sampled view for binding=%u ('%s') "
"textureId=%d imageFormat=%d numericDomain=%d",
@@ -307,8 +358,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static_cast<Int>(resource->format), static_cast<Int>(numericDomain));
return false;
}
// No reinterpretation requested: bind the depth-or-color aspect view the sync above
// already produced instead of re-entering GetOrCreateSampledImageView's sync path.
const VkImageView sampledImageView =
m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat);
sampledViewFormat == resource->format
? resource->sampledView
: m_textureManager->GetOrCreateSampledImageView(*texture, sampledViewFormat);
if (sampledImageView == VK_NULL_HANDLE) {
MGLOG_E("ResolveSamplerDescriptor: failed to resolve sampled view for binding=%u ('%s') "
"textureId=%d imageFormat=%d viewFormat=%d numericDomain=%d",
@@ -331,24 +386,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Uint16 samplerVersion = samplerToUse->GetVersion();
const Uint64 textureLifetimeId = texture->GetLifetimeId();
const Uint16 textureParamsVersion = texture->GetTextureParamsVersion();
// The sampler's LOD clamp depends on how many levels the sampled view exposes, and that
// follows uploads as well as GL parameters - so it belongs in the memo key too.
const Uint32 viewLevelCount = resource->sampledLevelCount;
if (memo.valid && memo.samplerLifetimeId == samplerLifetimeId && memo.samplerVersion == samplerVersion &&
memo.textureLifetimeId == textureLifetimeId && memo.textureParamsVersion == textureParamsVersion &&
memo.forceNearestFiltering == forceNearestFiltering) {
memo.forceNearestFiltering == forceNearestFiltering && memo.viewLevelCount == viewLevelCount) {
resolvedSampler = memo.sampler;
} else {
resolvedSampler =
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering);
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture,
forceNearestFiltering, viewLevelCount);
memo.samplerLifetimeId = samplerLifetimeId;
memo.samplerVersion = samplerVersion;
memo.textureLifetimeId = textureLifetimeId;
memo.textureParamsVersion = textureParamsVersion;
memo.forceNearestFiltering = forceNearestFiltering;
memo.viewLevelCount = viewLevelCount;
memo.sampler = resolvedSampler;
memo.valid = true;
}
} else {
resolvedSampler =
m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering);
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering,
resource->sampledLevelCount);
}
outImageInfo = {
.sampler = resolvedSampler,
@@ -389,6 +448,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return outImageInfo.sampler != VK_NULL_HANDLE;
}
Bool UniformManager::ProgramSamplesOnlySingleLevelTextures(
const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj) {
Bool sawSampler = false;
for (Uint32 binding = 0; binding < programObj.bindingKinds.size(); ++binding) {
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
continue;
}
const auto* texture = ResolveSamplerTextureRaw(program, programObj, binding);
if (texture == nullptr) return false;
const auto& levelRange = texture->GetLevelRange();
if (levelRange.x() != levelRange.y()) return false;
// An explicit-LOD sample is a single filtered tap, so it also gives up anisotropic
// filtering - which a single-level view can still have. Resolve the sampler exactly
// the way ResolveSamplerDescriptor does and bail if anisotropy would apply.
const Int location = programObj.samplerUniformLocationByBinding[binding];
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
const auto& samplerOverride = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();
const auto* effectiveSampler =
samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get();
if (effectiveSampler == nullptr) return false;
if (effectiveSampler->GetMaxAnisotropy() > 1.0f &&
effectiveSampler->GetMinFilter() == SamplerFilterMode::Linear &&
effectiveSampler->GetMagFilter() == SamplerFilterMode::Linear) {
return false;
}
// An explicit LOD 0 makes lambda exactly 0, which is the magnification side of the
// min/mag decision. That only matches the implicit form when lambda could not have been
// positive anyway (the LOD clamp already pins it at or below 0), or when the two
// filters are the same and the choice cannot be observed.
const Float effectiveMaxLod = effectiveSampler->GetMipmapMode() == SamplerMipmapMode::None
? 0.0f
: effectiveSampler->GetMaxLod();
if (effectiveMaxLod > 0.0f && effectiveSampler->GetMinFilter() != effectiveSampler->GetMagFilter()) {
return false;
}
sawSampler = true;
}
return sawSampler;
}
Bool UniformManager::ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
SharedPtr<MG_State::GLState::ITextureObject>& outTexture) {
@@ -746,7 +847,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
UboBindResult& out) const {
Uint32 arrayElement, UboBindResult& out) const {
const void* outData = nullptr;
VkDeviceSize outSize = 0;
@@ -772,7 +873,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(binding < programObj.uniformBlockIndexByBinding.size(),
"ResolveUniformBufferPayload: UBO mapping binding %u out of range", binding);
const Int blockIndex = programObj.uniformBlockIndexByBinding[binding];
Int blockIndex = programObj.uniformBlockIndexByBinding[binding];
if (arrayElement > 0) {
const auto arrayIt = programObj.arrayedUniformBlockIndicesByBinding.find(binding);
const Bool elementValid = arrayIt != programObj.arrayedUniformBlockIndicesByBinding.end() &&
arrayElement < arrayIt->second.size();
MOBILEGL_ASSERT(elementValid,
"ResolveUniformBufferPayload: UBO binding %u has no array element %u", binding,
arrayElement);
if (!elementValid) {
return false;
}
blockIndex = arrayIt->second[arrayElement];
}
MOBILEGL_ASSERT(blockIndex >= 0,
"ResolveUniformBufferPayload: no uniform block mapped to descriptor binding %u", binding);
@@ -880,6 +993,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
// FREE_DESCRIPTOR_SET_BIT lets a destroyed layout's cached sets be freed back
// (OnDescriptorSetLayoutDestroyed) so program churn recycles pool capacity.
// The cost is on set allocation only, which happens when a layout's per-frame
// cache grows - never on the per-draw reuse path.
poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
poolInfo.maxSets = maxSets;
poolInfo.poolSizeCount = static_cast<Uint32>(std::size(poolSizes));
poolInfo.pPoolSizes = poolSizes;
@@ -959,7 +1077,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& frame = m_frames[frameIndex];
auto& cache = frame.descriptorSetCacheByLayout[programObj.descriptorSetLayout];
if (cache.cursor < cache.sets.size()) {
outDescriptorSet = cache.sets[cache.cursor++];
outDescriptorSet = cache.sets[cache.cursor++].set;
} else {
VkResult allocResult = AllocateDescriptorSetsFromActivePool(frameIndex, programObj, outDescriptorSet);
if (allocResult == VK_ERROR_OUT_OF_POOL_MEMORY || allocResult == VK_ERROR_FRAGMENTED_POOL) {
@@ -973,7 +1091,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return allocResult;
}
cache.sets.push_back(outDescriptorSet);
// The successful allocation came from the bucket the alloc helper left
// active; record it so a layout-destroyed purge can free the set back.
cache.sets.push_back({outDescriptorSet, frame.descriptorPools[frame.activeDescriptorPoolIndex].handle});
++cache.cursor;
MGLOG_D("UniformDescriptorBinder: cached descriptor set count for frame=%u grew to %zu", frameIndex,
cache.sets.size());
@@ -1019,11 +1139,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
imageInfos.clear();
texelBufferViews.clear();
dynamicOffsets.clear();
// Arrayed UBO bindings contribute extra buffer infos and dynamic offsets; reserve for
// the worst case so the pBufferInfo pointers taken below never dangle on reallocation.
Uint32 uboArrayExtra = 0;
for (const auto& arrayEntry : programObj.arrayedUniformBlockIndicesByBinding) {
uboArrayExtra += static_cast<Uint32>(arrayEntry.second.size()) - 1u;
}
writes.reserve(m_maxBindings);
bufferInfos.reserve(m_maxBindings);
bufferInfos.reserve(m_maxBindings + uboArrayExtra);
imageInfos.reserve(m_maxBindings);
texelBufferViews.reserve(m_maxBindings);
dynamicOffsets.reserve(programObj.dynamicBindings.size());
dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra);
const Uint32 bindingCount =
std::min<Uint32>(m_maxBindings, static_cast<Uint32>(programObj.bindingKinds.size()));
@@ -1041,40 +1167,82 @@ namespace MobileGL::MG_Backend::DirectVulkan {
write.descriptorCount = 1;
if (kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic) {
UboBindResult ubo{};
const Bool hasPayload = ResolveUniformBufferPayload(program, programObj, binding, ubo);
MOBILEGL_ASSERT(hasPayload && ubo.payload != nullptr && ubo.payloadSize > 0,
"UniformDescriptorBinder::BindProgramUniformBuffers failed: missing UBO payload on binding %u",
binding);
const Uint32 descriptorCount =
binding < programObj.bindingDescriptorCounts.size()
? std::max<Uint32>(1, programObj.bindingDescriptorCounts[binding])
: 1u;
const SizeT firstBufferInfoIndex = bufferInfos.size();
for (Uint32 element = 0; element < descriptorCount; ++element) {
UboBindResult ubo{};
const Bool hasPayload =
ResolveUniformBufferPayload(program, programObj, binding, element, ubo);
MOBILEGL_ASSERT(hasPayload && ubo.payload != nullptr && ubo.payloadSize > 0,
"UniformDescriptorBinder::BindProgramUniformBuffers failed: missing UBO payload on binding %u element %u",
binding, element);
VkDescriptorBufferInfo bufferInfo{};
// Keep offset 0 (sub-range selected via the dynamic offset) so the hashed bufferInfo
// is stable across draws and the descriptor-set reuse cache keeps hitting.
bufferInfo.offset = 0;
Uint32 dynOffset;
if (ubo.directBindable) {
// Zero-copy: bind the app's resident VkBuffer directly, no per-draw memcpy.
bufferInfo.buffer = ubo.buffer;
bufferInfo.range = ubo.range;
dynOffset = static_cast<Uint32>(ubo.dynamicOffset);
} else {
BufferSlice slice{};
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload,
ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) {
MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u",
binding);
return false;
VkDescriptorBufferInfo bufferInfo{};
// Keep offset 0 (sub-range selected via the dynamic offset) so the hashed bufferInfo
// is stable across draws and the descriptor-set reuse cache keeps hitting.
bufferInfo.offset = 0;
Uint32 dynOffset;
if (ubo.directBindable) {
// Zero-copy: bind the app's resident VkBuffer directly, no per-draw memcpy.
bufferInfo.buffer = ubo.buffer;
bufferInfo.range = ubo.range;
dynOffset = static_cast<Uint32>(ubo.dynamicOffset);
} else {
// Global-UBO slice reuse (see GlobalUboSliceMemo): unchanged
// uniform bytes re-use the slice already uploaded this frame.
const Bool isGlobalUbo =
programObj.globalUboBinding == static_cast<Int>(binding) && element == 0;
const Uint64 uboFrameSerial = m_bufferManager->GetFrameSerial();
const Uint64 uboProgramLifetimeId = program.GetLifetimeId();
const Uint32 uboContentVersion = program.GetUBOContentVersion();
Bool reusedSlice = false;
if (isGlobalUbo) {
for (const auto& memo : m_globalUboMemo) {
if (memo.buffer != VK_NULL_HANDLE &&
memo.programLifetimeId == uboProgramLifetimeId &&
memo.frameSerial == uboFrameSerial &&
memo.uboContentVersion == uboContentVersion &&
memo.range == static_cast<VkDeviceSize>(ubo.payloadSize)) {
bufferInfo.buffer = memo.buffer;
bufferInfo.range = memo.range;
dynOffset = static_cast<Uint32>(memo.offset);
reusedSlice = true;
break;
}
}
}
if (!reusedSlice) {
BufferSlice slice{};
if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload,
ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) {
MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u element %u",
binding, element);
return false;
}
bufferInfo.buffer = slice.buffer;
bufferInfo.range = ubo.payloadSize;
dynOffset = static_cast<Uint32>(slice.offset);
if (isGlobalUbo) {
m_globalUboMemo[m_globalUboMemoNext] = GlobalUboSliceMemo{
uboProgramLifetimeId, uboFrameSerial, uboContentVersion,
slice.buffer, slice.offset, static_cast<VkDeviceSize>(ubo.payloadSize)};
m_globalUboMemoNext = (m_globalUboMemoNext + 1) % kGlobalUboMemoSize;
}
}
}
bufferInfo.buffer = slice.buffer;
bufferInfo.range = ubo.payloadSize;
dynOffset = static_cast<Uint32>(slice.offset);
bufferInfos.push_back(bufferInfo);
// Dynamic offsets are consumed in binding order, then array element order,
// matching Vulkan's dynamic-offset consumption rules.
dynamicOffsets.push_back(dynOffset);
}
bufferInfos.push_back(bufferInfo);
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
write.pBufferInfo = &bufferInfos.back();
write.descriptorCount = descriptorCount;
write.pBufferInfo = &bufferInfos[firstBufferInfoIndex];
writes.push_back(write);
dynamicOffsets.push_back(dynOffset);
} else if (kind == ProgramFactory::DescriptorBindingKind::UniformTexelBuffer) {
VkBufferView bufferView = VK_NULL_HANDLE;
if (!ResolveTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) ||
@@ -1202,8 +1370,34 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_hasLastDescriptor = cacheable;
}
vkCmdBindDescriptorSets(commandBuffer, bindPoint, programObj.pipelineLayout, 0, 1,
&descriptorSet, static_cast<Uint32>(dynamicOffsets.size()), dynamicOffsets.data());
// Skip the driver call when this exact binding is already live on the
// command buffer (see the bind-dedup shadow in the header).
const Uint32 offsetCount = static_cast<Uint32>(dynamicOffsets.size());
Bool identicalBind = m_lastBindValid && m_lastBindSet == descriptorSet &&
m_lastBindLayout == programObj.pipelineLayout && m_lastBindPoint == bindPoint &&
m_lastBindOffsetCount == offsetCount && offsetCount <= kMaxShadowedDynamicOffsets;
if (identicalBind) {
for (Uint32 i = 0; i < offsetCount; ++i) {
if (m_lastBindOffsets[i] != dynamicOffsets[i]) {
identicalBind = false;
break;
}
}
}
if (!identicalBind) {
vkCmdBindDescriptorSets(commandBuffer, bindPoint, programObj.pipelineLayout, 0, 1,
&descriptorSet, offsetCount, dynamicOffsets.data());
if (offsetCount <= kMaxShadowedDynamicOffsets) {
m_lastBindValid = true;
m_lastBindSet = descriptorSet;
m_lastBindLayout = programObj.pipelineLayout;
m_lastBindPoint = bindPoint;
m_lastBindOffsetCount = offsetCount;
std::copy_n(dynamicOffsets.data(), offsetCount, m_lastBindOffsets);
} else {
m_lastBindValid = false;
}
}
return true;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -39,6 +39,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void Shutdown();
void BeginFrame(Uint32 frameIndex);
// A command buffer (re)began recording: descriptor bindings recorded into
// the previous buffer do not carry over, so drop the bind-dedup shadow.
void OnCommandBufferBoundary() { m_lastBindValid = false; }
// A ProgramFactory eviction just destroyed this layout: purge every frame
// slot's cached descriptor sets for it, so a recycled handle value can never
// stale-hit sets written for the dead layout's bindings. The sets are
// vkFreeDescriptorSets'd back to their pools (created with
// FREE_DESCRIPTOR_SET_BIT) and the pool accounting is credited, so program
// churn recycles pool capacity instead of abandoning it. GPU-safe: the layout
// only dies after >1024 idle frame boundaries, so no in-flight command buffer
// references its sets. This is the only eviction path for the per-layout
// caches - a live layout's entry must never be purged (its sets would be
// unreachable pool slots), so there is deliberately no age-based sweep here.
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj,
Vector<MG_State::GLState::ITextureObject*>& outTextures);
@@ -58,6 +72,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat,
VkFormat resourceFormat, Bool useBindingFormat);
// True when the program reads at least one sampler and every one of them is bound to a
// texture whose GL level range is a single level. Such a sampler resolves to
// minLod = maxLod = 0 (see VkSamplerManager::GetOrCreateSampler), so an implicit-LOD sample
// and an explicit LOD 0 sample must read the same texel - which is what makes the
// ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads
// only GL state, so a texture that ends up single-level for another reason (one uploaded
// level under a wide level range) merely misses the rewrite.
static Bool ProgramSamplesOnlySingleLevelTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj);
private:
struct DescriptorPoolBucket {
VkDescriptorPool handle = VK_NULL_HANDLE;
@@ -65,8 +89,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 allocatedSets = 0;
};
// A cached descriptor set together with the pool it was allocated from, so a
// layout-destroyed purge can vkFreeDescriptorSets it back and credit the
// owning bucket's accounting.
struct CachedDescriptorSet {
VkDescriptorSet set = VK_NULL_HANDLE;
VkDescriptorPool pool = VK_NULL_HANDLE;
};
struct DescriptorSetCacheEntry {
Vector<VkDescriptorSet> sets;
Vector<CachedDescriptorSet> sets;
Uint32 cursor = 0;
};
@@ -116,7 +148,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
};
Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
UboBindResult& out) const;
Uint32 arrayElement, UboBindResult& out) const;
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
VkResult AllocateDescriptorSetsFromActivePool(
@@ -156,6 +188,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 m_lastDescriptorSignature = 0;
Bool m_hasLastDescriptor = false;
// vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform
// block resolve to the same set AND the same dynamic offsets, so the
// driver call can be skipped outright. Command-buffer-scope state; reset
// via OnCommandBufferBoundary whenever a recording (re)begins. Keyed on
// layout+bind point, so a pipeline-layout switch always rebinds.
static constexpr Uint32 kMaxShadowedDynamicOffsets = 8;
Bool m_lastBindValid = false;
VkDescriptorSet m_lastBindSet = VK_NULL_HANDLE;
VkPipelineLayout m_lastBindLayout = VK_NULL_HANDLE;
VkPipelineBindPoint m_lastBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
Uint32 m_lastBindOffsetCount = 0;
Uint32 m_lastBindOffsets[kMaxShadowedDynamicOffsets] = {};
// Global-UBO transient-slice reuse: MC leaves the default uniform block
// untouched across long GUI/terrain runs, so the per-draw re-upload of
// the same bytes can reuse the slice uploaded earlier THIS frame (frame
// serial guards arena recycling; the content version guards writes).
struct GlobalUboSliceMemo {
Uint64 programLifetimeId = 0;
Uint64 frameSerial = 0;
Uint32 uboContentVersion = 0;
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceSize offset = 0;
VkDeviceSize range = 0;
};
static constexpr Uint32 kGlobalUboMemoSize = 4;
GlobalUboSliceMemo m_globalUboMemo[kGlobalUboMemoSize];
Uint32 m_globalUboMemoNext = 0;
// Per-binding fast path over VkSamplerManager's content-hashed sampler cache, which
// stays the source of truth: its key hashes all sampler+texture state, so two distinct
// sampler objects with identical state still resolve to one VkSampler. This memo only
@@ -172,10 +233,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 samplerLifetimeId = 0;
Uint64 textureLifetimeId = 0;
VkSampler sampler = VK_NULL_HANDLE;
Uint32 viewLevelCount = 0;
Uint16 samplerVersion = 0;
Uint16 textureParamsVersion = 0;
Bool forceNearestFiltering = false;
Bool valid = false;
// ResolveSampledImageViewFormat is pure in (image format, numeric domain), but a
// domain mismatch walks a ~184-entry format table. Memo the resolution per binding
// so a reinterpreted sampler pays that scan once, not once per draw.
VkFormat viewFormatSource = VK_FORMAT_UNDEFINED;
SamplerNumericDomain viewFormatDomain = SamplerNumericDomain::Unknown;
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
Bool viewFormatValid = false;
};
mutable Vector<SamplerResolveMemo> m_samplerResolveMemo;
};
@@ -32,6 +32,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.IsBgra, sizeof(attr.IsBgra)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attr.Divisor, sizeof(attr.Divisor)));
// The buffer's heap address is an identity component of the key: a freed
// buffer's reused address can alias an old cache entry, but only under a
// byte-identical attribute layout - and the entry payload is a pure function
// of the hashed inputs, with the draw path re-resolving bindingBufferKeys
// against the live VAO attribute pointers, so an aliased hit returns exactly
// what a rebuild would. Address drift only grows the map; the OnFrameBoundary
// aging sweep bounds that.
const SizeT bufferKey = reinterpret_cast<SizeT>(attr.Buffer.get());
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
}
@@ -51,14 +58,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao) {
return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
// Per-draw fast path: the VAO carries a pointer to its resolved entry,
// valid while its config version and the cache's eviction epoch both
// match - no re-hash, no map lookup.
const void* memoState = nullptr;
Uint64 memoEpoch = 0;
if (vao.GetBackendStateMemo(memoState, memoEpoch) && memoEpoch == m_evictionEpoch) {
const auto* entry = static_cast<const BackendVertexInputState*>(memoState);
entry->lastUsedFrameBoundary = m_frameBoundaryCounter;
return *entry;
}
const BackendVertexInputState& entry = GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
vao.SetBackendStateMemo(&entry, m_evictionEpoch);
return entry;
}
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao, HashType hash) {
auto it = m_cache.find(hash);
if (it != m_cache.end()) {
return it->second;
it->second->lastUsedFrameBoundary = m_frameBoundaryCounter;
return *it->second;
}
VertexInputStateBuilder builder;
@@ -126,8 +146,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool packedAttribute = attr.Type == DataType::Int2101010Rev ||
attr.Type == DataType::Uint2101010Rev;
const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type);
// For a client-memory array attr.Offset holds the raw client pointer, and the
// draw path re-uploads the data to a 16-aligned transient slice with attribute
// offset 0, so only the stride can violate Vulkan's fetch alignment there.
const Bool clientMemoryAttribute = attr.Buffer == nullptr;
if (conversion == VertexStreamConversion::None && requiredAlignment > 1 &&
((sourceStride % requiredAlignment) != 0 || (attr.Offset % requiredAlignment) != 0)) {
((sourceStride % requiredAlignment) != 0 ||
(!clientMemoryAttribute && (attr.Offset % requiredAlignment) != 0))) {
// GL accepts arbitrary byte strides and offsets. Core Vulkan vertex fetches do not
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
// attribute into a tightly packed transient stream without changing its format.
@@ -159,10 +184,37 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const auto& state = builder.Build();
auto& entry = m_cache[hash];
auto& slot = m_cache[hash];
if (!slot) {
slot = MakeUnique<BackendVertexInputState>();
}
BackendVertexInputState& entry = *slot;
entry.hash = hash;
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
entry.bindings = builder.GetBindings();
entry.attributes = builder.GetAttributes();
// See the layoutHash declaration: hash only the resolved layout, never
// buffer identities, so identical layouts across VAOs/buffers agree.
XXHASH_VERIFY(XXH64_reset(m_hashState, 0));
for (const auto& binding : entry.bindings) {
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.binding, sizeof(binding.binding)));
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.stride, sizeof(binding.stride)));
XXHASH_VERIFY(XXH64_update(m_hashState, &binding.inputRate, sizeof(binding.inputRate)));
}
for (const auto& attribute : entry.attributes) {
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.location, sizeof(attribute.location)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.binding, sizeof(attribute.binding)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.format, sizeof(attribute.format)));
XXHASH_VERIFY(XXH64_update(m_hashState, &attribute.offset, sizeof(attribute.offset)));
}
XXHASH_VERIFY(XXH64_update(m_hashState, &unsupportedAttribMask, sizeof(unsupportedAttribMask)));
entry.layoutHash = XXH64_digest(m_hashState);
entry.attributeLocationMask = 0;
for (const auto& attribute : entry.attributes) {
if (attribute.location < 32u) {
entry.attributeLocationMask |= (1u << attribute.location);
}
}
entry.bindingBufferKeys = std::move(bindingBufferKeys);
entry.bindingBaseOffsets = std::move(bindingBaseOffsets);
entry.bindingAttributeLocations = std::move(bindingAttributeLocations);
@@ -175,6 +227,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return entry;
}
void VertexInputStateFactory::OnFrameBoundary() {
++m_frameBoundaryCounter;
// Sweep occasionally; evict entries whose last hit is far in the past.
// Erasure happens only here, never mid-frame: the draw path holds a
// reference into the current entry across its setup, and unordered_map
// erase would invalidate it. Entries are CPU-side only, so no GPU-idle
// proof is needed; an evicted entry that is used again is simply rebuilt
// from the VAO state (same hash, same content).
constexpr Uint64 kSweepInterval = 256;
constexpr Uint64 kRetireAgeBoundaries = 1024;
if ((m_frameBoundaryCounter % kSweepInterval) != 0) {
return;
}
for (auto it = m_cache.begin(); it != m_cache.end();) {
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
it = m_cache.erase(it);
// Invalidate every VAO's state-pointer memo: the erased node's
// address may be reused by a future insert.
++m_evictionEpoch;
} else {
++it;
}
}
}
VkFormat VertexInputStateFactory::ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger,
Bool isBgra) {
if (isBgra) {
@@ -27,6 +27,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
struct BackendVertexInputState {
HashType hash = 0;
// Hash of the resolved Vulkan vertex layout only (bindings, attributes,
// unsupported mask) - NO buffer identities. `hash` mixes buffer heap
// addresses so per-chunk VBOs mint a fresh identity per buffer; keying
// pipelines on that minted one VkPipeline per chunk section for an
// identical layout, defeating pipeline reuse and the per-draw memo.
// Pipelines depend only on the layout, so they key on this instead.
HashType layoutHash = 0;
// Frame boundary of the last cache hit; entries idle past the
// OnFrameBoundary retirement age are evicted (CPU heap only).
// Mutable: the VAO's state-pointer memo fast path stamps it through
// a const entry reference.
mutable Uint64 lastUsedFrameBoundary = 0;
Vector<VkVertexInputBindingDescription> bindings;
Vector<VkVertexInputAttributeDescription> attributes;
Vector<SizeT> bindingBufferKeys;
@@ -38,6 +50,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// absent from `attributes`, so without this mask the draw path cannot tell them apart from
// a genuinely disabled array and would silently feed the shader the current attribute value.
Uint32 unsupportedAttribMask = 0;
// Bitmask of `attributes[i].location` - the draw path needs it up to
// three times per draw, so it is baked once at build time.
Uint32 attributeLocationMask = 0;
VkPipelineVertexInputStateCreateInfo state{
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
};
@@ -55,6 +70,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const BackendVertexInputState& GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
// Frame boundary hook: ages the cache and evicts entries not hit for many
// frames. The key mixes buffer heap addresses, so buffer/VAO churn keeps
// minting fresh keys; without eviction the map grows for the whole session.
// Entries hold no Vulkan handles (pipeline creation copies the descriptions)
// and the draw path's entry reference never spans a frame boundary, so
// eviction here needs no GPU-idle proof. Self-gated: one counter bump and
// compare except on sweep boundaries.
void OnFrameBoundary();
static SizeT GetComponentSize(DataType type);
// Tightly-packed byte size of one vertex element for this attribute: componentSize * size for
// normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for
@@ -69,7 +92,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VulkanRendererConfig& m_config;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
UnorderedMap<HashType, BackendVertexInputState> m_cache;
// Values are heap-allocated: FastSTL::unordered_map is open-addressing,
// so INSERT invalidates references to stored values. The draw path (and
// the VAOs' state-pointer memos) hold entry pointers across inserts;
// only the unique_ptr cell moves, never the pointee.
UnorderedMap<HashType, UniquePtr<BackendVertexInputState>> m_cache;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameBoundaryCounter = 0;
// Bumped whenever any cache entry is erased. VAOs memo a raw pointer to
// their heap-allocated entry (stable across map insert/rehash by
// construction); a memo is honored only while its recorded epoch
// matches, so an evicted entry can never be dereferenced through a
// stale memo.
Uint64 m_evictionEpoch = 1;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -22,6 +22,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
// Appended to kPersistentBackedUsage when VK_EXT_transform_feedback is enabled
// (see VkBufferManagerInitInfo::transformFeedbackUsageEnabled).
constexpr VkBufferUsageFlags kTransformFeedbackUsage =
VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT;
// The app writes into the persistent map with no explicit flush, so its memory must
// be host-coherent (Adreno host-visible memory is; requiring it keeps us portable).
constexpr VkMemoryPropertyFlags kPersistentBackedRequiredFlags =
@@ -141,6 +145,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_transientUploadArena.BeginFrame(frameIndex);
}
void VkBufferManager::CollectAllDeferredReleases() {
for (Uint32 frameIndex = 0; frameIndex < m_deferredBufferReleases.size(); ++frameIndex) {
CollectDeferredReleases(frameIndex);
}
for (Uint32 frameIndex = 0; frameIndex < m_transientUploadArena.GetFrameCount(); ++frameIndex) {
m_transientUploadArena.CollectDeferredReleases(frameIndex);
}
}
void VkBufferManager::NotifyDeviceIdle() {
// Everything submitted so far has completed. Work recorded for the
// current frame has not been submitted yet, so the current serial
@@ -456,7 +469,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// it from the current shadow - MappedData() is still the shadow here because the
// frontend adopts (and drops) the shadow only after this returns.
DeferRelease(std::move(resource->buffer));
if (!CreateResidentStorage(*resource, size, kPersistentBackedUsage, kPersistentBackedRequiredFlags)) {
const VkBufferUsageFlags persistentUsage =
kPersistentBackedUsage |
(m_initInfo.transformFeedbackUsageEnabled ? kTransformFeedbackUsage : 0);
if (!CreateResidentStorage(*resource, size, persistentUsage, kPersistentBackedRequiredFlags)) {
resource->persistentMapped = false;
resource->storageSize = 0;
resource->usageFlags = 0;
@@ -31,6 +31,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VmaMemoryUsage transientMemoryUsage = VMA_MEMORY_USAGE_AUTO;
VmaAllocationCreateFlags transientAllocationFlags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
Bool transientPersistentMapping = false;
// VK_EXT_transform_feedback is enabled: persistent-map storage additionally
// carries the transform feedback usage so capture targets can bind directly.
Bool transformFeedbackUsageEnabled = false;
};
// The DirectVulkan storage behind one frontend buffer (pipe_resource analogue).
@@ -77,6 +80,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Recreate all per-frame transient arenas
Bool RecreateTransientArenas(Uint32 frameCount);
void BeginFrame(Uint32 frameIndex);
// Drains every frame slot's deferred buffer/resource releases (and the
// transient arena's parked superseded blocks). Only valid when the
// caller has proven every queue submission complete; used by the
// present-less frame-boundary drain.
void CollectAllDeferredReleases();
// All previously submitted GPU work has completed (vkDeviceWaitIdle).
void NotifyDeviceIdle();
// A frame slot's submission fence has been waited: every serial up to
@@ -93,6 +93,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const std::lock_guard<std::mutex> lock(m_mutex);
m_pendingClears.clear();
m_aliveObjects.clear();
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) {
@@ -127,6 +128,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_pendingClears.erase(key);
}
m_aliveObjects.erase(identity);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
Bool VkClearManager::LockTextureIdentityLocked(const TextureIdentity& identity,
@@ -221,6 +223,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
void VkClearManager::QueueClear(const ClearAttachmentPayload& clearPayload,
@@ -238,6 +241,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
auto& pending = m_pendingClears[key];
MergeClearPayload(pending, clearPayload);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
Bool VkClearManager::HasPendingClear(MG_State::GLState::ITextureObject* texture) {
@@ -245,6 +249,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
const Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex);
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
@@ -260,6 +268,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (key.texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
const std::lock_guard<std::mutex> lock(m_mutex);
if (m_pendingClears.find(key) == m_pendingClears.end()) {
@@ -287,6 +298,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (key.texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
const std::lock_guard<std::mutex> lock(m_mutex);
if (!LockTextureLocked(key, outTexture)) {
@@ -325,6 +339,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (texture == nullptr) {
return false;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return false; // per-draw hot path: nothing pending anywhere
}
const Uint64 lifetimeId = texture->GetLifetimeId();
const std::lock_guard<std::mutex> lock(m_mutex);
@@ -345,6 +362,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return;
}
if (m_pendingCount.load(std::memory_order_relaxed) == 0) {
return; // per-draw hot path: nothing pending anywhere
}
const TextureIdentity identity = MakeTextureIdentity(texture);
MGLOG_D("%s: Pop all pending clears for texture %d", __func__, texture->GetExternalIndex());
const std::lock_guard<std::mutex> lock(m_mutex);
@@ -361,6 +381,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto it = m_pendingClears.find(key);
if (it != m_pendingClears.end()) {
m_pendingClears.erase(it);
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
}
}
@@ -14,6 +14,7 @@
#include "MG_Util/Math/VectorTypes.h"
#include <Includes.h>
#include <atomic>
#include <unordered_map>
namespace MobileGL::MG_Backend::DirectVulkan {
@@ -120,7 +121,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
Uint8 m_gcCounter = 0;
public:
// Lock-free probe for the consecutive-draw fast path: any pending clear
// forces the full SetupDraw path (which materializes/consumes it).
Bool HasAnyPendingClears() const { return m_pendingCount.load(std::memory_order_relaxed) != 0; }
private:
mutable std::mutex m_mutex;
// Lock-free mirror of m_pendingClears.size(), maintained under m_mutex
// by every mutation. The per-draw probes (HasPendingClear/GetPending*)
// read it before taking the lock: during draw batches the pending set
// is almost always empty, so this turns several locked map probes per
// draw into one relaxed load.
std::atomic<Uint32> m_pendingCount{0};
std::unordered_map<PendingClearKey, ClearAttachmentPayload, PendingClearKeyHash> m_pendingClears;
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
};
@@ -16,31 +16,21 @@
namespace MobileGL::MG_Backend::DirectVulkan {
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
switch (requestedSamples <= 0 ? 1 : requestedSamples) {
case 1:
// GL promises "at least the requested samples", so a non-power-of-two
// request (legal in GL, e.g. 3) rounds up to the next Vulkan bit.
if (requestedSamples <= 1) {
outSampleCount = VK_SAMPLE_COUNT_1_BIT;
return true;
case 2:
outSampleCount = VK_SAMPLE_COUNT_2_BIT;
return true;
case 4:
outSampleCount = VK_SAMPLE_COUNT_4_BIT;
return true;
case 8:
outSampleCount = VK_SAMPLE_COUNT_8_BIT;
return true;
case 16:
outSampleCount = VK_SAMPLE_COUNT_16_BIT;
return true;
case 32:
outSampleCount = VK_SAMPLE_COUNT_32_BIT;
return true;
case 64:
outSampleCount = VK_SAMPLE_COUNT_64_BIT;
return true;
default:
}
if (requestedSamples > 64) {
return false;
}
Uint32 bit = 1;
while (bit < static_cast<Uint32>(requestedSamples)) {
bit <<= 1;
}
outSampleCount = static_cast<VkSampleCountFlagBits>(bit);
return true;
}
static VkImageAspectFlags ResolveImageAspectMaskForFormat(VkFormat format) {
@@ -166,6 +156,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (view != VK_NULL_HANDLE) {
vkDestroyImageView(device, view, nullptr);
}
if (unormTwinView != VK_NULL_HANDLE) {
vkDestroyImageView(device, unormTwinView, nullptr);
}
if (image != VK_NULL_HANDLE && allocation != nullptr) {
vmaDestroyImage(allocator, image, allocation);
}
@@ -173,6 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
image = VK_NULL_HANDLE;
allocation = nullptr;
view = VK_NULL_HANDLE;
unormTwinView = VK_NULL_HANDLE;
layout = VK_IMAGE_LAYOUT_UNDEFINED;
format = VK_FORMAT_UNDEFINED;
aspect = VK_IMAGE_ASPECT_NONE;
@@ -180,6 +174,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
sampleCount = VK_SAMPLE_COUNT_1_BIT;
internalFormat = TextureInternalFormat::Unknown;
samples = 0;
deadSinceFrame = kNeverObservedDead;
}
VkRenderPassManager::VkRenderPassManager(VkDevice device,
@@ -206,6 +201,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.Destroy(m_device, m_allocator);
}
m_renderbufferResources.clear();
CollectDeferredRenderbufferReleases(/*destroyAll=*/true); // caller guarantees device idle
m_pendingRenderbufferClears.clear();
RenderPassEntry::s_textureResourcesScratch.clear();
s_activeRenderPass = {};
@@ -213,22 +209,80 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_rpFastValid = false;
}
void VkRenderPassManager::CollectRenderbufferGarbage() {
Vector<MG_State::GLState::RenderbufferObject*> deadRenderbuffers;
deadRenderbuffers.reserve(m_renderbufferResources.size());
for (auto& [renderbuffer, resource] : m_renderbufferResources) {
const auto liveRenderbuffer = resource.renderbuffer.lock();
if (!liveRenderbuffer || liveRenderbuffer.get() != renderbuffer) {
deadRenderbuffers.emplace_back(renderbuffer);
}
Uint64 VkRenderPassManager::RetireAgeFrames() const {
// MaxFramesInFlight + 2 covers the frame ring plus one boundary for the
// recording-to-submit gap and one because OnPresent runs ahead of Present's
// fence wait; the floor of 8 keeps a margin over the default ring of 3 while
// still releasing multi-MB attachment memory promptly (the render-pass cache's
// 1024-frame retirement would pin it for no additional safety).
return std::max<Uint64>(8, static_cast<Uint64>(m_config.MaxFramesInFlight) + 2);
}
void VkRenderPassManager::DeferRenderbufferBackingRelease(RenderbufferResource& resource) {
// The superseded backing may still be referenced by in-flight command buffers
// (glRenderbufferStorage can respecify a renderbuffer drawn this very frame),
// so it is parked and destroyed only after RetireAgeFrames() boundaries.
if (resource.image == VK_NULL_HANDLE && resource.view == VK_NULL_HANDLE) {
return;
}
for (auto* renderbuffer : deadRenderbuffers) {
auto resourceIt = m_renderbufferResources.find(renderbuffer);
if (resourceIt != m_renderbufferResources.end()) {
resourceIt->second.Destroy(m_device, m_allocator);
m_renderbufferResources.erase(resourceIt);
m_deferredRenderbufferReleases.push_back(
{resource.image, resource.allocation, resource.view, resource.unormTwinView, m_frameCounter});
resource.image = VK_NULL_HANDLE;
resource.allocation = nullptr;
resource.view = VK_NULL_HANDLE;
resource.unormTwinView = VK_NULL_HANDLE;
}
void VkRenderPassManager::CollectDeferredRenderbufferReleases(Bool destroyAll) {
if (m_deferredRenderbufferReleases.empty()) {
return;
}
const Uint64 retireAgeFrames = RetireAgeFrames();
std::erase_if(m_deferredRenderbufferReleases, [&](DeferredRenderbufferRelease& release) {
if (!destroyAll && m_frameCounter - release.deferredAtFrame < retireAgeFrames) {
return false;
}
m_pendingRenderbufferClears.erase(renderbuffer);
if (release.view != VK_NULL_HANDLE) {
vkDestroyImageView(m_device, release.view, nullptr);
}
if (release.unormTwinView != VK_NULL_HANDLE) {
vkDestroyImageView(m_device, release.unormTwinView, nullptr);
}
if (release.image != VK_NULL_HANDLE) {
vmaDestroyImage(m_allocator, release.image, release.allocation);
}
return true;
});
}
void VkRenderPassManager::CollectRenderbufferGarbage() {
// Two-phase reclamation: a dead renderbuffer's VkImage may still be referenced by
// command buffers submitted up to frames-in-flight frames ago (it was legally
// attached and drawn right up to its deletion), so the first observation of an
// expired weak reference only stamps the current frame counter; Destroy runs once
// enough frame boundaries have passed that the stamping frame's submission fence
// has provably been waited (see RetireAgeFrames).
const Uint64 retireAgeFrames = RetireAgeFrames();
for (auto it = m_renderbufferResources.begin(); it != m_renderbufferResources.end();) {
auto& resource = it->second;
const auto liveRenderbuffer = resource.renderbuffer.lock();
if (liveRenderbuffer && liveRenderbuffer.get() == it->first) {
resource.deadSinceFrame = RenderbufferResource::kNeverObservedDead;
++it;
continue;
}
if (resource.deadSinceFrame == RenderbufferResource::kNeverObservedDead) {
resource.deadSinceFrame = m_frameCounter;
++it;
continue;
}
if (m_frameCounter - resource.deadSinceFrame < retireAgeFrames) {
++it;
continue;
}
m_pendingRenderbufferClears.erase(it->first);
resource.Destroy(m_device, m_allocator);
it = m_renderbufferResources.erase(it);
}
}
@@ -249,12 +303,94 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const auto internalFormat = renderbuffer->GetInternalFormat();
const VkFormat format = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
// Three-channel color formats widen to their RGBA twin exactly like textures do
// (VkTextureManager::ResolveTextureFormatInfo): blits/resolves between a
// renderbuffer and a texture of the same GL format then see one VkFormat.
const VkFormat format = [&]() -> VkFormat {
switch (internalFormat) {
case TextureInternalFormat::RGB:
case TextureInternalFormat::RGB8:
case TextureInternalFormat::R3G3B2:
case TextureInternalFormat::RGB4:
case TextureInternalFormat::RGB5:
return VK_FORMAT_R8G8B8A8_UNORM;
case TextureInternalFormat::SRGB8:
return VK_FORMAT_R8G8B8A8_SRGB;
case TextureInternalFormat::RGB8Snorm:
return VK_FORMAT_R8G8B8A8_SNORM;
case TextureInternalFormat::RGB10:
case TextureInternalFormat::RGB12:
case TextureInternalFormat::RGB16:
return VK_FORMAT_R16G16B16A16_UNORM;
case TextureInternalFormat::RGB16Snorm:
return VK_FORMAT_R16G16B16A16_SNORM;
case TextureInternalFormat::RGB16F:
return VK_FORMAT_R16G16B16A16_SFLOAT;
case TextureInternalFormat::RGB32F:
return VK_FORMAT_R32G32B32A32_SFLOAT;
case TextureInternalFormat::RGB8I:
return VK_FORMAT_R8G8B8A8_SINT;
case TextureInternalFormat::RGB8UI:
return VK_FORMAT_R8G8B8A8_UINT;
case TextureInternalFormat::RGB16I:
return VK_FORMAT_R16G16B16A16_SINT;
case TextureInternalFormat::RGB16UI:
return VK_FORMAT_R16G16B16A16_UINT;
case TextureInternalFormat::RGB32I:
return VK_FORMAT_R32G32B32A32_SINT;
case TextureInternalFormat::RGB32UI:
return VK_FORMAT_R32G32B32A32_UINT;
default:
return MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
}
}();
const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format);
if ((aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) {
MGLOG_E("GetOrCreateRenderbufferResource: color renderbuffer %u is not supported by DirectVulkan render passes yet",
renderbuffer->GetExternalIndex());
return nullptr;
// Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the
// usage set is attachment + transfer: transfer covers readback (vkCmdCopyImageToBuffer),
// BlitFramebuffer, CopyTexImage sources, and out-of-render-pass clear materialization.
const VkImageUsageFlags imageUsage =
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT
: VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
// GL allows the implementation to allocate more samples than requested
// (glRenderbufferStorageMultisample only promises "at least"), and devices
// like llvmpipe expose 1x/4x but not 2x. Round the request up to the
// nearest supported count for this format.
if (renderbuffer->GetSamples() > 0) {
auto supportedIt = m_attachmentSampleCountsByFormat.find(format);
if (supportedIt == m_attachmentSampleCountsByFormat.end()) {
VkImageFormatProperties formatProperties{};
VkSampleCountFlags supported = VK_SAMPLE_COUNT_1_BIT;
if (vkGetPhysicalDeviceImageFormatProperties(m_physicalDevice, format, VK_IMAGE_TYPE_2D,
VK_IMAGE_TILING_OPTIMAL, imageUsage, 0,
&formatProperties) == VK_SUCCESS) {
supported = formatProperties.sampleCounts;
}
supportedIt = m_attachmentSampleCountsByFormat.emplace(format, supported).first;
}
const VkSampleCountFlags supported = supportedIt->second;
if ((supported & sampleCount) == 0) {
// Smallest supported count above the request, else the largest below it.
Uint32 rounded = 0;
for (Uint32 bit = static_cast<Uint32>(sampleCount) << 1; bit <= VK_SAMPLE_COUNT_64_BIT; bit <<= 1) {
if ((supported & bit) != 0) {
rounded = bit;
break;
}
}
if (rounded == 0) {
for (Uint32 bit = static_cast<Uint32>(sampleCount) >> 1; bit != 0; bit >>= 1) {
if ((supported & bit) != 0) {
rounded = bit;
break;
}
}
}
if (rounded != 0) {
sampleCount = static_cast<VkSampleCountFlagBits>(rounded);
}
}
}
auto& resource = m_renderbufferResources[renderbuffer.get()];
@@ -268,9 +404,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.samples != renderbuffer->GetSamples();
if (!needsCreate) {
resource.renderbuffer = renderbuffer;
// A new renderbuffer at a recycled address may adopt a compatible entry that
// was already stamped dead; it is alive again, so cancel the aging.
resource.deadSinceFrame = RenderbufferResource::kNeverObservedDead;
return &resource;
}
// Respecify: park the old backing for aged destruction instead of destroying
// inline - it may still be referenced by in-flight command buffers.
DeferRenderbufferBackingRelease(resource);
resource.Destroy(m_device, m_allocator);
resource.renderbuffer = renderbuffer;
@@ -285,9 +427,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
imageInfo.format = format;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
imageInfo.usage = imageUsage;
imageInfo.samples = sampleCount;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
// sRGB renderbuffers attach through their UNORM twin while GL_FRAMEBUFFER_SRGB
// is disabled, which needs a format-reinterpreting second view.
const Bool hasUnormTwin = ResolveSrgbAttachmentWriteFormat(format, false) != format;
if (hasUnormTwin) {
imageInfo.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
}
VkImageFormatProperties imageFormatProperties{};
const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
@@ -322,6 +470,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
viewInfo.subresourceRange.layerCount = 1;
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.view),
"vkCreateImageView(renderbuffer)");
if (hasUnormTwin) {
viewInfo.format = ResolveSrgbAttachmentWriteFormat(format, false);
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.unormTwinView),
"vkCreateImageView(renderbuffer unorm twin)");
}
resource.layout = VK_IMAGE_LAYOUT_UNDEFINED;
resource.format = format;
@@ -386,6 +539,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void VkRenderPassManager::QueueRenderbufferClear(
GLbitfield mask, const ClearFramebufferPayload& clearPayload,
const MG_State::GLState::FramebufferObject& drawFbo) {
if ((mask & GL_COLOR_BUFFER_BIT) != 0) {
// Color renderbuffer draw buffers take the framebuffer-level clear too; texture
// attachments are skipped by the per-attachment overload's IsRenderbuffer guard.
for (const auto attachmentType : drawFbo.GetDrawBuffers()) {
if (attachmentType == FramebufferAttachmentType::None) {
continue;
}
QueueRenderbufferClear(
ClearAttachmentPayload{.mask = GL_COLOR_BUFFER_BIT, .color = clearPayload.color},
drawFbo.GetAttachment(attachmentType));
}
}
if ((mask & GL_DEPTH_BUFFER_BIT) != 0) {
QueueRenderbufferClear(
ClearAttachmentPayload{.mask = GL_DEPTH_BUFFER_BIT, .depth = clearPayload.depth},
@@ -406,12 +571,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
VkRenderPassManager::HashType VkRenderPassManager::ComputeHash(
const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear) {
const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, Bool includePendingClear,
Bool includeDefaultFboDepthStencil) {
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
const Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
if (isDefaultFbo) {
XXHASH_VERIFY(XXH64_update(m_hashState, &swapchainImageIndex, sizeof(swapchainImageIndex)));
}
// sRGB attachments switch between their sRGB and UNORM-twin views with this
// capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats.
const Bool framebufferSrgbEnabled =
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
XXHASH_VERIFY(XXH64_update(m_hashState, &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled)));
auto& drawBuffers = fbo.GetDrawBuffers();
XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0])));
auto readBuffer = fbo.GetReadBuffer();
@@ -485,9 +656,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
attachment <= FramebufferAttachmentType::BackRight);
if (isDefaultColorAttachment) {
currentLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex);
// Content validity feeds the attachment's loadOp (see the
// creation path), so it must key the cache as well.
if (!m_swapchainObject.IsImageContentDefined(swapchainImageIndex)) {
currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
}
} else if (attachment == FramebufferAttachmentType::Depth ||
attachment == FramebufferAttachmentType::Stencil) {
currentLayout = m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex);
if (!m_swapchainObject.IsDepthStencilContentDefined(swapchainImageIndex)) {
currentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
}
}
} else {
auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
@@ -542,14 +721,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
combineFramebufferAttachmentObjHash(drawbuf);
}
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth);
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil);
// The depth-less default-FBO flavor omits the depth/stencil attachment
// entirely, so it must hash differently from the depth-full flavor.
const Bool depthStencilIncluded = !isDefaultFbo || includeDefaultFboDepthStencil;
XXHASH_VERIFY(XXH64_update(m_hashState, &depthStencilIncluded, sizeof(depthStencilIncluded)));
if (depthStencilIncluded) {
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Depth);
combineFramebufferAttachmentObjHash(FramebufferAttachmentType::Stencil);
}
return XXH64_digest(m_hashState);
}
RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex) {
Uint32 swapchainImageIndex,
Bool drawUsesDepthStencil) {
// Resolve the default-FBO depth flavor (see the header comment): keep the
// depth attachment when the caller needs it, when a depth/stencil clear is
// pending, or when the active pass already carries it (escalate-only, so
// alternating depth-less draws never split an established depth pass).
Bool includeDefaultFboDepthStencil = true;
if (fbo.IsDefaultFramebuffer()) {
Bool activeDefaultHasDepthStencil = false;
if (const auto* active = GetActiveRenderPass()) {
Bool activeIsSwapchainPass = false;
Bool activeHasSwapchainDepthStencil = false;
for (const auto& tracked : active->trackedAttachmentLayouts) {
activeIsSwapchainPass |= tracked.target == TrackedAttachmentTarget::SwapchainColor;
activeHasSwapchainDepthStencil |=
tracked.target == TrackedAttachmentTarget::SwapchainDepthStencil;
}
activeDefaultHasDepthStencil = activeIsSwapchainPass && activeHasSwapchainDepthStencil;
}
const auto& defaultDepthAtt = fbo.GetAttachment(FramebufferAttachmentType::Depth);
const auto& defaultStencilAtt = fbo.GetAttachment(FramebufferAttachmentType::Stencil);
const Bool pendingDepthStencilClear =
(defaultDepthAtt.IsTexture() && m_clearManager.HasPendingClear(defaultDepthAtt)) ||
HasPendingRenderbufferClear(defaultDepthAtt) ||
(defaultStencilAtt.IsTexture() && m_clearManager.HasPendingClear(defaultStencilAtt)) ||
HasPendingRenderbufferClear(defaultStencilAtt);
includeDefaultFboDepthStencil =
drawUsesDepthStencil || activeDefaultHasDepthStencil || pendingDepthStencilClear;
}
auto hasPendingClearOnFramebuffer = [&]() -> Bool {
const auto& drawBuffers = fbo.GetDrawBuffers();
for (auto attachment : drawBuffers) {
@@ -599,6 +813,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
m_rpFastRbEpoch == m_renderbufferImageEpoch &&
(!fbo.IsDefaultFramebuffer() || m_rpFastHadDepthStencil == includeDefaultFboDepthStencil) &&
m_rpFastRenderPassHash == activeRenderPass->hash && !hasPendingClearOnFramebuffer()) {
auto activeIt = m_renderPasses.find(activeRenderPass->hash);
if (activeIt != m_renderPasses.end()) {
@@ -607,7 +822,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
auto compatibilityHash = ComputeHash(fbo, swapchainImageIndex, false);
auto compatibilityHash = ComputeHash(fbo, swapchainImageIndex, false, includeDefaultFboDepthStencil);
if (activeRenderPass != nullptr &&
activeRenderPass->CompatibleWith(compatibilityHash) &&
!hasPendingClearOnFramebuffer()) {
@@ -624,10 +839,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
m_rpFastRbEpoch = m_renderbufferImageEpoch;
m_rpFastRenderPassHash = activeRenderPass->hash;
m_rpFastHadDepthStencil = activeIt->second.hasDepthStencilAttachment;
activeIt->second.lastUsedFrame = m_frameCounter;
return activeIt->second;
}
auto hash = ComputeHash(fbo, swapchainImageIndex, true);
auto hash = ComputeHash(fbo, swapchainImageIndex, true, includeDefaultFboDepthStencil);
auto it = m_renderPasses.find(hash);
if (it != m_renderPasses.end()) {
it->second.lastUsedFrame = m_frameCounter;
@@ -682,6 +898,88 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// assuming default FBO has the right param
for (Uint32 i = 0; i < colorAttachmentSlotCount; ++i) {
auto drawbuf = drawbufs[i];
// Renderbuffer color attachments mirror the texture path below, with the
// resource (image/view/format/layout) coming from the render-pass manager's
// renderbuffer store instead of the texture manager.
if (drawbuf != FramebufferAttachmentType::None && !isDefaultFbo) {
const auto& rbAtt = fbo.GetAttachment(drawbuf);
if (rbAtt.IsRenderbuffer() && rbAtt.IsComplete()) {
const auto& renderbuffer = rbAtt.GetRenderbuffer();
auto* rbResource = GetOrCreateRenderbufferResource(renderbuffer);
if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
MGLOG_E("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
"renderbuffer %u; using VK_ATTACHMENT_UNUSED",
i, fbo.GetExternalIndex(), renderbuffer->GetExternalIndex());
continue;
}
const Uint32 rbAttachmentIndex = static_cast<Uint32>(attachmentDescriptions.size());
attachmentDescriptions.emplace_back();
VkAttachmentDescription& rbDesc = attachmentDescriptions.back();
ClearAttachmentPayload rbClearPayload{};
Bool rbHasClear = GetPendingRenderbufferClear(renderbuffer.get(), rbClearPayload) &&
(rbClearPayload.mask & GL_COLOR_BUFFER_BIT) != 0;
if (rbHasClear &&
MG_Util::GetBaseInternalFormatComponentCount(renderbuffer->GetInternalFormat()) == 3) {
// RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1.
rbClearPayload.color =
FloatVec4(rbClearPayload.color.x(), rbClearPayload.color.y(),
rbClearPayload.color.z(), 1.0f);
}
const VkImageLayout trackedRbLayout = rbResource->layout;
const Bool rbFramebufferSrgb =
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
const VkFormat rbAttachmentFormat =
ResolveSrgbAttachmentWriteFormat(rbResource->format, rbFramebufferSrgb);
rbDesc.flags = 0;
rbDesc.format = rbAttachmentFormat;
rbDesc.samples = rbResource->sampleCount;
rbDesc.loadOp = rbHasClear ? VK_ATTACHMENT_LOAD_OP_CLEAR :
(trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ATTACHMENT_LOAD_OP_DONT_CARE
: VK_ATTACHMENT_LOAD_OP_LOAD);
rbDesc.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
rbDesc.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
rbDesc.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
rbDesc.initialLayout = (rbHasClear || trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED) ?
VK_IMAGE_LAYOUT_UNDEFINED : trackedRbLayout;
rbDesc.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
adoptRenderPassSampleCount(rbResource->sampleCount, "color",
static_cast<Int>(renderbuffer->GetExternalIndex()));
if (rbHasClear) {
pendingClearAttachments.emplace_back(PendingClearAttachmentInfo {
.attachmentIndex = rbAttachmentIndex,
.colorAttachmentSlot = i,
.renderbuffer = renderbuffer.get(),
.hasInlinePayload = true,
.inlinePayload = rbClearPayload,
});
}
if (width == 0)
width = static_cast<Int>(rbResource->extent.width);
if (height == 0)
height = static_cast<Int>(rbResource->extent.height);
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
.target = TrackedAttachmentTarget::Renderbuffer,
.renderbuffer = renderbuffer,
.finalLayout = rbDesc.finalLayout,
});
textureResources.emplace_back(nullptr);
attachmentViews.emplace_back(rbAttachmentFormat != rbResource->format ? rbResource->unormTwinView
: rbResource->view);
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
"GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i);
colorAttachmentRefs[i].attachment = rbAttachmentIndex;
continue;
}
}
auto* texture = ResolveCompleteColorAttachmentTexture(fbo, drawbuf, i);
if (texture == nullptr)
continue;
@@ -700,6 +998,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case TextureTarget::Texture2D:
case TextureTarget::Texture2DArray:
case TextureTarget::Texture2DMultisample:
case TextureTarget::Texture2DMultisampleArray:
case TextureTarget::Texture3D:
case TextureTarget::TextureCubeMap:
case TextureTarget::TextureCubeMapArray:
case TextureTarget::TextureRectangle: {
desc.flags = 0;
desc.format = isDefaultFbo ?
@@ -738,6 +1040,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(swapchainImageIndex < swapchainViews.size(),
"GetOrCreateRenderPass: swapchain image index out of range");
trackedColorLayout = m_swapchainObject.GetImageLayout(swapchainImageIndex);
// EGL: a presented color buffer's content is undefined when its
// image comes back around (EGL_BUFFER_DESTROYED, the default
// swap behaviour) - skip the tile load instead of reloading
// stale pixels nobody may rely on.
if (!hasClear && !m_swapchainObject.IsImageContentDefined(swapchainImageIndex)) {
trackedColorLayout = VK_IMAGE_LAYOUT_UNDEFINED;
}
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
.target = TrackedAttachmentTarget::SwapchainColor,
.swapchainImageIndex = swapchainImageIndex,
@@ -751,12 +1060,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(textureResource,
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at color attachment %d", i);
textureResources.emplace_back(textureResource);
desc.format = textureResource->format;
desc.format = ResolveSrgbAttachmentWriteFormat(
textureResource->format,
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));
attachmentSampleCount = textureResource->sampleCount;
trackedColorLayout = textureResource->layout;
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
.target = TrackedAttachmentTarget::Texture,
.texture = att.GetTexture(),
.textureRaw = att.GetTexture().get(),
.textureMipLevel = attachmentMipLevel,
.finalLayout = desc.finalLayout,
});
@@ -820,6 +1132,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
};
const auto* selectedDepthStencilAttachment = isUsableDepthStencilAttachment(depthAtt) ? &depthAtt :
(isUsableDepthStencilAttachment(stencilAtt) ? &stencilAtt : nullptr);
// Depth-less default-FBO flavor: nothing in this pass touches depth/stencil
// and their content is undefined anyway (EGL swap), so drop the attachment
// and its whole tile load + store.
if (isDefaultFbo && !includeDefaultFboDepthStencil) {
selectedDepthStencilAttachment = nullptr;
}
const Bool hasDistinctDepthAndStencilAttachments =
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
@@ -838,6 +1156,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkImageLayout trackedDepthLayout = isDefaultFbo ?
m_swapchainObject.GetDepthStencilImageLayout(swapchainImageIndex) :
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
// EGL 1.5 §3.10.1: every ancillary (depth/stencil) buffer's content is
// undefined after a swap, so the first default-FBO pass of a frame can
// skip the depth/stencil tile load outright.
if (isDefaultFbo && !m_swapchainObject.IsDepthStencilContentDefined(swapchainImageIndex)) {
trackedDepthLayout = VK_IMAGE_LAYOUT_UNDEFINED;
}
depthAttachmentDescription.flags = 0;
VkSampleCountFlagBits depthAttachmentSampleCount = VK_SAMPLE_COUNT_1_BIT;
Int depthAttachmentId = 0;
@@ -921,6 +1245,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
.target = TrackedAttachmentTarget::Texture,
.texture = selectedDepthStencilAttachment->GetTexture(),
.textureRaw = selectedDepthStencilAttachment->GetTexture().get(),
.textureMipLevel = attachmentMipLevel,
.finalLayout = depthAttachmentDescription.finalLayout,
});
@@ -966,6 +1291,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const Bool hasDepthStencilAttachment = depthAttachmentRef.attachment != VK_ATTACHMENT_UNUSED;
// Declare only the used colour-reference span. The GL draw-buffer array
// always spans 8 slots, so passes used to declare colorAttachmentCount=8
// with trailing VK_ATTACHMENT_UNUSED holes - and Adreno configures its
// per-pixel render-backend/export path from the DECLARED count, so every
// fragment of every pass paid the 8-target export cost (measured on
// Adreno 650 / MC 26.2: 11.9 -> 7.5 ms of GPU time per frame, with the
// single-quad swapchain blit pass alone dropping 1.26 -> 0.40 ms).
// Interior GL_NONE holes keep their slots so fragment-output locations
// still line up; a fragment output at a location past the trimmed count
// is discarded, which is exactly GL's semantic for writing to a draw
// buffer set to GL_NONE.
while (!colorAttachmentRefs.empty() &&
colorAttachmentRefs.back().attachment == VK_ATTACHMENT_UNUSED) {
colorAttachmentRefs.pop_back();
}
// Subpass
VkSubpassDescription subpassDesc;
subpassDesc.flags = 0;
@@ -1083,6 +1424,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void VkRenderPassManager::OnPresent() {
++m_frameCounter;
// Runs every frame boundary, ahead of the render-pass sweep gate below: the walk
// is O(#renderbuffer resources) — single digits in practice — and per-frame
// invocation keeps dead-resource reclaim latency at the aging bound instead of
// coupling it to renderbuffer *use* (the GetOrCreateRenderbufferResource call
// site never runs again once an app stops using renderbuffers).
CollectRenderbufferGarbage();
CollectDeferredRenderbufferReleases(/*destroyAll=*/false);
// Sweep occasionally; evict entries whose last use is far past every
// in-flight frame so their VkRenderPass/VkFramebuffer can be destroyed
// safely (RenderPassEntry's destructor releases the handles).
@@ -1092,6 +1441,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return;
}
// Collect the dying handles and notify once after the loop: pipelines hashed
// on them share the entries' >kRetireAgeFrames idleness (they are only bound
// by draws that hit those entries), so the observer may destroy them
// immediately - and a single batched notification costs one pipeline-cache
// scan instead of one per evicted pass.
Vector<VkRenderPass> destroyedRenderPasses;
const Uint64 activeHash = s_hasActiveRenderPass ? s_activeRenderPass.hash : 0;
for (auto it = m_renderPasses.begin(); it != m_renderPasses.end();) {
const Bool isActive = s_hasActiveRenderPass && it->first == activeHash;
@@ -1099,11 +1454,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (m_rpFastValid && m_rpFastRenderPassHash == it->first) {
m_rpFastValid = false;
}
destroyedRenderPasses.push_back(it->second.renderPass);
it = m_renderPasses.erase(it);
} else {
++it;
}
}
if (!destroyedRenderPasses.empty() && m_evictionObserver != nullptr) {
m_evictionObserver->OnRenderPassesDestroyed(destroyedRenderPasses);
}
}
Bool VkRenderPassManager::BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry) {
@@ -1156,6 +1515,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
renderPassBeginInfo.pClearValues = clearValues.data();
vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
// Pre-pass stream bookkeeping: this pass's attachment images are now
// referenced by the open frame recording.
if (s_textureManager != nullptr) {
for (const auto& tracked : renderPassEntry.trackedAttachmentLayouts) {
if (tracked.target == TrackedAttachmentTarget::Texture) {
if (const auto texture = tracked.texture.lock()) {
s_textureManager->StampTextureRecordingUse(texture.get());
}
}
}
}
for (const auto& pending: renderPassEntry.pendingClearAttachments) {
if (pending.hasInlinePayload) {
if (s_renderPassManager != nullptr) {
@@ -1208,11 +1578,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case TrackedAttachmentTarget::SwapchainColor:
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
s_swapchainObject->SetImageLayout(trackedAttachment.swapchainImageIndex, trackedAttachment.finalLayout);
// The pass stored into the attachment: its content is defined
// until the image is next presented.
s_swapchainObject->SetImageContentDefined(trackedAttachment.swapchainImageIndex, true);
break;
case TrackedAttachmentTarget::SwapchainDepthStencil:
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
s_swapchainObject->SetDepthStencilImageLayout(trackedAttachment.swapchainImageIndex,
trackedAttachment.finalLayout);
s_swapchainObject->SetDepthStencilContentDefined(trackedAttachment.swapchainImageIndex, true);
break;
default:
MOBILEGL_ASSERT(false, "EndRenderPass: unsupported tracked attachment target=%d",
@@ -42,6 +42,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
struct TrackedAttachmentLayoutInfo {
TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture;
WeakPtr<MG_State::GLState::ITextureObject> texture;
// Identity-compare shortcut for the per-draw "does the active pass use
// this sampled texture" probe: comparing this against a LIVE texture's
// address needs no weak_ptr::lock (two refcount atomics per probe).
// May dangle once the texture dies - compare only, never dereference.
MG_State::GLState::ITextureObject* textureRaw = nullptr;
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
Uint32 textureMipLevel = 0;
Uint32 swapchainImageIndex = 0;
@@ -157,19 +162,53 @@ namespace MobileGL::MG_Backend::DirectVulkan {
class VkRenderPassManager {
public:
using HashType = Uint64;
// Notified once per OnPresent sweep with every aged-out entry's VkRenderPass
// value: pipelines are hashed on the raw handle, and once destroyed the value
// may be recycled for an incompatible pass, so dependent caches must purge
// everything keyed on them before any new pass can be created (the sweep and
// the notification run back-to-back with no creation in between; observers
// compare the values, never dereference them). Batched so a mass-idle cohort
// (shader-pack switch, dimension exit) costs the observer one pipeline-cache
// scan, not one per dying pass. The wholesale paths
// (Shutdown/RecreateSwapchain) do not notify - their callers already drop
// every pipeline outright.
class IEvictionObserver {
public:
virtual ~IEvictionObserver() = default;
virtual void OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) = 0;
};
VkRenderPassManager(VkDevice device,
VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config,
VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject);
~VkRenderPassManager();
// Observer may be null (no notifications). Not owned.
void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }
Bool Initialize();
void Shutdown();
HashType ComputeHash(
const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex,
Bool includePendingClear = true);
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex);
Bool includePendingClear = true,
Bool includeDefaultFboDepthStencil = true);
// drawUsesDepthStencil: whether the operation about to run inside the pass
// reads or writes the depth/stencil buffer (depth test or stencil test
// enabled, or a depth/stencil clear). Only consulted for the DEFAULT
// framebuffer: EGL undefines its ancillary buffers at every swap, so a
// default-FBO pass whose draws provably never touch depth/stencil is
// created WITHOUT the depth attachment - on a tiler that skips the whole
// depth tile load AND store. The flavor only escalates: once a pass with
// depth is active, later depth-less draws keep using it, and a depth-using
// draw against a depth-less active pass resolves to a new (incompatible)
// entry, which the caller's compatibility check turns into a pass split;
// the new pass's depth loads DONT_CARE (content was undefined all along).
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
Uint32 swapchainImageIndex,
Bool drawUsesDepthStencil = true);
void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
const MG_State::GLState::FramebufferObject& drawFbo);
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
@@ -192,12 +231,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
UnorderedMap<Uint64, RenderPassEntry> m_renderPasses;
// Monotonic frame counter (bumped in OnPresent) for render-pass cache aging.
Uint64 m_frameCounter = 0;
IEvictionObserver* m_evictionObserver = nullptr;
// Bumped whenever a renderbuffer VkImage is (re)created; together with the texture
// manager's image epoch this invalidates the render-pass fast path on any attachment
// image recreation.
Uint64 m_renderbufferImageEpoch = 1;
public:
// Bumped whenever a renderbuffer backing is (re)created; consecutive-draw
// snapshots include it so an attachment respecify forces a re-resolve.
Uint64 GetRenderbufferImageEpoch() const { return m_renderbufferImageEpoch; }
private:
// Per-draw fast-path memo for GetOrCreateRenderPass (dirty-flag state tracking): when the
// framebuffer state is provably unchanged since the last resolution, the active render pass
// is reused WITHOUT recomputing the expensive per-draw hash. Invalidated by FBO switch /
@@ -210,12 +257,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 m_rpFastTexEpoch = 0;
Uint64 m_rpFastRbEpoch = 0;
Uint64 m_rpFastRenderPassHash = 0;
// Whether the memoized entry carries a depth/stencil attachment; a
// default-FBO resolution whose effective depth request differs must
// miss the memo (the depth-less/depth-full flavors hash differently).
Bool m_rpFastHadDepthStencil = false;
public:
struct RenderbufferResource {
// deadSinceFrame sentinel: the owning weak reference has not been observed
// expired. Dead resources age past every in-flight frame before Destroy
// (see CollectRenderbufferGarbage); the GPU may still reference the image
// for frames-in-flight frames after the GL object dies.
static constexpr Uint64 kNeverObservedDead = UINT64_MAX;
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
VkImage image = VK_NULL_HANDLE;
VmaAllocation allocation = nullptr;
VkImageView view = VK_NULL_HANDLE;
// UNORM reinterpretation of an sRGB image, used as the attachment view while
// GL_FRAMEBUFFER_SRGB is disabled (raw writes). Null for non-sRGB formats.
VkImageView unormTwinView = VK_NULL_HANDLE;
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
@@ -223,25 +284,51 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
Int samples = 0;
// m_frameCounter value at which the weak reference was first seen expired.
Uint64 deadSinceFrame = kNeverObservedDead;
void Destroy(VkDevice device, VmaAllocator allocator);
};
// Public so the renderer's blit/copy/readback bindings can source renderbuffer
// attachments the same way texture attachments go through the texture manager.
RenderbufferResource* GetOrCreateRenderbufferResource(
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer,
ClearAttachmentPayload& outPayload) const;
private:
struct PendingRenderbufferClear {
WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
ClearAttachmentPayload payload{};
};
// A superseded renderbuffer backing (glRenderbufferStorage respecify) parked
// until enough frame boundaries have passed that no in-flight command buffer
// can still reference it; destroyed in OnPresent (see RetireAgeFrames).
struct DeferredRenderbufferRelease {
VkImage image = VK_NULL_HANDLE;
VmaAllocation allocation = nullptr;
VkImageView view = VK_NULL_HANDLE;
VkImageView unormTwinView = VK_NULL_HANDLE;
Uint64 deferredAtFrame = 0;
};
UnorderedMap<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
// Supported sample counts per attachment format, so per-draw resource lookups
// do not repeat vkGetPhysicalDeviceImageFormatProperties.
UnorderedMap<VkFormat, VkSampleCountFlags> m_attachmentSampleCountsByFormat;
RenderbufferResource* GetOrCreateRenderbufferResource(
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer,
ClearAttachmentPayload& outPayload) const;
Bool HasPendingRenderbufferClear(
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
void CollectRenderbufferGarbage();
// Frame-boundary margin after which a resource last referenced by a retired
// GL object (or superseded backing) is provably past every in-flight frame.
Uint64 RetireAgeFrames() const;
void DeferRenderbufferBackingRelease(RenderbufferResource& resource);
void CollectDeferredRenderbufferReleases(Bool destroyAll);
static inline XXH64_state_t* m_hashState = XXH64_createState();
static inline ActiveRenderPassInfo s_activeRenderPass{};
@@ -51,6 +51,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Float ResolveEffectiveMinLod(const MG_State::GLState::SamplerObject& sampler, Float effectiveMaxLod) {
return std::min(sampler.GetMinLod(), effectiveMaxLod);
}
// A single-level view can only ever deliver the base level, but the LOD clamp must not be
// collapsed to exactly 0: both GL and Vulkan pick magFilter over minFilter from the
// *clamped* lambda, so maxLod = 0 would make every fragment magnify and quietly retire the
// min filter. 0.25 is the value VkSamplerCreateInfo's own note prescribes for emulating
// GL's non-mipmapped minification - large enough for lambda to stay positive, small enough
// that a NEAREST mip mode still rounds down to level 0. Clamped rather than assigned, so a
// texture whose GL_TEXTURE_MAX_LOD really is 0 keeps magnifying as GL says it must.
Float ResolveSingleLevelMaxLod(const MG_State::GLState::SamplerObject& sampler, Bool singleLevelView) {
const Float maxLod = ResolveEffectiveMaxLod(sampler);
return singleLevelView ? std::min(maxLod, 0.25f) : maxLod;
}
} // namespace
Bool VkSamplerManager::Initialize(const InitInfo& initInfo) {
@@ -89,15 +101,43 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_device = VK_NULL_HANDLE;
m_config = nullptr;
m_frameBoundaryCounter = 0;
}
void VkSamplerManager::OnFrameBoundary() {
++m_frameBoundaryCounter;
// Sweep occasionally; destroy samplers whose last use is far past every
// in-flight frame. Destroy and erase must stay atomic, or Shutdown would
// double-free the handle; an evicted key that recurs simply re-creates
// its sampler on the next miss.
constexpr Uint64 kSweepInterval = 256;
constexpr Uint64 kRetireAgeBoundaries = 1024;
if ((m_frameBoundaryCounter % kSweepInterval) != 0) {
return;
}
for (auto it = m_samplers.begin(); it != m_samplers.end();) {
auto& entry = it->second;
if (m_frameBoundaryCounter - entry.lastUsedFrameBoundary > kRetireAgeBoundaries) {
if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) {
vkDestroySampler(m_device, entry.handle, nullptr);
}
it = m_samplers.erase(it);
} else {
++it;
}
}
}
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering) const {
Bool forceNearestFiltering, Bool singleLevelView) const {
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
XXHASH_VERIFY(XXH64_update(m_hashState, &forceNearestFiltering, sizeof(forceNearestFiltering)));
XXHASH_VERIFY(XXH64_update(m_hashState, &singleLevelView, sizeof(singleLevelView)));
const auto minFilter = sampler.GetMinFilter();
XXHASH_VERIFY(XXH64_update(m_hashState, &minFilter, sizeof(minFilter)));
@@ -111,7 +151,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapT, sizeof(wrapT)));
const auto wrapR = sampler.GetWrapR();
XXHASH_VERIFY(XXH64_update(m_hashState, &wrapR, sizeof(wrapR)));
const auto maxLod = ResolveEffectiveMaxLod(sampler);
const auto maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
const auto minLod = ResolveEffectiveMinLod(sampler, maxLod);
XXHASH_VERIFY(XXH64_update(m_hashState, &minLod, sizeof(minLod)));
XXHASH_VERIFY(XXH64_update(m_hashState, &maxLod, sizeof(maxLod)));
@@ -133,10 +173,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering) {
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering);
Bool forceNearestFiltering, Uint32 viewLevelCount) {
// A view that exposes a single mip level has no second level to blend with, so GL's
// *_MIPMAP_* minification filters degenerate to plain filtering on the base level -
// sampling is unchanged by pinning the Vulkan sampler to NEAREST mip mode at LOD 0.
// It is not cosmetic: MobileGL backs such a view with a fully allocated mip chain whose
// tail is never written, and a LINEAR mip mode lets the texture unit issue the level+1
// fetch anyway. On Adreno that fetch lands in uninitialized UBWC pages (or past the
// allocation for a genuinely single-level image) and faults the GPU - the same failure
// the default-framebuffer blit shader had to work around with an explicit-LOD sample.
const Bool singleLevelView = viewLevelCount == 1;
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering, singleLevelView);
auto it = m_samplers.find(key);
if (it != m_samplers.end()) {
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
return it->second.handle;
}
@@ -144,8 +194,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
samplerInfo.magFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMagFilter());
samplerInfo.minFilter = forceNearestFiltering ? VK_FILTER_NEAREST : ToVkFilter(sampler.GetMinFilter());
samplerInfo.mipmapMode = forceNearestFiltering ? VK_SAMPLER_MIPMAP_MODE_NEAREST
: ToVkMipmapMode(sampler.GetMipmapMode());
samplerInfo.mipmapMode = (forceNearestFiltering || singleLevelView)
? VK_SAMPLER_MIPMAP_MODE_NEAREST
: ToVkMipmapMode(sampler.GetMipmapMode());
samplerInfo.addressModeU = ToVkAddressMode(sampler.GetWrapS());
samplerInfo.addressModeV = ToVkAddressMode(sampler.GetWrapT());
samplerInfo.addressModeW = ToVkAddressMode(sampler.GetWrapR());
@@ -157,7 +208,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
samplerInfo.maxAnisotropy = maxAnisotropy;
samplerInfo.compareEnable = sampler.GetCompareMode() == SamplerCompareMode::CompareToTexture ? VK_TRUE : VK_FALSE;
samplerInfo.compareOp = ToVkCompareOp(ResolveCompareFunc(sampler, texture));
samplerInfo.maxLod = ResolveEffectiveMaxLod(sampler);
// Must match BuildSamplerKey's resolution exactly.
samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture);
samplerInfo.unnormalizedCoordinates = VK_FALSE;
@@ -169,6 +221,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.handle = vkSampler;
entry.externalIndex = sampler.GetExternalIndex();
entry.version = sampler.GetVersion();
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
m_samplers[key] = entry;
return vkSampler;
}
@@ -33,20 +33,38 @@ public:
Bool Initialize(const InitInfo& initInfo);
void Shutdown();
// viewLevelCount is the mip-level count of the image view this sampler will be paired
// with; 0 means "unknown, do not narrow". See GetOrCreateSampler for why it matters.
VkSampler GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering = false);
Bool forceNearestFiltering = false,
Uint32 viewLevelCount = 0);
// Frame boundary hook: ages the sampler cache and destroys samplers not used
// for many frames. The key hashes continuous float state (lodBias, LOD clamps,
// anisotropy), so an app animating those would otherwise mint an unbounded
// stream of never-destroyed VkSamplers and eventually exhaust the device's
// maxSamplerAllocationCount. A sampler idle for over a thousand frame
// boundaries cannot be referenced by any in-flight command buffer (frames in
// flight are single digits), and every descriptor set the GPU consumes is
// written that same frame with live handles (the per-binding resolve memo and
// descriptor-set reuse are both frame-reset), so destruction here needs no
// fence wait. Self-gated: one counter bump and compare except on sweep
// boundaries.
void OnFrameBoundary();
private:
struct SamplerCacheEntry {
VkSampler handle = VK_NULL_HANDLE;
Uint externalIndex = 0;
Uint16 version = 0;
// Frame boundary of the last cache hit; entries idle past the
// OnFrameBoundary retirement age have their VkSampler destroyed.
Uint64 lastUsedFrameBoundary = 0;
};
Uint64 BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
const MG_State::GLState::ITextureObject& texture,
Bool forceNearestFiltering) const;
Bool forceNearestFiltering, Bool singleLevelView) const;
static VkFilter ToVkFilter(SamplerFilterMode mode);
static VkSamplerMipmapMode ToVkMipmapMode(SamplerMipmapMode mode);
static VkSamplerAddressMode ToVkAddressMode(SamplerWrapMode mode);
@@ -67,6 +85,8 @@ private:
Bool m_samplerAnisotropySupported = false;
Float m_maxSamplerAnisotropy = 1.0f;
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameBoundaryCounter = 0;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -120,31 +120,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
switch (requestedSamples) {
case 1:
// GL promises "at least the requested samples", so a non-power-of-two
// request (legal in GL, e.g. 3) rounds up to the next Vulkan bit.
if (requestedSamples <= 1) {
outSampleCount = VK_SAMPLE_COUNT_1_BIT;
return true;
case 2:
outSampleCount = VK_SAMPLE_COUNT_2_BIT;
return true;
case 4:
outSampleCount = VK_SAMPLE_COUNT_4_BIT;
return true;
case 8:
outSampleCount = VK_SAMPLE_COUNT_8_BIT;
return true;
case 16:
outSampleCount = VK_SAMPLE_COUNT_16_BIT;
return true;
case 32:
outSampleCount = VK_SAMPLE_COUNT_32_BIT;
return true;
case 64:
outSampleCount = VK_SAMPLE_COUNT_64_BIT;
return true;
default:
}
if (requestedSamples > 64) {
return false;
}
Uint32 bit = 1;
while (bit < static_cast<Uint32>(requestedSamples)) {
bit <<= 1;
}
outSampleCount = static_cast<VkSampleCountFlagBits>(bit);
return true;
}
static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) {
@@ -375,7 +365,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
switch (format) {
case TextureInternalFormat::RGB:
case TextureInternalFormat::RGB8:
// Legacy low-bit RGB formats share the UNorm8 canonical shadow layout (see
// TextureFormatProcessor), so they upload exactly like RGB8 with an alpha expand.
case TextureInternalFormat::R3G3B2:
case TextureInternalFormat::RGB4:
case TextureInternalFormat::RGB5:
return {VK_FORMAT_R8G8B8A8_UNORM, true, 1, {0xFF, 0x00, 0x00, 0x00}};
// Low-bit RGBA formats: UNorm8x4 canonical shadow, no expansion needed.
case TextureInternalFormat::RGBA2:
case TextureInternalFormat::RGBA4:
case TextureInternalFormat::RGB5A1:
return {VK_FORMAT_R8G8B8A8_UNORM, false, 0, {0, 0, 0, 0}};
// 10/12-bit RGB(A): UNorm16 canonical shadow.
case TextureInternalFormat::RGB10:
case TextureInternalFormat::RGB12:
return {VK_FORMAT_R16G16B16A16_UNORM, true, 2, {0xFF, 0xFF, 0x00, 0x00}};
case TextureInternalFormat::RGBA12:
return {VK_FORMAT_R16G16B16A16_UNORM, false, 0, {0, 0, 0, 0}};
case TextureInternalFormat::SRGB8:
return {VK_FORMAT_R8G8B8A8_SRGB, true, 1, {0xFF, 0x00, 0x00, 0x00}};
case TextureInternalFormat::RGB8Snorm:
@@ -571,6 +577,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_allocator = initInfo.allocator;
m_commandPool = initInfo.commandPool;
m_graphicsQueue = initInfo.graphicsQueue;
m_imageFormatListSupported = initInfo.imageFormatListSupported;
m_currentFrameIndex = 0;
m_deferredReleases.clear();
m_deferredReleases.resize(initInfo.frameCount);
@@ -590,9 +597,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void VkTextureManager::Shutdown() {
if (m_device != VK_NULL_HANDLE) {
ReclaimCompletedUploads(/*waitAll=*/true);
}
DestroyDeferredReleases();
++m_resourceEraseEpoch; // every memoized resource pointer dies with the map
m_textureResources.clear();
m_aliveObjects.clear();
m_storageImageTextures.clear();
m_device = VK_NULL_HANDLE;
m_physicalDevice = VK_NULL_HANDLE;
@@ -611,6 +623,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
frameIndex, m_deferredViewReleases.size());
m_currentFrameIndex = frameIndex;
CollectDeferredReleases(frameIndex);
ReclaimCompletedUploads();
// Frame-boundary GC: every 64 frame boundaries (~1 s at 60 fps) bounds the reclaim
// latency for dead textures regardless of draw traffic — workloads that churn
// textures through clears/readbacks alone never reach the draw-gated
// CollectGarbage. Must run after CollectDeferredReleases above: the prune defers
// its releases into this frame's slot, which was just drained, so they are
// destroyed only after the slot's fence has been waited again one full frame-ring
// cycle from now (never while an in-flight frame may still reference them).
constexpr Uint32 kGcFrameInterval = 64;
++m_gcFrameCounter;
if (m_gcFrameCounter % kGcFrameInterval == 0) {
PruneDeadTextures();
}
}
void VkTextureManager::CollectAllDeferredReleases() {
const SizeT frameCount = std::min(m_deferredReleases.size(), m_deferredViewReleases.size());
for (SizeT frameIndex = 0; frameIndex < frameCount; ++frameIndex) {
CollectDeferredReleases(static_cast<Uint32>(frameIndex));
}
}
void VkTextureManager::EraseTrackedTexture(const TextureIdentity& identity) {
@@ -620,6 +653,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_textureResources.erase(resourceIt);
}
m_aliveObjects.erase(identity);
m_storageImageTextures.erase(identity);
// Invalidate every cross-draw sampled-texture memo: the erased
// resource's address may be reused by a future emplace.
++m_resourceEraseEpoch;
}
void VkTextureManager::PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture) {
@@ -675,32 +712,63 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
auto aliveIt = m_aliveObjects.find(identity);
if (aliveIt != m_aliveObjects.end() && aliveIt->second.expired()) {
EraseTrackedTexture(aliveIt->first);
aliveIt = m_aliveObjects.end();
}
// Only (re)register and prune when this (texture, lifetime) pair is new: stale
// aliases can only come into existence through an address reuse, which by
// construction introduces a new identity. Doing this unconditionally made every
// sampled-texture sync scan the entire alive-texture map per draw.
if (aliveIt == m_aliveObjects.end()) {
const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());
if (liveTexture && liveTexture.get() == &texture) {
m_aliveObjects[identity] = WeakPtr<MG_State::GLState::ITextureObject>(liveTexture);
PruneStaleTextureAliases(&texture);
// Cross-draw memo probe (see SyncedTextureMemoEntry): skips both map
// lookups and the (re)registration path for repeat-bound textures.
TextureResource* resourcePtr = nullptr;
for (Uint32 i = 0; i < kSyncedTextureMemoSize; ++i) {
const SyncedTextureMemoEntry& memo = m_syncedTextureMemo[i];
if (memo.texture == &texture && memo.lifetimeId == identity.lifetimeId &&
memo.eraseEpoch == m_resourceEraseEpoch) {
resourcePtr = memo.resource;
break;
}
}
auto it = m_textureResources.find(identity);
if (it == m_textureResources.end()) {
TextureResource initial{};
auto [insertIt, _] = m_textureResources.emplace(identity, Move(initial));
it = insertIt;
if (resourcePtr == nullptr) {
auto aliveIt = m_aliveObjects.find(identity);
if (aliveIt != m_aliveObjects.end() && aliveIt->second.expired()) {
EraseTrackedTexture(aliveIt->first);
aliveIt = m_aliveObjects.end();
}
// Only (re)register and prune when this (texture, lifetime) pair is new: stale
// aliases can only come into existence through an address reuse, which by
// construction introduces a new identity. Doing this unconditionally made every
// sampled-texture sync scan the entire alive-texture map per draw.
if (aliveIt == m_aliveObjects.end()) {
WeakPtr<MG_State::GLState::ITextureObject> aliveTexture;
const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());
if (liveTexture && liveTexture.get() == &texture) {
aliveTexture = liveTexture;
} else {
// The name lookup legally fails while the object is alive: the name was
// deleted with the texture still attached to an FBO (the attachment's
// SharedPtr keeps it alive), or the name was reused by a new texture, or
// this is a default texture object (name 0 lives outside the name map).
// Register through the object's own control block so the resource created
// below still participates in weak-expiry GC instead of becoming an
// orphan no reclamation path can reach until Shutdown.
aliveTexture = texture.weak_from_this();
}
if (!aliveTexture.expired()) {
m_aliveObjects[identity] = Move(aliveTexture);
PruneStaleTextureAliases(&texture);
}
}
auto it = m_textureResources.find(identity);
if (it == m_textureResources.end()) {
TextureResource initial{};
auto [insertIt, _] = m_textureResources.emplace(identity, Move(initial));
it = insertIt;
}
resourcePtr = &(it->second);
m_syncedTextureMemo[m_syncedTextureMemoNext] =
SyncedTextureMemoEntry{&texture, identity.lifetimeId, m_resourceEraseEpoch, resourcePtr};
m_syncedTextureMemoNext = (m_syncedTextureMemoNext + 1) % kSyncedTextureMemoSize;
}
if (!SyncTexture(texture, it->second)) {
if (!SyncTexture(texture, *resourcePtr)) {
MGLOG_D("%s: Syncing texture %d failed", __func__, texture.GetExternalIndex());
return nullptr;
}
@@ -714,11 +782,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
if (!recorded) {
m_drawSyncedThisDraw.push_back({identity, &(it->second)});
m_drawSyncedThisDraw.push_back({identity, resourcePtr});
}
}
return &(it->second);
return resourcePtr;
}
VkImageView VkTextureManager::GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel) {
@@ -762,7 +830,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return VK_NULL_HANDLE;
}
if (baseArrayLayer == 0 && layerCount == resource->arrayLayers && viewType == resource->viewType) {
const Bool framebufferSrgbEnabled =
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
const VkFormat attachmentFormat = ResolveSrgbAttachmentWriteFormat(resource->format, framebufferSrgbEnabled);
if (attachmentFormat == resource->format && baseArrayLayer == 0 && layerCount == resource->arrayLayers &&
viewType == resource->viewType) {
return GetOrCreateViewAtMipLevel(texture, mipLevel);
}
@@ -771,6 +844,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.baseArrayLayer = baseArrayLayer,
.layerCount = layerCount,
.viewType = viewType,
.viewFormat = attachmentFormat,
};
auto it = resource->attachmentViews.find(key);
if (it == resource->attachmentViews.end()) {
@@ -781,7 +855,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return attachmentView;
}
attachmentView = CreateImageView(resource->image, resource->format, resource->aspect, viewType,
attachmentView = CreateImageView(resource->image, attachmentFormat, resource->aspect, viewType,
mipLevel, 1, baseArrayLayer, layerCount);
if (attachmentView == VK_NULL_HANDLE) {
MGLOG_D("%s: CreateImageView failed for textureId=%d mipLevel=%u baseArrayLayer=%u layerCount=%u viewType=%d",
@@ -997,6 +1071,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return view;
}
void VkTextureManager::StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) {
return;
}
auto it = m_textureResources.find(MakeTextureIdentity(texture));
if (it != m_textureResources.end()) {
it->second.lastRecordingGeneration = m_recordingGeneration;
}
}
void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) {
MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null");
auto it = m_textureResources.find(MakeTextureIdentity(texture));
@@ -1024,6 +1108,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(writtenMipLevel < resource.mipLevels,
"UpdateTrackedImageLayoutAfterAttachmentWrite: textureId=%d mipLevel=%u out of range %u",
texture->GetExternalIndex(), writtenMipLevel, resource.mipLevels);
// Pre-pass stream bookkeeping: the render pass that just ended wrote this image.
StampResourceRecordingUse(resource);
if (resource.layout != newLayout && resource.mipLevels > 1) {
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
@@ -1108,6 +1194,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
resource->arrayLayers);
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
StampResourceRecordingUse(*resource);
return ok;
}
@@ -1137,9 +1225,52 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource->aspect, 0, resource->mipLevels, resource->arrayLayers);
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
texture.GetExternalIndex());
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
StampResourceRecordingUse(*resource);
return ok;
}
void VkTextureManager::MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture) {
m_storageImageTextures.insert(MakeTextureIdentity(&texture));
}
Bool VkTextureManager::NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const {
const TextureIdentity identity = MakeTextureIdentity(&texture);
if (m_storageImageTextures.find(identity) == m_storageImageTextures.end()) {
return false;
}
const auto it = m_textureResources.find(identity);
// No image yet: the first sync creates it with STORAGE straight away, so there is nothing
// to preserve and nothing to order against.
return it != m_textureResources.end() && it->second.image != VK_NULL_HANDLE &&
!it->second.storageUsageResolved;
}
Bool VkTextureManager::NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const {
const TextureIdentity identity = MakeTextureIdentity(&texture);
const auto it = m_textureResources.find(identity);
if (it == m_textureResources.end()) {
return true;
}
const TextureResource& resource = it->second;
if (resource.image == VK_NULL_HANDLE || resource.layout != VK_IMAGE_LAYOUT_GENERAL) {
return true;
}
// The image predates this texture's first image-unit binding, so it was created without
// STORAGE usage and has to be recreated - which is illegal inside a render pass.
if (!resource.storageUsageResolved &&
m_storageImageTextures.find(identity) != m_storageImageTextures.end()) {
return true;
}
// Mirror SyncTexture's cross-draw skip condition: any version drift means the sync
// path may upload or rebuild, both of which need the render pass ended first.
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u;
return resource.syncedContentVersion != texture.GetContentVersion() ||
resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() ||
resource.syncedMipLevelCount != mipLevelCount;
}
Bool VkTextureManager::TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image,
VkImageLayout& trackedLayout, VkImageLayout newLayout,
VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask,
@@ -1178,10 +1309,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
SizeT VkTextureManager::CollectGarbage() {
// Draw-gated stagger (1 in 256 calls): keeps the per-draw cost at one counter
// bump. The guaranteed reclaim path is the frame-boundary prune in BeginFrame;
// this remains as a cheap assist so draw-heavy workloads reclaim sooner.
m_gcCounter++;
if (m_gcCounter != 0) {
return 0;
}
return PruneDeadTextures();
}
SizeT VkTextureManager::PruneDeadTextures() {
// Erasing entries would dangle the raw TextureResource pointers memoized for the
// current draw; every call path (BeginFrame, and CollectGarbage at the top of a
// freshly opened draw-sync scope) runs before any memo entry is recorded.
MOBILEGL_ASSERT(m_drawSyncedThisDraw.empty(),
"PruneDeadTextures: draw-sync memo holds raw resource pointers an erase would dangle");
Vector<MG_State::GLState::ITextureObject*> expiredTextures;
expiredTextures.reserve(m_aliveObjects.size());
@@ -1193,7 +1336,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (auto* texture : expiredTextures) {
PruneStaleTextureAliases(texture);
}
return expiredTextures.size();
SizeT prunedCount = expiredTextures.size();
// Orphan sweep: after the pass above, m_aliveObjects holds only live entries.
// Registration in SyncTextureAndGetDescriptor cannot fail for a SharedPtr-owned
// texture (weak_from_this fallback), so a resource whose identity has no alive
// entry has no trackable owner: its GL-side object is gone, or was never
// shared-owned, in which case recreation on a later sync is the safe fallback.
// Destruction goes through the per-frame deferred queues, never immediate.
Vector<TextureIdentity> orphanIdentities;
for (auto it = m_textureResources.begin(); it != m_textureResources.end(); ++it) {
if (m_aliveObjects.find(it->first) == m_aliveObjects.end()) {
orphanIdentities.emplace_back(it->first);
}
}
for (const auto& identity : orphanIdentities) {
EraseTrackedTexture(identity);
}
prunedCount += orphanIdentities.size();
return prunedCount;
}
Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture,
@@ -1207,7 +1368,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture);
const Uint32 syncingMipLevelCount =
syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u;
if (outResource.image != VK_NULL_HANDLE &&
// A pending storage-usage upgrade also has to bust the skip: nothing about the texture's
// content or params changed, but the image itself must be recreated with STORAGE usage
// before it can back an image-unit descriptor.
const Bool storageUpgradePending =
!outResource.storageUsageResolved &&
m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end();
if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending &&
outResource.syncedContentVersion == syncingContentVersion &&
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
outResource.syncedMipLevelCount == syncingMipLevelCount) {
@@ -1277,11 +1444,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const IntVec3 &texelSize, SizeT byteSize, Uint32 mipLevels,
TextureResource &resource) {
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
const VkFormat format = formatInfo.format;
VkFormat format = formatInfo.format;
if (format == VK_FORMAT_UNDEFINED) {
MGLOG_D("%s: format == VK_FORMAT_UNDEFINED", __func__);
return false;
}
// X8_D24 lacks optimal-tiling support on several drivers (lavapipe included);
// D32_SFLOAT holds every 24-bit depth value exactly, and the upload path
// converts the shadow words to float (see the pure-depth branch below).
if (format == VK_FORMAT_X8_D24_UNORM_PACK32) {
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
constexpr VkFormatFeatureFlags kDepthAttachmentAndSample =
VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT;
if ((formatProperties.optimalTilingFeatures & kDepthAttachmentAndSample) != kDepthAttachmentAndSample) {
format = VK_FORMAT_D32_SFLOAT;
}
}
if (texelSize.x() <= 0 || texelSize.y() <= 0 /*|| byteSize == 0*/) {
MGLOG_D("%s: texelSize or byteSize is zero", __func__);
return false;
@@ -1291,8 +1470,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
const Bool isMultisampleTexture = IsMultisampleTextureUploadTarget(uploadTarget);
// A texture that has only ever defined level 0 gets a single-level backing
// (ANGLE's model). Preallocating the full chain put every render target
// onto Adreno's multi-mip image layout and grew each texture by a third
// for levels most textures never define. Once a second level is defined
// the backing is recreated ONE time with the full chain (the
// preserve-copy path below carries the pixels over), so sequentially-
// defined atlas mips do not recreate per level, and glGenerateMipmap -
// which defines every level before syncing - works unchanged.
const Uint32 backingMipLevels =
isMultisampleTexture ? 1u : std::max(mipLevels, ComputeFullMipLevelCount(texelSize));
isMultisampleTexture ? 1u
: (mipLevels > 1 ? std::max(mipLevels, ComputeFullMipLevelCount(texelSize)) : 1u);
TextureShapeInfo shapeInfo{};
const Bool supportedShape = TryResolveTextureShapeInfo(texture, uploadTarget, texelSize, shapeInfo);
MOBILEGL_ASSERT(supportedShape,
@@ -1318,14 +1506,89 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkImageAspectFlags aspect = GetAspectMaskForFormat(format);
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
const Bool supportsStorageImage =
// Only textures that have actually been bound to a GL image unit get STORAGE usage (and
// the MUTABLE_FORMAT it drags in for format-reinterpreting image views). Requesting it
// for every storage-capable colour texture costs real bandwidth: Adreno cannot keep UBWC
// compression on an image that may be written through a storage descriptor, so the whole
// render target - MC's included - runs uncompressed. MarkStorageImageTexture upgrades a
// texture before its first image-unit draw, and the usage below feeds the compatibility
// check so the upgrade recreates the image.
const Bool markedAsStorageImage =
m_storageImageTextures.find(MakeTextureIdentity(
const_cast<MG_State::GLState::ITextureObject*>(&texture))) != m_storageImageTextures.end();
// Storage-image CAPABILITY (does the format allow it at all) is deliberately separate from
// whether this texture actually needs the usage. MUTABLE_FORMAT keys off capability, as
// before: format-reinterpreting views are not a storage-only concern - the SAMPLED path
// needs them too (GetOrCreateSampledImageView bails out without it, see ~line 892), so
// tying MUTABLE_FORMAT to the image-unit mark would break sampled format reinterpretation
// for every texture that never becomes a storage image.
const Bool storageImageCapable =
!isMultisampleTexture &&
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
const Bool supportsStorageImage = storageImageCapable && markedAsStorageImage;
VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags;
if (supportsStorageImage && IsMutableStorageImageFormat(format)) {
if (storageImageCapable && IsMutableStorageImageFormat(format) &&
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
}
// sRGB color images attach through their UNORM twin while GL_FRAMEBUFFER_SRGB is
// disabled (see ResolveSrgbAttachmentWriteFormat), which needs format-reinterpreting
// views - multisample sRGB render targets included.
if (ResolveSrgbAttachmentWriteFormat(format, false) != format &&
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
}
VkImageUsageFlags desiredUsage =
VK_IMAGE_USAGE_SAMPLED_BIT |
(supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) |
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) |
(((aspect & VK_IMAGE_ASPECT_DEPTH_BIT) || (aspect & VK_IMAGE_ASPECT_STENCIL_BIT)) ?
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT :
0);
if (!isMultisampleTexture) {
desiredUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
}
// Round a multisample request up to a count the device supports for this
// format (GL only promises "at least"), mirroring the renderbuffer path.
if (isMultisampleTexture && resolvedSampleCount != VK_SAMPLE_COUNT_1_BIT) {
auto supportedIt = m_multisampleCountsByFormat.find(format);
if (supportedIt == m_multisampleCountsByFormat.end()) {
VkImageFormatProperties imageFormatProperties{};
VkSampleCountFlags supported = VK_SAMPLE_COUNT_1_BIT;
if (vkGetPhysicalDeviceImageFormatProperties(m_physicalDevice, format, shapeInfo.imageType,
VK_IMAGE_TILING_OPTIMAL, desiredUsage, imageCreateFlags,
&imageFormatProperties) == VK_SUCCESS) {
supported = imageFormatProperties.sampleCounts;
}
supportedIt = m_multisampleCountsByFormat.emplace(format, supported).first;
}
const VkSampleCountFlags supported = supportedIt->second;
if ((supported & resolvedSampleCount) == 0) {
Uint32 rounded = 0;
for (Uint32 bit = static_cast<Uint32>(resolvedSampleCount) << 1; bit <= VK_SAMPLE_COUNT_64_BIT;
bit <<= 1) {
if ((supported & bit) != 0) {
rounded = bit;
break;
}
}
if (rounded == 0) {
for (Uint32 bit = static_cast<Uint32>(resolvedSampleCount) >> 1; bit != 0; bit >>= 1) {
if ((supported & bit) != 0) {
rounded = bit;
break;
}
}
}
if (rounded != 0) {
resolvedSampleCount = static_cast<VkSampleCountFlagBits>(rounded);
}
}
}
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format &&
resource.extent.width == static_cast<Uint32>(texelSize.x()) &&
@@ -1335,6 +1598,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.viewType == shapeInfo.viewType &&
resource.sampleCount == resolvedSampleCount &&
resource.imageCreateFlags == imageCreateFlags &&
resource.usageFlags == desiredUsage &&
resource.mipLevels == backingMipLevels;
if (compatible) {
if (resource.perMipViews.size() != backingMipLevels) {
@@ -1343,6 +1607,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (resource.perMipSampledViews.size() != backingMipLevels) {
resource.perMipSampledViews.resize(backingMipLevels, VK_NULL_HANDLE);
}
// Keeping the image is itself the answer to the mark: either it already carries
// STORAGE, or this format can never carry it. Either way there is nothing left to
// recreate, so stop reporting the texture as needing preparation.
resource.storageUsageResolved = markedAsStorageImage;
return true;
}
@@ -1357,7 +1625,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.sampleCount == resolvedSampleCount &&
resource.imageCreateFlags == imageCreateFlags &&
resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT &&
resource.mipLevels < backingMipLevels &&
// '<=' rather than '<': a storage-usage upgrade recreates the image with an
// unchanged mip count, and its contents (a render target's pixels live only on the
// GPU) still have to survive. The vkCmdCopyImage below copies min(mipLevels).
resource.mipLevels <= backingMipLevels &&
resource.layout != VK_IMAGE_LAYOUT_UNDEFINED;
std::unique_ptr<TextureResource> preservedResource;
@@ -1379,21 +1650,60 @@ namespace MobileGL::MG_Backend::DirectVulkan {
imageInfo.format = format;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT |
(supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) |
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) |
(((aspect & VK_IMAGE_ASPECT_DEPTH_BIT) || (aspect & VK_IMAGE_ASPECT_STENCIL_BIT)) ?
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT :
0);
if (!isMultisampleTexture) {
imageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
}
imageInfo.usage = desiredUsage;
imageInfo.samples = resolvedSampleCount;
// Bound the mutability. A blindly-mutable image has to be laid out so that ANY format in
// its compatibility class can be viewed, which costs bandwidth compression on tilers;
// naming the exact set instead lets the driver keep it. Only safe when that set really is
// exhaustive, so it is restricted to textures that are not image-unit bound: sampled views
// can only ever ask for ResolveSampledImageViewFormat's output, whereas glBindImageTexture
// may name any compatible format, which nothing here can enumerate ahead of time.
Vector<VkFormat> viewFormats;
VkImageFormatListCreateInfo formatListInfo{};
if (m_imageFormatListSupported && !supportsStorageImage &&
(imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
viewFormats.push_back(format);
for (const SamplerNumericDomain domain : {SamplerNumericDomain::Float,
SamplerNumericDomain::SignedInteger,
SamplerNumericDomain::UnsignedInteger}) {
const VkFormat viewFormat = ResolveSampledImageViewFormat(format, domain);
if (viewFormat == VK_FORMAT_UNDEFINED) {
continue;
}
if (std::find(viewFormats.begin(), viewFormats.end(), viewFormat) == viewFormats.end()) {
viewFormats.push_back(viewFormat);
}
}
formatListInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO;
formatListInfo.viewFormatCount = static_cast<Uint32>(viewFormats.size());
formatListInfo.pViewFormats = viewFormats.data();
imageInfo.pNext = &formatListInfo;
}
if (isMultisampleTexture || (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
VkImageFormatProperties imageFormatProperties{};
const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
imageInfo.flags, &imageFormatProperties);
if (imageFormatResult != VK_SUCCESS && !isMultisampleTexture &&
(imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
// Losing reinterpreted views only degrades the formatless-image feature for
// this texture; failing creation would lose the texture entirely, so retry
// as a plain immutable-format image.
MGLOG_W("%s: mutable image format=%d is unsupported for textureId=%d; creating "
"without VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT (format reinterpretation "
"will be unavailable for it)",
__func__, static_cast<Int>(format), texture.GetExternalIndex());
// Remember the verdict so later syncs of same-format textures neither retry
// the probe nor flag-mismatch against this image and recreate it.
m_mutableFormatUnsupported.insert(format);
imageInfo.flags &= ~VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
imageCreateFlags = imageInfo.flags;
imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
imageInfo.flags, &imageFormatProperties);
}
if (imageFormatResult != VK_SUCCESS ||
(isMultisampleTexture && (imageFormatProperties.sampleCounts & resolvedSampleCount) == 0)) {
MGLOG_D("%s: image flags=0x%x sampleCount=%d are unsupported for textureId=%d target=%s "
@@ -1409,8 +1719,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VmaAllocationCreateInfo allocationInfo{};
allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
VK_VERIFY(vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &resource.image, &resource.allocation, nullptr),
"vmaCreateImage(texture)");
// Soft failure like the unsupported-sample-count path above: a driver can pass the
// vkGetPhysicalDeviceImageFormatProperties pre-check yet still refuse the creation
// (e.g. multisampled depth on lavapipe); the texture simply stays unbacked.
const VkResult createImageResult =
vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &resource.image, &resource.allocation, nullptr);
if (createImageResult != VK_SUCCESS) {
MGLOG_F("SyncTextureResource: vmaCreateImage failed (%d) textureId=%d extent=%ux%u depth=%u layers=%u "
"mips=%u samples=%d format=%d",
createImageResult, texture.GetExternalIndex(), imageInfo.extent.width, imageInfo.extent.height,
imageInfo.extent.depth, imageInfo.arrayLayers, imageInfo.mipLevels,
static_cast<Int>(imageInfo.samples), static_cast<Int>(imageInfo.format));
resource.image = VK_NULL_HANDLE;
resource.allocation = nullptr;
return false;
}
++m_textureImageEpoch; // a new attachment image invalidates cached render passes
resource.layout = VK_IMAGE_LAYOUT_UNDEFINED;
@@ -1427,6 +1750,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.viewType = shapeInfo.viewType;
resource.sampleCount = resolvedSampleCount;
resource.imageCreateFlags = imageCreateFlags;
resource.usageFlags = imageInfo.usage;
resource.storageUsageResolved = markedAsStorageImage;
resource.syncedTextureParamsVersion = 0;
if (preservedResource) {
@@ -1476,6 +1801,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_deferredViewReleases[frameIndex].clear();
}
void VkTextureManager::ReclaimCompletedUploads(Bool waitAll) {
if (m_pendingUploadReclaims.empty()) {
return;
}
SizeT completed = 0;
for (; completed < m_pendingUploadReclaims.size(); ++completed) {
PendingUploadReclaim& entry = m_pendingUploadReclaims[completed];
if (waitAll) {
VK_VERIFY(vkWaitForFences(m_device, 1, &entry.fence, VK_TRUE, UINT64_MAX),
"vkWaitForFences(texture upload reclaim)");
} else if (vkGetFenceStatus(m_device, entry.fence) != VK_SUCCESS) {
break;
}
vkDestroyFence(m_device, entry.fence, nullptr);
vkFreeCommandBuffers(m_device, m_commandPool, 1, &entry.commandBuffer);
vmaDestroyBuffer(m_allocator, entry.stagingBuffer, entry.stagingAllocation);
}
m_pendingUploadReclaims.erase(m_pendingUploadReclaims.begin(),
m_pendingUploadReclaims.begin() + static_cast<std::ptrdiff_t>(completed));
}
void VkTextureManager::DestroyDeferredReleases() {
for (auto& deferredReleases : m_deferredReleases) {
deferredReleases.clear();
@@ -1677,17 +2024,119 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
// Combined depth-stencil images need per-aspect de-interleaved copies (VkBufferImageCopy
// aspectMask must have exactly one bit set). Until that is implemented, skip the upload
// instead of recording an invalid command buffer that kills the process.
// Combined depth-stencil images need per-aspect copies (VkBufferImageCopy aspectMask
// must have exactly one bit set), so de-interleave the shadow's GL wire format into
// a depth plane followed by a stencil plane per upload item.
const VkImageAspectFlags uploadAspectMask = GetAspectMaskForFormat(outResource.format);
if ((uploadAspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) && (uploadAspectMask & VK_IMAGE_ASPECT_STENCIL_BIT)) {
MGLOG_E("UploadDirtyMipLevels: skipping unimplemented depth-stencil data upload for textureId=%d",
mipmapTexture.GetExternalIndex());
for (const auto& item : uploadItems) {
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
const Bool isCombinedDepthStencil =
(uploadAspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) && (uploadAspectMask & VK_IMAGE_ASPECT_STENCIL_BIT);
if (isCombinedDepthStencil) {
const Bool srcIsD24S8 = outResource.format == VK_FORMAT_D24_UNORM_S8_UINT;
const Bool srcIsD32FS8 = outResource.format == VK_FORMAT_D32_SFLOAT_S8_UINT;
if (!srcIsD24S8 && !srcIsD32FS8) {
MGLOG_E("UploadDirtyMipLevels: unsupported combined depth-stencil format %d for textureId=%d",
static_cast<Int>(outResource.format), mipmapTexture.GetExternalIndex());
for (const auto& item : uploadItems) {
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
}
return true;
}
stagingSize = 0;
for (auto& item : uploadItems) {
const SizeT texelCount = static_cast<SizeT>(item.texelSize.x()) *
static_cast<SizeT>(item.texelSize.y()) *
static_cast<SizeT>(std::max(item.texelSize.z(), 1));
const SizeT shadowTexelSize = item.uploadByteSize / std::max<SizeT>(texelCount, 1);
MOBILEGL_ASSERT(shadowTexelSize == 4 || shadowTexelSize == 8,
"UploadDirtyMipLevels: unexpected depth-stencil shadow texel size %zu for textureId=%d",
shadowTexelSize, mipmapTexture.GetExternalIndex());
// Depth plane as the aspect's buffer-copy format (32-bit word for
// D24: low 24 bits; float for D32F), then one stencil byte per texel.
Vector<Uint8> deinterleaved(texelCount * 4 + texelCount);
Uint8* depthPlane = deinterleaved.data();
Uint8* stencilPlane = deinterleaved.data() + texelCount * 4;
const Uint8* shadow = static_cast<const Uint8*>(item.source);
for (SizeT t = 0; t < texelCount; ++t) {
if (shadowTexelSize == 8) {
// GL_FLOAT_32_UNSIGNED_INT_24_8_REV: float depth, then a word
// with stencil in its low 8 bits.
float depthValue;
Uint32 stencilWord;
std::memcpy(&depthValue, shadow + t * 8, sizeof(depthValue));
std::memcpy(&stencilWord, shadow + t * 8 + 4, sizeof(stencilWord));
if (srcIsD32FS8) {
std::memcpy(depthPlane + t * 4, &depthValue, sizeof(depthValue));
} else {
const float clamped = std::min(std::max(depthValue, 0.0f), 1.0f);
const Uint32 depthWord = static_cast<Uint32>(clamped * 16777215.0f + 0.5f);
std::memcpy(depthPlane + t * 4, &depthWord, sizeof(depthWord));
}
stencilPlane[t] = static_cast<Uint8>(stencilWord & 0xFFu);
} else {
// GL_UNSIGNED_INT_24_8: depth in the high 24 bits, stencil low 8.
Uint32 packed;
std::memcpy(&packed, shadow + t * 4, sizeof(packed));
if (srcIsD24S8) {
const Uint32 depthWord = packed >> 8;
std::memcpy(depthPlane + t * 4, &depthWord, sizeof(depthWord));
} else {
const float depthValue = static_cast<float>(packed >> 8) / 16777215.0f;
std::memcpy(depthPlane + t * 4, &depthValue, sizeof(depthValue));
}
stencilPlane[t] = static_cast<Uint8>(packed & 0xFFu);
}
}
item.expandedData = Move(deinterleaved);
item.source = item.expandedData.data();
item.uploadByteSize = item.expandedData.size();
item.offset = stagingSize;
stagingSize += static_cast<VkDeviceSize>(item.uploadByteSize);
}
}
// Pure-depth images whose canonical shadow layout differs from the image texel
// layout (the shadow keeps a full-scale 16/32-bit unorm word or a float; the
// image may be X8_D24 or a D32_SFLOAT fallback) convert per texel here.
if (uploadAspectMask == VK_IMAGE_ASPECT_DEPTH_BIT) {
const TextureInternalFormat depthInternal = mipmapTexture.GetFormat();
const Bool shadowIsFloat = depthInternal == TextureInternalFormat::DepthComponent32F;
const Bool dstIsFloat = outResource.format == VK_FORMAT_D32_SFLOAT;
const Bool dstIsD24Word = outResource.format == VK_FORMAT_X8_D24_UNORM_PACK32;
stagingSize = 0;
for (auto& item : uploadItems) {
const SizeT texelCount = static_cast<SizeT>(item.texelSize.x()) *
static_cast<SizeT>(item.texelSize.y()) *
static_cast<SizeT>(std::max(item.texelSize.z(), 1));
const SizeT shadowTexelSize = item.uploadByteSize / std::max<SizeT>(texelCount, 1);
const Bool needsConversion =
(dstIsFloat && !shadowIsFloat) || (dstIsD24Word && shadowTexelSize == 4 && !shadowIsFloat);
if (needsConversion) {
Vector<Uint8> converted(texelCount * 4);
const Uint8* shadow = static_cast<const Uint8*>(item.source);
for (SizeT t = 0; t < texelCount; ++t) {
Uint32 wide = 0;
if (shadowTexelSize == 2) {
Uint16 raw = 0;
std::memcpy(&raw, shadow + t * 2, sizeof(raw));
wide = (static_cast<Uint32>(raw) << 16) | raw;
} else {
std::memcpy(&wide, shadow + t * 4, sizeof(wide));
}
if (dstIsFloat) {
const float value = static_cast<float>(static_cast<double>(wide) / 4294967295.0);
std::memcpy(converted.data() + t * 4, &value, sizeof(value));
} else { // X8_D24: depth in the low 24 bits of a 32-bit word
const Uint32 word = wide >> 8;
std::memcpy(converted.data() + t * 4, &word, sizeof(word));
}
}
item.expandedData = Move(converted);
item.source = item.expandedData.data();
item.uploadByteSize = item.expandedData.size();
}
item.offset = stagingSize;
stagingSize += static_cast<VkDeviceSize>(item.uploadByteSize);
}
return true;
}
VkBuffer stagingBuffer = VK_NULL_HANDLE;
@@ -1740,7 +2189,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
MOBILEGL_ASSERT(ok, "TransitionImageLayout to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL failed");
// Array textures keep their GL "depth" in VkImage array layers, so the
// copy must address layerCount, not imageExtent.depth (which is invalid
// for 2D images and silently dropped every layer past the first).
const Bool depthSelectsArrayLayer = outResource.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ||
outResource.viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY ||
outResource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
for (const auto& item : uploadItems) {
const Uint32 depthOrLayers = item.texelSize.z() > 0 ? static_cast<Uint32>(item.texelSize.z()) : 1u;
VkBufferImageCopy copy{};
copy.bufferOffset = item.offset;
copy.bufferRowLength = 0;
@@ -1748,10 +2204,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
copy.imageSubresource.aspectMask = aspectMask;
copy.imageSubresource.mipLevel = item.level;
copy.imageSubresource.baseArrayLayer = item.baseArrayLayer;
copy.imageSubresource.layerCount = 1;
copy.imageSubresource.layerCount = depthSelectsArrayLayer ? depthOrLayers : 1;
copy.imageOffset = {0, 0, 0};
copy.imageExtent = {static_cast<Uint32>(item.texelSize.x()), static_cast<Uint32>(item.texelSize.y()),
item.texelSize.z() > 0 ? static_cast<Uint32>(item.texelSize.z()) : 1u};
depthSelectsArrayLayer ? 1u : depthOrLayers};
if (isCombinedDepthStencil) {
const SizeT texelCount = static_cast<SizeT>(item.texelSize.x()) *
static_cast<SizeT>(item.texelSize.y()) *
static_cast<SizeT>(std::max(item.texelSize.z(), 1));
VkBufferImageCopy depthCopy = copy;
depthCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
VkBufferImageCopy stencilCopy = copy;
stencilCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
stencilCopy.bufferOffset = item.offset + static_cast<VkDeviceSize>(texelCount) * 4;
const VkBufferImageCopy copies[2] = {depthCopy, stencilCopy};
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 2, copies);
continue;
}
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &copy);
}
@@ -1782,11 +2252,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VK_VERIFY(vkCreateFence(m_device, &fenceInfo, nullptr, &uploadFence), "vkCreateFence(texture upload)");
VK_VERIFY(vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, uploadFence), "vkQueueSubmit(texture)");
VK_VERIFY(vkWaitForFences(m_device, 1, &uploadFence, VK_TRUE, UINT64_MAX), "vkWaitForFences(texture upload)");
vkDestroyFence(m_device, uploadFence, nullptr);
vkFreeCommandBuffers(m_device, m_commandPool, 1, &commandBuffer);
vmaDestroyBuffer(m_allocator, stagingBuffer, stagingAllocation);
// Do NOT wait the fence here: this submit sits behind the previous
// frame's rendering on the queue, so a synchronous wait stalls the CPU
// until the GPU drains - a per-frame vkQueueWaitIdle for any workload
// with animated textures. Ordering against the current frame's draws is
// already guaranteed (its command buffer is submitted later, at
// present), so only the transient objects need to survive execution;
// park them until the fence signals.
m_pendingUploadReclaims.push_back({uploadFence, commandBuffer, stagingBuffer, stagingAllocation});
ReclaimCompletedUploads();
// Backstop for pathological upload storms: bound in-flight staging
// memory by blocking on the oldest upload only once the list is deep.
constexpr SizeT kMaxPendingTextureUploads = 16;
if (m_pendingUploadReclaims.size() > kMaxPendingTextureUploads) {
VK_VERIFY(vkWaitForFences(m_device, 1, &m_pendingUploadReclaims.front().fence, VK_TRUE, UINT64_MAX),
"vkWaitForFences(texture upload backstop)");
ReclaimCompletedUploads();
}
if (!ok) {
MGLOG_D("%s: texture upload cmd failed", __func__);
@@ -1921,6 +2403,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkFormat VkTextureManager::ResolveSampledImageViewFormat(VkFormat imageFormat,
SamplerNumericDomain numericDomain) {
// Depth/stencil images always sample through the existing depth-aspect sampledView.
// Combined formats (D24S8, D32FS8) are multi-numeric, so vkuFormatIsSampledFloat is
// false for them by design, yet their depth aspect reads as float in every GL depth
// texture mode; Vulkan also forbids reinterpreting them through color-class views.
// Integer domains keep the same view (pre-reinterpretation behavior for stencil-index
// style access) rather than failing the draw.
if (vkuFormatIsDepthOrStencil(imageFormat)) {
return imageFormat;
}
if (imageFormat == VK_FORMAT_UNDEFINED || numericDomain == SamplerNumericDomain::Unknown ||
FormatMatchesSamplerNumericDomain(imageFormat, numericDomain)) {
return imageFormat;
@@ -13,6 +13,7 @@
#include <MG_State/GLState/TextureState/TextureObject.h>
#include <vk_mem_alloc.h>
#include <unordered_map>
#include <unordered_set>
namespace MobileGL::MG_State::GLState {
class ITextureObject;
@@ -27,6 +28,9 @@ public:
// manager keys its per-draw fast path on this so an attachment's image recreation
// invalidates the cached render pass (dirty-flag tracking; portable to Vulkan 1.1).
Uint64 GetTextureImageEpoch() const { return m_textureImageEpoch; }
// Bumped whenever any tracked texture resource is erased; cached
// TextureResource pointers are valid only while this is unchanged.
Uint64 GetResourceEraseEpoch() const { return m_resourceEraseEpoch; }
struct TextureIdentity {
MG_State::GLState::ITextureObject* texture = nullptr;
@@ -52,6 +56,9 @@ public:
VkCommandPool commandPool = VK_NULL_HANDLE;
VkQueue graphicsQueue = VK_NULL_HANDLE;
Uint32 frameCount = 0;
// VK_KHR_image_format_list is enabled: MUTABLE_FORMAT images can name the exact set of
// formats they will be viewed as, which is what lets a tiler keep them compressed.
Bool imageFormatListSupported = false;
};
struct TextureResource {
@@ -60,12 +67,16 @@ public:
Uint32 baseArrayLayer = 0;
Uint32 layerCount = 1;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
// May differ from the image format: sRGB images attach through their UNORM
// twin while GL_FRAMEBUFFER_SRGB is disabled.
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
Bool operator==(const AttachmentViewKey& other) const {
return mipLevel == other.mipLevel &&
baseArrayLayer == other.baseArrayLayer &&
layerCount == other.layerCount &&
viewType == other.viewType;
viewType == other.viewType &&
viewFormat == other.viewFormat;
}
};
@@ -76,6 +87,8 @@ public:
hash ^= std::hash<Uint32>{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewType)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<Uint32>{}(static_cast<Uint32>(key.viewFormat)) +
0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};
@@ -156,7 +169,25 @@ public:
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
VkImageCreateFlags imageCreateFlags = 0;
// Usage the live image was created with. STORAGE is only requested for textures that
// have actually been bound to a GL image unit, because on Adreno a storage-capable
// image loses UBWC bandwidth compression; a later image binding upgrades the usage
// and recreates the image, so the resolved usage has to be part of the compatibility
// check that decides whether the existing image can be kept.
VkImageUsageFlags usageFlags = 0;
// True once this image was (re)resolved while the texture was already marked as an
// image-unit texture. Distinguishes "not upgraded yet" from "cannot be upgraded"
// (a format whose optimalTilingFeatures lack STORAGE_IMAGE never gains the bit), so
// NeedsStorageImagePreparation cannot ask for a recreate that will never happen.
Bool storageUsageResolved = false;
Uint16 syncedTextureParamsVersion = 0;
// Recording generation (VkTextureManager::GetRecordingGeneration) of the last
// command referencing this image that was recorded into the CURRENT frame
// command buffer. An image untouched by the open recording may have its
// out-of-pass work (deferred clears, sampled-layout transitions) recorded
// into the frame's PRE command buffer - which executes strictly before the
// frame's commands - instead of splitting the active render pass.
Uint64 lastRecordingGeneration = 0;
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
Uint64 syncedContentVersion = 0;
@@ -189,7 +220,10 @@ public:
std::swap(this->viewType, that.viewType);
std::swap(this->sampleCount, that.sampleCount);
std::swap(this->imageCreateFlags, that.imageCreateFlags);
std::swap(this->usageFlags, that.usageFlags);
std::swap(this->storageUsageResolved, that.storageUsageResolved);
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
std::swap(this->syncedContentVersion, that.syncedContentVersion);
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
}
@@ -250,6 +284,8 @@ public:
viewType = VK_IMAGE_VIEW_TYPE_2D;
sampleCount = VK_SAMPLE_COUNT_1_BIT;
imageCreateFlags = 0;
usageFlags = 0;
storageUsageResolved = false;
syncedTextureParamsVersion = 0;
syncedContentVersion = 0;
syncedMipLevelCount = 0;
@@ -266,6 +302,10 @@ public:
Bool Initialize(const InitInfo& initInfo);
void Shutdown();
void BeginFrame(Uint32 frameIndex);
// Drains every frame slot's deferred image/view releases. Only valid when
// the caller has proven every queue submission complete; used by the
// present-less frame-boundary drain.
void CollectAllDeferredReleases();
TextureResource* SyncTextureAndGetDescriptor(
MG_State::GLState::ITextureObject& texture);
@@ -285,6 +325,37 @@ public:
Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture);
// Recording-generation bookkeeping for the pre-pass command stream. The
// generation advances every time the frame command buffer (re)begins
// recording; a resource whose stamp does not match was not referenced by
// any command in the open recording, so its out-of-pass work may safely
// execute ahead of the whole recording (in the pre command buffer).
void AdvanceRecordingGeneration() { ++m_recordingGeneration; }
void StampResourceRecordingUse(TextureResource& resource) const {
resource.lastRecordingGeneration = m_recordingGeneration;
}
// Map-lookup variant for callers that only hold the GL texture object.
void StampTextureRecordingUse(MG_State::GLState::ITextureObject* texture);
Bool WasTouchedThisRecording(const TextureResource& resource) const {
return resource.lastRecordingGeneration == m_recordingGeneration;
}
// Records that this texture is bound to a GL image unit, so its image must carry
// VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and
// therefore before the render pass is committed: an image that has to be upgraded is
// recreated, which is illegal inside a render pass. Sticky for the texture's lifetime -
// GL lets an image binding come and go, and re-creating the image every time it does
// would cost far more than the compression it wins back.
void MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture);
// True when this texture is marked but its live image predates the mark, i.e. the next sync
// will recreate it with STORAGE usage and copy the old contents forward. Callers use this to
// submit their pending recording first, so that copy cannot read pre-flush content.
Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const;
// Non-mutating probe for the per-draw storage-image fast path: true when preparing this
// texture as a storage image may need work that is illegal inside a render pass (resource
// creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports
// true - a false positive merely ends the render pass, a false negative would skip a barrier.
Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const;
static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect);
static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain);
static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat);
@@ -325,6 +396,9 @@ public:
private:
// Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch().
Uint64 m_textureImageEpoch = 1;
// See AdvanceRecordingGeneration. Starts above every resource's default
// stamp of 0 so a fresh resource counts as untouched.
Uint64 m_recordingGeneration = 1;
Bool SyncTexture(MG_State::GLState::ITextureObject &texture,
TextureResource &outResource);
@@ -355,18 +429,28 @@ private:
void DeferViewRelease(VkImageView view);
void CollectDeferredReleases(Uint32 frameIndex);
void DestroyDeferredReleases();
// Frees the fence/command buffer/staging buffer of every in-flight texture
// upload whose fence has signaled (submission order = completion order on
// the single queue, so the scan stops at the first still-pending entry).
// waitAll blocks on every entry - Shutdown's drain.
void ReclaimCompletedUploads(Bool waitAll = false);
static TextureIdentity MakeTextureIdentity(MG_State::GLState::ITextureObject* texture);
void EraseTrackedTexture(const TextureIdentity& identity);
void PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture);
SizeT PruneDeadTextures();
VkDevice m_device = VK_NULL_HANDLE;
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
VmaAllocator m_allocator = nullptr;
VkCommandPool m_commandPool = VK_NULL_HANDLE;
VkQueue m_graphicsQueue = VK_NULL_HANDLE;
Bool m_imageFormatListSupported = false;
Uint32 m_currentFrameIndex = 0;
Uint8 m_gcCounter = 0;
// Frame-boundary GC gate: counts BeginFrame calls, not draws, so texture churn
// through non-draw paths (FBO clears, readbacks) still reaches the prune.
Uint32 m_gcFrameCounter = 0;
// Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of
// textures already fully synced in the current draw (small N -> flat scan).
Bool m_drawSyncScopeActive = false;
@@ -379,9 +463,45 @@ private:
TextureResource* resource = nullptr;
};
Vector<DrawSyncedTexture> m_drawSyncedThisDraw;
// Cross-draw sampled-texture memo: the same few textures (atlas, lightmap)
// are resolved on every draw, so cache their resource pointers and skip the
// alive/resource map lookups. Node-based std::unordered_map keeps the
// pointees stable across inserts; erases bump m_resourceEraseEpoch, which
// every memo entry must match. SyncTexture still runs on memo hits, so
// content/param freshness is unaffected. A dead-then-reused texture address
// cannot false-hit: the new object carries a new lifetime id.
struct SyncedTextureMemoEntry {
const MG_State::GLState::ITextureObject* texture = nullptr;
Uint64 lifetimeId = 0;
Uint64 eraseEpoch = 0;
TextureResource* resource = nullptr;
};
static constexpr Uint32 kSyncedTextureMemoSize = 8;
SyncedTextureMemoEntry m_syncedTextureMemo[kSyncedTextureMemoSize];
Uint32 m_syncedTextureMemoNext = 0;
Uint64 m_resourceEraseEpoch = 1;
// Formats whose mutable-image probe failed on this device; their images are created
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
std::unordered_set<TextureIdentity, TextureIdentityHash> m_storageImageTextures;
// Supported multisample counts per format, so repeat texture syncs do not
// re-query vkGetPhysicalDeviceImageFormatProperties.
std::unordered_map<VkFormat, VkSampleCountFlags> m_multisampleCountsByFormat;
Vector<Vector<TextureResource>> m_deferredReleases;
Vector<Vector<VkImageView>> m_deferredViewReleases;
// Texture uploads are submitted out-of-band but NOT waited on (waiting
// behind the queue serialized the CPU against the previous frame's GPU
// work every time an animated atlas re-uploaded). Their transient objects
// are parked here and reclaimed once the upload fence signals.
struct PendingUploadReclaim {
VkFence fence = VK_NULL_HANDLE;
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
VkBuffer stagingBuffer = VK_NULL_HANDLE;
VmaAllocation stagingAllocation = nullptr;
};
Vector<PendingUploadReclaim> m_pendingUploadReclaims;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
@@ -51,6 +51,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 instanceCount = 1;
Uint32 firstVertex = 0;
Uint32 firstInstance = 0;
// Indexed-draw metadata for bounding vertex-stream conversion. baseVertex is the
// draw's base-vertex offset; indexRangeIsExactView is true only when the draw
// fetches exactly the indices its IndexBufferView describes (direct DrawElements;
// multi/indirect forms leave it false because the CPU cannot bound their ranges).
Int32 baseVertex = 0;
Bool indexRangeIsExactView = false;
};
struct DrawIndexedCmdParam {
@@ -70,6 +76,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLenum indexType = GL_UNSIGNED_SHORT;
SizeT indexByteOffset = 0;
SizeT indexByteSize = 0;
// Interpret indexByteOffset as a raw client pointer even when an element
// array buffer is bound (backend-synthesized index lists, e.g. the
// GL_LINE_LOOP -> LINE_STRIP rewrite).
Bool forceClientMemory = false;
};
struct DrawIndexedCmd {
@@ -108,7 +118,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
};
class VulkanRenderer : public IBufferCopyCommandProvider, public FrameContext::IRecordingObserver {
class VulkanRenderer : public IBufferCopyCommandProvider,
public FrameContext::IRecordingObserver,
public VkRenderPassManager::IEvictionObserver,
public ProgramFactory::IEvictionObserver {
public:
VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg = {});
~VulkanRenderer();
@@ -125,9 +138,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// recording, before any render pass.
void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) override;
// VkRenderPassManager::IEvictionObserver: the render-pass aging sweep just
// destroyed these VkRenderPasses; evict every graphics pipeline hashed on a
// dying handle (they share its >1024-boundary idleness, so immediate
// destruction is safe) and drop the last-pipeline memo if any went.
void OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) override;
// ProgramFactory::IEvictionObserver: an aged-out program entry was
// destroyed; evict its compute pipeline and graphics pipelines (same
// idleness guarantee - they are only bound through draws/dispatches that
// stamp the program entry) and purge the descriptor-set cache entries
// keyed by its now-recyclable VkDescriptorSetLayout handle.
void OnProgramEvicted(ProgramFactory::HashType programHash,
VkDescriptorSetLayout descriptorSetLayout) override;
Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
const DrawCmdParam& drawParams,
const IndexBufferView* pIndexBufferView = nullptr);
// ANGLE-style consecutive-draw fast path: SetupDraw snapshots the fully
// resolved draw configuration; the next draw whose cheap version/identity
// checks all match skips the resolution half (LOD probe, sampled-set
// walk, render-pass and pipeline resolution) and jumps straight to the
// per-draw tail. Returns false (leaving no side effects that the full
// path cannot redo idempotently) whenever anything might have changed.
Bool TrySetupDrawFastPath(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView);
void ClearAttachmentsOnActiveRenderPass(VkCommandBuffer commandBuffer,
const RenderPassEntry& compatibleRenderPassEntry);
@@ -165,6 +200,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target);
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
// GL_DEPTH_COMPONENT / GL_DEPTH_STENCIL / GL_STENCIL_INDEX readback from the
// read framebuffer's depth/stencil attachment (per-aspect buffer copies with
// CPU repacking into the requested client layout).
void ReadDepthStencilPixels(MG_State::GLState::FramebufferObject& readFbo, GLint x, GLint y, GLsizei width,
GLsizei height, GLenum format, GLenum type, void* pixels);
// Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage;
// expects command recording to be active and any render pass already ended.
void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
void* pixels);
// Same-extent depth blit between images of different depth formats: host
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
VkImageLayout* srcTrackedLayout, Uint32 srcMipLevel, Uint32 srcBaseArrayLayer,
VkImage dstImage, VkFormat dstFormat, VkImageLayout* dstTrackedLayout,
Uint32 dstMipLevel, Uint32 dstBaseArrayLayer, GLint srcX, GLint srcY, GLint dstX,
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
VkImageLayout dstRestoreLayout, Bool stencilAspect);
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
GLsizei width, GLsizei height, GLenum destinationFormat,
@@ -250,8 +304,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkTimerQueryManager::TimestampRecord& end) const;
Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const;
// GL_SAMPLES_PASSED occlusion queries: every app draw between Start and Stop is
// wrapped in a Vulkan occlusion query slot; the result is the slot sum. Requires
// hostQueryReset for slot recycling - Start fails (frontend keeps the query
// unsupported) when the device lacks it.
Bool StartOcclusionQueryCapture();
void StopOcclusionQueryCapture(Vector<Uint32>& outSlots);
// Flushes pending commands, waits, sums the slots, and recycles them.
Bool ResolveOcclusionQueryResult(const Vector<Uint32>& slots, Uint64& outSamples);
void RequestSwapchainResize(Uint32 width, Uint32 height);
void RecreateSwapchain();
// Re-query the surface and report whether the live swapchain no longer matches it
// (size or orientation). This - not a VK_SUBOPTIMAL_KHR result - is what decides a
// rebuild, so a surface the driver merely considers suboptimal cannot thrash.
Bool SwapchainIsOutOfDate();
// Returns false when the surface is zero-area (minimized/hidden window):
// no new swapchain is installed and presentation must stay suspended.
Bool RecreateSwapchain();
private:
struct BlitUniformData {
@@ -337,24 +406,55 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkFence AcquirePooledSubmitFence();
void DestroySubmitFencePool();
Bool HasPendingRecordedWork() const;
// Frame-boundary housekeeping for paths that never reach Present's
// tail (present-less readback loops, suspended presentation, blocking
// sync waits): runs the same per-frame drains Present performs, but
// only when every queue submission has been observed complete AND no
// recorded-but-unsubmitted commands exist - i.e. when CPU-GPU overlap
// is provably already zero. Never blocks (non-blocking fence poll
// only), so the presenting path's frames-in-flight pipelining is
// untouched. Returns true when the drain ran.
Bool TryDrainFrameTransients();
Vector<SubmitRecord> m_inFlightSubmits;
Vector<VkFence> m_freeSubmitFences;
Uint64 m_submitCounter = 0;
Uint64 m_completedSubmitCounter = 0;
// Drains since the last Present, gating the drain's frame-boundary-equivalent
// work (arena rewind + cache aging): a presenting app's mid-frame
// readbacks/waits must neither churn the transient caches nor accelerate the
// aging clocks, while present-less loops still cross a boundary every few
// iterations. Reset in Present.
Uint32 m_drainsSinceLastPresent = 0;
NativeWindowType m_window = 0;
void* m_platformDisplay = nullptr;
void* m_platformLibrary = nullptr;
void* m_platformCloseDisplay = nullptr;
// Some real ICDs (e.g. NVIDIA's proprietary Linux driver) don't implement
// VK_EXT_headless_surface at all. Detected once in CreateInstance() from the
// enumerated instance extensions; when false, CreateSurface() falls back to a
// hidden Xlib window instead of vkCreateHeadlessSurfaceEXT.
Bool m_headlessSurfaceSupported = true;
// Set when CreateSurface() had to create its own Xlib window for the fallback
// above (rather than being handed one by the caller), so Shutdown() knows it
// owns that window and must destroy it.
Bool m_ownsFallbackXlibWindow = false;
VulkanRendererConfig m_config;
Bool m_swapchainResizeRequested = false;
// Presentation is suspended while the window is zero-area (minimized): the
// swapchain is unusable/out of date, so Present drops frames instead of
// submitting on a signaled fence / presenting never-acquired images.
Bool m_presentSuspended = false;
// Vulkan objects
Bool m_validationLayersEnabled = false;
Vector<VkExtensionProperties> m_extensions;
VkInstance m_instance = VK_NULL_HANDLE;
VkDebugUtilsMessengerEXT m_debugMessenger = VK_NULL_HANDLE;
// Fallback reporting channel for drivers that ship the validation layers but
// only expose the older VK_EXT_debug_report (Adreno 650 / Vulkan 1.1.128).
VkDebugReportCallbackEXT m_debugReportCallback = VK_NULL_HANDLE;
PhysicalDevice m_physicalDevice;
VkDevice m_device = VK_NULL_HANDLE;
VmaAllocator m_allocator = nullptr;
@@ -392,6 +492,59 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 stride);
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
// VK_EXT_transform_feedback (GL transform feedback capture)
Bool m_transformFeedbackFeatureEnabled = false;
static inline PFN_vkCmdBindTransformFeedbackBuffersEXT s_vkCmdBindTransformFeedbackBuffersEXT = nullptr;
static inline PFN_vkCmdBeginTransformFeedbackEXT s_vkCmdBeginTransformFeedbackEXT = nullptr;
static inline PFN_vkCmdEndTransformFeedbackEXT s_vkCmdEndTransformFeedbackEXT = nullptr;
// Counter buffers (one 4-byte slot per capture binding) let consecutive
// draws within one glBeginTransformFeedback append GL-style.
VkBufferObject m_xfbCounterBuffer;
// Non-zero while inside a GL Begin/End with at least one captured draw
// recorded; selects counter-buffer resume on the next captured draw.
Bool m_xfbCountersValid = false;
Uint64 m_xfbLastSeenGeneration = 0;
// Wraps a recorded draw with BeginTransformFeedbackEXT/EndTransformFeedbackEXT
// when GL transform feedback is active; binds capture buffers on demand.
Bool BeginXfbCaptureForDraw(FrameContext::FrameData& frame);
void EndXfbCaptureForDraw(FrameContext::FrameData& frame, Bool began);
// Wrap one app draw in an occlusion-query slot while a GL_SAMPLES_PASSED
// query is active. Returns whether a slot was begun (End must mirror it).
Bool BeginOcclusionForDraw(VkCommandBuffer commandBuffer);
void EndOcclusionForDraw(VkCommandBuffer commandBuffer, Bool began);
Bool m_occlusionQueryPreciseEnabled = false;
Bool m_hostQueryResetEnabled = false;
PFN_vkResetQueryPool s_vkResetQueryPool = nullptr;
VkQueryPool m_occlusionQueryPool = VK_NULL_HANDLE;
static constexpr Uint32 kOcclusionQuerySlots = 8192;
Uint32 m_occlusionSlotCursor = 0;
Bool m_occlusionCaptureActive = false;
Vector<Uint32> m_occlusionActiveSlots;
// Transform feedback primitive queries: one pool slot per captured draw yields
// the (written, needed) pair; GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN sums the
// first, GL_PRIMITIVES_GENERATED the second - exact with geometry shaders,
// unlike the CPU fallback accounting.
Bool m_xfbQueriesSupported = false;
PFN_vkCmdBeginQueryIndexedEXT s_vkCmdBeginQueryIndexedEXT = nullptr;
PFN_vkCmdEndQueryIndexedEXT s_vkCmdEndQueryIndexedEXT = nullptr;
VkQueryPool m_xfbQueryPool = VK_NULL_HANDLE;
static constexpr Uint32 kXfbQuerySlots = 8192;
Uint32 m_xfbQuerySlotCursor = 0;
Bool m_xfbQueryCaptureActive[2] = {false, false}; // [0]=written, [1]=generated
Vector<Uint32> m_xfbQueryActiveSlots[2];
Bool m_xfbQuerySlotOpen = false;
Uint32 m_xfbQueryOpenSlot = 0;
public:
// kind: 0 = PRIMITIVES_WRITTEN, 1 = PRIMITIVES_GENERATED.
Bool StartXfbQueryCapture(Uint32 kind);
void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots);
Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, Bool wantGenerated, Uint64& outPrimitives);
private:
void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer);
void EndXfbQueryForDraw(VkCommandBuffer commandBuffer);
VkCommandPool m_commandPool = VK_NULL_HANDLE;
VkBufferManager m_bufferManager;
@@ -404,14 +557,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// gather + synthetic vertex-input rebuild + payload hash + lookup) when the full pipeline
// state is unchanged from the previous draw. The key provably covers every pipeline field.
// Reset per-frame and on pipeline destruction so the cached handle can never dangle.
Bool m_lastPipelineValid = false;
GLenum m_lastPipelineMode = 0;
Uint64 m_lastPipelineProgramHash = 0;
Uint64 m_lastPipelineVertexInputHash = 0;
Uint64 m_lastPipelineRenderPassHash = 0;
Uint m_lastPipelineRenderStateVersion = 0;
ProgramFactory::CompileOptionFlags m_lastPipelineTransformFlags = {};
VkPipeline m_lastPipelineResult = VK_NULL_HANDLE;
// Small N-way pipeline-resolution memo (round-robin replacement). A
// single-entry memo thrashed on draw sequences that alternate a few
// pipelines (GUI text/quad program ping-pong), paying the full
// payload-hash lookup per draw; eight entries cover such working sets
// while keeping the hit path a trivial linear scan.
struct PipelineMemoEntry {
GLenum mode = 0;
Uint64 programHash = 0;
Uint64 vertexInputHash = 0;
Uint64 renderPassHash = 0;
Uint renderStateVersion = 0;
ProgramFactory::CompileOptionFlags transformFlags = {};
VkPipeline pipeline = VK_NULL_HANDLE;
};
static constexpr Uint32 kPipelineMemoSize = 8;
PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize];
Uint32 m_pipelineMemoCount = 0;
Uint32 m_pipelineMemoNext = 0;
// Drops every memoized pipeline handle. Required at command-buffer
// boundaries and whenever any pipeline may have been destroyed.
void InvalidatePipelineMemo() {
m_pipelineMemoCount = 0;
m_pipelineMemoNext = 0;
}
UnorderedMap<ProgramFactory::HashType, VkPipeline> m_computePipelines;
UniquePtr<ProgramFactory> m_programFactory;
UniquePtr<UniformManager> m_uniformManager;
@@ -440,9 +609,61 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
Uint64 m_lastSampledSetBindGeneration = 0;
// Memo for the per-draw explicit-LOD-0 eligibility probe
// (ProgramSamplesOnlySingleLevelTextures): same key family as the
// sampled-set memo, plus the sampled textures' params-version sum so a
// level-range or filter change re-probes. On a hit the resolved
// transform flags are reused, which also collapses the two
// GetOrCreateProgram lookups into one.
Bool m_lastLodDecisionValid = false;
Uint64 m_lastLodProgramLifetimeId = 0;
Uint32 m_lastLodProgramVersion = 0;
Uint64 m_lastLodBindGeneration = 0;
Uint64 m_lastLodParamsSum = 0;
ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};
// Snapshot behind TrySetupDrawFastPath. Values only: the program and
// render-pass caches are open-addressing maps whose entries move on
// insert, so no pointers into them are cached; the pipeline handle is
// protected by the command-buffer-boundary reset plus the mid-frame
// pipeline-destruction resets, and monotonic epochs guard everything
// that can be destroyed or recreated between draws.
struct SetupDrawSnapshot {
Bool valid = false;
Uint8 aspects = 0;
GLenum mode = 0;
Uint64 programLifetimeId = 0;
Uint32 programVersion = 0;
const void* vao = nullptr;
Uint32 vaoConfigVersion = 0;
const void* drawFbo = nullptr;
Uint16 fboVersion = 0;
Bool drawFboIsDefault = false;
Uint renderStateVersion = 0;
Uint64 bindGeneration = 0;
Uint32 baseTransformFlags = 0;
Uint32 resolvedTransformFlags = 0;
Uint64 renderPassHash = 0;
Uint32 imageIndex = 0;
Uint64 textureEraseEpoch = 0;
Uint64 textureImageEpoch = 0;
Uint64 renderbufferImageEpoch = 0;
Uint64 sampledContentSum = 0;
Uint64 sampledParamsSum = 0;
IntVec2 renderPassExtent = {0, 0};
VkPipeline pipeline = VK_NULL_HANDLE;
};
SetupDrawSnapshot m_setupDrawSnapshot;
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every
// draw call and must not allocate.
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
// Parallel to m_sampledTexturesScratch, refilled by every SetupDraw's
// first sampled-texture loop: the resolved backend resources, so the
// post-transition loop can skip re-resolving textures whose layout is
// already sampleable.
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
Vector<VkBuffer> m_vertexBuffersScratch;
Vector<VkDeviceSize> m_vertexOffsetsScratch;
@@ -488,12 +709,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
};
UnorderedMap<ConvertedVertexStreamKey, BufferSlice, ConvertedVertexStreamKeyHash>
struct ConvertedVertexStream {
BufferSlice slice;
// Number of source elements the cached slice covers. A draw needing a prefix of
// this range reuses the slice (converted streams are tightly packed); a draw
// needing more reconverts and replaces the entry, so per (buffer, layout) a
// frame converts at most the largest range any draw asked for.
SizeT elementCount = 0;
// Pins the source buffer for the frame so its heap address cannot be reused by
// a new BufferObject while this pointer-keyed entry is alive.
SharedPtr<const MG_State::GLState::BufferObject> sourcePin;
};
UnorderedMap<ConvertedVertexStreamKey, ConvertedVertexStream, ConvertedVertexStreamKeyHash>
m_convertedVertexStreams;
void CreateInstance();
VkResult SetupDebugMessenger();
VkResult DestroyDebugMessenger();
VkResult SetupDebugReportCallback();
void DestroyDebugReportCallback();
VkDebugUtilsMessengerCreateInfoEXT PopulateDebugMessengerCreateInfo();
void CreateSurface();
void PickPhysicalDevice();
@@ -512,14 +746,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const RenderPassEntry& renderPassEntry);
VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
void DestroyComputePipelines();
// Takes the frame rather than a command buffer: a first-time storage-usage upgrade has to
// flush the pending recording (see the body), which retires the current command buffer.
Bool PrepareStorageImageTextures(
VkCommandBuffer commandBuffer,
FrameContext::FrameData& frame,
const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj);
Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
const ProgramFactory::VkProgramObject& programObj,
const DrawCmdParam& drawParams, Bool indexedDraw);
const DrawCmdParam& drawParams,
const IndexBufferView* pIndexBufferView);
Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame,
const MG_State::GLState::VertexArrayObject& vao,
const IndexBufferView* pIndexBufferView = nullptr);
@@ -537,6 +774,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLenum filter);
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture);
Bool MaterializePendingClearForRenderbuffer(
VkCommandBuffer commandBuffer,
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry);
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
MG_State::GLState::ITextureObject& texture,
@@ -571,6 +811,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const PhysicalDevice& compareWithDevice,
PhysicalDevice& outBetterDevice);
static constexpr const char* s_validationLayerNames[] = {"VK_LAYER_KHRONOS_validation"};
// VK_KHR_image_format_list: lets MUTABLE_FORMAT images declare their exact view-format
// set so the driver can keep bandwidth compression (see CreateLogicalDeviceAndQueues).
Bool m_imageFormatListExtensionEnabled = false;
static constexpr const char* s_deviceExtensionNames[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
static Bool CheckValidationLayerSupport();
+32 -3
View File
@@ -52,19 +52,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
} // namespace MobileGL::MG_Backend::DirectVulkan
namespace MobileGL::MG_Backend::DirectVulkan {
// GL renders into sRGB color attachments RAW while GL_FRAMEBUFFER_SRGB is disabled
// (the core-profile default); Vulkan sRGB attachments always encode on write. The
// attachment view (and render pass format) therefore drops to the UNORM twin
// whenever the capability is off. Sampled views keep the sRGB format (decode on
// sample is unconditional in GL).
inline VkFormat ResolveSrgbAttachmentWriteFormat(VkFormat format, bool framebufferSrgbEnabled) {
if (framebufferSrgbEnabled) return format;
switch (format) {
case VK_FORMAT_R8G8B8A8_SRGB:
return VK_FORMAT_R8G8B8A8_UNORM;
case VK_FORMAT_B8G8R8A8_SRGB:
return VK_FORMAT_B8G8R8A8_UNORM;
default:
return format;
}
}
} // namespace MobileGL::MG_Backend::DirectVulkan
// The context line (__VA_ARGS__ = its own format string + args) must be a SEPARATE log
// call: appending its format to the base format while its arguments precede the base
// arguments makes every conversion read the wrong slot (a %s pulling an int crashes).
#define VK_VERIFY(expr, ...) \
do { \
VkResult _vk_verify_result = (expr); \
if (_vk_verify_result != VK_SUCCESS) { \
MGLOG_F("Vulkan error %s (%d) at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, \
__VA_OPT__(MGLOG_F(__VA_ARGS__);) \
MGLOG_F("Vulkan error %s (%d) at %s:%d", \
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
_vk_verify_result, __FILE__, __LINE__); \
} \
MOBILEGL_ASSERT(_vk_verify_result == VK_SUCCESS, "Vulkan error %s (%d) at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), _vk_verify_result, __FILE__, __LINE__); \
MOBILEGL_ASSERT(_vk_verify_result == VK_SUCCESS, "Vulkan error %s (%d) at %s:%d", \
MobileGL::MG_Backend::DirectVulkan::VkResultToString(_vk_verify_result), \
_vk_verify_result, __FILE__, __LINE__); \
} while (0)
#define XXHASH_VERIFY(expr, ...) \
do { \
XXH_errorcode _xxh_verify_result = (expr); \
MOBILEGL_ASSERT(_xxh_verify_result == XXH_OK, "XXHash error %d at %s:%d" __VA_OPT__(" - ") __VA_ARGS__, _xxh_verify_result, __FILE__, __LINE__); \
if (_xxh_verify_result != XXH_OK) { \
__VA_OPT__(MGLOG_F(__VA_ARGS__);) \
} \
MOBILEGL_ASSERT(_xxh_verify_result == XXH_OK, "XXHash error %d at %s:%d", _xxh_verify_result, __FILE__, \
__LINE__); \
} while (0)
+33
View File
@@ -24,6 +24,7 @@ namespace MobileGL::MG_Impl::CGLImpl {
GLint Samples = 0;
GLint Profile = kCGLOGLPVersion_3_2_Core;
GLint RendererId = 0x4d474c;
GLint DisplayMask = 0;
};
struct ContextObject {
@@ -134,6 +135,9 @@ namespace MobileGL::MG_Impl::CGLImpl {
case kCGLPFARendererID:
pixelFormat.RendererId = value;
break;
case kCGLPFADisplayMask:
pixelFormat.DisplayMask = value;
break;
default:
break;
}
@@ -343,6 +347,9 @@ namespace MobileGL::MG_Impl::CGLImpl {
case kCGLPFARendererID:
*value = pixelFormat->RendererId;
return kCGLNoError;
case kCGLPFADisplayMask:
*value = pixelFormat->DisplayMask;
return kCGLNoError;
case kCGLPFAOpenGLProfile:
*value = pixelFormat->Profile;
return kCGLNoError;
@@ -481,6 +488,32 @@ namespace MobileGL::MG_Impl::CGLImpl {
return it == currentContexts.end() ? nullptr : it->second;
}
CGLError SetVirtualScreen(CGLContextObj ctx, GLint screen) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
if (!object) {
return kCGLBadContext;
}
if (screen != 0) {
return kCGLBadValue;
}
object->VirtualScreen = screen;
return kCGLNoError;
}
CGLError GetVirtualScreen(CGLContextObj ctx, GLint* screen) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
if (!object) {
return kCGLBadContext;
}
if (!screen) {
return kCGLBadAddress;
}
*screen = object->VirtualScreen;
return kCGLNoError;
}
CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(ctx);
+2
View File
@@ -32,6 +32,8 @@ namespace MobileGL::MG_Impl::CGLImpl {
CGLError SetCurrentContext(CGLContextObj ctx);
CGLContextObj GetCurrentContext();
CGLError SetVirtualScreen(CGLContextObj ctx, GLint screen);
CGLError GetVirtualScreen(CGLContextObj ctx, GLint* screen);
CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params);
CGLError GetParameter(CGLContextObj ctx, CGLContextParameter pname, GLint* params);
CGLError UpdateContext(CGLContextObj ctx);
@@ -71,6 +71,14 @@ MOBILEGL_CGL_API CGLContextObj CGLGetCurrentContext(void) {
return MobileGL::MG_Impl::CGLImpl::GetCurrentContext();
}
MOBILEGL_CGL_API CGLError CGLSetVirtualScreen(CGLContextObj ctx, GLint screen) {
return MobileGL::MG_Impl::CGLImpl::SetVirtualScreen(ctx, screen);
}
MOBILEGL_CGL_API CGLError CGLGetVirtualScreen(CGLContextObj ctx, GLint* screen) {
return MobileGL::MG_Impl::CGLImpl::GetVirtualScreen(ctx, screen);
}
MOBILEGL_CGL_API CGLError CGLSetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) {
return MobileGL::MG_Impl::CGLImpl::SetParameter(ctx, pname, params);
}
@@ -10,8 +10,12 @@
#if defined(__APPLE__)
#include "MG_Impl/CGLImpl/CGLImpl.h"
#include "MG_Impl/GetProcAddress.h"
#include <CoreGraphics/CoreGraphics.h>
#include <CoreVideo/CVDisplayLink.h>
#include <cstdint>
#include <dlfcn.h>
namespace {
@@ -47,10 +51,52 @@ namespace {
return dlsym(handle, symbol);
}
CGDirectDisplayID DisplayForMask(GLint displayMask) {
constexpr std::uint32_t MaxDisplays = sizeof(CGOpenGLDisplayMask) * 8;
CGDirectDisplayID displays[MaxDisplays] = {};
std::uint32_t displayCount = 0;
if (displayMask != 0 &&
CGGetActiveDisplayList(MaxDisplays, displays, &displayCount) == kCGErrorSuccess) {
const auto mask = static_cast<CGOpenGLDisplayMask>(displayMask);
for (std::uint32_t i = 0; i < displayCount; ++i) {
if ((CGDisplayIDToOpenGLDisplayMask(displays[i]) & mask) != 0) {
return displays[i];
}
}
}
return CGMainDisplayID();
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
CVReturn MobileGLCVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(
CVDisplayLinkRef displayLink,
CGLContextObj context,
CGLPixelFormatObj pixelFormat) {
GLint virtualScreen = 0;
if (MobileGL::MG_Impl::CGLImpl::GetVirtualScreen(context, &virtualScreen) == kCGLNoError) {
GLint displayMask = 0;
if (!displayLink ||
MobileGL::MG_Impl::CGLImpl::DescribePixelFormat(
pixelFormat, virtualScreen, kCGLPFADisplayMask, &displayMask) != kCGLNoError) {
return kCVReturnInvalidArgument;
}
return CVDisplayLinkSetCurrentCGDisplay(displayLink, DisplayForMask(displayMask));
}
using OriginalFunction = CVReturn (*)(CVDisplayLinkRef, CGLContextObj, CGLPixelFormatObj);
static const auto original = reinterpret_cast<OriginalFunction>(
dlsym(RTLD_NEXT, "CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext"));
return original ? original(displayLink, context, pixelFormat) : kCVReturnError;
}
__attribute__((used)) static const DyldInterposeEntry kMobileGLDyldInterpose[]
__attribute__((section("__DATA,__interpose"))) = {
{reinterpret_cast<const void*>(MobileGLDlsym), reinterpret_cast<const void*>(dlsym)},
{reinterpret_cast<const void*>(MobileGLCVDisplayLinkSetCurrentCGDisplayFromOpenGLContext),
reinterpret_cast<const void*>(CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext)},
};
#pragma clang diagnostic pop
} // namespace
#endif
@@ -0,0 +1,10 @@
# Public CGL entry points.
_CGL*
# Public EGL entry points.
_egl*
# Public OpenGL and GLX entry points. OpenGL function names always use an
# uppercase letter or digit after the "gl" prefix; excluding lowercase here
# deliberately prevents glslang_* from matching this pattern.
_gl[A-Z0-9]*
+28 -5
View File
@@ -8,6 +8,7 @@
#include "EGLImpl.h"
#include "../GetProcAddress.h"
#include <Init.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_State/EGLState/Core.h>
#include <mutex>
@@ -25,6 +26,17 @@ namespace MobileGL::MG_Impl::EGLImpl {
return MG_State::pEGLContext.get();
}
// Entry points that can legitimately be an application's FIRST EGL
// call (display/proc-address/string queries) lazily bring MobileGL
// up here, so the library needs no static constructor and can
// re-initialize after the last eglTerminate tore everything down.
// Teardown-ish entry points keep using GetState() and fail benignly
// when MobileGL is not initialized.
EGLStateContext* GetStateEnsureInitialized() {
MobileGL::EnsureInitialized();
return GetState();
}
MG_Backend::BackendObject* GetBackendObject(EGLStateContext* state) {
auto* backendObject = MG_Backend::pActiveBackendObject.get();
if (!backendObject && state) {
@@ -49,6 +61,8 @@ namespace MobileGL::MG_Impl::EGLImpl {
return MG_Backend::WindowBackend::Android;
#elif defined(__APPLE__)
return MG_Backend::WindowBackend::MetalLayer;
#elif defined(_WIN32)
return MG_Backend::WindowBackend::Win32;
#elif defined(__linux__)
return MG_Backend::WindowBackend::X11;
#else
@@ -187,7 +201,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_FALSE;
}
@@ -208,7 +222,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
EGLDisplay GetDisplay(NativeDisplayType display) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_NO_DISPLAY;
}
@@ -313,6 +327,14 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
backendObject->ReleaseEGLResources();
}
// The last initialized display is gone and nothing is current on any
// thread: tear the whole library down deterministically inside the
// EGL lifecycle (backend, GL/EGL state, glslang). A later EGL call
// re-initializes lazily via GetStateEnsureInitialized(); process exit
// then has nothing left to destroy.
if (!state->HasAnyInitializedDisplay() && !state->HasAnyCurrentContext()) {
MobileGL::Destroy();
}
return EGL_TRUE;
}
@@ -345,7 +367,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
EGLBoolean BindAPI(EGLenum api) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_FALSE;
}
@@ -378,7 +400,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
char const* QueryString(EGLDisplay display, EGLint name) {
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return nullptr;
}
@@ -641,7 +663,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list) {
(void)attrib_list;
auto* state = GetState();
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_NO_DISPLAY;
}
@@ -737,6 +759,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!name) {
return nullptr;
}
MobileGL::EnsureInitialized();
MGLOG_D("eglGetProcAddress(%s)", name);
void* proc = MG_Impl::GetProcAddress(name);
@@ -1351,6 +1351,14 @@ namespace MobileGL::MG_Impl::GLImpl {
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, pointIndex)) return;
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Transform feedback buffer bindings cannot change while transform "
"feedback is active."));
return;
}
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, pointIndex);
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, pointIndex);
@@ -1384,6 +1392,14 @@ namespace MobileGL::MG_Impl::GLImpl {
BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return;
if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Transform feedback buffer bindings cannot change while transform "
"feedback is active."));
return;
}
MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, index);
auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, index);
@@ -60,6 +60,11 @@ namespace MobileGL::MG_Impl::GLImpl::BufferImpl {
MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings;
pointCount = std::min(pointCount, static_cast<SizeT>(std::max(backendCount, 0)));
}
if (target == BufferTarget::TransformFeedback) {
// GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS bounds the indexed capture
// binding points in GL 3.3 (no ARB_transform_feedback3).
pointCount = std::min<SizeT>(pointCount, 4);
}
if (index < pointCount) {
return true;
+227 -9
View File
@@ -48,6 +48,73 @@ namespace MobileGL::MG_Impl::GLImpl {
return true;
}
// Primitives a draw of `count` vertices in `mode` assembles (0 for
// incomplete primitives). Used for the CPU-side transform feedback
// primitive accounting.
static Uint64 CountPrimitivesForDraw(GLenum mode, GLsizei count) {
if (count <= 0) return 0;
switch (mode) {
case GL_POINTS: return static_cast<Uint64>(count);
case GL_LINES: return static_cast<Uint64>(count / 2);
case GL_LINE_STRIP: return count >= 2 ? static_cast<Uint64>(count - 1) : 0;
case GL_LINE_LOOP: return count >= 2 ? static_cast<Uint64>(count) : 0;
case GL_TRIANGLES: return static_cast<Uint64>(count / 3);
case GL_TRIANGLE_STRIP:
case GL_TRIANGLE_FAN: return count >= 3 ? static_cast<Uint64>(count - 2) : 0;
default: return 0;
}
}
// Accumulate the transform feedback primitive counter for a captured draw.
// Draws without a geometry stage write exactly the primitives they assemble,
// clamped by the capture buffers' remaining capacity (a full buffer stops
// recording whole primitives, which is what PRIMITIVES_WRITTEN reports).
// Geometry amplification is not modelled here.
static void AccountTransformFeedbackPrimitives(GLenum mode, GLsizei count) {
if (!MG_State::pGLContext->IsTransformFeedbackActive()) return;
Uint64 primitives = CountPrimitivesForDraw(mode, count);
if (primitives == 0) return;
MG_State::pGLContext->AddTransformFeedbackInputPrimitives(primitives);
Uint64 verticesPerPrimitive = 1;
switch (mode) {
case GL_LINES:
case GL_LINE_STRIP:
case GL_LINE_LOOP:
verticesPerPrimitive = 2;
break;
case GL_TRIANGLES:
case GL_TRIANGLE_STRIP:
case GL_TRIANGLE_FAN:
verticesPerPrimitive = 3;
break;
default:
break;
}
const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
if (program != nullptr) {
// Capacity in captured vertices = the tightest bound buffer.
Uint64 capacityVertices = ~0ull;
for (SizeT i = 0; i < program->GetTransformFeedbackBufferCount(); ++i) {
const Uint32 stride = program->GetTransformFeedbackStride(static_cast<Uint32>(i));
if (stride == 0) continue;
const auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
static_cast<Uint>(i));
const Range1D range = point.GetRange();
const Uint64 bytes = range.end > range.start ? static_cast<Uint64>(range.end - range.start) : 0;
capacityVertices = std::min<Uint64>(capacityVertices, bytes / stride);
}
if (capacityVertices != ~0ull) {
const Uint64 usedVertices = MG_State::pGLContext->GetTransformFeedbackCapturedVertices();
const Uint64 remainingVertices = capacityVertices > usedVertices ? capacityVertices - usedVertices : 0;
primitives = std::min<Uint64>(primitives, remainingVertices / verticesPerPrimitive);
}
}
MG_State::pGLContext->AddTransformFeedbackPrimitives(primitives);
MG_State::pGLContext->AddTransformFeedbackCapturedVertices(primitives * verticesPerPrimitive);
}
static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) {
const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
if (!activeBackendObject) {
@@ -57,15 +124,6 @@ namespace MobileGL::MG_Impl::GLImpl {
return false;
}
if (activeBackendObject->GetBackendType() == BackendType::DirectVulkan && mode == GL_LINE_LOOP) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
"Primitive mode GL_LINE_LOOP is not supported by the DirectVulkan backend."));
return false;
}
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
if (vao && vao->GetExternalIndex() == 0 && !MG_State::IsRelaxedSemanticsActive()) {
MG_State::pGLContext->RecordError(
@@ -75,6 +133,38 @@ namespace MobileGL::MG_Impl::GLImpl {
return false;
}
// While transform feedback is active the draw's primitive type must match
// the feedback primitive mode (GL 3.3 core 13.2.2). With a geometry shader
// the constraint moves to the shader's output primitive type instead, so
// the draw mode itself is unconstrained here.
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
!(MG_State::pGLContext->GetTransformFeedbackProgram() &&
MG_State::pGLContext->GetTransformFeedbackProgram()->GetShaderIndexByStage(ShaderStage::Geometry) >= 0)) {
const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode();
Bool compatible = false;
switch (feedbackMode) {
case GL_POINTS:
compatible = mode == GL_POINTS;
break;
case GL_LINES:
compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP;
break;
case GL_TRIANGLES:
compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN;
break;
default:
break;
}
if (!compatible) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", functionName,
"Primitive mode is incompatible with the active transform feedback primitive mode."));
return false;
}
}
return true;
}
@@ -402,12 +492,14 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count);
DrawElementsBaseVertex_Backend(mode, count, type, indices, basevertex);
}
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count);
DrawArrays_Backend(mode, first, count);
}
@@ -444,7 +536,133 @@ namespace MobileGL::MG_Impl::GLImpl {
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
if (!ValidateCurrentProgramForExecution(__func__)) return;
if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
AccountTransformFeedbackPrimitives(mode, count);
DrawElements_Backend(mode, count, type, indices);
}
void BeginTransformFeedback(GLenum primitiveMode) {
if (primitiveMode != GL_POINTS && primitiveMode != GL_LINES && primitiveMode != GL_TRIANGLES) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"primitiveMode must be GL_POINTS, GL_LINES or GL_TRIANGLES."));
return;
}
if (MG_State::pGLContext->IsTransformFeedbackActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is already active."));
return;
}
const auto& program = MG_State::pGLContext->GetCurrentProgram();
if (!program || !program->GetLinkStatus() || program->GetTransformFeedbackVaryingCount() == 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"No program with transform feedback varyings is active."));
return;
}
// Every capture buffer slot the program's mode uses must have a buffer bound.
const SizeT usedBufferCount = program->GetTransformFeedbackBufferCount();
for (SizeT i = 0; i < usedBufferCount; ++i) {
const auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
static_cast<Uint>(i));
if (point.GetBoundObject() == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"Transform feedback buffer binding point " + std::to_string(i) + " has no buffer bound."));
return;
}
}
MG_State::pGLContext->BeginTransformFeedback(primitiveMode, program);
}
// Vulkan transform feedback captures triangle strips in plain (i, i+1, i+2)
// vertex order, but GL decomposes odd strip triangles as (i+1, i, i+2)
// (GL 4.6 table 10.1). With the geometry stage's statically-known strip
// lengths the captured records are reordered in place: swap the first two
// vertex records of every odd triangle within each emitted strip.
static void FixupGsStripCaptureOrder(const SharedPtr<MG_State::GLState::ProgramObject>& program,
Uint64 inputPrimitives) {
if (program == nullptr || !program->HasGsTriangleStripCaptureFixup() || inputPrimitives == 0) {
return;
}
const auto& stripTriangles = program->GetGsStripTriangles();
// Global triangle indices whose leading vertex pair must swap.
Vector<Uint64> swapTriangles;
Uint64 triangleBase = 0;
for (Uint64 input = 0; input < inputPrimitives; ++input) {
for (const Uint32 stripLength : stripTriangles) {
for (Uint32 t = 1; t < stripLength; t += 2) {
swapTriangles.push_back(triangleBase + t);
}
triangleBase += stripLength;
}
}
if (swapTriangles.empty()) {
return;
}
for (SizeT bufferIndex = 0; bufferIndex < program->GetTransformFeedbackBufferCount(); ++bufferIndex) {
const Uint32 stride = program->GetTransformFeedbackStride(static_cast<Uint32>(bufferIndex));
if (stride == 0) continue;
const auto& bindingPoint =
MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
static_cast<Uint>(bufferIndex));
const auto& buffer = bindingPoint.GetBoundObject();
if (buffer == nullptr) continue;
const Range1D range = bindingPoint.GetRange();
const Uint8* mapped = buffer->MappedData();
if (mapped == nullptr) continue;
// The geometry stage amplifies, so the CPU vertex counter does not bound
// the capture; the binding range's whole-triangle capacity does.
const Uint64 rangeBytes = range.end > range.start ? static_cast<Uint64>(range.end - range.start) : 0;
const Uint64 capturedTriangles = std::min<Uint64>(triangleBase, (rangeBytes / stride) / 3);
// Observed Vulkan capture order for odd strip triangles is (i, i+2, i+1)
// (winding preserved by swapping the trailing pair); GL wants
// (i+1, i, i+2), which is one rotation away: (a,b,c) -> (c,a,b).
Vector<Uint8> scratch(stride);
for (const Uint64 triangle : swapTriangles) {
if (triangle >= capturedTriangles) break;
const SizeT v0Offset = static_cast<SizeT>(range.start) + static_cast<SizeT>(triangle * 3) * stride;
const SizeT v1Offset = v0Offset + stride;
const SizeT v2Offset = v1Offset + stride;
Memcpy(scratch.data(), mapped + v2Offset, stride);
buffer->WritebackFromBackend({const_cast<Uint8*>(mapped) + v1Offset, stride}, v2Offset);
buffer->WritebackFromBackend({const_cast<Uint8*>(mapped) + v0Offset, stride}, v1Offset);
buffer->WritebackFromBackend({scratch.data(), stride}, v0Offset);
}
}
}
void EndTransformFeedback(void) {
if (!MG_State::pGLContext->IsTransformFeedbackActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is not active."));
return;
}
const auto capturedProgram = MG_State::pGLContext->GetTransformFeedbackProgram();
const Uint64 inputPrimitives = MG_State::pGLContext->GetTransformFeedbackInputPrimitives();
MG_State::pGLContext->EndTransformFeedback();
// Captured results must be visible to MapBuffer/GetBufferSubData after
// End; the capture targets are host-coherent GPU memory, so completing
// the GPU work is all that is required.
auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
if (backendGL.FenceSync && backendGL.ClientWaitSync) {
if (auto sync = backendGL.FenceSync()) {
backendGL.ClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, ~0ull);
if (backendGL.DeleteSync) {
backendGL.DeleteSync(sync);
}
}
}
FixupGsStripCaptureOrder(capturedProgram, inputPrimitives);
}
} // namespace MobileGL::MG_Impl::GLImpl
@@ -11,6 +11,8 @@
namespace MobileGL::MG_Impl::GLImpl {
/* @INSERTION_POINT:FUNCTION_DECLARATION@ */
void BeginTransformFeedback(GLenum primitiveMode);
void EndTransformFeedback(void);
void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
void DispatchComputeIndirect(GLintptr indirect);
void MemoryBarrier(GLbitfield barriers);
@@ -236,12 +236,12 @@ DECLARE_GL_FUNCTION_HEAD(void, DeleteVertexArrays, GLsizei n, const GLuint* arra
DECLARE_GL_FUNCTION_HEAD(void, GenVertexArrays, GLsizei n, GLuint* arrays) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GenVertexArrays, n, arrays)
DECLARE_GL_FUNCTION_HEAD(GLboolean, IsVertexArray, GLuint array) DECLARE_GL_FUNCTION_END(GLboolean, IsVertexArray, array)
DECLARE_GL_FUNCTION_HEAD(void, GetIntegeri_v, GLenum target, GLuint index, GLint* data) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetIntegeri_v, target, index, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, BeginTransformFeedback, GLenum primitiveMode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BeginTransformFeedback, primitiveMode)
DECLARE_GL_FUNCTION_STUB_HEAD(void, EndTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EndTransformFeedback)
DECLARE_GL_FUNCTION_HEAD(void, BeginTransformFeedback, GLenum primitiveMode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BeginTransformFeedback, primitiveMode)
DECLARE_GL_FUNCTION_HEAD(void, EndTransformFeedback) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EndTransformFeedback)
DECLARE_GL_FUNCTION_HEAD(void, BindBufferRange, GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBufferRange, target, index, buffer, offset, size)
DECLARE_GL_FUNCTION_HEAD(void, BindBufferBase, GLenum target, GLuint index, GLuint buffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, BindBufferBase, target, index, buffer)
DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackVaryings, GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, TransformFeedbackVaryings, program, count, varyings, bufferMode)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetTransformFeedbackVarying, GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size, GLenum* type, GLchar* name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetTransformFeedbackVarying, program, index, bufSize, length, size, type, name)
DECLARE_GL_FUNCTION_HEAD(void, TransformFeedbackVaryings, GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TransformFeedbackVaryings, program, count, varyings, bufferMode)
DECLARE_GL_FUNCTION_HEAD(void, GetTransformFeedbackVarying, GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size, GLenum* type, GLchar* name) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetTransformFeedbackVarying, program, index, bufSize, length, size, type, name)
DECLARE_GL_FUNCTION_HEAD(void, VertexAttribIPointer, GLuint index, GLint size, GLenum type, GLsizei stride, const void* pointer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, VertexAttribIPointer, index, size, type, stride, pointer)
DECLARE_GL_FUNCTION_HEAD(void, GetVertexAttribIiv, GLuint index, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexAttribIiv, index, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, GetVertexAttribIuiv, GLuint index, GLenum pname, GLuint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetVertexAttribIuiv, index, pname, params)
@@ -295,7 +295,13 @@ DECLARE_GL_FUNCTION_HEAD(void, VertexAttribDivisor, GLuint index, GLuint divisor
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedback, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedback, target, id)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacks, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacks, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacks, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(GLboolean, IsTransformFeedback, GLuint id) DECLARE_GL_FUNCTION_STUB_END(GLboolean, IsTransformFeedback, id)
// Transform feedback objects are not implemented, so no name is ever a live object. The shared
// stub returns (type)1, telling a probing caller that every id it invents already exists; GL_FALSE
// is both truthful and what the spec requires for a name that was never generated.
MOBILEGL_GL_API GLboolean glIsTransformFeedback(GLuint id) {
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
return GL_FALSE;
}
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedback)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ResumeTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedback)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramBinary, GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramBinary, program, bufSize, length, binaryFormat, binary)
@@ -418,7 +424,7 @@ DECLARE_GL_FUNCTION_HEAD(void, DrawRangeElementsBaseVertex, GLenum mode, GLuint
DECLARE_GL_FUNCTION_HEAD(void, DrawElementsInstancedBaseVertex, GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElementsInstancedBaseVertex, mode, count, type, indices, instancecount, basevertex)
DECLARE_GL_FUNCTION_HEAD(void, FramebufferTexture, GLenum target, GLenum attachment, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferTexture, target, attachment, texture, level)
DECLARE_GL_FUNCTION_STUB_HEAD(void, PrimitiveBoundingBox, GLfloat minX, GLfloat minY, GLfloat minZ, GLfloat minW, GLfloat maxX, GLfloat maxY, GLfloat maxZ, GLfloat maxW) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PrimitiveBoundingBox, minX, minY, minZ, minW, maxX, maxY, maxZ, maxW)
DECLARE_GL_FUNCTION_STUB_HEAD(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_STUB_END(GLenum, GetGraphicsResetStatus)
DECLARE_GL_FUNCTION_HEAD(GLenum, GetGraphicsResetStatus) DECLARE_GL_FUNCTION_END(GLenum, GetGraphicsResetStatus)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ReadnPixels, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void* data) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ReadnPixels, x, y, width, height, format, type, bufSize, data)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformfv, GLuint program, GLint location, GLsizei bufSize, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformfv, program, location, bufSize, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnUniformiv, GLuint program, GLint location, GLsizei bufSize, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnUniformiv, program, location, bufSize, params)
@@ -2583,7 +2589,10 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackStreamAttribsNV, GLsizei co
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedbackNV, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedbackNV, target, id)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacksNV, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacksNV, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacksNV, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacksNV, n, ids)
DECLARE_GL_FUNCTION_STUB_HEAD(GLboolean, IsTransformFeedbackNV, GLuint id) DECLARE_GL_FUNCTION_STUB_END(GLboolean, IsTransformFeedbackNV, id)
MOBILEGL_GL_API GLboolean glIsTransformFeedbackNV(GLuint id) {
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
return GL_FALSE;
}
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedbackNV, )
DECLARE_GL_FUNCTION_STUB_HEAD(void, ResumeTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedbackNV, )
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackNV, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackNV, mode, id)
@@ -45,10 +45,32 @@ namespace MobileGL::MG_Impl::GLImpl {
depthAttachment.GetTextureLevel() != stencilAttachment.GetTextureLevel();
}
// Mirrors the renderer-side gate: distinct depth/stencil renderbuffers (or a
// renderbuffer paired with a texture) cannot form one Vulkan depth-stencil
// attachment, and GL permits reporting such framebuffers as UNSUPPORTED.
Bool HasDistinctCompleteDepthStencilRenderbufferAttachments(
const MG_State::GLState::FramebufferObject& framebufferObject) {
if (framebufferObject.GetExternalIndex() == 0) {
return false;
}
const auto& depthAttachment = framebufferObject.GetAttachment(FramebufferAttachmentType::Depth);
const auto& stencilAttachment = framebufferObject.GetAttachment(FramebufferAttachmentType::Stencil);
if (!depthAttachment.IsComplete() || !stencilAttachment.IsComplete()) {
return false;
}
if (depthAttachment.IsRenderbuffer() && stencilAttachment.IsRenderbuffer()) {
return depthAttachment.GetRenderbuffer().get() != stencilAttachment.GetRenderbuffer().get();
}
return (depthAttachment.IsRenderbuffer() || stencilAttachment.IsRenderbuffer()) &&
(depthAttachment.IsTexture() || stencilAttachment.IsTexture());
}
Bool IsUnsupportedFramebufferForDirectVulkan(
const MG_State::GLState::FramebufferObject& framebufferObject) {
// TODO: Keep this in sync with DirectVulkan renderbuffer support as color renderbuffer rendering lands.
return HasDistinctCompleteDepthStencilTextureAttachments(framebufferObject);
return HasDistinctCompleteDepthStencilTextureAttachments(framebufferObject) ||
HasDistinctCompleteDepthStencilRenderbufferAttachments(framebufferObject);
}
Bool HasDefinedAttachment(const MG_State::GLState::FramebufferObject& framebufferObject) {
@@ -147,6 +169,148 @@ namespace MobileGL::MG_Impl::GLImpl {
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, detail));
}
GLint ClassifyAttachmentComponentType(TextureInternalFormat internalFormat,
FramebufferAttachmentType attachmentType) {
// A stencil value is an unsigned integer index regardless of the depth half
// of a packed format.
if (attachmentType == FramebufferAttachmentType::Stencil) return GL_UNSIGNED_INT;
switch (internalFormat) {
case TextureInternalFormat::R16F:
case TextureInternalFormat::RG16F:
case TextureInternalFormat::RGB16F:
case TextureInternalFormat::RGBA16F:
case TextureInternalFormat::R32F:
case TextureInternalFormat::RG32F:
case TextureInternalFormat::RGB32F:
case TextureInternalFormat::RGBA32F:
case TextureInternalFormat::R11FG11FB10F:
case TextureInternalFormat::RGB9E5:
case TextureInternalFormat::DepthComponent32F:
case TextureInternalFormat::Depth32FStencil8:
return GL_FLOAT;
case TextureInternalFormat::R8I:
case TextureInternalFormat::R16I:
case TextureInternalFormat::R32I:
case TextureInternalFormat::RG8I:
case TextureInternalFormat::RG16I:
case TextureInternalFormat::RG32I:
case TextureInternalFormat::RGB8I:
case TextureInternalFormat::RGB16I:
case TextureInternalFormat::RGB32I:
case TextureInternalFormat::RGBA8I:
case TextureInternalFormat::RGBA16I:
case TextureInternalFormat::RGBA32I:
return GL_INT;
case TextureInternalFormat::R8UI:
case TextureInternalFormat::R16UI:
case TextureInternalFormat::R32UI:
case TextureInternalFormat::RG8UI:
case TextureInternalFormat::RG16UI:
case TextureInternalFormat::RG32UI:
case TextureInternalFormat::RGB8UI:
case TextureInternalFormat::RGB16UI:
case TextureInternalFormat::RGB32UI:
case TextureInternalFormat::RGBA8UI:
case TextureInternalFormat::RGBA16UI:
case TextureInternalFormat::RGBA32UI:
case TextureInternalFormat::RGB10A2UI:
return GL_UNSIGNED_INT;
case TextureInternalFormat::R8Snorm:
case TextureInternalFormat::R16Snorm:
case TextureInternalFormat::RG8Snorm:
case TextureInternalFormat::RG16Snorm:
case TextureInternalFormat::RGB8Snorm:
case TextureInternalFormat::RGB16Snorm:
case TextureInternalFormat::RGBA8Snorm:
case TextureInternalFormat::RGBA16Snorm:
return GL_SIGNED_NORMALIZED;
case TextureInternalFormat::Unknown:
return GL_NONE;
default:
return GL_UNSIGNED_NORMALIZED;
}
}
// Handles the format-derived pnames shared by GetFramebufferAttachmentParameteriv
// and its DSA variant. Returns true when pname was one of them.
Bool TryAnswerAttachmentFormatQuery(const MG_State::GLState::FramebufferAttachmentObject* attachmentObject,
FramebufferAttachmentType attachmentType, Bool depthStencilAlias,
GLenum pname, GLint* params, const char* caller) {
switch (pname) {
case GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE:
case GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE:
case GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE:
case GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE:
case GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE:
case GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE:
case GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE:
case GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING:
break;
default:
return false;
}
if (pname == GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE && depthStencilAlias) {
// The depth and stencil components have different types, so the combined
// attachment name has no single answer.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", caller,
"GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE cannot be queried on "
"GL_DEPTH_STENCIL_ATTACHMENT."));
return true;
}
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
// With OBJECT_TYPE == GL_NONE only OBJECT_TYPE and OBJECT_NAME may be queried.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"No image is attached to the queried attachment point."));
return true;
}
TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
if (attachmentObject->IsTexture() && attachmentObject->GetTexture()) {
internalFormat = attachmentObject->GetTexture()->GetFormat();
} else if (attachmentObject->IsRenderbuffer() && attachmentObject->GetRenderbuffer()) {
internalFormat = attachmentObject->GetRenderbuffer()->GetInternalFormat();
}
const auto sizes = MG_Util::GetComponentSizesForInternalFormat(internalFormat);
switch (pname) {
case GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE:
*params = sizes.Red;
break;
case GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE:
*params = sizes.Green;
break;
case GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE:
*params = sizes.Blue;
break;
case GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE:
*params = sizes.Alpha;
break;
case GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE:
*params = sizes.Depth;
break;
case GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE:
*params = sizes.Stencil;
break;
case GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING:
*params = (internalFormat == TextureInternalFormat::SRGB8 ||
internalFormat == TextureInternalFormat::SRGB8Alpha8)
? GL_SRGB
: GL_LINEAR;
break;
case GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE:
*params = ClassifyAttachmentComponentType(internalFormat, attachmentType);
break;
}
return true;
}
Bool ResolveRepresentableFramebufferTextureUploadTarget(const MG_State::GLState::ITextureObject& textureObject,
TextureUploadTarget& outUploadTarget,
Bool& outLayered) {
@@ -412,6 +576,27 @@ namespace MobileGL::MG_Impl::GLImpl {
FramebufferTarget framebufferTarget = MG_Util::ConvertGLEnumToFramebufferTarget(target);
if (!FramebufferImpl::ValidateFramebufferTarget(framebufferTarget)) return;
// Default-framebuffer attachment names (GL_DEPTH, GL_STENCIL, GL_FRONT/GL_BACK
// variants) alias onto the equivalent attachment points.
switch (attachment) {
case GL_DEPTH:
attachment = GL_DEPTH_ATTACHMENT;
break;
case GL_STENCIL:
attachment = GL_STENCIL_ATTACHMENT;
break;
case GL_FRONT:
case GL_FRONT_LEFT:
case GL_FRONT_RIGHT:
case GL_BACK:
case GL_BACK_LEFT:
case GL_BACK_RIGHT:
attachment = GL_COLOR_ATTACHMENT0;
break;
default:
break;
}
const Bool depthStencilAlias = attachment == GL_DEPTH_STENCIL_ATTACHMENT;
FramebufferAttachmentType attachmentType = depthStencilAlias
? FramebufferAttachmentType::Depth
@@ -428,20 +613,40 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
Bool depthStencilMismatch = false;
const auto* attachmentObject = [&]() -> const MG_State::GLState::FramebufferAttachmentObject* {
if (!depthStencilAlias) {
return &framebufferObject->GetAttachment(attachmentType);
}
const auto& depthAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Depth);
if (depthAttachment.IsValid() && !depthAttachment.IsEmpty()) return &depthAttachment;
const auto& stencilAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Stencil);
if (stencilAttachment.IsValid() && !stencilAttachment.IsEmpty()) return &stencilAttachment;
const Bool depthLive = depthAttachment.IsValid() && !depthAttachment.IsEmpty();
const Bool stencilLive = stencilAttachment.IsValid() && !stencilAttachment.IsEmpty();
if (depthLive && stencilLive) {
const Bool sameObject = depthAttachment.IsTexture() == stencilAttachment.IsTexture() &&
(!depthAttachment.IsTexture() || depthAttachment.GetTexture() == stencilAttachment.GetTexture()) &&
(!depthAttachment.IsRenderbuffer() ||
depthAttachment.GetRenderbuffer() == stencilAttachment.GetRenderbuffer());
depthStencilMismatch = !sameObject;
} else {
// GL_DEPTH_STENCIL_ATTACHMENT means "both halves"; a lone half does not answer it.
depthStencilMismatch = depthLive != stencilLive;
}
if (depthLive) return &depthAttachment;
if (stencilLive) return &stencilAttachment;
return nullptr;
}();
if (depthStencilMismatch) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetFramebufferAttachmentParameteriv_State",
"GL_DEPTH_STENCIL_ATTACHMENT query with different depth and stencil "
"attachment images."));
return;
}
switch (pname) {
case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE:
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
@@ -505,6 +710,10 @@ namespace MobileGL::MG_Impl::GLImpl {
: GL_FALSE;
break;
default:
if (TryAnswerAttachmentFormatQuery(attachmentObject, attachmentType, depthStencilAlias, pname, params,
"GetFramebufferAttachmentParameteriv_State")) {
return;
}
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
@@ -1468,20 +1677,40 @@ namespace MobileGL::MG_Impl::GLImpl {
: MG_Util::ConvertGLEnumToFramebufferAttachmentType(attachment);
if (!FramebufferImpl::ValidateFramebufferAttachmentType(attachmentType)) return;
Bool depthStencilMismatch = false;
const auto* attachmentObject = [&]() -> const MG_State::GLState::FramebufferAttachmentObject* {
if (!depthStencilAlias) {
return &framebufferObject->GetAttachment(attachmentType);
}
const auto& depthAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Depth);
if (depthAttachment.IsValid() && !depthAttachment.IsEmpty()) return &depthAttachment;
const auto& stencilAttachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Stencil);
if (stencilAttachment.IsValid() && !stencilAttachment.IsEmpty()) return &stencilAttachment;
const Bool depthLive = depthAttachment.IsValid() && !depthAttachment.IsEmpty();
const Bool stencilLive = stencilAttachment.IsValid() && !stencilAttachment.IsEmpty();
if (depthLive && stencilLive) {
const Bool sameObject = depthAttachment.IsTexture() == stencilAttachment.IsTexture() &&
(!depthAttachment.IsTexture() || depthAttachment.GetTexture() == stencilAttachment.GetTexture()) &&
(!depthAttachment.IsRenderbuffer() ||
depthAttachment.GetRenderbuffer() == stencilAttachment.GetRenderbuffer());
depthStencilMismatch = !sameObject;
} else {
// GL_DEPTH_STENCIL_ATTACHMENT means "both halves"; a lone half does not answer it.
depthStencilMismatch = depthLive != stencilLive;
}
if (depthLive) return &depthAttachment;
if (stencilLive) return &stencilAttachment;
return nullptr;
}();
if (depthStencilMismatch) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"GL_DEPTH_STENCIL_ATTACHMENT query with different depth and stencil "
"attachment images."));
return;
}
switch (pname) {
case GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE:
if (attachmentObject == nullptr || attachmentObject->IsEmpty() || !attachmentObject->IsValid()) {
@@ -1527,6 +1756,10 @@ namespace MobileGL::MG_Impl::GLImpl {
: GL_FALSE;
break;
default:
if (TryAnswerAttachmentFormatQuery(attachmentObject, attachmentType, depthStencilAlias, pname, params,
caller)) {
return;
}
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
@@ -2144,6 +2377,7 @@ namespace MobileGL::MG_Impl::GLImpl {
}
namespace FramebufferImpl {
UniquePtr<DefaultFramebufferInfo> pDefaultFramebufferInfo;
// Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<DefaultFramebufferInfo>& pDefaultFramebufferInfo = *new UniquePtr<DefaultFramebufferInfo>();
} // namespace FramebufferImpl
} // namespace MobileGL::MG_Impl::GLImpl
@@ -78,6 +78,6 @@ namespace MobileGL::MG_Impl::GLImpl {
SharedPtr<MG_State::GLState::ITextureObject> stencilAttachment;
};
extern UniquePtr<DefaultFramebufferInfo> pDefaultFramebufferInfo;
extern UniquePtr<DefaultFramebufferInfo>& pDefaultFramebufferInfo;
} // namespace FramebufferImpl
} // namespace MobileGL::MG_Impl::GLImpl
+61 -2
View File
@@ -213,8 +213,13 @@ namespace MobileGL::MG_Impl::GLImpl {
GLint maxSamples = 0;
for (const auto& attachment : drawFbo->GetAllAttachmentObjects()) {
if (!attachment.IsRenderbuffer() || !attachment.GetRenderbuffer()) continue;
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetRenderbuffer()->GetSamples()));
} else if (attachment.IsTexture() && attachment.GetTexture()) {
// Multisample texture attachments count too (GL_SAMPLE_BUFFERS must
// report 1 for any multisampled draw framebuffer).
maxSamples = std::max(maxSamples, static_cast<GLint>(attachment.GetTexture()->GetSamples()));
}
}
return maxSamples;
}
@@ -465,6 +470,14 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_STENCIL_TEST:
*params = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest) ? GL_TRUE : GL_FALSE;
return;
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
GLfloat value = 0.0f;
GetFloatv(pname, &value);
*params = value != 0.0f ? GL_TRUE : GL_FALSE;
return;
}
default:
break;
}
@@ -520,6 +533,19 @@ namespace MobileGL::MG_Impl::GLImpl {
params[1] = dynamicParameters.ViewportBoundsRangeMax;
return;
}
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS: {
const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters();
if (pname == GL_MIN_FRAGMENT_INTERPOLATION_OFFSET) {
params[0] = dynamicParameters.MinFragmentInterpolationOffset;
} else if (pname == GL_MAX_FRAGMENT_INTERPOLATION_OFFSET) {
params[0] = dynamicParameters.MaxFragmentInterpolationOffset;
} else {
params[0] = static_cast<GLfloat>(dynamicParameters.FragmentInterpolationOffsetBits);
}
return;
}
case GL_DEPTH_CLEAR_VALUE:
params[0] = MG_State::pGLContext->GetClearDepth();
return;
@@ -1901,6 +1927,22 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS:
*params = kFrontendMaxTransformFeedbackSeparateComponents;
break;
// ARB_transform_feedback3 limits. The GL CTS queries these before checking
// whether the extension is advertised and requires no GL error; desktop
// drivers all accept them, so answer with the separate-attrib capacity and
// the single vertex stream the backends provide.
case GL_MAX_TRANSFORM_FEEDBACK_BUFFERS:
*params = kFrontendMaxTransformFeedbackSeparateAttribs;
break;
case GL_MAX_VERTEX_STREAMS:
*params = 1;
break;
case GL_TRANSFORM_FEEDBACK_ACTIVE:
*params = MG_State::pGLContext->IsTransformFeedbackActive() ? 1 : 0;
break;
case GL_TRANSFORM_FEEDBACK_PAUSED:
*params = 0;
break;
case GL_MAX_TEXTURE_IMAGE_UNITS:
*params = dynamicParameters.MaxTextureImageUnits;
break;
@@ -1957,6 +1999,15 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_SUBPIXEL_BITS:
*params = std::max(dynamicParameters.ViewportSubpixelBits, kFrontendSubpixelBits);
break;
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
*params = static_cast<GLint>(std::lround(dynamicParameters.MinFragmentInterpolationOffset));
break;
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
*params = static_cast<GLint>(std::lround(dynamicParameters.MaxFragmentInterpolationOffset));
break;
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
*params = dynamicParameters.FragmentInterpolationOffsetBits;
break;
case GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT:
*params = static_cast<Int>(dynamicParameters.UniformBufferOffsetAlignment);
break;
@@ -1996,4 +2047,12 @@ namespace MobileGL::MG_Impl::GLImpl {
}
return MG_Util::ConvertErrorCodeToGLEnum(error->get()->code);
}
GLenum GetGraphicsResetStatus() {
// MobileGL does not implement robustness reset notification, so report GL_NO_ERROR
// ("no reset detected"). Returning the generic stub's (GLenum)1 makes dEQP read a lost
// device after every case (gl3cTestPackages.cpp:121) and, under the default
// --deqp-terminate-on-device-lost=enable, tear the whole CTS run down.
return GL_NO_ERROR;
}
} // namespace MobileGL::MG_Impl::GLImpl
@@ -21,4 +21,5 @@ namespace MobileGL::MG_Impl::GLImpl {
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
GLenum GetError();
GLenum GetGraphicsResetStatus();
} // namespace MobileGL::MG_Impl::GLImpl
+108 -13
View File
@@ -49,10 +49,18 @@ namespace MobileGL::MG_Impl::GLImpl {
static bool CheckProgramNameValidity(GLuint program) {
if (!MG_State::pGLContext->ValidateProgramName(program)) {
// Programs and shaders share one name space: a name that exists but
// belongs to a shader is INVALID_OPERATION, a name GL never handed
// out is INVALID_VALUE (GL 3.3 core 2.11.x).
const ErrorCode error = MG_State::pGLContext->ValidateShaderName(program)
? ErrorCode::InvalidOperation
: ErrorCode::InvalidValue;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
error,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
std::to_string(program) + " is not a valid name."));
std::to_string(program) +
(error == ErrorCode::InvalidOperation ? " is not a program object."
: " is not a valid name.")));
return false;
}
return true;
@@ -605,6 +613,18 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = programObject->GetActiveUniformBlocksMaxNameLength() + 1;
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
break;
case GL_TRANSFORM_FEEDBACK_VARYINGS:
*params = static_cast<GLint>(programObject->GetTransformFeedbackVaryingCount());
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
break;
case GL_TRANSFORM_FEEDBACK_BUFFER_MODE:
*params = static_cast<GLint>(programObject->GetTransformFeedbackBufferMode());
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
break;
case GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH:
*params = programObject->GetTransformFeedbackVaryingMaxLength();
MGLOG_D("%s: %s = %d", __func__, MG_Util::ConvertGLEnumToString(pname).c_str(), *params);
break;
case GL_COMPUTE_WORK_GROUP_SIZE: { // GL >= 4.3
if (!programObject->GetLinkStatus() || programObject->GetShaderIndexByStage(ShaderStage::Compute) < 0) {
MG_State::pGLContext->RecordError(
@@ -624,9 +644,6 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_PROGRAM_BINARY_LENGTH:
case GL_TRANSFORM_FEEDBACK_BUFFER_MODE:
case GL_TRANSFORM_FEEDBACK_VARYINGS:
case GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH:
case GL_GEOMETRY_VERTICES_OUT:
case GL_GEOMETRY_INPUT_TYPE:
case GL_GEOMETRY_OUTPUT_TYPE:
@@ -827,19 +844,13 @@ namespace MobileGL::MG_Impl::GLImpl {
}
GLboolean IsProgram_State(GLuint program) {
/* FIXME: Handle situations that:
* A program object marked for deletion with glDeleteProgram but still in use as part of current
* rendering state is still considered a program object and glIsProgram will return GL_TRUE.
*/
// Deletion-flagged names stay valid while the object is still GL-visible (program in
// use, shader attached), so name validity is exactly the Is* answer.
if (program == 0) return GL_FALSE;
return MG_State::pGLContext->ValidateProgramName(program) ? GL_TRUE : GL_FALSE;
}
GLboolean IsShader_State(GLuint shader) {
/* FIXME: Handle situations that:
* A shader object marked for deletion with glDeleteShader but still attached to a program object is still
* considered a shader object and glIsShader will return GL_TRUE.
*/
if (shader == 0) return GL_FALSE;
return MG_State::pGLContext->ValidateShaderName(shader) ? GL_TRUE : GL_FALSE;
}
@@ -849,6 +860,18 @@ namespace MobileGL::MG_Impl::GLImpl {
if (!programObject) return;
MGLOG_D("%s: linking program %d", __func__, program);
// Relinking the program an active transform feedback captures from would
// invalidate its varyings mid-capture (GL 3.3 core 2.11.3).
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
MG_State::pGLContext->GetTransformFeedbackProgram().get() == programObject.get()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"The program used by active transform feedback cannot be relinked."));
return;
}
static Bool allowVSOnlyPrograms;
static Bool initialized = false;
if (!initialized) {
@@ -892,6 +915,16 @@ namespace MobileGL::MG_Impl::GLImpl {
void UseProgram_State(GLuint program) {
MGLOG_D("UseProgram_State: program=%u", program);
// GL 3.3 core 2.11.3: the program in use may not change while transform
// feedback is active (there is no pause in 3.3).
if (MG_State::pGLContext->IsTransformFeedbackActive()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"The current program cannot change while transform feedback is active."));
return;
}
if (program == 0) {
MG_State::pGLContext->UseProgram(0);
return;
@@ -2217,4 +2250,66 @@ namespace MobileGL::MG_Impl::GLImpl {
void ValidateProgram(GLuint program) {
ValidateProgram_State(program);
}
void TransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode) {
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (bufferMode != GL_INTERLEAVED_ATTRIBS && bufferMode != GL_SEPARATE_ATTRIBS) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "bufferMode is not a valid capture mode."));
return;
}
if (count < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "count must be non-negative."));
return;
}
// GL 3.3 core: SEPARATE_ATTRIBS count may not exceed the separate-attrib limit.
if (bufferMode == GL_SEPARATE_ATTRIBS && count > 4) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"count exceeds GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS."));
return;
}
Vector<String> names;
names.reserve(static_cast<SizeT>(count));
for (GLsizei i = 0; i < count; ++i) {
names.emplace_back(varyings != nullptr && varyings[i] != nullptr ? varyings[i] : "");
}
programObject->SetTransformFeedbackVaryings(Move(names), bufferMode);
}
void GetTransformFeedbackVarying(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size,
GLenum* type, GLchar* name) {
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
if (!programObject->GetLinkStatus()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
std::to_string(program) + " has not been successfully linked."));
return;
}
const auto* varying = programObject->GetTransformFeedbackVarying(index);
if (varying == nullptr) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"index is not an active transform feedback varying of the program."));
return;
}
if (size != nullptr) *size = varying->size;
if (type != nullptr) *type = varying->type;
GLsizei written = 0;
if (name != nullptr && bufSize > 0) {
written = std::min<GLsizei>(bufSize - 1, static_cast<GLsizei>(varying->name.size()));
Memcpy(name, varying->name.data(), static_cast<SizeT>(written));
name[written] = '\0';
}
if (length != nullptr) *length = written;
}
} // namespace MobileGL::MG_Impl::GLImpl
@@ -138,4 +138,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
void ValidateProgram(GLuint program);
void TransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar* const* varyings, GLenum bufferMode);
void GetTransformFeedbackVarying(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLsizei* size,
GLenum* type, GLchar* name);
} // namespace MobileGL::MG_Impl::GLImpl
+127 -20
View File
@@ -27,6 +27,8 @@ namespace MobileGL::MG_Impl::GLImpl {
Bool ended = false;
Bool resultCached = false;
Uint64 cachedResult = 0;
// Transform feedback primitive counter at BeginQuery time.
Uint64 counterSnapshot = 0;
};
// Query calls may arrive from any thread (launchers migrate the context
@@ -41,6 +43,11 @@ namespace MobileGL::MG_Impl::GLImpl {
GLuint g_nextQueryId = 1;
// Id of the query currently active on GL_TIME_ELAPSED (0 = none).
GLuint g_activeTimeElapsedQueryId = 0;
// Ids of the queries active on the transform feedback targets (0 = none).
GLuint g_activePrimitivesWrittenQueryId = 0;
GLuint g_activePrimitivesGeneratedQueryId = 0;
// Id of the query active on GL_SAMPLES_PASSED (0 = none).
GLuint g_activeSamplesPassedQueryId = 0;
Bool TimerQueryDisabled() {
return MG_Config::Features.DisableTimerQuery;
@@ -126,6 +133,11 @@ namespace MobileGL::MG_Impl::GLImpl {
outValue = 0;
return true;
}
// ANY_SAMPLES_PASSED* report a boolean.
if (queryObject->target == GL_ANY_SAMPLES_PASSED ||
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
result = result != 0 ? 1 : 0;
}
// Final value produced (or no GetQueryResult64 hook: the
// query degrades to a zero result); the backend handle is
// consumed and the value cached for later reads.
@@ -180,7 +192,24 @@ namespace MobileGL::MG_Impl::GLImpl {
}
QueryObject* queryObject = it->second;
if (queryObject->active) {
EndTimeElapsedQueryLocked(queryObject); // implicitly end before deletion
// Implicitly end before deletion, releasing the matching active slot.
if (queryObject->target == GL_SAMPLES_PASSED || queryObject->target == GL_ANY_SAMPLES_PASSED ||
queryObject->target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
endOcclusionQuery && queryObject->backendHandle) {
endOcclusionQuery(queryObject->backendHandle);
}
queryObject->active = false;
g_activeSamplesPassedQueryId = 0;
} else if (queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ||
queryObject->target == GL_PRIMITIVES_GENERATED) {
queryObject->active = false;
(queryObject->target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN
? g_activePrimitivesWrittenQueryId
: g_activePrimitivesGeneratedQueryId) = 0;
} else {
EndTimeElapsedQueryLocked(queryObject);
}
}
if (queryObject->backendHandle) {
if (const auto deleteBackendQuery = MG_Backend::gBackendFunctionsTable.GL.DeleteBackendQuery) {
@@ -204,10 +233,15 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void BeginQuery(GLenum target, GLuint id) {
if (target != GL_TIME_ELAPSED) {
// Only GL_TIME_ELAPSED timer queries are implemented (occlusion and
// primitive queries remain stubs); GL_TIMESTAMP is not a valid
// BeginQuery target either.
const Bool isTransformFeedbackQuery =
target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || target == GL_PRIMITIVES_GENERATED;
const Bool isOcclusionQuery =
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
// GL_TIMESTAMP is not a valid BeginQuery target; the occlusion targets
// need backend support.
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
return;
}
@@ -221,9 +255,13 @@ namespace MobileGL::MG_Impl::GLImpl {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "Query object does not exist.");
return;
}
if (g_activeTimeElapsedQueryId != 0) {
GLuint& activeQueryId = isTransformFeedbackQuery
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
: g_activePrimitivesGeneratedQueryId)
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
if (activeQueryId != 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__,
"A query is already active on GL_TIME_ELAPSED.");
"A query is already active on this target.");
return;
}
if (queryObject->active) {
@@ -239,25 +277,72 @@ namespace MobileGL::MG_Impl::GLImpl {
ResetQueryObjectLocked(queryObject); // discard any previous result
queryObject->target = target;
queryObject->active = true;
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
queryObject->backendHandle =
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
g_activeTimeElapsedQueryId = id;
if (isTransformFeedbackQuery) {
// Prefer real GPU transform-feedback queries (exact with geometry shaders);
// the CPU accounting delta stays as the fallback when the backend lacks them.
const auto beginXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.BeginXfbPrimitivesQuery;
queryObject->backendHandle =
beginXfbPrimitivesQuery ? beginXfbPrimitivesQuery(target == GL_PRIMITIVES_GENERATED) : nullptr;
queryObject->counterSnapshot = MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter();
} else if (isOcclusionQuery) {
queryObject->backendHandle = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery();
} else {
const auto beginTimeElapsedQuery = MG_Backend::gBackendFunctionsTable.GL.BeginTimeElapsedQuery;
queryObject->backendHandle =
(!TimerQueryDisabled() && beginTimeElapsedQuery) ? beginTimeElapsedQuery() : nullptr;
}
activeQueryId = id;
}
void EndQuery(GLenum target) {
if (target != GL_TIME_ELAPSED) {
const Bool isTransformFeedbackQuery =
target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN || target == GL_PRIMITIVES_GENERATED;
const Bool isOcclusionQuery =
(target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) &&
MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
if (target != GL_TIME_ELAPSED && !isTransformFeedbackQuery && !isOcclusionQuery) {
RecordQueryError(ErrorCode::InvalidEnum, __FUNCTION__, "Query target is not supported.");
return;
}
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
if (g_activeTimeElapsedQueryId == 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on GL_TIME_ELAPSED.");
GLuint& activeQueryId = isTransformFeedbackQuery
? (target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN ? g_activePrimitivesWrittenQueryId
: g_activePrimitivesGeneratedQueryId)
: (isOcclusionQuery ? g_activeSamplesPassedQueryId : g_activeTimeElapsedQueryId);
if (activeQueryId == 0) {
RecordQueryError(ErrorCode::InvalidOperation, __FUNCTION__, "No query is active on this target.");
return;
}
auto* queryObject = FindQueryObjectLocked(g_activeTimeElapsedQueryId);
auto* queryObject = FindQueryObjectLocked(activeQueryId);
if (!queryObject) {
g_activeTimeElapsedQueryId = 0; // should not happen; keep state consistent
activeQueryId = 0; // should not happen; keep state consistent
return;
}
if (isTransformFeedbackQuery) {
if (queryObject->backendHandle) {
if (const auto endXfbPrimitivesQuery = MG_Backend::gBackendFunctionsTable.GL.EndXfbPrimitivesQuery) {
endXfbPrimitivesQuery(queryObject->backendHandle);
}
// Result comes from the GPU query at read time.
} else {
queryObject->cachedResult =
MG_State::pGLContext->GetTransformFeedbackPrimitiveCounter() - queryObject->counterSnapshot;
queryObject->resultCached = true;
}
queryObject->active = false;
queryObject->ended = true;
activeQueryId = 0;
return;
}
if (isOcclusionQuery) {
if (const auto endOcclusionQuery = MG_Backend::gBackendFunctionsTable.GL.EndOcclusionQuery;
endOcclusionQuery && queryObject->backendHandle) {
endOcclusionQuery(queryObject->backendHandle);
}
queryObject->active = false;
queryObject->ended = true;
activeQueryId = 0;
return;
}
EndTimeElapsedQueryLocked(queryObject);
@@ -303,9 +388,25 @@ namespace MobileGL::MG_Impl::GLImpl {
switch (pname) {
case GL_CURRENT_QUERY: {
const std::lock_guard<std::mutex> lock(g_queryObjectsMutex);
// Only GL_TIME_ELAPSED queries can be active; GL_TIMESTAMP queries
// never are, and other targets remain unimplemented.
*params = target == GL_TIME_ELAPSED ? static_cast<GLint>(g_activeTimeElapsedQueryId) : 0;
switch (target) {
case GL_TIME_ELAPSED:
*params = static_cast<GLint>(g_activeTimeElapsedQueryId);
break;
case GL_SAMPLES_PASSED:
case GL_ANY_SAMPLES_PASSED:
case GL_ANY_SAMPLES_PASSED_CONSERVATIVE:
*params = static_cast<GLint>(g_activeSamplesPassedQueryId);
break;
case GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN:
*params = static_cast<GLint>(g_activePrimitivesWrittenQueryId);
break;
case GL_PRIMITIVES_GENERATED:
*params = static_cast<GLint>(g_activePrimitivesGeneratedQueryId);
break;
default:
*params = 0;
break;
}
return;
}
case GL_QUERY_COUNTER_BITS: {
@@ -313,7 +414,13 @@ namespace MobileGL::MG_Impl::GLImpl {
// time: IsTimerQuerySupported is the dynamic truth (extension /
// entry points / timestamp valid bits at call time, not at table
// init), and the MOBILEGL_DISABLE_TIMERQUERY kill switch always
// wins. Non-timer targets remain unimplemented and report 0.
// wins.
if (target == GL_SAMPLES_PASSED || target == GL_ANY_SAMPLES_PASSED ||
target == GL_ANY_SAMPLES_PASSED_CONSERVATIVE) {
const Bool occlusionSupported = MG_Backend::gBackendFunctionsTable.GL.BeginOcclusionQuery != nullptr;
*params = occlusionSupported ? (target == GL_SAMPLES_PASSED ? 32 : 1) : 0;
return;
}
const Bool timerTarget = target == GL_TIME_ELAPSED || target == GL_TIMESTAMP;
const auto isTimerQuerySupported = MG_Backend::gBackendFunctionsTable.GL.IsTimerQuerySupported;
const Bool supported =
@@ -185,6 +185,11 @@ namespace MobileGL::MG_Impl::GLImpl {
static thread_local Vector<GLuint> names;
MG_State::pGLContext->GenSamplerNames(count, names);
Memcpy(samplers, names.data(), count * sizeof(GLuint));
// Unlike textures/buffers, glGenSamplers CREATES the sampler objects: each name
// is immediately a sampler (glIsSampler == GL_TRUE before any bind).
for (GLsizei i = 0; i < count; ++i) {
MG_State::pGLContext->CreateSamplerObject(names[i]);
}
}
void DeleteSamplers_State(GLsizei count, const GLuint* samplers) {
+29
View File
@@ -133,4 +133,33 @@ namespace MobileGL::MG_Impl::GLImpl {
values[0] = value;
}
}
void DestroyAllSyncObjects() {
// Detach the registry under the lock, release outside it. Entries the app
// already deleted were erased by DeleteSync, so nothing here double-frees;
// a DeleteSync racing this sweep finds an empty registry and returns. A
// thread still blocked inside ClientWaitSync/GetSynciv during teardown
// holds a raw SyncObject* these deletes invalidate - the same undefined
// race an app-driven DeleteSync already has.
UnorderedMap<GLsync, SyncObject*> orphans;
{
const std::lock_guard<std::mutex> lock(g_syncObjectsMutex);
orphans.swap(g_liveSyncObjects);
}
if (orphans.empty()) {
return;
}
// Both backends' DeleteSync only free the heap wrapper once their GL
// context/renderer is gone (generation/current-thread guards), so this is
// safe after the backend has released its EGL resources - but not after
// the function table itself is cleared.
const auto backendDeleteSync = MG_Backend::gBackendFunctionsTable.GL.DeleteSync;
for (const auto& [_, syncObject] : orphans) {
if (backendDeleteSync && syncObject->backendHandle) {
backendDeleteSync(syncObject->backendHandle);
}
delete syncObject;
}
MGLOG_D("DestroyAllSyncObjects: reclaimed %zu sync object(s) the app left undeleted", orphans.size());
}
} // namespace MobileGL::MG_Impl::GLImpl
+8
View File
@@ -16,4 +16,12 @@ namespace MobileGL::MG_Impl::GLImpl {
void WaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout);
void DeleteSync(GLsync sync);
void GetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values);
// Destroys every still-registered sync object exactly as DeleteSync would.
// GL requires syncs to die with their context; called only from full library
// teardown (DestroyImpl), where no context survives on any thread, so the
// process-global registry can be drained wholesale. Must run while the
// backend function table is still populated: each backend handle has to be
// released by the backend that created it, never by a later re-initialized
// one.
void DestroyAllSyncObjects();
} // namespace MobileGL::MG_Impl::GLImpl
+147 -18
View File
@@ -350,6 +350,10 @@ namespace MobileGL::MG_Impl::GLImpl {
texture.AllocateStorage(uploadTarget, level, {levelTexelSize, levelByteSize});
texture.MarkStorageDirty(uploadTarget, level, false);
}
// glGenerateMipmap defines exactly levels 0..requiredLevelCount-1. AllocateStorage only
// grows, so a previously longer chain (a bigger base image before respecification) would
// otherwise keep a tail of stale levels here and read as incomplete.
texture.TruncateMipmapLevels(uploadTarget, requiredLevelCount);
// Mip generation grows/regenerates the level set on the GPU without marking any CPU
// level dirty (MarkStorageDirty(...,false) above). Bump the content version so the
// backend re-syncs: a cached sampled VkImageView built for the pre-generate level
@@ -429,8 +433,10 @@ namespace MobileGL::MG_Impl::GLImpl {
const Int maxSamples = GetMaxSupportedTextureSamples(textureInternalFormat);
if (samples > maxSamples) {
// GL specifies INVALID_OPERATION - not INVALID_VALUE - when the sample count
// exceeds what the format supports, and the native Adreno driver agrees.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", caller,
std::format("Sample count {} exceeds the supported maximum {} for this texture format.",
@@ -454,8 +460,56 @@ namespace MobileGL::MG_Impl::GLImpl {
textureObject->SetSamples(samples);
textureObject->SetFixedSampleLocations(fixedsamplelocations == GL_TRUE);
textureMipmapObject->AllocateStorage(textureUploadTarget, 0, {{width, height, depth}, 0});
// Multisample textures are single-level by definition, so a name that previously held a
// mip chain must not keep its tail now that AllocateStorage only grows.
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, 1);
textureMipmapObject->MarkStorageDirty(textureUploadTarget, 0, false);
}
// Redefining level 0 of a texture that already had a base image drops the rest of the chain,
// which is exactly what AllocateLevel used to do implicitly for every level. Keeping that
// behaviour for level 0 - and only for level 0 - is what makes the grow-only change safe:
// any level-0 respecification leaves the chain in precisely the state it would have had
// before, while an upload to level N no longer destroys the levels beneath it.
//
// Why it has to be *every* level-0 respecification and not just a size change: Minecraft's
// Mipmap Levels setting rebuilds the block atlas at the SAME dimensions with a different
// level count. A size-only test would leave the old tail in place, and because Mojang
// terminates its chains with a 0x0 level the result is the zero-then-nonzero pattern that
// IsComplete() rejects (TextureObject.cpp) - whereupon DirectGLES skips syncing the texture
// entirely (Managers.cpp) and the atlas samples black.
//
// The "already has a base image" test is what lets the fix work at all: a level that was
// never written reads back as {0,0,0}, so building a chain top-down - upload level N first,
// then level 0 - must not discard the levels just uploaded. That ordering is what
// KHR-GL33.texture_repeat_mode does.
// Scoped to the respecified upload target only, which is what AllocateLevel already did.
// Cube maps keep six independent chains while reporting a single level count (face +X), so
// respecifying a face other than +X can leave the count longer than that face - but that
// asymmetry predates this change and widening the truncation to all six faces would destroy
// mip data for faces the application never touched. Left alone deliberately.
void DiscardMipmapChainOnBaseRespecification(MG_State::GLState::TextureObjectMipmap* texture,
TextureUploadTarget uploadTarget, Uint level) {
if (level != 0) return;
const IntVec3 existingBaseSize = texture->GetMipmapTexelSize(uploadTarget, 0);
const Bool hasExistingBaseImage =
existingBaseSize.x() > 0 && existingBaseSize.y() > 0 && existingBaseSize.z() > 0;
if (!hasExistingBaseImage) return;
texture->TruncateMipmapLevels(uploadTarget, 1);
}
// Compressed texture upload is not implemented yet. GL_NUM_COMPRESSED_TEXTURE_FORMATS
// reports 0, so every compressed internalformat is by definition unsupported and
// GL_INVALID_ENUM is the specified error - unlike THROW_UNIMPL_EXCEPTION, which unwinds
// a C++ exception through the C GL ABI and takes the process down.
void RecordUnsupportedCompressedFormat(const char* caller) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
"Compressed texture formats are not supported."));
}
} // namespace
const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByName(GLuint texture, const char* caller) {
@@ -493,8 +547,15 @@ namespace MobileGL::MG_Impl::GLImpl {
}
auto mipmapTexture = std::static_pointer_cast<MG_State::GLState::TextureObjectMipmap>(textureObject);
if (level < 0 || static_cast<Uint>(level) >= mipmapTexture->GetMipmapLevelCount()) {
if (level < 0) {
RecordClearTextureError(caller, ErrorCode::InvalidValue,
std::format("Texture level {} is negative.", level));
return nullptr;
}
// ARB_clear_texture: clearing an image that was never defined by TexImage*/
// TexStorage* is INVALID_OPERATION, not INVALID_VALUE.
if (static_cast<Uint>(level) >= mipmapTexture->GetMipmapLevelCount()) {
RecordClearTextureError(caller, ErrorCode::InvalidOperation,
std::format("Texture level {} is not defined.", level));
return nullptr;
}
@@ -536,6 +597,12 @@ namespace MobileGL::MG_Impl::GLImpl {
return true;
}
// Writes the clear into the CPU shadow and marks the whole level dirty, exactly like
// TexSubImage*_State does. Shared limitation of the level-granular shadow sync: the
// shadow does not reflect GPU-side writes (FBO rendering, imageStore), so a PARTIAL
// clear of a GPU-written level re-uploads stale shadow bytes outside the region on
// the next sync. Full-level clears (glClearTexImage, or a sub-clear covering the
// level) rewrite the entire shadow and are always correct.
Bool ClearMipmapRegion(const SharedPtr<MG_State::GLState::TextureObjectMipmap>& textureObject,
TextureUploadTarget uploadTarget, GLint level,
GLint xoffset, GLint yoffset, GLint zoffset,
@@ -1657,6 +1724,21 @@ namespace MobileGL::MG_Impl::GLImpl {
return;
}
// RGTC is a 2D-only compression scheme, so a 3D target rejects it. This has to be tested on
// the raw enum: the RGTC formats resolve to plain R8/RG8/SNORM storage on the way in (see
// GLToMG's TextureEnumConverter), so once the internal format is converted there is nothing
// left to distinguish them from an ordinary one- or two-channel upload.
if ((textureUploadTarget == TextureUploadTarget::Texture3D ||
textureUploadTarget == TextureUploadTarget::ProxyTexture3D) &&
(internalformat == GL_COMPRESSED_RED_RGTC1 || internalformat == GL_COMPRESSED_SIGNED_RED_RGTC1 ||
internalformat == GL_COMPRESSED_RG_RGTC2 || internalformat == GL_COMPRESSED_SIGNED_RG_RGTC2)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"RGTC compressed formats are invalid for 3D texture targets"));
return;
}
// TODO: GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the
// GL_PIXEL_UNPACK_BUFFER target and the buffer object's data store is currently mapped.
// GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the GL_PIXEL_UNPACK_BUFFER
@@ -1714,6 +1796,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (isProxy) {
MGLOG_D("%s: isProxy = true, not allocating", __func__);
} else {
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
}
@@ -1841,6 +1924,7 @@ namespace MobileGL::MG_Impl::GLImpl {
MGLOG_D("%s: isProxy = true, not allocating", __func__);
} else {
MGLOG_D("%s: Allocating %d bytes at mip %d", __func__, internalBytes, level);
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
textureMipmapObject->AllocateStorage(textureUploadTarget, level,
{{width, height, 1}, internalBytes});
}
@@ -1929,6 +2013,7 @@ namespace MobileGL::MG_Impl::GLImpl {
"Texture object here should always be an object with mipmap");
auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
if (!isProxy) {
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, 1, 1}, internalBytes});
}
@@ -2581,7 +2666,13 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void GetCompressedTexImage_State(GLenum target, GLint level, void* img) {
// TODO: implement
// TODO: implement compressed readback. Reporting success while writing nothing hands
// the caller stale memory with GL_NO_ERROR; no texture can be compressed yet, and GL
// specifies GL_INVALID_OPERATION when the bound level is not compressed.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"Texture level is not stored in a compressed format."));
}
void GenTextures_State(GLsizei n, GLuint* textures) {
@@ -2729,7 +2820,22 @@ namespace MobileGL::MG_Impl::GLImpl {
"The attachment specified by the read buffer is incomplete.")); \
return false; \
}
if (isDepth) {
if (isDepth && isStencil) {
// A combined internalformat copies both halves, so the read framebuffer
// must populate both attachment points.
const auto& stencilAttachment = currentReadFBO->GetAttachment(FramebufferAttachmentType::Stencil);
const auto& depthAttachment = currentReadFBO->GetAttachment(FramebufferAttachmentType::Depth);
if (!depthAttachment.IsValid() || depthAttachment.IsEmpty() || !stencilAttachment.IsValid() ||
stencilAttachment.IsEmpty()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", "CopyTexImage2D_State",
"DEPTH_STENCIL copy requires both depth and stencil attachments in the read framebuffer."));
return false;
}
GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Depth);
} else if (isDepth) {
GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Depth);
} else if (isStencil) {
GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Stencil);
@@ -2768,20 +2874,20 @@ namespace MobileGL::MG_Impl::GLImpl {
void CompressedTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize,
const void* data) {
// TODO: implement
THROW_UNIMPL_EXCEPTION;
// TODO: implement compressed upload - see CompressedTexImage2D_State.
RecordUnsupportedCompressedFormat(__func__);
}
void CompressedTexSubImage2D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
GLsizei height, GLenum format, GLsizei imageSize, const void* data) {
// TODO: implement
THROW_UNIMPL_EXCEPTION;
// TODO: implement compressed upload - see CompressedTexImage2D_State.
RecordUnsupportedCompressedFormat(__func__);
}
void CompressedTexSubImage1D_State(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format,
GLsizei imageSize, const void* data) {
// TODO: implement
THROW_UNIMPL_EXCEPTION;
// TODO: implement compressed upload - see CompressedTexImage2D_State.
RecordUnsupportedCompressedFormat(__func__);
}
void CompressedTexImage3D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
@@ -2791,8 +2897,8 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!ValidateTextureMutable(textureObject, __func__)) return;
// TODO: implement
THROW_UNIMPL_EXCEPTION;
// TODO: implement compressed upload - see CompressedTexImage2D_State.
RecordUnsupportedCompressedFormat(__func__);
}
void CompressedTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
@@ -2802,8 +2908,11 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!ValidateTextureMutable(textureObject, __func__)) return;
// TODO: implement
THROW_UNIMPL_EXCEPTION;
// TODO: implement compressed upload. Until then report the spec error for an
// unsupported compressed format rather than throwing - a C++ exception unwinding
// through the C GL ABI is a hard crash for the caller, while GL_INVALID_ENUM is
// exactly what GL_NUM_COMPRESSED_TEXTURE_FORMATS == 0 promises.
RecordUnsupportedCompressedFormat(__func__);
}
void CompressedTexImage1D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border,
@@ -2813,8 +2922,8 @@ namespace MobileGL::MG_Impl::GLImpl {
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
if (!ValidateTextureMutable(textureObject, __func__)) return;
// TODO: implement
THROW_UNIMPL_EXCEPTION;
// TODO: implement compressed upload - see CompressedTexImage2D_State.
RecordUnsupportedCompressedFormat(__func__);
}
void BindTexture_State(GLenum target, GLuint texture) {
@@ -3160,6 +3269,9 @@ namespace MobileGL::MG_Impl::GLImpl {
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize});
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
}
// Immutable storage defines exactly `levels` levels; AllocateStorage only grows, so a
// longer pre-existing chain has to be dropped explicitly.
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
}
@@ -3212,6 +3324,8 @@ namespace MobileGL::MG_Impl::GLImpl {
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, levelHeight, 1}, byteSize});
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
}
// See TextureStorage1D.
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
}
@@ -3264,6 +3378,8 @@ namespace MobileGL::MG_Impl::GLImpl {
{{levelWidth, levelHeight, levelDepth}, byteSize});
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
}
// See TextureStorage1D.
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
}
@@ -4025,8 +4141,21 @@ namespace MobileGL::MG_Impl::GLImpl {
void CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height) {
auto textureObject = GetTextureObjectByName(texture, __func__);
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
CopyTexSubImage2D_Backend(target, level, xoffset, yoffset, x, y, width, height);
if (!textureObject) return;
// GL 4.6 sec. 8.8: the 2D form only accepts these effective targets; cube maps must
// go through CopyTextureSubImage3D with the face as a layer.
const auto target = textureObject->GetTarget();
if (target != TextureTarget::Texture2D && target != TextureTarget::Texture1DArray &&
target != TextureTarget::TextureRectangle) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"CopyTextureSubImage2D requires a 2D, 1D-array, or "
"rectangle texture."));
return;
}
WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum glTarget) {
CopyTexSubImage2D_Backend(glTarget, level, xoffset, yoffset, x, y, width, height);
});
}
@@ -14,7 +14,8 @@
#include <MG_State/GLState/TextureState/TextureObjectStubs.h>
namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
UniquePtr<ProxyTextureManager> pProxyTextureManager;
// Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<ProxyTextureManager>& pProxyTextureManager = *new UniquePtr<ProxyTextureManager>();
Bool IsProxyTextureTarget(TextureUploadTarget target) {
switch (target) {
@@ -23,5 +23,5 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl {
UnorderedMap<TextureUploadTarget, SharedPtr<MG_State::GLState::ITextureObject>> m_proxyTexturesMap;
};
extern UniquePtr<ProxyTextureManager> pProxyTextureManager;
extern UniquePtr<ProxyTextureManager>& pProxyTextureManager;
} // namespace MobileGL::MG_Impl::GLImpl::TextureImpl
+2
View File
@@ -85,6 +85,8 @@ namespace MobileGL::MG_Impl {
GETPROC(CGLGetPixelFormat, name);
GETPROC(CGLSetCurrentContext, name);
GETPROC(CGLGetCurrentContext, name);
GETPROC(CGLSetVirtualScreen, name);
GETPROC(CGLGetVirtualScreen, name);
GETPROC(CGLSetParameter, name);
GETPROC(CGLGetParameter, name);
GETPROC(CGLUpdateContext, name);
+36 -4
View File
@@ -29,10 +29,19 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
char kContextViewKey;
char kContextLayerKey;
std::once_flag g_installOnce;
IMP g_pixelFormatDealloc = nullptr;
IMP g_contextDealloc = nullptr;
std::mutex& HookInstallMutex() {
static auto* mutex = new std::mutex();
return *mutex;
}
Bool& HooksInstalled() {
static auto* installed = new Bool(false);
return *installed;
}
template <typename Fn>
Fn ObjcMsgSend() {
return reinterpret_cast<Fn>(objc_msgSend);
@@ -431,12 +440,12 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
method_setImplementation(method, replacement);
}
void InstallHooksOnce() {
Bool InstallHooksOnce() {
Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat");
Class contextClass = objc_getClass("NSOpenGLContext");
if (!pixelFormatClass || !contextClass) {
MGLOG_W("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
return;
return false;
}
ReplaceInstanceMethod(pixelFormatClass, "initWithAttributes:",
@@ -471,11 +480,34 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl {
ReplaceInstanceMethod(contextClass, "dealloc", reinterpret_cast<IMP>(ContextDealloc), &g_contextDealloc);
MGLOG_I("NSOpenGLImpl hooks installed");
return true;
}
} // namespace
void InstallHooks() {
std::call_once(g_installOnce, InstallHooksOnce);
const std::lock_guard<std::mutex> lock(HookInstallMutex());
if (!HooksInstalled()) {
// Do not permanently consume the install attempt when the OpenGL
// framework has not registered its Objective-C classes yet. The
// dyld bootstrap normally runs after framework dependencies, but
// an explicitly loaded/static-linked MobileGL can arrive earlier.
HooksInstalled() = InstallHooksOnce();
}
}
} // namespace MobileGL::MG_Impl::NSOpenGLImpl
namespace {
// SDL's Cocoa backend creates NSOpenGLPixelFormat/NSOpenGLContext before
// its first dlsym("glGetString") or other MobileGL host-API call. Install
// only the lightweight Objective-C dispatch hooks while the injected dylib
// is loading so those first Cocoa objects are routed through CGLImpl. The
// hooked context constructor reaches EGLImpl::GetDisplay(), which performs
// the full, thread-safe MobileGL initialization outside this bootstrap.
//
// There is intentionally no matching destructor: backend teardown remains
// owned by the EGL lifecycle and process-exit globals remain leak-at-exit.
__attribute__((constructor)) void BootstrapNSOpenGLHooks() {
MobileGL::MG_Impl::NSOpenGLImpl::InstallHooks();
}
} // namespace
#endif
@@ -0,0 +1,156 @@
// MobileGL - MobileGL/MG_Impl/WGLImpl/Exporting/Definitions.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// wingdi.h declares most wgl* entry points as WINGDIAPI (__declspec(dllimport)),
// which would reject our definitions. _GDI32_ is the SDK's "I am the module that
// implements these" switch: it turns WINGDIAPI into a plain declaration. It must
// be defined before the first windows.h inclusion in this translation unit.
#if defined(_WIN32) && !defined(_GDI32_)
#define _GDI32_ 1
#endif
#include <Includes.h>
#if defined(_WIN32)
#include "../WGLImpl.h"
namespace WGL = MobileGL::MG_Impl::WGLImpl;
// ---- Pixel-format entry points (gdi32 forwards ChoosePixelFormat/SetPixelFormat/
// ---- DescribePixelFormat/GetPixelFormat/SwapBuffers into these exports) ----
extern "C" int WINAPI wglChoosePixelFormat(HDC hdc, CONST PIXELFORMATDESCRIPTOR* ppfd) {
return WGL::ChoosePixelFormat(hdc, ppfd);
}
extern "C" int WINAPI wglDescribePixelFormat(HDC hdc, int iPixelFormat, UINT nBytes,
LPPIXELFORMATDESCRIPTOR ppfd) {
return WGL::DescribePixelFormat(hdc, iPixelFormat, nBytes, ppfd);
}
extern "C" int WINAPI wglGetPixelFormat(HDC hdc) {
return WGL::GetPixelFormat(hdc);
}
extern "C" BOOL WINAPI wglSetPixelFormat(HDC hdc, int iPixelFormat, CONST PIXELFORMATDESCRIPTOR* ppfd) {
return WGL::SetPixelFormat(hdc, iPixelFormat, ppfd);
}
extern "C" BOOL WINAPI wglSwapBuffers(HDC hdc) {
return WGL::SwapBuffers(hdc);
}
// ---- Context management ----
extern "C" HGLRC WINAPI wglCreateContext(HDC hdc) {
return WGL::CreateContext(hdc);
}
extern "C" HGLRC WINAPI wglCreateLayerContext(HDC hdc, int iLayerPlane) {
return iLayerPlane == 0 ? WGL::CreateContext(hdc) : nullptr;
}
extern "C" BOOL WINAPI wglCopyContext(HGLRC, HGLRC, UINT) {
MGLOG_W("wglCopyContext is not supported");
SetLastError(ERROR_NOT_SUPPORTED);
return FALSE;
}
extern "C" BOOL WINAPI wglDeleteContext(HGLRC hglrc) {
return WGL::DeleteContext(hglrc);
}
extern "C" HGLRC WINAPI wglGetCurrentContext(VOID) {
return WGL::GetCurrentContext();
}
extern "C" HDC WINAPI wglGetCurrentDC(VOID) {
return WGL::GetCurrentDC();
}
extern "C" BOOL WINAPI wglMakeCurrent(HDC hdc, HGLRC hglrc) {
return WGL::MakeCurrent(hdc, hglrc);
}
extern "C" BOOL WINAPI wglShareLists(HGLRC hglrcShare, HGLRC hglrcDest) {
return WGL::ShareLists(hglrcShare, hglrcDest);
}
// ---- Proc address ----
extern "C" PROC WINAPI wglGetProcAddress(LPCSTR lpszProc) {
return WGL::GetProcAddress(lpszProc);
}
extern "C" PROC WINAPI wglGetDefaultProcAddress(LPCSTR lpszProc) {
return WGL::GetProcAddress(lpszProc);
}
// ---- Layer planes and palettes (unsupported; overlay planes do not exist here) ----
extern "C" BOOL WINAPI wglDescribeLayerPlane(HDC, int, int, UINT, LPLAYERPLANEDESCRIPTOR) {
return FALSE;
}
extern "C" int WINAPI wglSetLayerPaletteEntries(HDC, int, int, int, CONST COLORREF*) {
return 0;
}
extern "C" int WINAPI wglGetLayerPaletteEntries(HDC, int, int, int, COLORREF*) {
return 0;
}
extern "C" BOOL WINAPI wglRealizeLayerPalette(HDC, int, BOOL) {
return FALSE;
}
extern "C" BOOL WINAPI wglSwapLayerBuffers(HDC hdc, UINT fuPlanes) {
if (fuPlanes & WGL_SWAP_MAIN_PLANE) {
return WGL::SwapBuffers(hdc);
}
return FALSE;
}
extern "C" DWORD WINAPI wglSwapMultipleBuffers(UINT n, CONST WGLSWAP* ps) {
if (!ps) {
return 0;
}
DWORD swapped = 0;
for (UINT i = 0; i < n; ++i) {
if (WGL::SwapBuffers(ps[i].hdc)) {
++swapped;
}
}
return swapped;
}
// ---- Font rendering (legacy immediate-mode feature; not supported) ----
extern "C" BOOL WINAPI wglUseFontBitmapsA(HDC, DWORD, DWORD, DWORD) {
MGLOG_W("wglUseFontBitmapsA is not supported");
return FALSE;
}
extern "C" BOOL WINAPI wglUseFontBitmapsW(HDC, DWORD, DWORD, DWORD) {
MGLOG_W("wglUseFontBitmapsW is not supported");
return FALSE;
}
extern "C" BOOL WINAPI wglUseFontOutlinesA(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
LPGLYPHMETRICSFLOAT) {
MGLOG_W("wglUseFontOutlinesA is not supported");
return FALSE;
}
extern "C" BOOL WINAPI wglUseFontOutlinesW(HDC, DWORD, DWORD, DWORD, FLOAT, FLOAT, int,
LPGLYPHMETRICSFLOAT) {
MGLOG_W("wglUseFontOutlinesW is not supported");
return FALSE;
}
#endif // _WIN32
@@ -0,0 +1,30 @@
; MobileGL WGL exports. The wgl* entry points are defined without
; __declspec(dllexport) because wingdi.h pre-declares them (with _GDI32_ they
; become plain declarations, and MSVC rejects adding dllexport afterwards),
; so this .def file is what actually exports them from the DLL.
EXPORTS
wglChoosePixelFormat
wglCopyContext
wglCreateContext
wglCreateLayerContext
wglDeleteContext
wglDescribeLayerPlane
wglDescribePixelFormat
wglGetCurrentContext
wglGetCurrentDC
wglGetDefaultProcAddress
wglGetLayerPaletteEntries
wglGetPixelFormat
wglGetProcAddress
wglMakeCurrent
wglRealizeLayerPalette
wglSetLayerPaletteEntries
wglSetPixelFormat
wglShareLists
wglSwapBuffers
wglSwapLayerBuffers
wglSwapMultipleBuffers
wglUseFontBitmapsA
wglUseFontBitmapsW
wglUseFontOutlinesA
wglUseFontOutlinesW
+732
View File
@@ -0,0 +1,732 @@
// MobileGL - MobileGL/MG_Impl/WGLImpl/WGLImpl.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "WGLImpl.h"
#if defined(_WIN32)
#include "../EGLImpl/EGLImpl.h"
#include "../GetProcAddress.h"
#include <Init.h>
namespace MobileGL::MG_Impl::WGLImpl {
namespace {
// WGL_ARB_pixel_format
constexpr int WGL_NUMBER_PIXEL_FORMATS_ARB = 0x2000;
constexpr int WGL_DRAW_TO_WINDOW_ARB = 0x2001;
constexpr int WGL_DRAW_TO_BITMAP_ARB = 0x2002;
constexpr int WGL_ACCELERATION_ARB = 0x2003;
constexpr int WGL_NEED_PALETTE_ARB = 0x2004;
constexpr int WGL_NEED_SYSTEM_PALETTE_ARB = 0x2005;
constexpr int WGL_SWAP_LAYER_BUFFERS_ARB = 0x2006;
constexpr int WGL_SWAP_METHOD_ARB = 0x2007;
constexpr int WGL_NUMBER_OVERLAYS_ARB = 0x2008;
constexpr int WGL_NUMBER_UNDERLAYS_ARB = 0x2009;
constexpr int WGL_TRANSPARENT_ARB = 0x200A;
constexpr int WGL_SHARE_DEPTH_ARB = 0x200C;
constexpr int WGL_SHARE_STENCIL_ARB = 0x200D;
constexpr int WGL_SHARE_ACCUM_ARB = 0x200E;
constexpr int WGL_SUPPORT_GDI_ARB = 0x200F;
constexpr int WGL_SUPPORT_OPENGL_ARB = 0x2010;
constexpr int WGL_DOUBLE_BUFFER_ARB = 0x2011;
constexpr int WGL_STEREO_ARB = 0x2012;
constexpr int WGL_PIXEL_TYPE_ARB = 0x2013;
constexpr int WGL_COLOR_BITS_ARB = 0x2014;
constexpr int WGL_RED_BITS_ARB = 0x2015;
constexpr int WGL_RED_SHIFT_ARB = 0x2016;
constexpr int WGL_GREEN_BITS_ARB = 0x2017;
constexpr int WGL_GREEN_SHIFT_ARB = 0x2018;
constexpr int WGL_BLUE_BITS_ARB = 0x2019;
constexpr int WGL_BLUE_SHIFT_ARB = 0x201A;
constexpr int WGL_ALPHA_BITS_ARB = 0x201B;
constexpr int WGL_ALPHA_SHIFT_ARB = 0x201C;
constexpr int WGL_ACCUM_BITS_ARB = 0x201D;
constexpr int WGL_ACCUM_RED_BITS_ARB = 0x201E;
constexpr int WGL_ACCUM_GREEN_BITS_ARB = 0x201F;
constexpr int WGL_ACCUM_BLUE_BITS_ARB = 0x2020;
constexpr int WGL_ACCUM_ALPHA_BITS_ARB = 0x2021;
constexpr int WGL_DEPTH_BITS_ARB = 0x2022;
constexpr int WGL_STENCIL_BITS_ARB = 0x2023;
constexpr int WGL_AUX_BUFFERS_ARB = 0x2024;
constexpr int WGL_NO_ACCELERATION_ARB = 0x2025;
constexpr int WGL_FULL_ACCELERATION_ARB = 0x2027;
constexpr int WGL_SWAP_EXCHANGE_ARB = 0x2028;
constexpr int WGL_TYPE_RGBA_ARB = 0x202B;
// WGL_ARB_multisample
constexpr int WGL_SAMPLE_BUFFERS_ARB = 0x2041;
constexpr int WGL_SAMPLES_ARB = 0x2042;
// WGL_ARB_create_context / _profile / _no_error
constexpr int WGL_CONTEXT_MAJOR_VERSION_ARB = 0x2091;
constexpr int WGL_CONTEXT_MINOR_VERSION_ARB = 0x2092;
constexpr int WGL_CONTEXT_LAYER_PLANE_ARB = 0x2093;
constexpr int WGL_CONTEXT_FLAGS_ARB = 0x2094;
constexpr int WGL_CONTEXT_PROFILE_MASK_ARB = 0x9126;
constexpr int WGL_CONTEXT_DEBUG_BIT_ARB = 0x0001;
constexpr int WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB = 0x0002;
constexpr int WGL_CONTEXT_CORE_PROFILE_BIT_ARB = 0x00000001;
constexpr int WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB = 0x00000002;
constexpr int WGL_CONTEXT_OPENGL_NO_ERROR_ARB = 0x31B3;
constexpr DWORD ERROR_INVALID_VERSION_ARB = 0x2095;
constexpr DWORD ERROR_INVALID_PROFILE_ARB = 0x2096;
struct PixelFormatInfo {
GLint AlphaBits;
GLint DepthBits;
GLint StencilBits;
};
// Mirrors the two EGLState configs (RGBA8 + depth24, stencil 8 / stencil 0).
constexpr PixelFormatInfo kPixelFormats[] = {
{8, 24, 8},
{8, 24, 0},
};
constexpr int kPixelFormatCount = static_cast<int>(std::size(kPixelFormats));
struct ContextObject {
EGLDisplay Display = EGL_NO_DISPLAY;
EGLConfig Config = nullptr;
EGLContext Context = EGL_NO_CONTEXT;
};
struct WindowSurface {
EGLDisplay Display = EGL_NO_DISPLAY;
EGLSurface Surface = EGL_NO_SURFACE;
Uint32 Width = 0;
Uint32 Height = 0;
};
std::recursive_mutex& RegistryMutex() {
static auto* mutex = new std::recursive_mutex();
return *mutex;
}
UnorderedMap<HGLRC, ContextObject>& Contexts() {
static auto* contexts = new UnorderedMap<HGLRC, ContextObject>();
return *contexts;
}
UnorderedMap<HWND, WindowSurface>& WindowSurfaces() {
static auto* surfaces = new UnorderedMap<HWND, WindowSurface>();
return *surfaces;
}
UnorderedMap<HWND, int>& WindowPixelFormats() {
static auto* formats = new UnorderedMap<HWND, int>();
return *formats;
}
Uint64& NextContextHandle() {
static auto* handle = new Uint64(0x10000);
return *handle;
}
struct ThreadCurrent {
HDC DC = nullptr;
HGLRC Context = nullptr;
};
thread_local ThreadCurrent t_current;
Int& SwapIntervalShadow() {
static auto* interval = new Int(1);
return *interval;
}
void EnsureInitialized() {
// Initialize() loads backend libraries and glslang, which must not run
// under the loader lock; first WGL call is the earliest safe moment.
// MobileGL::EnsureInitialized (not a local once_flag) so a fresh init
// can follow a full teardown from the last eglTerminate.
MobileGL::EnsureInitialized();
}
EGLDisplay EnsureDisplay() {
EnsureInitialized();
EGLDisplay display = EGLImpl::GetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) {
return EGL_NO_DISPLAY;
}
if (!EGLImpl::Initialize(display, nullptr, nullptr)) {
return EGL_NO_DISPLAY;
}
return display;
}
HGLRC EncodeContext(Uint64 handle) {
return reinterpret_cast<HGLRC>(static_cast<SizeT>(handle));
}
ContextObject* TryGetContext(HGLRC hglrc) {
auto& contexts = Contexts();
auto it = contexts.find(hglrc);
return it == contexts.end() ? nullptr : &it->second;
}
const PixelFormatInfo& PixelFormatForWindow(HWND hwnd) {
auto& formats = WindowPixelFormats();
auto it = formats.find(hwnd);
int index = it == formats.end() ? 1 : it->second;
if (index < 1 || index > kPixelFormatCount) {
index = 1;
}
return kPixelFormats[index - 1];
}
Bool QueryClientSize(HWND hwnd, Uint32& width, Uint32& height) {
RECT rect{};
if (!GetClientRect(hwnd, &rect)) {
return false;
}
width = static_cast<Uint32>(std::max<LONG>(rect.right - rect.left, 1));
height = static_cast<Uint32>(std::max<LONG>(rect.bottom - rect.top, 1));
return true;
}
// The backends never query the HWND client size themselves; the WGL layer
// owns size discovery and pushes changes through the internal resize hook
// (same contract as the macOS CGL layer).
void SyncSurfaceSize(HWND hwnd, WindowSurface& surface) {
Uint32 width = 0;
Uint32 height = 0;
if (!QueryClientSize(hwnd, width, height)) {
return;
}
if (width == surface.Width && height == surface.Height) {
return;
}
if (EGLImpl::ResizePlatformWindowSurface(surface.Display, surface.Surface,
static_cast<EGLint>(width), static_cast<EGLint>(height))) {
surface.Width = width;
surface.Height = height;
}
}
WindowSurface* EnsureWindowSurface(HWND hwnd, const ContextObject& context) {
auto& surfaces = WindowSurfaces();
auto it = surfaces.find(hwnd);
if (it != surfaces.end()) {
SyncSurfaceSize(hwnd, it->second);
return &it->second;
}
Uint32 width = 0;
Uint32 height = 0;
if (!QueryClientSize(hwnd, width, height)) {
MGLOG_E("wgl: GetClientRect failed for HWND %p", hwnd);
return nullptr;
}
const EGLAttrib attribs[] = {
EGL_WIDTH, static_cast<EGLAttrib>(width),
EGL_HEIGHT, static_cast<EGLAttrib>(height),
EGL_NONE,
};
EGLSurface surface =
EGLImpl::CreatePlatformWindowSurface(context.Display, context.Config, hwnd, attribs);
if (surface == EGL_NO_SURFACE) {
MGLOG_E("wgl: failed to create window surface for HWND %p (%ux%u)", hwnd, width, height);
return nullptr;
}
WindowSurface record;
record.Display = context.Display;
record.Surface = surface;
record.Width = width;
record.Height = height;
auto [inserted, _] = surfaces.emplace(hwnd, record);
return &inserted->second;
}
HGLRC CreateContextFromEGLAttribs(HDC hdc, HGLRC share, const EGLint* contextAttribs) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
EGLDisplay display = EnsureDisplay();
if (display == EGL_NO_DISPLAY) {
MGLOG_E("wgl: no EGL display");
return nullptr;
}
EGLImpl::BindAPI(EGL_OPENGL_API);
EGLContext shareContext = EGL_NO_CONTEXT;
if (share) {
auto* shareObject = TryGetContext(share);
if (!shareObject) {
SetLastError(ERROR_INVALID_HANDLE);
return nullptr;
}
shareContext = shareObject->Context;
}
HWND hwnd = WindowFromDC(hdc);
const PixelFormatInfo& pixelFormat = PixelFormatForWindow(hwnd);
const EGLint configAttribs[] = {
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, pixelFormat.AlphaBits,
EGL_DEPTH_SIZE, pixelFormat.DepthBits,
EGL_STENCIL_SIZE, pixelFormat.StencilBits,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT | EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
EGL_NONE,
};
EGLConfig config = nullptr;
EGLint configCount = 0;
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) || configCount <= 0) {
MGLOG_E("wgl: eglChooseConfig failed");
return nullptr;
}
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
if (eglContext == EGL_NO_CONTEXT) {
MGLOG_E("wgl: eglCreateContext failed");
return nullptr;
}
ContextObject object;
object.Display = display;
object.Config = config;
object.Context = eglContext;
const auto handle = EncodeContext(NextContextHandle()++);
Contexts()[handle] = object;
MGLOG_I("wgl: created context %p (EGL context %p)", handle, eglContext);
return handle;
}
// ---- WGL extension entry points (resolved via wglGetProcAddress only) ----
const char* WINAPI Ext_GetExtensionsStringARB(HDC) {
return "WGL_ARB_create_context WGL_ARB_create_context_no_error WGL_ARB_create_context_profile "
"WGL_ARB_extensions_string WGL_ARB_pixel_format WGL_EXT_extensions_string WGL_EXT_swap_control";
}
const char* WINAPI Ext_GetExtensionsStringEXT() {
return Ext_GetExtensionsStringARB(nullptr);
}
HGLRC WINAPI Ext_CreateContextAttribsARB(HDC hdc, HGLRC hShareContext, const int* attribList) {
EnsureInitialized();
int major = 1;
int minor = 0;
int profileMask = 0;
int flags = 0;
if (attribList) {
for (SizeT i = 0; attribList[i] != 0; i += 2) {
const int attrib = attribList[i];
const int value = attribList[i + 1];
switch (attrib) {
case WGL_CONTEXT_MAJOR_VERSION_ARB:
major = value;
break;
case WGL_CONTEXT_MINOR_VERSION_ARB:
minor = value;
break;
case WGL_CONTEXT_PROFILE_MASK_ARB:
profileMask = value;
break;
case WGL_CONTEXT_FLAGS_ARB:
flags = value;
break;
case WGL_CONTEXT_LAYER_PLANE_ARB:
if (value != 0) {
SetLastError(ERROR_INVALID_PARAMETER);
return nullptr;
}
break;
case WGL_CONTEXT_OPENGL_NO_ERROR_ARB:
// Accepted and ignored: MobileGL always validates.
break;
default:
MGLOG_D("wglCreateContextAttribsARB: ignoring attrib 0x%04x = 0x%x", attrib, value);
break;
}
}
}
if (major < 1 || (profileMask & ~(WGL_CONTEXT_CORE_PROFILE_BIT_ARB |
WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB))) {
SetLastError(profileMask ? ERROR_INVALID_PROFILE_ARB : ERROR_INVALID_VERSION_ARB);
return nullptr;
}
Vector<EGLint> attribs = {
EGL_CONTEXT_MAJOR_VERSION, major,
EGL_CONTEXT_MINOR_VERSION, minor,
};
const Bool wantsCompat = (profileMask & WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB) != 0;
if (major > 3 || (major == 3 && minor >= 2) || profileMask != 0) {
attribs.push_back(EGL_CONTEXT_OPENGL_PROFILE_MASK);
attribs.push_back(wantsCompat ? EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT
: EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT);
}
if (flags & WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB) {
attribs.push_back(EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE);
attribs.push_back(EGL_TRUE);
}
if (flags & WGL_CONTEXT_DEBUG_BIT_ARB) {
attribs.push_back(EGL_CONTEXT_OPENGL_DEBUG);
attribs.push_back(EGL_TRUE);
}
attribs.push_back(EGL_NONE);
return CreateContextFromEGLAttribs(hdc, hShareContext, attribs.data());
}
BOOL WINAPI Ext_SwapIntervalEXT(int interval) {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
EGLDisplay display = EnsureDisplay();
if (display == EGL_NO_DISPLAY) {
return FALSE;
}
if (interval < 0) {
// Adaptive vsync is not supported; clamp to regular vsync.
interval = 1;
}
EGLImpl::SwapInterval(display, interval);
SwapIntervalShadow() = interval;
return TRUE;
}
int WINAPI Ext_GetSwapIntervalEXT() {
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
return SwapIntervalShadow();
}
int PixelFormatAttribValue(int format, int attrib) {
const PixelFormatInfo& info = kPixelFormats[format - 1];
switch (attrib) {
case WGL_NUMBER_PIXEL_FORMATS_ARB:
return kPixelFormatCount;
case WGL_SUPPORT_OPENGL_ARB:
case WGL_DRAW_TO_WINDOW_ARB:
case WGL_DOUBLE_BUFFER_ARB:
return 1;
case WGL_ACCELERATION_ARB:
return WGL_FULL_ACCELERATION_ARB;
case WGL_PIXEL_TYPE_ARB:
return WGL_TYPE_RGBA_ARB;
case WGL_COLOR_BITS_ARB:
return 32;
case WGL_RED_BITS_ARB:
case WGL_GREEN_BITS_ARB:
case WGL_BLUE_BITS_ARB:
return 8;
case WGL_RED_SHIFT_ARB:
return 16;
case WGL_GREEN_SHIFT_ARB:
return 8;
case WGL_BLUE_SHIFT_ARB:
return 0;
case WGL_ALPHA_BITS_ARB:
return info.AlphaBits;
case WGL_ALPHA_SHIFT_ARB:
return 24;
case WGL_DEPTH_BITS_ARB:
return info.DepthBits;
case WGL_STENCIL_BITS_ARB:
return info.StencilBits;
case WGL_SWAP_METHOD_ARB:
return WGL_SWAP_EXCHANGE_ARB;
case WGL_DRAW_TO_BITMAP_ARB:
case WGL_NEED_PALETTE_ARB:
case WGL_NEED_SYSTEM_PALETTE_ARB:
case WGL_SWAP_LAYER_BUFFERS_ARB:
case WGL_NUMBER_OVERLAYS_ARB:
case WGL_NUMBER_UNDERLAYS_ARB:
case WGL_TRANSPARENT_ARB:
case WGL_SHARE_DEPTH_ARB:
case WGL_SHARE_STENCIL_ARB:
case WGL_SHARE_ACCUM_ARB:
case WGL_SUPPORT_GDI_ARB:
case WGL_STEREO_ARB:
case WGL_ACCUM_BITS_ARB:
case WGL_ACCUM_RED_BITS_ARB:
case WGL_ACCUM_GREEN_BITS_ARB:
case WGL_ACCUM_BLUE_BITS_ARB:
case WGL_ACCUM_ALPHA_BITS_ARB:
case WGL_AUX_BUFFERS_ARB:
case WGL_SAMPLE_BUFFERS_ARB:
case WGL_SAMPLES_ARB:
default:
return 0;
}
}
BOOL WINAPI Ext_GetPixelFormatAttribivARB(HDC, int iPixelFormat, int iLayerPlane, UINT nAttributes,
const int* piAttributes, int* piValues) {
if (iLayerPlane != 0 || !piAttributes || !piValues) {
return FALSE;
}
// Format 0 is only valid for WGL_NUMBER_PIXEL_FORMATS_ARB queries.
if (iPixelFormat < 0 || iPixelFormat > kPixelFormatCount) {
return FALSE;
}
const int format = iPixelFormat == 0 ? 1 : iPixelFormat;
for (UINT i = 0; i < nAttributes; ++i) {
piValues[i] = PixelFormatAttribValue(format, piAttributes[i]);
}
return TRUE;
}
BOOL WINAPI Ext_GetPixelFormatAttribfvARB(HDC hdc, int iPixelFormat, int iLayerPlane, UINT nAttributes,
const int* piAttributes, FLOAT* pfValues) {
if (!pfValues) {
return FALSE;
}
Vector<int> values(nAttributes);
if (!Ext_GetPixelFormatAttribivARB(hdc, iPixelFormat, iLayerPlane, nAttributes, piAttributes,
values.data())) {
return FALSE;
}
for (UINT i = 0; i < nAttributes; ++i) {
pfValues[i] = static_cast<FLOAT>(values[i]);
}
return TRUE;
}
BOOL WINAPI Ext_ChoosePixelFormatARB(HDC, const int* piAttribIList, const FLOAT*, UINT nMaxFormats,
int* piFormats, UINT* nNumFormats) {
if (!piFormats || !nNumFormats) {
return FALSE;
}
int wantedStencil = 0;
if (piAttribIList) {
for (SizeT i = 0; piAttribIList[i] != 0; i += 2) {
if (piAttribIList[i] == WGL_STENCIL_BITS_ARB) {
wantedStencil = piAttribIList[i + 1];
}
}
}
UINT count = 0;
const int preferred = wantedStencil > 0 ? 1 : 2;
const int fallback = wantedStencil > 0 ? 2 : 1;
if (count < nMaxFormats) {
piFormats[count++] = preferred;
}
if (count < nMaxFormats) {
piFormats[count++] = fallback;
}
*nNumFormats = count;
return TRUE;
}
struct WGLExtensionProc {
const char* Name;
PROC Proc;
};
const WGLExtensionProc kWGLExtensionProcs[] = {
{"wglGetExtensionsStringARB", reinterpret_cast<PROC>(Ext_GetExtensionsStringARB)},
{"wglGetExtensionsStringEXT", reinterpret_cast<PROC>(Ext_GetExtensionsStringEXT)},
{"wglCreateContextAttribsARB", reinterpret_cast<PROC>(Ext_CreateContextAttribsARB)},
{"wglSwapIntervalEXT", reinterpret_cast<PROC>(Ext_SwapIntervalEXT)},
{"wglGetSwapIntervalEXT", reinterpret_cast<PROC>(Ext_GetSwapIntervalEXT)},
{"wglGetPixelFormatAttribivARB", reinterpret_cast<PROC>(Ext_GetPixelFormatAttribivARB)},
{"wglGetPixelFormatAttribfvARB", reinterpret_cast<PROC>(Ext_GetPixelFormatAttribfvARB)},
{"wglChoosePixelFormatARB", reinterpret_cast<PROC>(Ext_ChoosePixelFormatARB)},
};
} // namespace
int ChoosePixelFormat(HDC hdc, const PIXELFORMATDESCRIPTOR* pfd) {
EnsureInitialized();
MGLOG_D("wglChoosePixelFormat(hdc=%p)", hdc);
// Format 1 (RGBA8 + depth24/stencil8) satisfies every request; a format
// exceeding the asked-for capabilities is a legal ChoosePixelFormat answer.
(void)pfd;
return 1;
}
int DescribePixelFormat(HDC hdc, int format, UINT size, PIXELFORMATDESCRIPTOR* pfd) {
EnsureInitialized();
MGLOG_D("wglDescribePixelFormat(hdc=%p, format=%d)", hdc, format);
if (!pfd) {
return kPixelFormatCount;
}
if (size < sizeof(PIXELFORMATDESCRIPTOR) || format < 1 || format > kPixelFormatCount) {
return 0;
}
const PixelFormatInfo& info = kPixelFormats[format - 1];
std::memset(pfd, 0, sizeof(PIXELFORMATDESCRIPTOR));
pfd->nSize = sizeof(PIXELFORMATDESCRIPTOR);
pfd->nVersion = 1;
pfd->dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER | PFD_SWAP_EXCHANGE
#if defined(PFD_SUPPORT_COMPOSITION)
| PFD_SUPPORT_COMPOSITION
#endif
;
pfd->iPixelType = PFD_TYPE_RGBA;
pfd->cColorBits = 32;
pfd->cRedBits = 8;
pfd->cRedShift = 16;
pfd->cGreenBits = 8;
pfd->cGreenShift = 8;
pfd->cBlueBits = 8;
pfd->cBlueShift = 0;
pfd->cAlphaBits = static_cast<BYTE>(info.AlphaBits);
pfd->cAlphaShift = 24;
pfd->cDepthBits = static_cast<BYTE>(info.DepthBits);
pfd->cStencilBits = static_cast<BYTE>(info.StencilBits);
pfd->iLayerType = PFD_MAIN_PLANE;
return kPixelFormatCount;
}
int GetPixelFormat(HDC hdc) {
EnsureInitialized();
HWND hwnd = WindowFromDC(hdc);
if (!hwnd) {
return 0;
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto& formats = WindowPixelFormats();
auto it = formats.find(hwnd);
return it == formats.end() ? 0 : it->second;
}
BOOL SetPixelFormat(HDC hdc, int format, const PIXELFORMATDESCRIPTOR*) {
EnsureInitialized();
MGLOG_D("wglSetPixelFormat(hdc=%p, format=%d)", hdc, format);
if (format < 1 || format > kPixelFormatCount) {
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}
HWND hwnd = WindowFromDC(hdc);
if (!hwnd) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
WindowPixelFormats()[hwnd] = format;
return TRUE;
}
BOOL SwapBuffers(HDC hdc) {
HWND hwnd = WindowFromDC(hdc);
if (!hwnd) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto& surfaces = WindowSurfaces();
auto it = surfaces.find(hwnd);
if (it == surfaces.end()) {
MGLOG_W("wglSwapBuffers: no surface for HWND %p", hwnd);
return FALSE;
}
SyncSurfaceSize(hwnd, it->second);
return EGLImpl::SwapBuffers(it->second.Display, it->second.Surface) == EGL_TRUE ? TRUE : FALSE;
}
HGLRC CreateContext(HDC hdc) {
EnsureInitialized();
MGLOG_I("wglCreateContext(hdc=%p)", hdc);
// A legacy WGL context is a compatibility-profile context; MobileGL keys
// its relaxed-semantics mode off the explicit compatibility bit.
const EGLint attribs[] = {
EGL_CONTEXT_MAJOR_VERSION, 3,
EGL_CONTEXT_MINOR_VERSION, 3,
EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT,
EGL_NONE,
};
return CreateContextFromEGLAttribs(hdc, nullptr, attribs);
}
BOOL DeleteContext(HGLRC hglrc) {
EnsureInitialized();
MGLOG_I("wglDeleteContext(%p)", hglrc);
if (t_current.Context == hglrc) {
MakeCurrent(nullptr, nullptr);
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(hglrc);
if (!object) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
if (object->Context != EGL_NO_CONTEXT) {
EGLImpl::DestroyContext(object->Display, object->Context);
}
Contexts().erase(hglrc);
return TRUE;
}
BOOL MakeCurrent(HDC hdc, HGLRC hglrc) {
EnsureInitialized();
MGLOG_D("wglMakeCurrent(hdc=%p, hglrc=%p)", hdc, hglrc);
if (!hglrc) {
if (!t_current.Context) {
t_current = {};
return TRUE;
}
const EGLBoolean released =
EGLImpl::MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
t_current = {};
return released == EGL_TRUE ? TRUE : FALSE;
}
HWND hwnd = WindowFromDC(hdc);
if (!hwnd) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto* object = TryGetContext(hglrc);
if (!object) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
WindowSurface* surface = EnsureWindowSurface(hwnd, *object);
if (!surface) {
return FALSE;
}
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface, object->Context)) {
MGLOG_E("wglMakeCurrent: eglMakeCurrent failed (hdc=%p, hglrc=%p)", hdc, hglrc);
return FALSE;
}
t_current = {hdc, hglrc};
return TRUE;
}
HGLRC GetCurrentContext() {
return t_current.Context;
}
HDC GetCurrentDC() {
return t_current.DC;
}
BOOL ShareLists(HGLRC hglrcShare, HGLRC hglrcDest) {
// All MobileGL contexts alias one global GL object namespace, so every
// pair of contexts already shares.
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
if (!TryGetContext(hglrcShare) || !TryGetContext(hglrcDest)) {
SetLastError(ERROR_INVALID_HANDLE);
return FALSE;
}
return TRUE;
}
PROC GetProcAddress(const char* name) {
EnsureInitialized();
if (!name) {
return nullptr;
}
if (name[0] == 'w' && name[1] == 'g' && name[2] == 'l') {
for (const auto& entry : kWGLExtensionProcs) {
if (std::strcmp(entry.Name, name) == 0) {
return entry.Proc;
}
}
MGLOG_D("wglGetProcAddress: unknown wgl entry point %s", name);
return nullptr;
}
return reinterpret_cast<PROC>(MG_Impl::GetProcAddress(name));
}
} // namespace MobileGL::MG_Impl::WGLImpl
#endif // _WIN32
+34
View File
@@ -0,0 +1,34 @@
// MobileGL - MobileGL/MG_Impl/WGLImpl/WGLImpl.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#if defined(_WIN32)
namespace MobileGL::MG_Impl::WGLImpl {
// Classic opengl32.dll surface. gdi32's ChoosePixelFormat/SetPixelFormat/
// DescribePixelFormat/GetPixelFormat/SwapBuffers forward into the loaded
// opengl32.dll's wgl* exports, so these back both call paths.
int ChoosePixelFormat(HDC hdc, const PIXELFORMATDESCRIPTOR* pfd);
int DescribePixelFormat(HDC hdc, int format, UINT size, PIXELFORMATDESCRIPTOR* pfd);
int GetPixelFormat(HDC hdc);
BOOL SetPixelFormat(HDC hdc, int format, const PIXELFORMATDESCRIPTOR* pfd);
BOOL SwapBuffers(HDC hdc);
HGLRC CreateContext(HDC hdc);
BOOL DeleteContext(HGLRC hglrc);
BOOL MakeCurrent(HDC hdc, HGLRC hglrc);
HGLRC GetCurrentContext();
HDC GetCurrentDC();
BOOL ShareLists(HGLRC hglrcShare, HGLRC hglrcDest);
PROC GetProcAddress(const char* name);
} // namespace MobileGL::MG_Impl::WGLImpl
#endif // _WIN32
+22 -1
View File
@@ -368,6 +368,26 @@ namespace MobileGL {
return true;
}
Bool EGLContext::HasAnyInitializedDisplay() const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
for (const auto& [handle, displayObject] : m_displays) {
if (displayObject.Initialized) {
return true;
}
}
return false;
}
Bool EGLContext::HasAnyCurrentContext() const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
for (const auto& [threadId, current] : m_threadCurrents) {
if (current.Context != nullptr) {
return true;
}
}
return false;
}
Bool EGLContext::ChooseConfig(EGLDisplayHandle display, const EGLint* attribList, EGLConfigHandle* configs,
EGLint configSize, EGLint* numConfig) {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
@@ -1415,6 +1435,7 @@ namespace MobileGL {
}
} // namespace EGLState
UniquePtr<EGLState::EGLContext> pEGLContext;
// Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<EGLState::EGLContext>& pEGLContext = *new UniquePtr<EGLState::EGLContext>();
} // namespace MG_State
} // namespace MobileGL
+5 -1
View File
@@ -39,6 +39,10 @@ namespace MobileGL {
Bool IsDisplayInitialized(EGLDisplayHandle display) const;
Bool InitializeDisplay(EGLDisplayHandle display, EGLint* major, EGLint* minor);
Bool TerminateDisplay(EGLDisplayHandle display);
// Whole-library idle checks used by EGLImpl::Terminate to decide
// when the last eglTerminate may tear MobileGL down entirely.
Bool HasAnyInitializedDisplay() const;
Bool HasAnyCurrentContext() const;
// Config
Bool ChooseConfig(EGLDisplayHandle display, const EGLint* attribList, EGLConfigHandle* configs,
@@ -262,6 +266,6 @@ namespace MobileGL {
};
} // namespace EGLState
extern UniquePtr<EGLState::EGLContext> pEGLContext;
extern UniquePtr<EGLState::EGLContext>& pEGLContext;
} // namespace MG_State
} // namespace MobileGL
@@ -231,6 +231,21 @@ namespace MobileGL::MG_State::GLState {
return m_resource.Bytes();
}
Bool BufferObject::EnsureGpuResidentStorage() {
if (m_resource.IsGpuResident()) {
return true;
}
if (m_size == 0 || g_bufferBackendOps == nullptr || g_bufferBackendOps->AcquirePersistentMap == nullptr) {
return false;
}
void* base = g_bufferBackendOps->AcquirePersistentMap(*this);
if (base == nullptr) {
return false;
}
m_resource.AdoptPersistentMap(base);
return true;
}
void* BufferObject::AcquireMemoryRange(Range1D range, Flags<BufferMappingAccessBit> access) {
MOBILEGL_ASSERT(range.end <= m_size && range.start <= range.end,
"AcquireMemoryRange out of bounds: range (%zu, %zu) exceeds m_size (%zu)", range.start,
@@ -133,6 +133,11 @@ namespace MobileGL {
void* AcquireMemory(Bool markMapped, Bool read, Bool write);
void* AcquireMemoryRange(Range1D range, Flags<BufferMappingAccessBit> access);
// Adopt backend host-visible coherent GPU storage as the source of truth
// (used for GPU-written targets like transform feedback capture, so
// MapBuffer/GetBufferSubData read real GPU results). No-op when already
// resident or when the backend declines.
Bool EnsureGpuResidentStorage();
void ReleaseMemory();
void FlushMemoryRange(SizeT offset, SizeT length);
+2 -1
View File
@@ -721,5 +721,6 @@ namespace MobileGL::MG_State {
}
} // namespace GLState
UniquePtr<GLState::GLContext> pGLContext;
// Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<GLState::GLContext>& pGLContext = *new UniquePtr<GLState::GLContext>();
} // namespace MobileGL::MG_State
+49 -1
View File
@@ -211,6 +211,47 @@ namespace MobileGL {
void SetScissorBox(IntVec4 box); // x, y, width, height
const IntVec4& GetScissorBox() const; // x, y, width, height
// Transform feedback (GL 3.0 core Begin/End; no feedback objects yet)
void BeginTransformFeedback(GLenum primitiveMode, const SharedPtr<ProgramObject>& program) {
m_transformFeedbackActive = true;
m_transformFeedbackPrimitiveMode = primitiveMode;
m_transformFeedbackProgram = program;
++m_transformFeedbackGeneration;
m_transformFeedbackCapturedVertices = 0;
m_transformFeedbackInputPrimitives = 0;
}
void EndTransformFeedback() {
m_transformFeedbackActive = false;
m_transformFeedbackProgram.reset();
}
Bool IsTransformFeedbackActive() const { return m_transformFeedbackActive; }
GLenum GetTransformFeedbackPrimitiveMode() const { return m_transformFeedbackPrimitiveMode; }
const SharedPtr<ProgramObject>& GetTransformFeedbackProgram() const {
return m_transformFeedbackProgram;
}
// Bumped on every BeginTransformFeedback; the backend uses it to
// distinguish "resume appending" from "fresh capture".
Uint64 GetTransformFeedbackGeneration() const { return m_transformFeedbackGeneration; }
// CPU-side primitive accounting for the transform feedback queries:
// every captured draw adds its primitive count (draws without a
// geometry stage write exactly what they generate).
void AddTransformFeedbackPrimitives(Uint64 primitives) {
m_transformFeedbackPrimitiveCounter += primitives;
}
Uint64 GetTransformFeedbackPrimitiveCounter() const { return m_transformFeedbackPrimitiveCounter; }
// Vertices already captured since BeginTransformFeedback (drives the
// buffer-capacity clamp on the primitives-written accounting).
void AddTransformFeedbackCapturedVertices(Uint64 vertices) {
m_transformFeedbackCapturedVertices += vertices;
}
Uint64 GetTransformFeedbackCapturedVertices() const { return m_transformFeedbackCapturedVertices; }
// Raw assembled input primitives fed to the capture stage since Begin
// (pre-clamp; drives the GS strip capture-order fixup at EndTF).
void AddTransformFeedbackInputPrimitives(Uint64 primitives) {
m_transformFeedbackInputPrimitives += primitives;
}
Uint64 GetTransformFeedbackInputPrimitives() const { return m_transformFeedbackInputPrimitives; }
// Framebuffer
void GenFramebufferNames(Uint number, Vector<Uint>& framebuffers);
const SharedPtr<FramebufferObject>& GetFramebufferObject(Uint index);
@@ -243,6 +284,13 @@ namespace MobileGL {
BufferState m_bufferState;
VertexArrayState m_vertexArrayState;
Array<CurrentVertexAttributeValue, VertexArrayObject::MAX_VERTEX_ATTRIBS> m_currentVertexAttributes{};
Bool m_transformFeedbackActive = false;
GLenum m_transformFeedbackPrimitiveMode = GL_POINTS;
SharedPtr<ProgramObject> m_transformFeedbackProgram;
Uint64 m_transformFeedbackGeneration = 0;
Uint64 m_transformFeedbackPrimitiveCounter = 0;
Uint64 m_transformFeedbackCapturedVertices = 0;
Uint64 m_transformFeedbackInputPrimitives = 0;
TextureState m_textureState;
ProgramState m_programState;
RenderState m_renderState;
@@ -252,7 +300,7 @@ namespace MobileGL {
};
} // namespace GLState
extern UniquePtr<GLState::GLContext> pGLContext;
extern UniquePtr<GLState::GLContext>& pGLContext;
// True when relaxed GL semantics apply. Strict core rules are enforced only when the
// current EGL context explicitly requested a core profile (core bit in
@@ -170,9 +170,230 @@ namespace MobileGL::MG_State::GLState {
m_uniformNameMaxLength = 0;
m_attribInNameMaxLength = 0;
m_uniformBlockNameMaxLength = 0;
m_xfbVaryings.clear();
m_xfbStrides.clear();
m_xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
m_xfbVaryingNameMaxLength = 0;
m_linkStatus = false;
}
namespace {
// GL type enum for a vertex-stage output symbol captured by transform
// feedback. Covers the scalar/vector/matrix float+integer types transform
// feedback may legally capture in GL 3.3.
Bool ResolveXfbSymbolType(const glslang::TType& type, GLenum& outType, GLint& outArraySize,
Uint32& outBytesPerElement) {
outArraySize = type.isArray() ? type.getOuterArraySize() : 1;
const Int columns = type.isMatrix() ? type.getMatrixCols() : 1;
const Int components = type.isMatrix() ? type.getMatrixRows()
: (type.isVector() ? type.getVectorSize() : 1);
const glslang::TBasicType basic = type.getBasicType();
static constexpr GLenum kFloatTypes[5] = {0, GL_FLOAT, GL_FLOAT_VEC2, GL_FLOAT_VEC3, GL_FLOAT_VEC4};
static constexpr GLenum kIntTypes[5] = {0, GL_INT, GL_INT_VEC2, GL_INT_VEC3, GL_INT_VEC4};
static constexpr GLenum kUintTypes[5] = {0, GL_UNSIGNED_INT, GL_UNSIGNED_INT_VEC2, GL_UNSIGNED_INT_VEC3,
GL_UNSIGNED_INT_VEC4};
if (type.isMatrix()) {
if (basic != glslang::EbtFloat) return false;
static constexpr GLenum kMatTypes[5][5] = {
{}, {},
{0, 0, GL_FLOAT_MAT2, GL_FLOAT_MAT2x3, GL_FLOAT_MAT2x4},
{0, 0, GL_FLOAT_MAT3x2, GL_FLOAT_MAT3, GL_FLOAT_MAT3x4},
{0, 0, GL_FLOAT_MAT4x2, GL_FLOAT_MAT4x3, GL_FLOAT_MAT4},
};
if (columns < 2 || columns > 4 || components < 2 || components > 4) return false;
outType = kMatTypes[columns][components];
} else if (components >= 1 && components <= 4) {
switch (basic) {
case glslang::EbtFloat: outType = kFloatTypes[components]; break;
case glslang::EbtInt: outType = kIntTypes[components]; break;
case glslang::EbtUint: outType = kUintTypes[components]; break;
default: return false;
}
} else {
return false;
}
outBytesPerElement = static_cast<Uint32>(columns * components) * 4u;
return true;
}
} // namespace
Bool ProgramObject::ResolveTransformFeedbackVaryings() {
m_xfbVaryings.clear();
m_xfbStrides.clear();
m_xfbBufferMode = m_requestedXfbBufferMode;
m_xfbVaryingNameMaxLength = 0;
if (m_requestedXfbVaryings.empty()) {
return true;
}
// Capture happens at the last vertex-processing stage (geometry, then
// tessellation evaluation, then vertex).
const glslang::TIntermediate* captureIntermediate = nullptr;
for (EShLanguage stage : {EShLangGeometry, EShLangTessEvaluation, EShLangVertex}) {
captureIntermediate = m_program->getIntermediate(stage);
if (captureIntermediate != nullptr) {
break;
}
}
if (captureIntermediate == nullptr) {
m_infoLog = "Transform feedback varyings requested but the program has no vertex-processing stage.";
return false;
}
const glslang::TIntermAggregate* linkerObjects = captureIntermediate->findLinkerObjects();
const Bool interleaved = m_xfbBufferMode == GL_INTERLEAVED_ATTRIBS;
Uint32 interleavedOffset = 0;
for (SizeT i = 0; i < m_requestedXfbVaryings.size(); ++i) {
const String& name = m_requestedXfbVaryings[i];
for (SizeT j = 0; j < i; ++j) {
if (m_requestedXfbVaryings[j] == name) {
m_infoLog = "Transform feedback varying '" + name + "' is specified more than once.";
return false;
}
}
XfbVarying varying;
varying.name = name;
Uint32 bytesPerElement = 0;
Bool resolved = false;
if (name == "gl_Position") {
varying.type = GL_FLOAT_VEC4;
varying.size = 1;
bytesPerElement = 16;
resolved = true;
} else if (name == "gl_PointSize") {
varying.type = GL_FLOAT;
varying.size = 1;
bytesPerElement = 4;
resolved = true;
} else if (linkerObjects != nullptr) {
for (const auto* node : linkerObjects->getSequence()) {
const glslang::TIntermSymbol* symbol = node->getAsSymbolNode();
if (symbol == nullptr || symbol->getType().getQualifier().storage != glslang::EvqVaryingOut) {
continue;
}
if (symbol->getName() != name.c_str()) {
continue;
}
resolved = ResolveXfbSymbolType(symbol->getType(), varying.type, varying.size, bytesPerElement);
break;
}
}
if (!resolved) {
m_infoLog = "Transform feedback varying '" + name + "' is not an output of the vertex stage.";
return false;
}
varying.byteSize = bytesPerElement * static_cast<Uint32>(varying.size);
if (interleaved) {
varying.bufferIndex = 0;
varying.offsetBytes = interleavedOffset;
interleavedOffset += varying.byteSize;
} else {
varying.bufferIndex = static_cast<Uint32>(i);
varying.offsetBytes = 0;
}
m_xfbVaryingNameMaxLength =
std::max(m_xfbVaryingNameMaxLength, static_cast<Int>(name.size()) + 1);
m_xfbVaryings.push_back(Move(varying));
}
constexpr Uint32 kMaxSeparateAttribs = 4;
constexpr Uint32 kMaxSeparateComponents = 4;
constexpr Uint32 kMaxInterleavedComponents = 64;
if (interleaved) {
if (interleavedOffset > kMaxInterleavedComponents * 4) {
m_infoLog = "Transform feedback interleaved capture exceeds "
"GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS.";
return false;
}
m_xfbStrides.assign(1, interleavedOffset);
} else {
if (m_xfbVaryings.size() > kMaxSeparateAttribs) {
m_infoLog = "Transform feedback separate capture exceeds "
"GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS.";
return false;
}
m_xfbStrides.resize(m_xfbVaryings.size());
for (SizeT i = 0; i < m_xfbVaryings.size(); ++i) {
if (m_xfbVaryings[i].byteSize > kMaxSeparateComponents * 4) {
m_infoLog = "Transform feedback varying '" + m_xfbVaryings[i].name +
"' exceeds GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS.";
return false;
}
m_xfbStrides[i] = m_xfbVaryings[i].byteSize;
}
}
ResolveGsTriangleStripCapture(captureIntermediate);
return true;
}
namespace {
// Extracts a geometry shader's per-invocation EmitVertex/EndPrimitive sequence
// when it is statically knowable (no emit inside selection/loop/switch). Vulkan
// transform feedback captures triangle strips in plain (i, i+1, i+2) order while
// GL decomposes odd strip triangles as (i+1, i, i+2) (GL 4.6 table 10.1); with
// the static strip lengths the capture buffer can be reordered after EndTF.
class GsEmitSequenceTraverser final : public glslang::TIntermTraverser {
public:
bool visitAggregate(glslang::TVisit, glslang::TIntermAggregate* node) override {
if (node->getOp() == glslang::EOpEmitVertex) {
++emitCount;
hasEmit = true;
} else if (node->getOp() == glslang::EOpEndPrimitive) {
FlushStrip();
}
return true;
}
bool visitSelection(glslang::TVisit, glslang::TIntermSelection*) override {
inControlFlow = true;
return true;
}
bool visitLoop(glslang::TVisit, glslang::TIntermLoop*) override {
inControlFlow = true;
return true;
}
bool visitSwitch(glslang::TVisit, glslang::TIntermSwitch*) override {
inControlFlow = true;
return true;
}
void FlushStrip() {
if (emitCount >= 3) {
stripTriangles.push_back(static_cast<Uint32>(emitCount - 2));
}
emitCount = 0;
}
Vector<Uint32> stripTriangles;
Uint32 emitCount = 0;
Bool hasEmit = false;
Bool inControlFlow = false;
};
} // namespace
void ProgramObject::ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate) {
m_gsStripTriangles.clear();
m_gsStripCaptureFixup = false;
if (captureIntermediate == nullptr || m_program == nullptr) {
return;
}
if (m_program->getIntermediate(EShLangGeometry) != captureIntermediate) {
return;
}
if (captureIntermediate->getOutputPrimitive() != glslang::ElgTriangleStrip) {
return;
}
GsEmitSequenceTraverser traverser;
const_cast<glslang::TIntermediate*>(captureIntermediate)->getTreeRoot()->traverse(&traverser);
traverser.FlushStrip(); // the invocation end acts as an implicit EndPrimitive
if (!traverser.hasEmit || traverser.inControlFlow || traverser.stripTriangles.empty()) {
return;
}
m_gsStripTriangles = Move(traverser.stripTriangles);
m_gsStripCaptureFixup = true;
}
bool ProgramObject::ShaderIsAttached(const SharedPtr<ShaderObject>& shader) {
MGLOG_D("ProgramObject %u: ShaderIsAttached check for shader %p", m_externalIndex, shader.get());
auto it = std::find_if(m_shaders.begin(), m_shaders.end(),
@@ -327,6 +548,12 @@ namespace MobileGL::MG_State::GLState {
if (!ValidateFragmentOutputLocations()) {
return;
}
if (!ResolveTransformFeedbackVaryings()) {
m_linkStatus = false;
MGLOG_E("ProgramObject %u: transform feedback varying resolution failed: %s", m_externalIndex,
m_infoLog.c_str());
return;
}
MGLOG_D("ProgramObject %u: Starting binary generation", m_externalIndex);
GenerateBinary();
@@ -334,16 +334,32 @@ namespace MobileGL::MG_State::GLState {
// draw. The memo is keyed by (backendStateVersion, flags); ResetLinkArtifacts and
// the binding setters below invalidate it by bumping m_backendStateVersion.
Bool GetBackendHashMemo(Uint flags, Uint64& outHash) const {
if (m_backendHashMemoVersion != m_backendStateVersion || m_backendHashMemoFlags != flags) {
return false;
if (m_backendHashMemoVersion != m_backendStateVersion) return false;
for (const auto& slot : m_backendHashMemoSlots) {
if (slot.valid && slot.flags == flags) {
outHash = slot.hash;
return true;
}
}
outHash = m_backendHashMemo;
return true;
return false;
}
void SetBackendHashMemo(Uint flags, Uint64 hash) const {
m_backendHashMemo = hash;
m_backendHashMemoVersion = m_backendStateVersion;
m_backendHashMemoFlags = flags;
if (m_backendHashMemoVersion != m_backendStateVersion) {
for (auto& slot : m_backendHashMemoSlots) slot.valid = false;
m_backendHashMemoVersion = m_backendStateVersion;
m_backendHashMemoNextSlot = 0;
}
for (auto& slot : m_backendHashMemoSlots) {
if (slot.valid && slot.flags == flags) {
slot.hash = hash;
return;
}
}
auto& slot = m_backendHashMemoSlots[m_backendHashMemoNextSlot];
slot.flags = flags;
slot.hash = hash;
slot.valid = true;
m_backendHashMemoNextSlot = (m_backendHashMemoNextSlot + 1) % kBackendHashMemoSlotCount;
}
void SetUniformSamplerOrImageUnitIndex(Uint location, Int unit) {
@@ -449,6 +465,39 @@ namespace MobileGL::MG_State::GLState {
return it == m_shaders.end() ? -1 : (Int)std::distance(m_shaders.begin(), it);
}
// Transform feedback (GL 3.0 core: glTransformFeedbackVaryings applies on
// the NEXT link; the linked snapshot below is what draws and queries see).
struct XfbVarying {
String name;
GLenum type = GL_FLOAT;
GLint size = 1; // array element count
Uint32 bufferIndex = 0; // capture buffer slot
Uint32 offsetBytes = 0; // offset within the capture buffer
Uint32 byteSize = 0; // bytes captured per vertex for this varying
};
void SetTransformFeedbackVaryings(Vector<String>&& names, GLenum bufferMode) {
m_requestedXfbVaryings = Move(names);
m_requestedXfbBufferMode = bufferMode;
}
GLenum GetTransformFeedbackBufferMode() const { return m_xfbBufferMode; }
SizeT GetTransformFeedbackVaryingCount() const { return m_xfbVaryings.size(); }
const XfbVarying* GetTransformFeedbackVarying(SizeT index) const {
return index < m_xfbVaryings.size() ? &m_xfbVaryings[index] : nullptr;
}
const Vector<XfbVarying>& GetTransformFeedbackVaryings() const { return m_xfbVaryings; }
// Stride of one captured vertex in the given capture buffer slot.
Uint32 GetTransformFeedbackStride(Uint32 bufferIndex) const {
return bufferIndex < m_xfbStrides.size() ? m_xfbStrides[bufferIndex] : 0;
}
SizeT GetTransformFeedbackBufferCount() const { return m_xfbStrides.size(); }
Int GetTransformFeedbackVaryingMaxLength() const { return m_xfbVaryingNameMaxLength; }
// True when the capture stage is a triangle-strip geometry shader with a
// statically-known emit sequence: the Vulkan capture order then needs the GL
// odd-triangle vertex swap after EndTransformFeedback.
Bool HasGsTriangleStripCaptureFixup() const { return m_gsStripCaptureFixup; }
// Triangles per strip, in emission order, for ONE geometry invocation.
const Vector<Uint32>& GetGsStripTriangles() const { return m_gsStripTriangles; }
Uint GetExternalIndex() const { return m_externalIndex; }
// Globally-unique, never-reused id for this program object's lifetime. Unlike the GL
// name (external index), which is freed to a LIFO list and immediately handed back by
@@ -459,6 +508,11 @@ namespace MobileGL::MG_State::GLState {
private:
void ResetLinkArtifacts();
void DoReflection();
// Resolves the requested transform feedback varyings against the linked
// vertex stage; fails the link (GL semantics) on unknown or duplicate
// names or exceeded capture limits.
Bool ResolveTransformFeedbackVaryings();
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
void GenerateBinary();
void WaitUntilGenerationCompleted() const;
void AddDefaultFragmentShaderIfMissing();
@@ -527,11 +581,30 @@ namespace MobileGL::MG_State::GLState {
Uint32 m_backendStateVersion = 0;
// Backend-owned content-hash memo (see GetBackendHashMemo): valid only while
// m_backendStateVersion and the compile flags match the recorded values.
mutable Uint64 m_backendHashMemo = 0;
// m_backendStateVersion matches. Several slots, not one: a backend may resolve the same
// program under more than one compile-flag set within a frame (surface rotation, and the
// explicit-LOD sampling variant), and a single slot would then miss on every lookup and
// re-hash the program's whole SPIR-V once per draw.
static constexpr SizeT kBackendHashMemoSlotCount = 4;
struct BackendHashMemoSlot {
Uint64 hash = 0;
Uint flags = 0;
Bool valid = false;
};
mutable Array<BackendHashMemoSlot, kBackendHashMemoSlotCount> m_backendHashMemoSlots{};
mutable SizeT m_backendHashMemoNextSlot = 0;
mutable Uint32 m_backendHashMemoVersion = ~0u;
mutable Uint m_backendHashMemoFlags = 0;
Uint32 m_uboContentVersion = 0;
Uint32 m_linkVersion = 0;
// Transform feedback: request (applies at next link) and linked snapshot.
Vector<String> m_requestedXfbVaryings;
GLenum m_requestedXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
Vector<XfbVarying> m_xfbVaryings;
Vector<Uint32> m_xfbStrides;
Vector<Uint32> m_gsStripTriangles;
Bool m_gsStripCaptureFixup = false;
GLenum m_xfbBufferMode = GL_INTERLEAVED_ATTRIBS;
Int m_xfbVaryingNameMaxLength = 0;
};
} // namespace MobileGL::MG_State::GLState
@@ -29,17 +29,25 @@ namespace MobileGL::MG_State::GLState {
if (!CheckIndexAvail(program, m_programObjects)) return; // FIXME: add error reporting here
auto& programObject = m_programObjects[program];
if (programObject != nullptr) {
// Snapshot the attachments: deleting the program is a detach point for shaders
// that were flagged with glDeleteShader while still attached.
const Vector<SharedPtr<ShaderObject>> attachedShaders = programObject->GetAttachedShaders();
programObject->MarkAsDeleted();
programObject.reset();
m_programIndexGenerator.Delete(program);
for (const auto& shader : attachedShaders) {
const Uint shaderName = shader->GetExternalIndex();
if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) {
ReleaseShaderNameIfOrphaned(shaderName);
}
// A program in use is only FLAGGED: its name (and every program query) stays
// valid until it stops being current, at which point UseProgram finishes the job.
if (programObject == m_currentProgram) return;
DestroyProgramSlot(program);
}
}
void ProgramState::DestroyProgramSlot(const Uint program) {
auto& programObject = m_programObjects[program];
// Snapshot the attachments: deleting the program is a detach point for shaders
// that were flagged with glDeleteShader while still attached.
const Vector<SharedPtr<ShaderObject>> attachedShaders = programObject->GetAttachedShaders();
programObject.reset();
m_programIndexGenerator.Delete(program);
for (const auto& shader : attachedShaders) {
const Uint shaderName = shader->GetExternalIndex();
if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) {
ReleaseShaderNameIfOrphaned(shaderName);
}
}
}
@@ -49,10 +57,22 @@ namespace MobileGL::MG_State::GLState {
}
void ProgramState::UseProgram(Uint program) {
const SharedPtr<ProgramObject> previous = m_currentProgram;
if (program == 0) m_currentProgram.reset();
if (!CheckIndexAvail(program, m_programObjects)) return;
m_currentProgram = m_programObjects[program];
if (CheckIndexAvail(program, m_programObjects)) {
m_currentProgram = m_programObjects[program];
}
// A deletion flagged while the program was current takes effect the moment it
// stops being current.
if (previous != nullptr && previous != m_currentProgram && previous->GetDeleteStatus()) {
const Uint previousName = previous->GetExternalIndex();
if (CheckIndexAvail(previousName, m_programObjects) && m_programObjects[previousName] == previous) {
DestroyProgramSlot(previousName);
}
}
}
Uint ProgramState::CreateShader(ShaderStage stage) {
@@ -35,6 +35,9 @@ namespace MobileGL::MG_State::GLState {
private:
Bool ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const;
// Frees the name slot and releases orphaned attached shaders; the immediate half
// of glDeleteProgram (deferred while the program is current).
void DestroyProgramSlot(Uint program);
template <typename T>
static Bool CheckIndexAvail(const SizeT idx, const Vector<T>& vec) {
@@ -169,6 +169,15 @@ namespace MobileGL::MG_State::GLState {
}
}
const std::optional<String> reservedError =
MG_Util::ShaderTranspiler::FindReservedIdentifierViolation(compileSource);
if (reservedError) {
m_compileStatus = false;
m_shader.reset();
m_infoLog = *reservedError;
return;
}
// Compile for OpenGL here, so that we can do validation and link
// like a real OpenGL driver at linking stage
// Will compile for other backends later.
@@ -16,17 +16,32 @@ namespace MobileGL {
}
void MipmapStorage::AllocateLevel(Uint level, MipmapInput input) {
m_data.reserve(std::bit_ceil(level + 1));
m_data.resize(level + 1);
m_texelSizes.reserve(std::bit_ceil(level + 1));
m_texelSizes.resize(level + 1);
m_texelSizes[level] = input.texelSize;
m_isDirty.resize(level + 1, false);
// Grow only. GL respecifies exactly the level it is handed, so allocating level 0
// must not disturb the levels above it - but resize() shrinks as readily as it
// grows, so this used to truncate the whole chain to a single level. Callers that
// genuinely redefine the complete level set say so with TruncateToLevelCount.
const SizeT requiredLevelCount = static_cast<SizeT>(level) + 1;
if (m_data.size() < requiredLevelCount) {
m_data.reserve(std::bit_ceil(requiredLevelCount));
m_data.resize(requiredLevelCount);
m_texelSizes.reserve(std::bit_ceil(requiredLevelCount));
m_texelSizes.resize(requiredLevelCount);
m_isDirty.resize(requiredLevelCount, false);
}
m_texelSizes[level] = input.texelSize;
auto& data = m_data[level];
data.resize(input.byteSize, 0);
}
void MipmapStorage::TruncateToLevelCount(SizeT levelCount) {
if (levelCount >= m_data.size()) return;
m_data.resize(levelCount);
m_texelSizes.resize(levelCount);
m_isDirty.resize(levelCount);
}
void MipmapStorage::UpdateSubData(Uint level, DataPtr input) {
auto& targetData = m_data;
MOBILEGL_ASSERT(level < targetData.size(), "UpdateSubData: level out of range");
@@ -55,6 +70,7 @@ namespace MobileGL {
}
SizeT MipmapStorage::GetByteSize(Uint level) const {
if (level >= m_data.size()) return 0;
return m_data[level].size();
}
@@ -19,6 +19,10 @@ namespace MobileGL {
public:
SizeT GetLevelCount() const;
void AllocateLevel(Uint level, MipmapInput input);
// Discard every level at or above levelCount. AllocateLevel never shrinks, so this
// is the only way a chain gets shorter - use it where the caller defines the whole
// level set (glTexStorage*, mip regeneration, atlas respecification).
void TruncateToLevelCount(SizeT levelCount);
void UpdateSubData(Uint level, DataPtr input);
void* MapData(Uint level);
IntVec3 GetTexelSize(Uint level) const;
@@ -29,6 +29,14 @@ namespace MobileGL {
m_storage[targetIndex].AllocateLevel(level, input);
}
// Per-target, like AllocateLevel: cube-map faces are respecified independently, so
// truncating one face must not disturb the others.
void TruncateToLevelCount(Uint targetIndex, SizeT levelCount) {
MOBILEGL_ASSERT(targetIndex < TargetCount, "TruncateToLevelCount: target invalid");
m_storage[targetIndex].TruncateToLevelCount(levelCount);
}
void UpdateSubData(Uint targetIndex, Uint level, DataPtr input) {
MOBILEGL_ASSERT(targetIndex < TargetCount, "UpdateSubData: target invalid");
m_storage[targetIndex].UpdateSubData(level, input);
@@ -162,6 +162,7 @@ namespace MobileGL {
DepthComponent32F,
Depth24Stencil8,
Depth32FStencil8,
StencilIndex8,
DepthComponent,
DepthStencil,
@@ -271,6 +271,10 @@ namespace MobileGL {
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
}
void TextureObjectWithOneMipmap::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
}
void TextureObjectWithOneMipmap::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel,
DataPtr input) {
m_textureStorage.UpdateSubData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
@@ -15,7 +15,11 @@
#include <MG_Util/Math/VectorTypes.h>
namespace MobileGL::MG_State::GLState {
class ITextureObject {
// Texture objects are always SharedPtr-owned (TextureState creates every instance via
// MakeShared, including the per-target default objects). enable_shared_from_this lets
// backends that only receive a reference (e.g. syncing a name-deleted texture kept
// alive by an FBO attachment) still register a weak liveness reference for GC.
class ITextureObject : public std::enable_shared_from_this<ITextureObject> {
public:
using TargetEnum = TextureTarget;
virtual ~ITextureObject() = default;
@@ -134,6 +138,10 @@ namespace MobileGL::MG_State::GLState {
virtual const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const = 0;
virtual const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const = 0;
virtual void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) = 0;
// AllocateStorage only ever grows the chain. Callers that define the complete level set -
// glTexStorage*, mip regeneration, or a level-0 respecification at a new size - drop the
// leftovers explicitly, so a stale tail can never make the texture silently incomplete.
virtual void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) = 0;
virtual void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) = 0;
virtual void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) = 0;
virtual void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty = true) = 0;
@@ -175,6 +183,7 @@ namespace MobileGL::MG_State::GLState {
const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const override;
const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const override;
void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) override;
void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) override;
void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) override;
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) override;
@@ -31,6 +31,10 @@ namespace MobileGL {
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
}
void TextureObject2DCube::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
}
void TextureObject2DCube::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel,
DataPtr input) {
m_textureStorage.UpdateSubData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
@@ -22,6 +22,7 @@ namespace MobileGL {
const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const override;
const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const override;
void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) override;
void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) override;
void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) override;
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, bool dirty) override;
@@ -104,6 +104,23 @@ namespace MobileGL {
m_backendHashMemoVersion = m_configVersion;
}
// Backend-owned resolved-state memo: an opaque pointer into the
// backend's vertex-input-state cache plus the cache's eviction
// epoch, valid while the config version matches. Lets the
// per-draw path skip the content hash AND the cache lookup; the
// epoch guards against the cache evicting the pointee.
Bool GetBackendStateMemo(const void*& outState, Uint64& outEpoch) const {
if (m_backendStateMemoVersion != m_configVersion) return false;
outState = m_backendStateMemo;
outEpoch = m_backendStateMemoEpoch;
return true;
}
void SetBackendStateMemo(const void* state, Uint64 epoch) const {
m_backendStateMemo = state;
m_backendStateMemoEpoch = epoch;
m_backendStateMemoVersion = m_configVersion;
}
private:
void BumpAttributeFormatVersion(Uint index);
void BumpAttributeBufferVersion(Uint index);
@@ -137,6 +154,9 @@ namespace MobileGL {
Uint32 m_configVersion = 0;
mutable Uint64 m_backendHashMemo = 0;
mutable Uint32 m_backendHashMemoVersion = ~0u;
mutable const void* m_backendStateMemo = nullptr;
mutable Uint64 m_backendStateMemoEpoch = 0;
mutable Uint32 m_backendStateMemoVersion = ~0u;
};
} // namespace GLState
} // namespace MG_State
@@ -34,6 +34,11 @@ namespace {
GLint maxFragmentImageUniforms = 4;
GLint maxComputeImageUniforms = 5;
bool maxGeometryImageUniformsQueried = false;
GLfloat minFragmentInterpolationOffset = -0.75f;
GLfloat maxFragmentInterpolationOffset = 0.625f;
GLint fragmentInterpolationOffsetBits = 6;
bool fragmentInterpolationLimitsQueried = false;
bool fragmentInterpolationQueryRaisesError = false;
// Emulates ANGLE-on-Vulkan: the draw reads the indirect command's
// baseInstance word and exposes it through gl_InstanceID.
bool drawLeaksBaseInstanceWord = false;
@@ -108,6 +113,14 @@ namespace {
case GL_MAX_COMPUTE_IMAGE_UNIFORMS:
*data = g_fake.maxComputeImageUniforms;
break;
case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS:
g_fake.fragmentInterpolationLimitsQueried = true;
if (g_fake.fragmentInterpolationQueryRaisesError) {
g_fake.pendingError = GL_INVALID_ENUM;
} else {
*data = g_fake.fragmentInterpolationOffsetBits;
}
break;
// FillInGLESCapabilities reads the context version before running the
// baseInstance probe, which requires ES >= 3.1.
case GL_MAJOR_VERSION:
@@ -155,6 +168,22 @@ namespace {
g_fake.maxTextureMaxAnisotropyQueried = true;
data[0] = g_fake.maxTextureMaxAnisotropy;
break;
case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET:
g_fake.fragmentInterpolationLimitsQueried = true;
if (g_fake.fragmentInterpolationQueryRaisesError) {
g_fake.pendingError = GL_INVALID_ENUM;
} else {
data[0] = g_fake.minFragmentInterpolationOffset;
}
break;
case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET:
g_fake.fragmentInterpolationLimitsQueried = true;
if (g_fake.fragmentInterpolationQueryRaisesError) {
g_fake.pendingError = GL_INVALID_ENUM;
} else {
data[0] = g_fake.maxFragmentInterpolationOffset;
}
break;
// Two-component range queries.
case GL_ALIASED_LINE_WIDTH_RANGE:
case GL_SMOOTH_LINE_WIDTH_RANGE:
@@ -462,6 +491,52 @@ TEST(ImageUniformCapabilities, QueriesRealPerStageLimitsAndConservativelyGatesGe
EXPECT_TRUE(g_fake.maxGeometryImageUniformsQueried);
}
TEST(FragmentInterpolationCapabilities, QueriesOnlyWhenSupportedAndPreservesDriverLimits) {
const auto funcs = MakeFakeGLESFunctions();
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
MobileGL::MG_External::GLESCapabilities unsupportedCaps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(unsupportedCaps, funcs));
EXPECT_FALSE(unsupportedCaps.SupportsShaderMultisampleInterpolation);
EXPECT_FALSE(g_fake.fragmentInterpolationLimitsQueried);
EXPECT_FLOAT_EQ(unsupportedCaps.MinFragmentInterpolationOffset, -0.5f);
EXPECT_FLOAT_EQ(unsupportedCaps.MaxFragmentInterpolationOffset, 0.4375f);
EXPECT_EQ(unsupportedCaps.FragmentInterpolationOffsetBits, 4);
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.extensions.emplace_back("GL_OES_shader_multisample_interpolation");
// A stale error from an earlier capability probe must not make the optional
// interpolation query look like it failed.
g_fake.pendingError = GL_INVALID_OPERATION;
MobileGL::MG_External::GLESCapabilities supportedCaps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(supportedCaps, funcs));
EXPECT_TRUE(supportedCaps.SupportsShaderMultisampleInterpolation);
EXPECT_TRUE(g_fake.fragmentInterpolationLimitsQueried);
EXPECT_FLOAT_EQ(supportedCaps.MinFragmentInterpolationOffset, g_fake.minFragmentInterpolationOffset);
EXPECT_FLOAT_EQ(supportedCaps.MaxFragmentInterpolationOffset, g_fake.maxFragmentInterpolationOffset);
EXPECT_EQ(supportedCaps.FragmentInterpolationOffsetBits, g_fake.fragmentInterpolationOffsetBits);
EXPECT_EQ(funcs.glGetError(), GL_NO_ERROR);
}
TEST(FragmentInterpolationCapabilities, QueryErrorIsDrainedAndFallsBackToCoreMinimums) {
ResetFakeDriver();
g_fake.maxVertexSsboBlocks = 0;
g_fake.extensions.emplace_back("GL_OES_shader_multisample_interpolation");
g_fake.fragmentInterpolationQueryRaisesError = true;
const auto funcs = MakeFakeGLESFunctions();
MobileGL::MG_External::GLESCapabilities caps;
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(caps, funcs));
EXPECT_TRUE(g_fake.fragmentInterpolationLimitsQueried);
EXPECT_FLOAT_EQ(caps.MinFragmentInterpolationOffset, -0.5f);
EXPECT_FLOAT_EQ(caps.MaxFragmentInterpolationOffset, 0.4375f);
EXPECT_EQ(caps.FragmentInterpolationOffsetBits, 4);
EXPECT_EQ(funcs.glGetError(), GL_NO_ERROR);
}
// The extension string is what apps gate on (LWJGL builds GLCapabilities from it), so advertising
// it on a driver that cannot filter anisotropically would leave them silently on trilinear.
TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSupportsIt) {
+2
View File
@@ -72,6 +72,8 @@ add_subdirectory(Texture)
add_subdirectory(VertexArray)
add_subdirectory(Program)
add_subdirectory(Query)
add_subdirectory(Pipeline)
add_subdirectory(ShaderTranspiler)
if (ENABLE_INTEGRATION_TESTS)
add_subdirectory(Backend/DirectVulkan)
endif()

Some files were not shown because too many files have changed in this diff Show More