Compare commits

..
Author SHA1 Message Date
BZLZHH 10d0441040 DirectGLES: gate the replicate blit's stencil pass on ES 3.1
Reading the stencil half of a packed depth/stencil texture goes through
GL_DEPTH_STENCIL_TEXTURE_MODE, which is ES 3.1 state. On an older driver
the pname would raise GL_INVALID_ENUM and the shader would go on sampling
depth bits as if they were stencil, so decline the emulation instead.
2026-08-02 04:55:05 -04:00
BZLZHH ad03e059e0 Ask whether a colour format is renderable per target, not in general
The framebuffer-completeness check scanned every row of the backend's
format-capability cache and called the format renderable if any target
said so. That was already loose, and it broke outright once DirectGLES
started widening three-channel formats so they stay renderable as
multisample storage: the caveat capability recorded for the multisample
target made GL_RGB8_SNORM look renderable everywhere, so an ordinary 2D
GL_RGB8_SNORM texture attachment reported GL_FRAMEBUFFER_COMPLETE while
the driver's own framebuffer was INCOMPLETE_ATTACHMENT.

KHR-GL3x.packed_pixels stopped skipping those formats and read a
framebuffer that could not be read, so all 18 of its rgb8_snorm cases got
back an untouched buffer.

Pass the row the attachment actually lives in - the texture's target, or
the renderbuffer row - and consult only that one; a format is still asked
about in general when the caller has no target.
2026-08-02 04:54:48 -04:00
BZLZHH 440e569c98 DirectGLES: emulate a depth/stencil blit into a multisample framebuffer
Desktop GL replicates the source sample into every destination sample
when the read framebuffer is single-sampled and the draw framebuffer is
not. ES forbids the call outright - "an INVALID_OPERATION error is
generated if SAMPLE_BUFFERS for the draw framebuffer is greater than
zero" - so the blit did nothing at all, and every one of
KHR-GL3x.packed_depth_stencil.blit's replicate iterations verified a
destination that still held its clear values.

Emulate it by drawing a full-screen triangle into the multisample
framebuffer: every pixel is fully covered, so every sample of it receives
the same value, which is precisely the replicate rule. The source
rectangle is first copied into a scratch texture of its own format (both
sides single-sampled, which ES does allow), then depth is written through
gl_FragDepth and stencil - which has no shader output on ES - one bit
plane at a time with REPLACE and a discard for the pixels whose source
bit is clear.

The draw runs inside the caller's framebuffer, so every piece of pipeline
state it touches is read back and restored, including the per-draw-buffer
colour masks the non-indexed glColorMask does not cover: the sync layer's
shadow of the driver state has to stay true across this.

Colour replicate is not emulated (it would need a sampler variant per
component type); it now says so instead of failing silently.
2026-08-02 04:34:25 -04:00
BZLZHH cb62431299 DirectGLES: report the alpha added by the multisample widening as ONE
A three-channel format widened to four for a multisample target gains an
alpha channel the application never asked for, and it holds whatever the
draw that filled the texture happened to write there. GL says a format
without alpha reads back as 1.0, so KHR-GL33.texture_swizzle - which
fills such a texture by rendering vec4(r, g, b, 0.0) and then swizzles
red from alpha - read 0 where it expected the maximum.

Fold ONE into the texture's swizzle for exactly those textures, composed
with the swizzle the application set, so the promotion stays invisible.
2026-08-02 04:25:08 -04:00
BZLZHH 06ce55dac3 DirectGLES: keep 16-bit SNORM precision through the multisample widening
GL_RGB16_SNORM widened to GL_RGBA16F to stay renderable as multisample
storage, and a half float's 11-bit mantissa cannot hold a 16-bit
signed-normalized channel: KHR-GL33.texture_swizzle's blue channel came
back several units of 32767 away from the value the reference computes,
well outside its one-unit tolerance.

GL_EXT_render_snorm makes the signed-normalized formats colour-renderable
on ES, so widen to GL_RGBA16_SNORM instead wherever it and
EXT_texture_norm16 are both present, and only fall back to the half float
otherwise. Threaded through as its own normalize option so the capability
probe and the runtime pick the same format, the way every other
driver-dependent substitution here is decided.
2026-08-02 04:24:23 -04:00
BZLZHH 7400f46955 DirectGLES: probe format capabilities on the target ES stores them on
1D, 1D-array and rectangle textures are emulated on ES 2D and 2D-array
targets, but the capability probe kept asking the driver about the
desktop-only target itself. glTexImage2D(GL_TEXTURE_1D, ...) is not
something an ES driver has ever accepted, so those rows of the cache
stayed empty - and an empty row reads as "nothing is known", not as "the
format needs help", so no fallback format was ever selected for them.

GL_DEPTH_COMPONENT32 on a 1D texture therefore went to the driver
unchanged instead of as GL_DEPTH_COMPONENT24, and the texture ended up
with no storage (KHR-GL33.texture_swizzle format_idx_65 on both 1D
targets read the wrong value for every pixel).

Probe the ES target the texture will actually live on, while still
recording the capabilities against the target the frontend asked for.
2026-08-02 04:18:20 -04:00
BZLZHH 1679bf9a1e DirectGLES: turn off the driver's sRGB framebuffer encoding
GLES core always encodes a fragment written into an sRGB colour
attachment, and offers no switch to stop it. Desktop GL has one,
GL_FRAMEBUFFER_SRGB, and it starts out disabled - so a GL application
that never touches it expects its writes to land raw. The frontend models
exactly that (the capability reads as disabled and DirectVulkan attaches
the UNORM twin to honour it), but DirectGLES was passing the draw
straight to a driver that encodes anyway.

The value therefore came back one conversion short of the reference
wherever it was written and then read again: rendering into an sRGB
texture and fetching it in a shader decodes once but had encoded twice,
which is how KHR-GL32.texture_size_promotion read 0.0142 for
GL_SRGB8_ALPHA8 where 0.00111 was expected.

Detect GL_EXT_sRGB_write_control and sync GL_FRAMEBUFFER_SRGB from the
frontend capability alongside the other enables, starting from the
driver's enabled state so the first sync always pushes the disable down.
2026-08-02 04:10:13 -04:00
BZLZHH 8673e89b13 DirectGLES: widen three-channel formats for multisample textures
GLES has no colour-renderable three-channel format beyond RGB8, so
glTexStorage2DMultisample rejects GL_RGB16 (and the SNORM variants) with
GL_INVALID_ENUM and the texture is left with no storage at all - every
draw into it then hit GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT and every read
came back zero.

The existing fallback machinery could not help: it picks one replacement
format per requested format, from the driver's capabilities, and never
re-checks that replacement against the target it is going to be used
with. GL_RGB16's fallback is GL_RGB32F, which is a perfectly legal ES
texture format and a perfectly illegal multisample storage format, and
with EXT_texture_norm16 present no fallback was selected at all.

Add PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget, applied
only to multisample targets, mapping GL_RGB16 to GL_RGBA32F and the
three-channel SNORM formats to GL_RGBA16F. Widening the channel count is
safe precisely there and nowhere else: a multisample texture can never be
uploaded to, only rendered into, so no transfer path has to expand
three-channel client data, and the alpha a draw writes for a
three-channel source is already the 1.0 the frontend format implies.

The capability probe recomputes its fallback per target for the same
reason, so the probed format and the format the texture is actually
created with stay in agreement.
2026-08-02 04:10:01 -04:00
BZLZHH 6de38c666c [Feat] (DirectGLES): support rectangle textures where the emulation is exact
ES has no rectangle target and no rectangle sampler, so DirectGLES declared
GL_TEXTURE_RECTANGLE unsupported outright: the texture was never synced or bound,
and SPIRV-Cross refused the shader ("Rectangle textures are not supported on
OpenGL ES") which left the whole program unlinkable.

A rectangle texture is a single-level, clamped 2D texture whose only real
difference is that its lookups take non-normalized coordinates. Where every use
takes *integer* texel coordinates - texelFetch, textureSize - that difference
does not exist at all, and the two are the same thing. So:

- A new SPIR-V pass rewrites Dim::Rect image types to Dim::2D before
  transpiling, and restates the rectangle capabilities as Shader. It declines
  any module containing a normalized-coordinate lookup rather than emitting
  something subtly wrong; SPIRV-Cross then rejects that module exactly as
  before, so nothing that used to work changes and nothing new renders wrongly.
- The target maps to GL_TEXTURE_2D for storage, uploads and binding, alongside
  the existing 1D and 1D-array emulation.

Fixes KHR-GL31.texture_size_promotion.functional outright, which takes GL31 to
100% conformance. GL32/GL33 advance past their rectangle cases to a separate
GL_RGB16 multisample issue. No regressions across texture_swizzle, shaders30,
texture_lod_*, framebuffer_blit, packed_depth_stencil, transform_feedback,
clip_distance or draw_buffers; DirectVulkan re-verified unaffected.
2026-08-02 01:56:22 -04:00
BZLZHH 9dff24f3e1 [Fix] (DirectGLES): never leave a stale program bound when the new one is broken
When a shader stage fails to transpile or compile, SyncToBackend logs it and
carries on, so the program is linked without that stage - or does not link at
all. Use() then issued glUseProgram for it, which is an INVALID_OPERATION for an
unlinked program and, crucially, leaves the *previous* program current. The draw
went ahead and rendered with an entirely unrelated shader.

That is how KHR-GL3x.texture_size_promotion's GL_TEXTURE_RECTANGLE cases
produced 1.0 for a red channel: SPIRV-Cross refuses sampler2DRect for ESSL
("Rectangle textures are not supported on OpenGL ES"), so every rectangle
program was broken, and the draws kept running the previous case's 1D-array
alpha shader - whose alpha is 1.0. Wrong pixels from a shader the app never
bound are far worse to debug than a blank result.

The program now records whether the last sync produced something usable, and
Use() binds 0 rather than the broken program, making the draw a visible no-op.
The redundancy cache tracks whatever was actually bound, so it stays correct
across the switch.

Does not fix the rectangle cases themselves - those need a SPIR-V pass lowering
Dim::Rect to Dim::2D before SPIRV-Cross runs (plus the coordinate divide for
non-texelFetch lookups), alongside mapping the target to GL_TEXTURE_2D.
2026-08-02 01:40:53 -04:00
BZLZHH 5a400e0297 Merge branch 'dev' of github.com:MobileGL-Dev/MobileGL into dev 2026-08-02 12:21:25 +08:00
BZLZHH b6a2bf08d4 [Fix] (DirectGLES): resolve an aliased texture unit by the sampler's type
Desktop GL_TEXTURE_1D/1D_ARRAY are emulated on ES GL_TEXTURE_2D/2D_ARRAY, so one
native binding serves two of a unit's frontend slots. An earlier fix settled the
real-versus-default case; two REAL textures can collide just as easily, and there
the slot iteration order decided it. KHR-GL3x.texture_size_promotion keeps its 1D
source texture and its 2D destination texture bound to the same unit, so the
shader sampled the render target it was drawing into instead of the source.

GL resolves this from the shader's sampler type, so ask the program: the
frontend's uniform reflection still carries the original GLSL type, which maps
straight back to the target the lookup means. Only consulted when a collision
actually happens, so an ordinary unit costs nothing, and the first binding
placed stands when the program gives no answer rather than being overwritten by
whichever slot happens to come last.

Also adds the read-colour clamp that goes with it: GL clamps a glReadPixels from
a fixed-point colour buffer to [0,1] (GL_CLAMP_READ_COLOR defaults to
GL_FIXED_ONLY), which ES has no equivalent for at all - a GL_R16_SNORM target
holding -0.125 read back unclamped. Applied to the wide rows before they are
repacked, for float, half, short and byte reads alike, and deliberately NOT for
glGetTexImage, which reaches the same helper through a scratch framebuffer but
is not subject to read-colour clamping.

texture_size_promotion now clears every 1D case (it stops at the first failure
and has moved on to GL_TEXTURE_RECTANGLE, which DirectGLES does not emulate at
all yet), and KHR-GL33.texture_swizzle's GL_DEPTH_COMPONENT32 1D cases pass.
DirectVulkan re-verified unchanged.
2026-08-01 16:21:28 -04:00
BZLZHH 6b2a2b5e00 [Fix] (MG_Util): emulate GL_DEPTH_COMPONENT32 with the 24-bit sized format
GL_DEPTH_COMPONENT32 has no ES equivalent. The previous commit routed it to
GL_DEPTH_COMPONENT32F, which gives the attachment storage but changes the
encoding: the transfer type has to become GL_FLOAT for ES to accept the store,
and the upload path hands over the caller's fixed-point GL_UNSIGNED_INT bytes
unchanged, so the texels came out as garbage.

GL_DEPTH_COMPONENT24 is the nearest sized ES format that keeps the same
fixed-point encoding, so GL_UNSIGNED_INT still describes the data and no
conversion is needed. Fixes KHR-GL33.texture_swizzle's GL_DEPTH_COMPONENT32
cases on the 2D and 2D-array targets; framebuffer_blit's GL_DEPTH_COMPONENT32
config still passes, since the depth values it compares are exactly
representable in 24 bits.

(The 1D and 1D-array targets still fail, but for the separate desktop-1D-on-ES
emulation reason that also holds back texture_size_promotion.)
2026-08-01 14:59:19 -04:00
BZLZHH 512c857f18 [Fix] (DirectGLES): emulate a format-converting multisample resolve blit
ES rejects any blit out of a multisample read framebuffer whose format differs
from the draw framebuffer's. Desktop GL only requires identical formats when
BOTH framebuffers are multisampled - a multisample resolve into a single-sample
target is allowed to convert on the way out, and KHR-GL3x.framebuffer_blit
resolves an R8 multisample texture straight into the RGBA8 default framebuffer.
The forwarded blit failed with GL_INVALID_OPERATION, and since the driver's
error never reaches the frontend error queue the caller saw a successful call
that had written nothing.

Retried in two steps when the first blit fails and the read framebuffer really
is the multisampled one: resolve into a scratch renderbuffer of the source's own
format, then run the caller's blit from there - single-sample on both sides,
which is exactly where ES does allow the conversion. The scratch buffer is
cached and grown on demand, keyed on the source format and dropped with its ES
context. Only reached on the failure path, so an ordinary blit is untouched.

KHR-GL3{0,1,2,3}.framebuffer_blit is now 3/3 on all four versions; DirectVulkan
(lavapipe) re-verified at 3/3 as well.
2026-08-01 14:37:58 -04:00
BZLZHH 9f3cac6691 [Fix] (DirectGLES): report distinct depth/stencil framebuffers as unsupported
GL only requires framebuffers whose depth and stencil attachments refer to the
same image; anything else may be answered GL_FRAMEBUFFER_UNSUPPORTED, and both
backends' real targets do exactly that - DirectVulkan cannot form two separate
attachments at all, and the ES drivers behind DirectGLES return UNSUPPORTED for
a separate depth renderbuffer plus stencil renderbuffer.

The frontend already knew how to detect the configuration, but only consulted it
for DirectVulkan. On DirectGLES it answered GL_FRAMEBUFFER_COMPLETE for a
framebuffer the driver had rejected, so every clear and draw against it was
silently dropped and the results read back as zeros - which is what
KHR-GL3x.packed_depth_stencil.verify_mixed_attachments saw. (That test
explicitly tolerates GL_FRAMEBUFFER_UNSUPPORTED; what it cannot survive is being
told the framebuffer works.)

Turned into a backend capability rather than a backend-type check, probed once
at init from a scratch framebuffer the same way the format-capability cache is,
so a driver that does support the configuration keeps using it. Defaults to
supported, leaving any backend that does not set it on the permissive path.

Fixes KHR-GL3{2,3}.packed_depth_stencil.verify_mixed_attachments for both
formats; DirectVulkan re-verified unchanged at 23/25 pass + 2 not-supported.
2026-08-01 14:05:28 -04:00
BZLZHH 4c7332d5e6 [Fix] (DirectGLES): broadcast legacy gl_FragColor to every draw buffer
Legacy GLSL's gl_FragColor goes to every enabled draw buffer (GL 4.6 15.2.3),
but ShaderSourceProcessor lowers it to a single mg_FragColor output, which only
ever reaches draw buffer 0. Everything past the first attachment kept its
pre-draw contents.

Replicated across the enabled draw buffers with copies at the end of main.
Gated on the count so the ordinary single-target shader is byte-for-byte what it
was: the pass is a no-op below two draw buffers, and the count comes from the
frontend draw framebuffer at program-sync time (not from the backend framebuffer
sync, which only runs later in PrepareForDraw - a program compiled against a
stale count would not be relinked until the draw after the one that needed it).
It joins the snorm/unorm clamp masks as framebuffer state the shader is compiled
against, with the same relink-on-change check.

Also advertises GL_ARB_explicit_attrib_location and GL_ARB_texture_multisample,
which DirectGLES implements for every version it advertises but only listed for
DirectVulkan. Both are core from GL 3.2/3.3 on, so an app targeting 3.0/3.1
reaches them only through the extension string - without the former the CTS
picks an entirely different draw_buffers shader, and without the latter
KHR-GL31.texture_size_promotion.functional crashed outright.

KHR-GL3{0,1,2,3}.draw_buffers.draw_buffers_1 now passes on all four versions,
and texture_size_promotion.functional on GL31 downgrades from a crash to a
(still open) comparison failure.
2026-08-01 13:59:11 -04:00
BZLZHH 1c5f6c0986 [Chore] (tools/cts): pick a run config the suite does not contradict
Two harness settings were producing failures that say nothing about the backend:

- dEQP's FboRenderContext picks the first entry of its own depth/stencil format
  list, GL_DEPTH32F_STENCIL8, when the config leaves the bit counts DONT_CARE.
  framebuffer_blit meanwhile hardcodes GL_DEPTH24_STENCIL8 for its own buffers
  as soon as it detects an FBO surface, and then blits depth between the two -
  which the spec forbids for mismatched formats, so a conformant driver has no
  choice but to fail it. Default to --deqp-gl-config-name=rgba8888d24s8 so the
  wrapper framebuffer and the test agree.

- --deqp-watchdog aborts the whole process when one case exceeds a hardcoded 30
  seconds (framework/common/tcuApp.hpp). That is not a hang on a CPU rasterizer:
  several texture_swizzle cases take ~17s each standalone and cross the limit
  once the process is warm, which came back as ten spurious Timeouts. dEQP's own
  default is off, and --chunk-timeout is what actually rescues a genuinely
  wedged case, so default it off too and leave it selectable.
2026-08-01 13:48:58 -04:00
BZLZHH 8b75628dec [Fix] (DirectGLES): depth/stencil clear value and readback gaps
Three separate holes, all of them silent, that KHR-GL3x.framebuffer_blit walks
straight into because it clears and reads back depth and stencil directly:

- glClearStencil was frontend-only. The value was recorded in render state and
  never synced, so the real driver kept its default of 0 and every
  glClear(GL_STENCIL_BUFFER_BIT) wrote zeros. glClearColor and glClearDepthf
  were already synced right next to it.

- Stencil readback assumed GL_STENCIL_INDEX works. It is not part of core ES
  (it needs GL_NV_read_stencil) and a driver without it rejects the read
  outright, which left the caller's buffer untouched. Where the attachment is a
  combined depth-stencil buffer the packed GL_DEPTH_STENCIL read carries the
  same bytes in its low octet, so that is now the fallback; the widening to
  GL_UNSIGNED_SHORT/INT moved into the same helper, since even a byte-for-byte
  read needs it.

- Depth readback always went through GL_UNSIGNED_INT. A floating-point depth
  attachment (GL_DEPTH_COMPONENT32F, GL_DEPTH32F_STENCIL8 - the latter is what
  dEQP's own fbo-surface-type wrapper framebuffer picks) rejects that with
  GL_INVALID_OPERATION and only reads back as GL_FLOAT. Try both.

And one format gap behind the same test: GL_DEPTH_COMPONENT32 has no ES
equivalent and was being normalized to the *unsized* GL_DEPTH_COMPONENT base
format, which is not a legal glTexStorage/glRenderbufferStorage internal format
there - the attachment ended up with no storage and the framebuffer read back as
incomplete. GL_DEPTH_COMPONENT32F is the sized ES format that keeps the
requested 32-bit depth footprint; the transfer type follows it to GL_FLOAT.

Takes KHR-GL3x.framebuffer_blit from 0/3 to 2/3 (the remaining
multisampled_to_singlesampled_blit_color_config_test is a separate
single-channel MSAA resolve issue). Note that scissor_blit additionally needs
the suite to run with a depth/stencil config the test agrees with
(--deqp-gl-config-name=rgba8888d24s8): under FBO surfaces the test hardcodes
GL_DEPTH24_STENCIL8 for its own buffers while dEQP's wrapper framebuffer
defaults to GL_DEPTH32F_STENCIL8, and blitting depth between mismatched formats
is a spec error that any conformant driver has to report.
2026-08-01 13:24:30 -04:00
BZLZHH 8269a1786f [Feat] (DirectGLES): emulate GL_TEXTURE_LOD_BIAS in the transpiled ESSL
ES has no per-texture or per-sampler LOD bias at all - GL_TEXTURE_LOD_BIAS is
desktop only, and Vulkan spells it VkSamplerCreateInfo::mipLodBias, which is why
DirectVulkan already honours it. DirectGLES stored the value in sampler state
and then dropped it, so every lookup sampled at the unbiased level of detail.

The bias now reaches the shader as a uniform: a new SPIRV-Cross post-pass
declares one `uniform highp float mg_lodBias_<sampler>;` per mip-capable sampler
and folds it into the level of detail of every lookup that has somewhere to put
it - appended as the bias argument, added to an existing bias, or added to an
explicit textureLod level (Vulkan applies mipLodBias to explicit-LOD fetches too,
and the CTS reference expects the same). texelFetch/textureGather have no bias
by definition, textureGrad offers no argument to fold one into, and the
array-shadow lookups have no bias overload in GLSL at all, so all of those are
left alone. Draws push the bound texture's (or the bound sampler object's, which
overrides it as in GL) value into the uniform, and only when it changed - a
shader whose samplers all have a zero bias issues no extra call at all.

Fixes KHR-GL3{0,2,3}.texture_lod_bias.texture_lod_bias_all.
2026-08-01 13:24:12 -04:00
BZLZHH 3019c68945 [Fix] (MG_Util): glBindBufferBase must not freeze the buffer's size
BindBufferBase_State stored Range1D(0, bufferObject->GetSize()) as the binding
point's range, so the range reflected whatever size the buffer happened to have
at bind time. Binding an empty buffer and giving it storage afterwards is
ordinary application code - glGenBuffers / glBindBufferBase / glBufferData is
exactly the order KHR-GL3{0,2,3}.clip_distance.coverage uses - and the binding
then stayed frozen at [0, 0).

Every backend consumer reads GetRange() as the range the binding actually
covers, so the stale window meant the capture buffer was bound with
glBindBufferRange(..., 0, 0) instead of glBindBufferBase, transform feedback
captured nothing, and the test read back its pre-draw zeros. The same stale
range also under-counted the CPU-side transform feedback capacity accounting.

GL resolves a whole-buffer binding against the object's size at every use;
only glBindBufferRange pins a fixed window, and the binding point already
tracked which of the two it was for the glGetIntegeri_v START/SIZE queries.
GetRange() now resolves the non-explicit case dynamically.

Fixes KHR-GL3{0,2,3}.clip_distance.coverage on Espryt; transform_feedback stays
21/21 on all four versions, and DirectVulkan (lavapipe) re-verified unaffected.
2026-08-01 12:30:46 -04:00
BZLZHH 800142c104 [Fix] (DirectGLES): stop the default texture clobbering an aliased real binding
Desktop GL_TEXTURE_1D/1D_ARRAY have no ES equivalent and are emulated on
GL_TEXTURE_2D/2D_ARRAY, so one native binding serves two frontend slots of the
same texture unit. BindCurrentTextures walked the slots in enum order and let
the last one win, which is wrong as soon as one of an aliased pair holds a real
texture and the other holds the unit's default (name 0) object: the default
would be bound over the real texture and the shader sampled an empty texture,
which GL resolves to opaque black.

The default is only skipped while it has never been given an image, so this
needed nothing more than some earlier test in the same glcts process defining
one on texture name 0 - after which every later case that sampled a 1D texture
returned black. That is the mechanism behind a whole family of failures that
only reproduced when another case ran first: texture_lod_basic.lod_selection,
packed_pixels.varied_rectangle.rgba4_format_bgra, shaders.arrays.{return,
unnamed_parameter}.float_vertex and clip_distance.functional all pass in the
full-suite ordering now.

Resolved with a second pass, mirroring the intent the unbind half of the
function already had ("a default alias must not clear a real binding"): real
textures are placed first, then defaults fill only the native targets nothing
else claimed.
2026-08-01 12:07:23 -04:00
BZLZHH e9382f5329 [Fix] (DirectGLES): exact transform feedback primitive queries
Two leftovers from the capture passthrough, both only observable with a
geometry shader in the pipeline:

- GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN / GL_PRIMITIVES_GENERATED fell back
  to the frontend's CPU accounting, which counts the primitives the draw call
  assembles and so cannot see a geometry stage's amplification. Both are core ES
  query targets (GL_PRIMITIVES_GENERATED from 3.2 on, gated accordingly so an
  older driver doesn't get a stray GL_INVALID_ENUM), so they now go straight to
  the driver's own counters. Generalized the occlusion-query handle's isOcclusion
  flag into the glBeginQuery target it already had to remember for glEndQuery,
  which is what tells the result read to use the core 32-bit getter.

- FixupGsStripCaptureOrder rewrites captured strip triangles from Vulkan's
  (i, i+1, i+2) order into GL's (i+1, i, i+2). A driver-side capture already
  emits GL order, so the rewrite corrupted it - KHR-GL33.transform_feedback
  .geometry read back the odd triangle rotated one vertex. Skipped when the
  backend owns the capture span.

KHR-GL3{0,1,2,3}.transform_feedback is now 21/21 on Espryt; DirectVulkan
(lavapipe) re-verified at 21/21 for the shared frontend change.
2026-08-01 11:57:18 -04:00
BZLZHH df7d1edeca [Feat] (DirectGLES): implement transform feedback capture
Transform feedback was frontend-only on DirectGLES: glBeginTransformFeedback
just flipped MobileGL's own capture state and the real ES driver was never told
to capture anything, so every capture buffer read back as whatever it held
before the draw (zeros for a fresh glBufferData(NULL)). DirectVulkan drives its
capture from its own draw recording, so the shared GLFunctionsTable had no
entries for the span at all.

Capture now runs on the real driver:

- The backend program declares the capture set with glTransformFeedbackVaryings
  before it links. SPIRV-Cross keeps user output names verbatim in the
  transpiled ESSL, so the frontend's requested names carry over unchanged.
- New GLFunctionsTable Begin/EndTransformFeedback entries hand the span
  boundaries to the backend (null for DirectVulkan, which is unaffected).
- The driver-side begin is deferred to the first draw of the span: ES needs the
  capturing program current and the capture buffers bound, and both only become
  true once PrepareForDraw has run. A span that never draws never touches the
  driver, which is what the GL semantics amount to anyway.
- The end mirrors the captured ranges back into the frontend buffer shadows -
  the GPU wrote them behind the frontend's back, so MapBuffer/GetBufferSubData
  would otherwise still return the pre-draw bytes.

Takes KHR-GL32.transform_feedback from 13/21 to 19/21; the two remaining
failures are the geometry-amplified primitive queries, which still go through
the frontend's CPU accounting.
2026-08-01 11:51:19 -04:00
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
97 changed files with 11847 additions and 1075 deletions
+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]
+1 -1
View File
@@ -14,7 +14,7 @@ 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;
+30
View File
@@ -220,6 +220,22 @@ 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);
// Transform feedback capture spans, for backends whose own GL/ES driver
// performs the capture (DirectGLES). Both optional; null means the backend
// drives capture from its draw recording instead (DirectVulkan). End is
// called while the frontend capture state is still active, so the backend
// can still see the capture program and buffer bindings.
void (*BeginTransformFeedback)(GLenum primitiveMode);
void (*EndTransformFeedback)();
Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported
};
struct GlobalBackendFunctionsTable {
@@ -300,7 +316,21 @@ 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;
// Whether a framebuffer whose depth and stencil attachments are distinct
// images can be rendered to. GL only requires support when both refer to the
// same image and lets an implementation answer GL_FRAMEBUFFER_UNSUPPORTED
// otherwise, which is what DirectVulkan (one combined attachment) and the
// real ES drivers behind DirectGLES both do. Defaults to true so a backend
// that never sets it keeps the permissive behaviour.
Bool SupportsDistinctDepthStencilAttachments = true;
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
Uint32 SubgroupSize = 0;
Uint32 SubgroupSupportedStages = 0;
@@ -20,6 +20,7 @@
#include <MG_Util/Texture/TextureFormatProcessor.h>
#include <Config.h>
#include <algorithm>
#include <cmath>
#include <format>
namespace MobileGL::MG_Backend::DirectGLES {
@@ -209,6 +210,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (options & PixelFormatNormalizeOptionBit::NoDepthComponent32) {
reasons.push_back("GL_DEPTH_COMPONENT32 native probe failed on OpenGL ES");
}
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
reasons.push_back("no three-channel multisample storage format on OpenGL ES");
}
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
reasons.push_back("EXT_render_snorm not supported");
}
String reason;
for (SizeT i = 0; i < reasons.size(); ++i) {
@@ -356,6 +363,42 @@ namespace MobileGL::MG_Backend::DirectGLES {
return complete;
}
// Whether the driver renders to a framebuffer whose depth and stencil come from
// two different renderbuffers. GL only requires support when both attachments are
// the same image, and ES drivers commonly answer GL_FRAMEBUFFER_UNSUPPORTED here;
// reporting COMPLETE from the frontend and then rendering into a framebuffer the
// driver refuses leaves the results silently empty.
Bool ProbeDistinctDepthStencilAttachments(const MG_External::GLESFunctionsTable& gl) {
if (!gl.glGenFramebuffers || !gl.glBindFramebuffer || !gl.glFramebufferRenderbuffer ||
!gl.glCheckFramebufferStatus || !gl.glDeleteFramebuffers || !gl.glGenRenderbuffers ||
!gl.glBindRenderbuffer || !gl.glRenderbufferStorage || !gl.glDeleteRenderbuffers) {
return true;
}
GLint prevFramebuffer = 0, prevRenderbuffer = 0;
gl.glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFramebuffer);
gl.glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer);
GLuint framebuffer = 0;
GLuint renderbuffers[2] = {0, 0};
gl.glGenFramebuffers(1, &framebuffer);
gl.glGenRenderbuffers(2, renderbuffers);
gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[0]);
gl.glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, 4, 4);
gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffers[1]);
gl.glRenderbufferStorage(GL_RENDERBUFFER, GL_STENCIL_INDEX8, 4, 4);
gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, renderbuffers[0]);
gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, renderbuffers[1]);
const Bool supported = gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
gl.glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(prevFramebuffer));
gl.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(prevRenderbuffer));
gl.glDeleteFramebuffers(1, &framebuffer);
gl.glDeleteRenderbuffers(2, renderbuffers);
return supported;
}
Bool ProbeFramebufferCompletenessForRenderbuffer(const MG_External::GLESFunctionsTable& gl,
GLuint renderbuffer,
TextureInternalFormat format) {
@@ -519,20 +562,50 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const GLESProbeFormatInfo nativeInfo = BuildNativeProbeFormatInfo(requestedInternalFormat);
GLESProbeFormatInfo fallbackInfo;
const Bool hasForcedFallback =
BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedOptions, true, fallbackInfo);
if (!hasForcedFallback) {
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions, false, fallbackInfo);
GLESProbeFormatInfo outerFallbackInfo;
const Bool outerHasForcedFallback =
BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedOptions, true, outerFallbackInfo);
if (!outerHasForcedFallback) {
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions, false, outerFallbackInfo);
}
for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) {
const auto target = static_cast<TextureTarget>(targetIndex);
// A multisample texture can only ever be rendered into, so its storage format
// has to stay colour-renderable; the ordinary fallback for a three-channel
// format is a three-channel one, which ES accepts as a texture but rejects as
// multisample storage. Recompute the fallback per target so those formats get
// widened here and nowhere else.
Flags<PixelFormatNormalizeOptionBit> targetOptions;
if (IsGLESProbeMultisampleTarget(target)) {
targetOptions |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) {
targetOptions |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
}
}
GLESProbeFormatInfo fallbackInfo = outerFallbackInfo;
Bool hasForcedFallback = outerHasForcedFallback;
if (targetOptions) {
hasForcedFallback = BuildFallbackProbeFormatInfo(
requestedInternalFormat, forcedOptions | targetOptions, true, fallbackInfo);
if (!hasForcedFallback) {
BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions | targetOptions,
false, fallbackInfo);
}
}
// 1D, 1D-array and rectangle textures live on an ES target (see
// TextureImpl::MapToBackendTextureTarget), so they have to be probed there too -
// probing the desktop-only target itself always failed, which left those slots
// of the cache empty and stopped any fallback format from being selected for
// them (a GL_DEPTH_COMPONENT32 1D texture then got no storage at all).
const TextureTarget probeTarget = TextureImpl::MapToBackendTextureTarget(target);
Bool shouldProbeFallback = hasForcedFallback;
if (!hasForcedFallback) {
Bool nativeRenderable = false;
const Bool nativeCreated =
ProbeTexture(gl, target, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
ProbeTexture(gl, probeTarget, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
nativeInfo.ImageType, logicalFormat, &nativeRenderable);
if (nativeCreated) {
AddFullFormatCaps(cache, targetIndex, formatIndex,
@@ -547,7 +620,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (shouldProbeFallback && fallbackInfo.InternalFormat != GL_UNKNOWN_MGL) {
Bool fallbackRenderable = false;
const Bool fallbackCreated =
ProbeTexture(gl, target, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
ProbeTexture(gl, probeTarget, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
fallbackInfo.ImageType, logicalFormat, &fallbackRenderable);
if (fallbackCreated) {
if (AddCaveatFormatCaps(cache, targetIndex, formatIndex,
@@ -563,8 +636,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex();
Bool shouldProbeFallbackRenderbuffer = hasForcedFallback;
if (!hasForcedFallback) {
Bool shouldProbeFallbackRenderbuffer = outerHasForcedFallback;
if (!outerHasForcedFallback) {
const Bool nativeRenderbufferComplete =
ProbeRenderbuffer(gl, nativeInfo.InternalFormat, logicalFormat, false, 1);
if (nativeRenderbufferComplete) {
@@ -578,16 +651,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
shouldProbeFallbackRenderbuffer = true;
}
}
if (shouldProbeFallbackRenderbuffer && fallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
ProbeRenderbuffer(gl, fallbackInfo.InternalFormat, logicalFormat, false, 1)) {
if (shouldProbeFallbackRenderbuffer && outerFallbackInfo.InternalFormat != GL_UNKNOWN_MGL &&
ProbeRenderbuffer(gl, outerFallbackInfo.InternalFormat, logicalFormat, false, 1)) {
if (AddCaveatFormatCaps(cache, renderbufferTargetIndex, formatIndex,
GetRenderbufferFeatureCaps(logicalFormat))) {
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, fallbackInfo);
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, outerFallbackInfo);
}
const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, fallbackInfo.ImageFormat);
GetGLESFormatMaxSamples(capabilities, logicalFormat, outerFallbackInfo.ImageFormat);
cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
ProbeRenderbufferSampleCounts(gl, fallbackInfo.InternalFormat, logicalFormat, maxSamples);
ProbeRenderbufferSampleCounts(gl, outerFallbackInfo.InternalFormat, logicalFormat, maxSamples);
}
}
}
@@ -603,7 +676,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.
@@ -831,6 +904,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
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,
// Both are core from GL 3.2/3.3 on and implemented here for
// every advertised version, but an app targeting 3.0/3.1
// only reaches them through the extension string - the CTS
// picks a whole different shader for draw_buffers without
// explicit_attrib_location. DirectVulkan advertises both.
E_GL_ARB_explicit_attrib_location, E_GL_ARB_texture_multisample,
E_GL_ARB_shader_image_size};
// Only advertised when the device driver actually has usable timer queries
// (GL_EXT_disjoint_timer_query plus its entry points) and the
@@ -934,11 +1013,25 @@ 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;
// Real driver primitive counters: the frontend's CPU accounting cannot see a
// geometry shader's amplification.
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
// Transform feedback is captured by the real ES driver rather than
// reconstructed from the draw recording, so the frontend has to hand the
// span boundaries over.
funcsTable.GL.BeginTransformFeedback = XfbImpl::BeginTransformFeedback;
funcsTable.GL.EndTransformFeedback = XfbImpl::EndTransformFeedback;
funcsTableInitialized = true;
}
return funcsTable;
@@ -1025,6 +1118,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
clampStageImageUniforms(m_GLESCapabilities.MaxFragmentImageUniforms);
m_dynamicParameters.MaxComputeImageUniforms =
clampStageImageUniforms(m_GLESCapabilities.MaxComputeImageUniforms);
m_dynamicParameters.SupportsDistinctDepthStencilAttachments =
ProbeDistinctDepthStencilAttachments(DirectGLES::g_GLESFuncs);
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
@@ -1034,6 +1129,24 @@ 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;
File diff suppressed because it is too large Load Diff
@@ -130,6 +130,16 @@ 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);
// GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN / GL_PRIMITIVES_GENERATED, also core ES
// (GL_PRIMITIVES_GENERATED from ES 3.2 on). Null when the target is unavailable, in
// which case the frontend falls back to counting primitives from the draw calls.
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
void EndXfbPrimitivesQuery(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
@@ -154,6 +164,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
void DestroyEGLContext();
// Transform feedback capture spans, performed by the real ES driver. The
// capture set is declared on the backend program at link time; the driver-side
// begin is deferred to the first draw of the span (ES needs the capturing
// program current and the capture buffers bound), and the end also mirrors the
// captured bytes back into the frontend buffer shadows.
namespace XfbImpl {
Bool AreTransformFeedbacksSupported();
void BeginTransformFeedback(GLenum primitiveMode);
void EndTransformFeedback();
void OnBackendContextDestroyed();
} // namespace XfbImpl
extern MG_External::EGLFunctionsTable g_EGLFuncs;
extern MG_External::GLESFunctionsTable g_GLESFuncs;
extern MG_External::GLESCapabilities g_GLESCapabilities;
+92 -5
View File
@@ -2405,7 +2405,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
});
const auto& swizzleParams = stateTextureObject->GetAllSwizzleParams();
// A three-channel format widened to four for a multisample target (see
// NormalizePixelFormat) gains an alpha channel the frontend format does not have, and
// whatever the draw that filled it wrote there is not what GL would report: a format
// without alpha reads back as 1.0. Answer the ALPHA swizzle source with ONE so the
// promotion stays invisible, composed with the swizzle the application asked for.
Vec4<TextureSwizzleParam> swizzleParams = stateTextureObject->GetAllSwizzleParams();
if (TextureImpl::BackendTextureFormatAddsAlpha(stateTextureObject->GetFormat(), targetInternal)) {
for (SizeT channel = 0; channel < 4; ++channel) {
if (swizzleParams[channel] == TextureSwizzleParam::Alpha) {
swizzleParams[channel] = TextureSwizzleParam::One;
}
}
}
if (swizzleParams != m_cacheSwizzleParams) {
#define SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(func, glEnum) \
if (m_cacheSwizzleParams.func != swizzleParams.func) { \
@@ -2679,6 +2691,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false;
}
Bool IsFixedPointFallbackReadAttachment() {
const auto& readFBO =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
if (!readFBO) {
return false;
}
const auto readBuffer = readFBO->GetReadBuffer();
if (readBuffer < FramebufferAttachmentType::Color0 || readBuffer > FramebufferAttachmentType::Color31) {
return false;
}
// Any signed-normalized attachment, not just the ones currently substituted:
// ES has no GL_CLAMP_READ_COLOR at all, so even a natively stored SNORM buffer
// hands back the negative half that desktop GL clamps away.
const auto& attachmentObject = readFBO->GetAttachment(readBuffer);
if (attachmentObject.IsTexture()) {
const auto& textureObject = attachmentObject.GetTexture();
return textureObject && IsSnormFormat(textureObject->GetFormat());
}
if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
return renderbufferObject && IsSnormFormat(renderbufferObject->GetInternalFormat());
}
return false;
}
void BackendFramebufferObject::SyncReadBufferToBackend(
const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject) {
if (!stateFBOObject) {
@@ -3181,6 +3218,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
namespace PrgramImpl {
Uint32 g_snormFallbackClampOutputMask = 0;
Uint g_fragColorBroadcastCount = 1;
Uint32 g_unormFallbackClampOutputMask = 0;
Uint g_lastUsedBackendProgramId = 0;
StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl> g_backendProgramObjects;
@@ -3232,8 +3270,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("Syncing program to backend. State program ID: %u, Backend ID: %u",
stateProgramObject->GetExternalIndex(), m_backendProgramId);
m_backendProgramUsable = true;
m_snormFallbackClampOutputMask = g_snormFallbackClampOutputMask;
m_unormFallbackClampOutputMask = g_unormFallbackClampOutputMask;
m_fragColorBroadcastCount = g_fragColorBroadcastCount;
// Detach all existing shaders
GLint attachedCount = 0;
@@ -3316,6 +3356,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
effectiveSpirv = &noperspectiveSpirv;
}
// ES has no rectangle sampler, and SPIRV-Cross refuses the whole module rather
// than approximating one. Where every use takes integer texel coordinates a
// rectangle image is indistinguishable from a 2D one, so rewrite the type and let
// it through; the pass declines anything it cannot convert exactly.
Vector<unsigned int> rectLoweredSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImagesForEssl(*effectiveSpirv,
rectLoweredSpirv) &&
!rectLoweredSpirv.empty()) {
effectiveSpirv = &rectLoweredSpirv;
}
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
@@ -3338,6 +3389,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
r.log += spvcSession.GetLastErrorString();
r.errc = -5;
MGLOG_E("%s", r.log.c_str());
m_backendProgramUsable = false;
continue;
}
@@ -3347,6 +3399,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
source = RemoveLayoutBinding(source);
source = ProcessOutColorLocations(source);
source = ForceFlatIntegerVaryings(source, glShaderType);
source = BroadcastLegacyFragColor(std::move(source), glShaderType, m_fragColorBroadcastCount);
source = EmulateTextureLodBias(source);
source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType);
source = PromoteDrawParameterGlobalsToUniforms(std::move(source), glShaderType);
source = ForceSupporterOutput(source);
@@ -3377,6 +3431,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Vector<GLchar> log(logLength);
g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data());
MGLOG_E("Shader compilation failed for backend ID %u: %s", backendShaderId, log.data());
m_backendProgramUsable = false;
continue;
}
@@ -3386,12 +3441,33 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("Processed shader source length: %zu", source.length());
}
// Transform feedback capture runs on the real driver (see XfbImpl in
// DirectGLES.cpp), so the capture set has to be declared on the backend
// program before it links. SPIRV-Cross keeps user output names verbatim in
// the transpiled ESSL (`out vec4 result_0;` stays `result_0`), so the
// frontend's requested names carry over unchanged.
if (stateProgramObject->GetTransformFeedbackVaryingCount() > 0 &&
g_GLESFuncs.glTransformFeedbackVaryings != nullptr) {
const auto& xfbVaryings = stateProgramObject->GetTransformFeedbackVaryings();
Vector<const GLchar*> xfbNames;
xfbNames.reserve(xfbVaryings.size());
for (const auto& xfbVarying : xfbVaryings) {
xfbNames.push_back(xfbVarying.name.c_str());
}
MGLOG_D("Declaring %zu transform feedback varyings on program %u", xfbNames.size(),
m_backendProgramId);
g_GLESFuncs.glTransformFeedbackVaryings(m_backendProgramId, static_cast<GLsizei>(xfbNames.size()),
xfbNames.data(),
stateProgramObject->GetTransformFeedbackBufferMode());
}
// Link program
MGLOG_D("Linking program %u", m_backendProgramId);
g_GLESFuncs.glLinkProgram(m_backendProgramId);
GLint linkStatus;
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_LINK_STATUS, &linkStatus);
m_backendProgramUsable = m_backendProgramUsable && linkStatus == GL_TRUE;
if (linkStatus != GL_TRUE) {
GLint logLength;
g_GLESFuncs.glGetProgramiv(m_backendProgramId, GL_INFO_LOG_LENGTH, &logLength);
@@ -3504,6 +3580,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
binding.backendLocation = backendLoc;
binding.uniformType = uniformType;
binding.lastAssignedUnit = -1;
// Present only for the samplers EmulateTextureLodBias actually rewrote; the
// pass names it after the sampler, which SPIRV-Cross preserves verbatim.
binding.lodBiasLocation =
g_GLESFuncs.glGetUniformLocation(m_backendProgramId, (String(LOD_BIAS_UNIFORM_PREFIX) + name).c_str());
binding.lastAssignedLodBias = 0.0f;
m_samplerUniformBindings.push_back(binding);
}
}
@@ -3512,12 +3593,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (g_lastUsedBackendProgramId == m_backendProgramId) {
// glUseProgram on a program that did not link is an INVALID_OPERATION and
// leaves the *previous* program current, so the draw would silently render
// with an unrelated shader (KHR-GL3x.texture_size_promotion read another
// test case's alpha that way once a sampler2DRect stage failed to
// transpile). Bind nothing instead: the draw is then a visible no-op.
const Uint programToBind = m_backendProgramUsable ? m_backendProgramId : 0;
if (g_lastUsedBackendProgramId == programToBind) {
return;
}
MGLOG_D("Using program %u", m_backendProgramId);
g_GLESFuncs.glUseProgram(m_backendProgramId);
g_lastUsedBackendProgramId = m_backendProgramId;
MGLOG_D("Using program %u", programToBind);
g_GLESFuncs.glUseProgram(programToBind);
g_lastUsedBackendProgramId = programToBind;
}
void BackendProgramObjectImpl::SetBaseInstance(Uint32 baseInstance) const {
+37 -7
View File
@@ -270,18 +270,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
namespace TextureImpl {
inline Bool IsSupportedTextureTarget(TextureTarget target) {
// Rectangle textures need non-normalized sampling ES cannot express; everything else is
// either native or emulated (1D -> 2D with height 1, 1D array -> 2D array, see
// MapToBackendTextureTarget). SPIRV-Cross already emits the matching ESSL samplers and
// coordinate padding for 1D/1D-array shaders.
return target != TextureTarget::TextureRectangle;
// Every desktop-only target is stored on an ES one; see MapToBackendTextureTarget.
(void)target;
return true;
}
// ES has no 1D targets: 1D textures are stored as 2D (height 1) and 1D arrays as 2D arrays
// (height 1, layers in depth). Must match SPIRV-Cross's ES 1D-as-2D shader emulation.
// ES has none of the desktop-only targets: 1D textures are stored as 2D (height 1), 1D
// arrays as 2D arrays (height 1, layers in depth), and rectangle textures as plain 2D -
// they are single-level and already clamp, so only the non-normalized coordinates differ.
// Must match the shader-side emulation: SPIRV-Cross handles 1D/1D-array itself, and
// ShaderCompiler::LowerRectImagesForEssl rewrites rectangle images (declining any module
// whose lookups are not integer-coordinate, which SPIRV-Cross then still rejects).
inline TextureTarget MapToBackendTextureTarget(TextureTarget target) {
switch (target) {
case TextureTarget::Texture1D:
case TextureTarget::TextureRectangle:
return TextureTarget::Texture2D;
case TextureTarget::Texture1DArray:
return TextureTarget::Texture2DArray;
@@ -297,6 +300,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
inline GLenum ConvertTextureUploadTargetToBackendGLEnum(TextureUploadTarget uploadTarget) {
switch (uploadTarget) {
case TextureUploadTarget::Texture1D:
case TextureUploadTarget::TextureRectangle:
return GL_TEXTURE_2D;
case TextureUploadTarget::Texture1DArray:
return GL_TEXTURE_2D_ARRAY;
@@ -438,6 +442,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
g_backendFramebufferObjects;
// True when the read buffer names a fixed-point (norm/snorm) attachment that the
// backend actually stores in a floating-point format. GL clamps a read from a
// fixed-point colour buffer to [0,1] (GL_CLAMP_READ_COLOR defaults to
// GL_FIXED_ONLY); the substituted float storage would not, so the readback path
// has to apply the clamp itself.
Bool IsFixedPointFallbackReadAttachment();
extern Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboBindVersions;
// Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change)
// per target: re-attaching textures or changing draw buffers on an already-bound FBO
@@ -576,6 +587,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
Int backendLocation = -1;
GLenum uniformType = 0;
Int lastAssignedUnit = -1;
// Location of this sampler's emulated GL_TEXTURE_LOD_BIAS uniform
// (PrgramImpl::EmulateTextureLodBias), -1 when the shader has none.
// lastAssignedLodBias mirrors the value the program currently holds,
// so an unbiased shader issues no per-draw glUniform1f at all.
Int lodBiasLocation = -1;
Float lastAssignedLodBias = 0.0f;
};
BackendProgramObjectImpl();
@@ -587,9 +604,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
void SetDrawID(Uint32 drawId) const;
Int GetIndirectParamsBinding() const { return m_indirectParamsBinding; }
Uint GetBackendProgramId() const { return m_backendProgramId; }
// False when the last SyncToBackend could not produce a usable program (a
// shader failed to transpile or compile, or the link itself failed). Use()
// must not leave the previously bound program current in that case.
Bool IsBackendProgramUsable() const { return m_backendProgramUsable; }
Uint GetBackendGlobalUBOId() const { return m_backendGlobalUBOId; }
Uint32 GetSnormFallbackClampOutputMask() const { return m_snormFallbackClampOutputMask; }
Uint32 GetUnormFallbackClampOutputMask() const { return m_unormFallbackClampOutputMask; }
Uint GetFragColorBroadcastCount() const { return m_fragColorBroadcastCount; }
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
@@ -616,7 +638,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
Int m_indirectParamsBinding = -1;
Uint32 m_snormFallbackClampOutputMask = 0;
Uint32 m_unormFallbackClampOutputMask = 0;
// Draw buffers a legacy gl_FragColor write has to reach (see
// PrgramImpl::BroadcastLegacyFragColor); 1 keeps the plain single-output shader.
Uint m_fragColorBroadcastCount = 1;
Bool m_isInitialized = false;
Bool m_backendProgramUsable = false;
Int m_globalUboBackendBlockIndex = -1;
Int m_globalUboBackendBlockSize = 0;
@@ -629,6 +655,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
extern Uint32 g_snormFallbackClampOutputMask;
extern Uint32 g_unormFallbackClampOutputMask;
// Draw buffers the current draw framebuffer enables. Like the clamp masks above it
// is framebuffer state that the shader has to be compiled against, so a program
// whose snapshot no longer matches is relinked.
extern Uint g_fragColorBroadcastCount;
// Backend id of the last glUseProgram issued through this backend; lets Use()
// skip redundant rebinds. Reset to 0 wherever glUseProgram(0) is issued or the
// ES context is recreated.
+260 -6
View File
@@ -22,6 +22,9 @@
#include <MG_Util/Math/SmallFloat.h>
#include <cmath>
#include <cctype>
#include <cstring>
#include <regex>
namespace MobileGL::MG_Backend::DirectGLES {
namespace {
@@ -45,15 +48,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
return options;
}
Flags<PixelFormatNormalizeOptionBit> GetRuntimeFallbackNormalizeOptions(GLenum requestedInternalFormat) {
Flags<PixelFormatNormalizeOptionBit>
GetRuntimeFallbackNormalizeOptions(GLenum requestedInternalFormat,
Flags<PixelFormatNormalizeOptionBit> extraOptions) {
using namespace MG_Util::TextureFormatProcessor;
const Flags<PixelFormatNormalizeOptionBit> forcedOptions =
GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat, GetForcedPixelFormatNormalizeOptions());
const Flags<PixelFormatNormalizeOptionBit> forcedOptions = GetApplicablePixelFormatNormalizeOptions(
requestedInternalFormat, GetForcedPixelFormatNormalizeOptions() | extraOptions);
if (forcedOptions) {
return forcedOptions;
}
return GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat,
GetDriverPixelFormatNormalizeOptions());
return GetApplicablePixelFormatNormalizeOptions(
requestedInternalFormat, GetDriverPixelFormatNormalizeOptions() | extraOptions);
}
// Multisample textures can only ever be rendered into, never uploaded to, so a fallback
// format for them has to stay colour-renderable - a three-channel float fallback is a legal
// ES texture format but not a legal multisample storage format. Widening to four channels
// is safe here precisely because there is no transfer path that would have to expand
// three-channel client data, and the alpha the draw writes for a three-channel source is
// already the 1.0 the frontend format implies.
Bool TargetRequiresRenderableFormat(SizeT targetIndex) {
return targetIndex == static_cast<SizeT>(TextureTarget::Texture2DMultisample) ||
targetIndex == static_cast<SizeT>(TextureTarget::Texture2DMultisampleArray);
}
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(SizeT targetIndex) {
Flags<PixelFormatNormalizeOptionBit> options;
if (!TargetRequiresRenderableFormat(targetIndex)) {
return options;
}
options |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
if (!g_GLESCapabilities.SupportsRenderSnorm || !g_GLESCapabilities.SupportsNorm16Texture) {
options |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
}
return options;
}
Bool HasCachedFormatCapability(TextureInternalFormat internalFormat,
@@ -113,7 +141,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
Flags<PixelFormatNormalizeOptionBit> options;
if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) {
options = GetRuntimeFallbackNormalizeOptions(requestedInternalFormat);
options = GetRuntimeFallbackNormalizeOptions(requestedInternalFormat,
GetRenderTargetNormalizeOptions(targetIndex));
}
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
}
@@ -148,6 +177,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat) {
return ShouldUseCaveatFormat(internalFormat, GetRenderbufferFormatCapabilityTargetIndex());
}
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target) {
const SizeT targetIndex =
target == TextureTarget::Unknown ? kFormatCapabilityTargetCount : GetFormatCapabilityTargetIndex(target);
if (!TargetRequiresRenderableFormat(targetIndex)) {
return false;
}
if (pActiveBackendObject && !ShouldUseCaveatFormat(internalFormat, targetIndex)) {
return false;
}
const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat);
const Flags<PixelFormatNormalizeOptionBit> options =
GetRuntimeFallbackNormalizeOptions(requestedInternalFormat,
GetRenderTargetNormalizeOptions(targetIndex));
return static_cast<Bool>(options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget);
}
} // namespace TextureImpl
namespace PrgramImpl {
String ProcessOutColorLocations(const String& glslCode) {
@@ -276,6 +321,55 @@ namespace MobileGL::MG_Backend::DirectGLES {
return glslCode;
}
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// The name is the marker: ShaderSourceProcessor only emits it when the source
// wrote gl_FragColor, and such a shader can have no other output.
static const char* const kLoweredName = "mg_FragColor";
if (shaderType != GL_FRAGMENT_SHADER || drawBufferCount <= 1) {
return glslCode;
}
static const std::regex declRegex(
R"(layout\s*\(\s*location\s*=\s*0\s*\)\s*out\s+((?:lowp|mediump|highp)\s+)?vec4\s+mg_FragColor\s*;)");
std::smatch declMatch;
if (!std::regex_search(glslCode, declMatch, declRegex)) {
return glslCode;
}
const String precision = declMatch[1].matched ? declMatch[1].str() : String();
String replicaDecls;
String replicaCopies;
for (Uint location = 1; location < drawBufferCount; ++location) {
const String name = String(kLoweredName) + "_" + std::to_string(location);
replicaDecls += "\nlayout(location = " + std::to_string(location) + ") out " + precision + "vec4 " +
name + ";";
replicaCopies += "\n " + name + " = " + kLoweredName + ";";
}
static const std::regex mainRegex(R"(void\s+main\s*\([^)]*\)\s*\{)");
std::smatch mainMatch;
if (!std::regex_search(glslCode, mainMatch, mainRegex)) {
return glslCode;
}
SizeT bracePos = static_cast<SizeT>(mainMatch.position(0) + mainMatch.length(0) - 1);
Int depth = 0;
for (SizeT pos = bracePos; pos < glslCode.size(); ++pos) {
if (glslCode[pos] == '{') {
++depth;
} else if (glslCode[pos] == '}') {
--depth;
if (depth == 0) {
glslCode.insert(pos, replicaCopies + "\n");
break;
}
}
}
glslCode.insert(static_cast<SizeT>(declMatch.position(0)) + declMatch[0].str().size(), replicaDecls);
return glslCode;
}
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
@@ -342,6 +436,166 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
return result;
}
namespace {
// How a lookup carries its level of detail, and how many arguments it takes
// before the optional bias.
struct LodLookupForm {
const char* name;
Int requiredArgs; // arguments before the optional bias (implicit form)
Int explicitLodArg; // index of the explicit LOD argument, -1 for implicit
};
// texelFetch* is deliberately absent: an integer fetch names its level directly
// and takes no LOD bias. textureGather has no bias either. textureGrad* derives
// the LOD from gradients and offers no argument to fold a bias into, so it is
// left alone rather than rewritten incorrectly.
constexpr LodLookupForm LOD_LOOKUP_FORMS[] = {
{"textureProjLodOffset", 0, 2}, {"textureProjOffset", 4, -1}, {"textureProjLod", 0, 2},
{"textureLodOffset", 0, 2}, {"textureOffset", 3, -1}, {"textureProj", 2, -1},
{"textureLod", 0, 2}, {"texture", 2, -1},
};
// Sampler types with no mip chain, or whose GLSL lookups have no bias overload
// at all (the array-shadow forms), so nothing can or should be folded in.
Bool IsBiasableSamplerType(const String& samplerType) {
if (samplerType.find("MS") != String::npos) return false; // multisample
if (samplerType.find("Buffer") != String::npos) return false; // texture buffer
if (samplerType.find("Rect") != String::npos) return false; // rectangle: no mips
if (samplerType == "sampler2DArrayShadow") return false;
if (samplerType == "samplerCubeArrayShadow") return false;
return true;
}
Bool IsIdentifierChar(char c) { return std::isalnum(static_cast<unsigned char>(c)) || c == '_'; }
// Byte offsets of the top-level argument separators and of the closing paren,
// starting from the '(' at openParen. Empty when the parentheses do not balance.
Vector<SizeT> SplitCallArguments(const String& code, SizeT openParen) {
Vector<SizeT> marks;
Int depth = 0;
for (SizeT i = openParen; i < code.size(); ++i) {
const char c = code[i];
if (c == '(' || c == '[') {
++depth;
} else if (c == ']') {
--depth;
} else if (c == ')') {
--depth;
if (depth == 0) {
marks.push_back(i);
return marks;
}
} else if (c == ',' && depth == 1) {
marks.push_back(i);
}
}
return {};
}
} // namespace
String EmulateTextureLodBias(const String& glslCode) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (glslCode.find("sampler") == String::npos || glslCode.find("texture") == String::npos) {
return glslCode;
}
// Collect the mip-capable sampler uniforms this shader declares.
static const std::regex samplerDeclRegex(
R"(uniform\s+(?:(?:highp|mediump|lowp)\s+)?([iu]?sampler[A-Za-z0-9]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*;)");
UnorderedMap<String, String> samplerNames; // name -> bias uniform name
for (std::sregex_iterator it(glslCode.begin(), glslCode.end(), samplerDeclRegex), end; it != end; ++it) {
const String samplerType = (*it)[1].str();
if (!IsBiasableSamplerType(samplerType)) continue;
const String name = (*it)[2].str();
samplerNames.emplace(name, String(LOD_BIAS_UNIFORM_PREFIX) + name);
}
if (samplerNames.empty()) {
return glslCode;
}
// Rewrite the lookups. Right-to-left so earlier offsets stay valid, and only for
// samplers named directly as the first argument (SPIRV-Cross never produces an
// expression there for ES output, which has no separate sampler objects).
String result = glslCode;
Vector<String> usedSamplers;
for (SizeT scan = result.size(); scan-- > 0;) {
if (result[scan] != 't') continue;
if (scan > 0 && IsIdentifierChar(result[scan - 1])) continue;
const LodLookupForm* form = nullptr;
SizeT openParen = 0;
for (const auto& candidate : LOD_LOOKUP_FORMS) {
const SizeT nameLength = std::strlen(candidate.name);
if (result.compare(scan, nameLength, candidate.name) != 0) continue;
SizeT after = result.find_first_not_of(" \t", scan + nameLength);
if (after == String::npos || result[after] != '(') continue;
form = &candidate;
openParen = after;
break;
}
if (form == nullptr) continue;
const Vector<SizeT> marks = SplitCallArguments(result, openParen);
if (marks.empty()) continue;
const SizeT argCount = marks.size();
const SizeT closeParen = marks.back();
// First argument must be one of our samplers.
const SizeT firstArgStart = result.find_first_not_of(" \t", openParen + 1);
SizeT firstArgEnd = marks.front();
while (firstArgEnd > firstArgStart && (result[firstArgEnd - 1] == ' ' || result[firstArgEnd - 1] == '\t')) {
--firstArgEnd;
}
if (firstArgStart == String::npos || firstArgEnd <= firstArgStart) continue;
const String samplerName = result.substr(firstArgStart, firstArgEnd - firstArgStart);
const auto samplerIt = samplerNames.find(samplerName);
if (samplerIt == samplerNames.end()) continue;
const String& biasName = samplerIt->second;
if (form->explicitLodArg >= 0) {
// Explicit LOD: the bias adds to it, as Vulkan does for
// OpImageSampleExplicitLod and as the CTS reference expects.
const SizeT lodIndex = static_cast<SizeT>(form->explicitLodArg);
if (argCount <= lodIndex) continue;
const SizeT lodStart = marks[lodIndex - 1] + 1;
const SizeT lodEnd = marks[lodIndex];
result.insert(lodEnd, String(") + ") + biasName + ")");
result.insert(lodStart, "((");
} else {
const SizeT required = static_cast<SizeT>(form->requiredArgs);
if (argCount == required) {
result.insert(closeParen, String(", ") + biasName);
} else if (argCount == required + 1) {
const SizeT biasStart = marks[argCount - 2] + 1;
result.insert(closeParen, String(") + ") + biasName + ")");
result.insert(biasStart, "((");
} else {
continue;
}
}
usedSamplers.push_back(samplerName);
}
if (usedSamplers.empty()) {
return glslCode;
}
// Declare the bias uniforms that were actually referenced, right after the
// sampler declaration line they belong to.
for (const auto& samplerName : usedSamplers) {
const String& biasName = samplerNames[samplerName];
if (result.find(String("float ") + biasName + ";") != String::npos) continue;
const std::regex declRegex(
R"(uniform\s+(?:(?:highp|mediump|lowp)\s+)?[iu]?sampler[A-Za-z0-9]*\s+)" + samplerName + R"(\s*;)");
std::smatch match;
if (!std::regex_search(result, match, declRegex)) continue;
const SizeT declEnd = static_cast<SizeT>(match.position(0)) + match[0].str().size();
result.insert(declEnd, String("\nuniform highp float ") + biasName + ";");
}
return result;
}
} // namespace PrgramImpl
namespace Utils {
+24
View File
@@ -40,6 +40,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
void GenerateRenderbufferFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType);
Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target);
// True when the format the texture is actually created with has an alpha channel the
// frontend format does not (the three-channel multisample widening). GL reads such a
// channel back as 1.0, so any swizzle source of ALPHA has to be answered with ONE.
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
} // namespace TextureImpl
@@ -104,7 +109,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
String ClampNormFallbackOutputs(String glslCode, GLenum shaderType, Uint32 snormOutputMask,
Uint32 unormOutputMask);
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
// Legacy GLSL's gl_FragColor is broadcast to every enabled draw buffer (GL 4.6
// 15.2.3), but ShaderSourceProcessor lowers it to the single output mg_FragColor,
// which only ever reaches draw buffer 0. Replicates it across `drawBufferCount`
// outputs and copies the value into them at the end of main. A no-op for
// drawBufferCount <= 1, i.e. for everything but a framebuffer that actually
// enables several draw buffers, so the ordinary single-target shader is untouched.
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount);
String RemoveLayoutBinding(const String& glslCode);
// Prefix of the per-sampler float uniform that carries GL_TEXTURE_LOD_BIAS into
// the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
// ES has no per-texture/sampler LOD bias at all (GL_TEXTURE_LOD_BIAS is desktop
// only; Vulkan spells it VkSamplerCreateInfo::mipLodBias), so it has to reach the
// shader as a uniform and be folded into every lookup's level of detail. Declares
// one `uniform highp float mg_lodBias_<sampler>;` per mip-capable sampler and adds
// it to the bias / explicit-LOD argument of every lookup that takes one. Draws push
// the bound texture's (or sampler object's) value into it; a shader whose samplers
// all have a zero bias is therefore unaffected. Returns the source unchanged when
// there is nothing to rewrite.
String EmulateTextureLodBias(const String& glslCode);
} // namespace PrgramImpl
namespace Utils {
@@ -18,6 +18,7 @@
#include "MG_Util/Texture/TextureFormatProcessor.h"
#include <Config.h>
#include <cmath>
#include <cstdlib>
#include <cstring>
@@ -525,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);
}
@@ -634,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;
@@ -796,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);
@@ -1250,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;
@@ -1266,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;
@@ -1334,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;
@@ -1483,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
@@ -1574,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
@@ -1606,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
@@ -123,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) {
@@ -202,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;
@@ -276,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;
@@ -289,10 +340,20 @@ 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;
}
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});
@@ -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};
};
@@ -52,10 +54,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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),
// appended in submit order; freed once their submission is known
@@ -77,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,
@@ -91,7 +109,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// can restart while the submitted buffer is still executing. Retired
// buffers are freed after the slot's fence is next waited, or as soon
// as their submission is observed complete.
VkResult RetireCurrentCommandBuffer();
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
@@ -12,10 +12,7 @@
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
#include "MG_Util/ShaderTranspiler/Types.h"
#include <cmath>
#include <cstdio>
#include <cstring>
#include <unordered_set>
#include <spirv-tools/libspirv.h>
#include <spirv-tools/optimizer.hpp>
#include <source/opt/build_module.h>
@@ -926,6 +923,163 @@ 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
@@ -1102,374 +1256,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return spvtools::Optimizer::PassToken(MakeUnique<ForceExplicitLod0SamplePass>());
}
// TEMP-PERFDIAG: measure what fragment-stage fp32 costs on this GPU. Desktop GLSL carries
// no precision qualifiers, so everything reaches the driver as full fp32 while Adreno runs
// fp16 at twice the rate. Decorating every float-typed result in a fragment entry point
// with RelaxedPrecision is the blunt "all mediump" upper bound - it changes results, so it
// is a probe, not a shipping transform. Toggled by /sdcard/MG/exp_relaxed_precision.
class RelaxedPrecisionProbePass final : public spvtools::opt::Pass {
public:
const char* name() const override { return "relaxed-precision-probe"; }
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;
// Every 32-bit-float scalar/vector/matrix type in the module. Anything wider (f64)
// or narrower is left alone: RelaxedPrecision only has meaning for 32-bit floats.
std::unordered_set<Uint32> relaxableTypes;
for (auto& type : get_module()->types_values()) {
const Uint32 typeId = type.result_id();
if (typeId == 0) continue;
switch (type.opcode()) {
case spv::Op::OpTypeFloat:
if (type.GetSingleWordInOperand(0) == 32) relaxableTypes.insert(typeId);
break;
case spv::Op::OpTypeVector:
case spv::Op::OpTypeMatrix:
if (relaxableTypes.count(type.GetSingleWordInOperand(0)) != 0) {
relaxableTypes.insert(typeId);
}
break;
default:
break;
}
}
if (relaxableTypes.empty()) return Status::SuccessWithoutChange;
Vector<Uint32> targets;
for (auto& function : *get_module()) {
for (auto& block : function) {
for (auto& inst : block) {
const Uint32 resultId = inst.result_id();
if (resultId == 0) continue;
if (relaxableTypes.count(inst.type_id()) == 0) continue;
targets.push_back(resultId);
}
}
}
if (targets.empty()) return Status::SuccessWithoutChange;
for (const Uint32 id : targets) {
context()->get_decoration_mgr()->AddDecoration(
id, static_cast<Uint32>(spv::Decoration::RelaxedPrecision));
}
context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
};
// Relax fragment-stage arithmetic that provably came out of a texture read. Desktop GLSL
// has no precision qualifiers, so every fragment value reaches the driver as fp32 while
// Adreno runs fp16 at twice the rate - and a texel is at most 8 bits per channel, which
// fp16's 11-bit mantissa carries exactly. Seeding at image reads and propagating only
// through operations whose every input is already relaxed keeps everything the shader
// computes from other sources (screen coordinates, depth, wide-range uniforms) at full
// precision, which is where fp16 would actually go wrong: fp16 cannot even represent a
// 3044-pixel gl_FragCoord.x exactly.
class RelaxTextureDerivedPrecisionPass final : public spvtools::opt::Pass {
public:
const char* name() const override { return "relax-texture-derived-precision"; }
Status Process() override {
if (!IsFragmentEntryPoint()) return Status::SuccessWithoutChange;
// A shader that drives depth or coverage itself is out of scope: those values must
// stay exact, and proving which computations feed them is not worth it here.
if (WritesDepthOrSampleMask()) return Status::SuccessWithoutChange;
CollectRelaxableFloatTypes();
if (m_relaxableTypes.empty()) return Status::SuccessWithoutChange;
// Whitelisting from texture reads captures nothing in practice: MC's fragment
// shaders multiply every texel by an interpolated colour and a UBO value, so one
// un-relaxed operand vetoes the whole expression (measured: no fps change).
// Taint the few genuinely precision-critical sources instead and relax the rest.
std::unordered_set<Uint32> tainted;
CollectPrecisionCriticalSeeds(tainted);
Bool grew = true;
while (grew) {
grew = false;
for (auto& function : *get_module()) {
for (auto& block : function) {
for (auto& inst : block) {
const Uint32 resultId = inst.result_id();
if (resultId == 0 || tainted.count(resultId) != 0) continue;
if (!AnyOperandTainted(inst, tainted)) continue;
tainted.insert(resultId);
grew = true;
}
}
}
}
std::unordered_set<Uint32> relaxed;
for (auto& function : *get_module()) {
for (auto& block : function) {
for (auto& inst : block) {
const Uint32 resultId = inst.result_id();
if (resultId == 0 || tainted.count(resultId) != 0) continue;
if (m_relaxableTypes.count(inst.type_id()) == 0) continue;
relaxed.insert(resultId);
}
}
}
if (relaxed.empty()) return Status::SuccessWithoutChange;
for (const Uint32 id : relaxed) {
context()->get_decoration_mgr()->AddDecoration(
id, static_cast<Uint32>(spv::Decoration::RelaxedPrecision));
}
context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
private:
std::unordered_set<Uint32> m_relaxableTypes;
Bool IsFragmentEntryPoint() const {
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) {
return true;
}
}
return false;
}
Bool WritesDepthOrSampleMask() const {
for (auto& annotation : get_module()->annotations()) {
if (annotation.opcode() != spv::Op::OpDecorate) continue;
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
spv::Decoration::BuiltIn) {
continue;
}
const auto builtIn = static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2));
if (builtIn == spv::BuiltIn::FragDepth || builtIn == spv::BuiltIn::SampleMask) {
return true;
}
}
return false;
}
void CollectRelaxableFloatTypes() {
m_relaxableTypes.clear();
for (auto& type : get_module()->types_values()) {
const Uint32 typeId = type.result_id();
if (typeId == 0) continue;
switch (type.opcode()) {
case spv::Op::OpTypeFloat:
if (type.GetSingleWordInOperand(0) == 32) m_relaxableTypes.insert(typeId);
break;
case spv::Op::OpTypeVector:
if (m_relaxableTypes.count(type.GetSingleWordInOperand(0)) != 0) {
m_relaxableTypes.insert(typeId);
}
break;
default:
break;
}
}
}
void CollectImageReadSeeds(std::unordered_set<Uint32>& relaxed) const {
for (auto& function : *get_module()) {
for (auto& block : function) {
for (auto& inst : block) {
const Uint32 resultId = inst.result_id();
if (resultId == 0 || m_relaxableTypes.count(inst.type_id()) == 0) continue;
// Interpolated user varyings seed too, or propagation dies at the
// first `texel * vertexColour`: the load of an Input can never be
// relaxed by the rule below (its operand is a pointer), so a single
// varying vetoes every downstream operation. This is what ESSL's
// mediump varyings already mean. Built-ins are excluded - gl_FragCoord
// carries pixel coordinates that fp16 cannot represent exactly.
if (inst.opcode() == spv::Op::OpLoad && IsNonBuiltInFragmentInput(inst)) {
relaxed.insert(resultId);
continue;
}
switch (inst.opcode()) {
case spv::Op::OpImageSampleImplicitLod:
case spv::Op::OpImageSampleExplicitLod:
case spv::Op::OpImageSampleProjImplicitLod:
case spv::Op::OpImageSampleProjExplicitLod:
case spv::Op::OpImageSampleDrefImplicitLod:
case spv::Op::OpImageSampleDrefExplicitLod:
case spv::Op::OpImageFetch:
case spv::Op::OpImageRead:
case spv::Op::OpImageGather:
relaxed.insert(resultId);
break;
default:
break;
}
}
}
}
}
// OpLoad straight out of a fragment Input variable that carries no BuiltIn decoration.
// Only a direct load counts: a load through an access chain could be indexing a
// structure whose other members are not interpolated colour data.
Bool IsNonBuiltInFragmentInput(const spvtools::opt::Instruction& load) const {
const Uint32 pointerId = load.GetSingleWordInOperand(0);
const auto* pointer = context()->get_def_use_mgr()->GetDef(pointerId);
if (pointer == nullptr || pointer->opcode() != spv::Op::OpVariable) return false;
if (static_cast<spv::StorageClass>(pointer->GetSingleWordInOperand(0)) !=
spv::StorageClass::Input) {
return false;
}
Bool isBuiltIn = false;
context()->get_decoration_mgr()->ForEachDecoration(
pointerId, static_cast<Uint32>(spv::Decoration::BuiltIn),
[&isBuiltIn](const spvtools::opt::Instruction&) { isBuiltIn = true; });
return !isBuiltIn;
}
// A float constant small enough that fp16 represents it without surprise. Colour math
// constants (0, 1, 0.5, 255, gamma exponents) all live here; anything larger is
// treated as unknown so it stops propagation.
Bool IsBoundedFloatConstant(Uint32 id) const {
const auto* constant = context()->get_constant_mgr()->FindDeclaredConstant(id);
if (constant == nullptr) return false;
if (const auto* scalar = constant->AsFloatConstant()) {
const float value = scalar->GetFloat();
return std::isfinite(value) && std::fabs(value) <= 1024.0f;
}
if (const auto* composite = constant->AsVectorConstant()) {
for (const auto* component : composite->GetComponents()) {
const auto* scalar = component->AsFloatConstant();
if (scalar == nullptr) return false;
const float value = scalar->GetFloat();
if (!std::isfinite(value) || std::fabs(value) > 1024.0f) return false;
}
return true;
}
return false;
}
// Precision-critical sources: a built-in fragment input. gl_FragCoord is the one that
// matters - fp16 cannot represent a 3044-pixel x coordinate exactly, and anything
// derived from it (screen-space effects, manual depth reconstruction) would visibly
// quantise. Everything else a fragment shader reads is colour-range data.
void CollectPrecisionCriticalSeeds(std::unordered_set<Uint32>& tainted) const {
for (auto& function : *get_module()) {
for (auto& block : function) {
for (auto& inst : block) {
if (inst.opcode() != spv::Op::OpLoad || inst.result_id() == 0) continue;
if (IsBuiltInInputLoad(inst)) tainted.insert(inst.result_id());
}
}
}
}
Bool IsBuiltInInputLoad(const spvtools::opt::Instruction& load) const {
const Uint32 pointerId = load.GetSingleWordInOperand(0);
const auto* pointer = context()->get_def_use_mgr()->GetDef(pointerId);
if (pointer == nullptr || pointer->opcode() != spv::Op::OpVariable) return false;
if (static_cast<spv::StorageClass>(pointer->GetSingleWordInOperand(0)) !=
spv::StorageClass::Input) {
return false;
}
Bool isBuiltIn = false;
context()->get_decoration_mgr()->ForEachDecoration(
pointerId, static_cast<Uint32>(spv::Decoration::BuiltIn),
[&isBuiltIn](const spvtools::opt::Instruction&) { isBuiltIn = true; });
return isBuiltIn;
}
Bool AnyOperandTainted(const spvtools::opt::Instruction& inst,
const std::unordered_set<Uint32>& tainted) const {
const Uint32 operandCount = inst.NumInOperands();
for (Uint32 i = 0; i < operandCount; ++i) {
const auto& operand = inst.GetInOperand(i);
if (!spvIsIdType(operand.type)) continue;
if (IsNonNumericOperand(inst, i)) continue;
if (tainted.count(operand.words[0]) != 0) return true;
}
return false;
}
Bool AllValueOperandsRelaxed(const spvtools::opt::Instruction& inst,
const std::unordered_set<Uint32>& relaxed) const {
switch (inst.opcode()) {
// Pointer-typed plumbing: relaxing the loaded value would say nothing about the
// memory it came from, and the pointer operand can never be in the set.
case spv::Op::OpLoad:
case spv::Op::OpStore:
case spv::Op::OpAccessChain:
case spv::Op::OpInBoundsAccessChain:
case spv::Op::OpFunctionCall:
return false;
default:
break;
}
Bool sawValueOperand = false;
Bool allRelaxed = true;
const Uint32 operandCount = inst.NumInOperands();
for (Uint32 i = 0; i < operandCount; ++i) {
const auto& operand = inst.GetInOperand(i);
if (!spvIsIdType(operand.type)) continue; // literals: selectors, swizzle indices
const Uint32 id = operand.words[0];
// OpPhi's block labels, OpSelect's condition and OpExtInst's instruction-set id
// are ids that carry no numeric precision; skip them rather than let them veto.
if (IsNonNumericOperand(inst, i)) continue;
sawValueOperand = true;
if (relaxed.count(id) != 0) continue;
if (IsBoundedFloatConstant(id)) continue;
allRelaxed = false;
break;
}
return sawValueOperand && allRelaxed;
}
static Bool IsNonNumericOperand(const spvtools::opt::Instruction& inst, Uint32 index) {
switch (inst.opcode()) {
case spv::Op::OpPhi:
return (index % 2) == 1; // parent block labels
case spv::Op::OpSelect:
return index == 0; // condition
case spv::Op::OpExtInst:
return index == 0; // extended instruction set
default:
return false;
}
}
};
// TEMP-PERFDIAG: A/B switch between the scoped transform and the all-float upper bound.
Bool PerfDiagRelaxAllPrecision() {
static const Bool enabled = [] {
std::FILE* probe = std::fopen("/sdcard/MG/exp_relaxed_precision_all", "rb");
if (probe == nullptr) return false;
std::fclose(probe);
MGLOG_I("[PERFDIAG] fragment RelaxedPrecision: ALL floats (upper-bound probe)");
return true;
}();
return enabled;
}
// TEMP-PERFDIAG: lets a run turn the transform off entirely for an A/B baseline.
Bool PerfDiagRelaxedPrecisionEnabled() {
static const Bool disabled = [] {
std::FILE* probe = std::fopen("/sdcard/MG/exp_no_relaxed_precision", "rb");
if (probe == nullptr) return false;
std::fclose(probe);
MGLOG_I("[PERFDIAG] fragment RelaxedPrecision DISABLED");
return true;
}();
return !disabled;
}
Bool TransformSpirvForExplicitLod0Sampling(const Vector<Uint>& input, Vector<Uint>& output) {
if (input.empty()) {
output.clear();
@@ -1499,32 +1285,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags));
}
// TEMP-PERFDIAG
Bool TransformSpirvForRelaxedPrecisionProbe(const Vector<Uint>& input, Vector<Uint>& output) {
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: relaxed-precision probe: %s", message != nullptr ? message : "");
MGLOG_E("Vulkan: xfb capture pass: %s", message != nullptr ? message : "");
});
// SSA promotion first: glslang emits function-local variables with stores and loads,
// and a load can never be relaxed (its operand is a pointer), so without this the
// propagation below dies at the first temporary.
optimizer.RegisterPass(spvtools::CreateLocalMultiStoreElimPass());
if (PerfDiagRelaxAllPrecision()) {
optimizer.RegisterPass(spvtools::Optimizer::PassToken(MakeUnique<RelaxedPrecisionProbePass>()));
} else {
optimizer.RegisterPass(
spvtools::Optimizer::PassToken(MakeUnique<RelaxTextureDerivedPrecisionPass>()));
}
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: relaxed-precision probe failed; keeping the original module");
MGLOG_E("Vulkan: xfb capture decoration pass failed; keeping the original module");
output = input;
}
return success;
@@ -2575,7 +2365,17 @@ 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;
}
@@ -2588,15 +2388,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
if ((flags & ProgramFactory::CompileOptionBit::RelaxedFragmentPrecision) &&
PerfDiagRelaxedPrecisionEnabled() && shaders[i] &&
shaders[i]->GetShaderStage() == ShaderStage::Fragment) {
Vector<Uint> relaxedSpirv;
if (TransformSpirvForRelaxedPrecisionProbe(moduleSpirvs[i], relaxedSpirv)) {
moduleSpirvs[i] = Move(relaxedSpirv);
}
}
// 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
@@ -47,11 +47,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// 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,
// Fragment arithmetic may run at relaxed (fp16) precision. Only requested for draws
// where every sampled texture and every colour attachment is an 8-bit-or-less
// normalized format, so nothing the shader reads or writes carries more precision
// than fp16 already represents exactly.
RelaxedFragmentPrecision = 1 << 6,
// 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;
@@ -262,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);
@@ -433,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];
@@ -52,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);
@@ -77,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
@@ -16,9 +16,9 @@
#include "MG_Util/Converters/GLToMG/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include <vulkan/utility/vk_format_utils.h>
#include "MG_Util/Metrics/TextureMetrics.h"
#include <Config.h>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -205,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) {
@@ -447,64 +448,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return outImageInfo.sampler != VK_NULL_HANDLE;
}
namespace {
// fp16 carries an 11-bit mantissa, so an 8-bit normalized channel round-trips exactly.
// Anything wider - 16-bit normalized, half float, full float, and every packed HDR
// encoding - holds precision or range that relaxing the arithmetic would throw away.
Bool IsLowPrecisionNormalizedFormat(VkFormat format) {
if (format == VK_FORMAT_UNDEFINED) return false;
if (!vkuFormatIsUNORM(format) && !vkuFormatIsSNORM(format) && !vkuFormatIsSRGB(format)) {
return false;
}
const struct VKU_FORMAT_INFO info = vkuGetFormatInfo(format);
for (Uint32 i = 0; i < info.component_count; ++i) {
if (info.components[i].size > 8) return false;
}
return info.component_count > 0;
}
} // namespace
Bool UniformManager::DrawTargetIsLowPrecision(const MG_State::GLState::FramebufferObject* drawFramebuffer) {
// Default framebuffer: the swapchain is an 8-bit normalized surface.
if (drawFramebuffer == nullptr) return true;
Bool sawColour = false;
for (Int i = static_cast<Int>(FramebufferAttachmentType::Color0);
i < static_cast<Int>(FramebufferAttachmentType::FramebufferAttachmentTypeCount);
++i) {
const auto& attachment =
drawFramebuffer->GetAttachment(static_cast<FramebufferAttachmentType>(i));
VkFormat format = VK_FORMAT_UNDEFINED;
if (const auto& texture = attachment.GetTexture()) {
format = MG_Util::ConvertTextureInternalFormatToVkEnum(texture->GetFormat());
} else if (const auto& renderbuffer = attachment.GetRenderbuffer()) {
format = MG_Util::ConvertTextureInternalFormatToVkEnum(
renderbuffer->GetInternalFormat());
} else {
continue;
}
if (!IsLowPrecisionNormalizedFormat(format)) return false;
sawColour = true;
}
return sawColour;
}
Bool UniformManager::ProgramSamplesOnlyLowPrecisionTextures(
const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj) {
for (Uint32 binding = 0; binding < programObj.bindingKinds.size(); ++binding) {
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
continue;
}
const auto* texture = ResolveSamplerTextureRaw(program, programObj, binding);
// An unresolvable binding is unknown territory, not licence to relax.
if (texture == nullptr) return false;
const VkFormat format =
MG_Util::ConvertTextureInternalFormatToVkEnum(texture->GetFormat());
if (!IsLowPrecisionNormalizedFormat(format)) return false;
}
return true;
}
Bool UniformManager::ProgramSamplesOnlySingleLevelTextures(
const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj) {
Bool sawSampler = false;
@@ -1248,16 +1191,47 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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 element %u",
binding, element);
return false;
// 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,
@@ -1396,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,9 @@ 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
@@ -76,15 +79,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// 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.
// True when every texture this program samples is an 8-bit-or-less normalized format, so
// relaxing the fragment stage to fp16 cannot lose a bit the texel ever carried. Says
// nothing about the render target - the caller must check that too.
static Bool ProgramSamplesOnlyLowPrecisionTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj);
// True when every colour attachment the draw writes is an 8-bit-or-less normalized
// format (nullptr = default framebuffer, which is). Blending happens at attachment
// precision, so a wider target must keep the fragment stage at full precision.
static Bool DrawTargetIsLowPrecision(const MG_State::GLState::FramebufferObject* drawFramebuffer);
static Bool ProgramSamplesOnlySingleLevelTextures(const MG_State::GLState::ProgramObject& program,
const ProgramFactory::VkProgramObject& programObj);
@@ -194,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
@@ -58,15 +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()) {
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
return it->second;
it->second->lastUsedFrameBoundary = m_frameBoundaryCounter;
return *it->second;
}
VertexInputStateBuilder builder;
@@ -172,11 +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);
@@ -205,8 +243,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
for (auto it = m_cache.begin(); it != m_cache.end();) {
if (m_frameBoundaryCounter - it->second.lastUsedFrameBoundary > kRetireAgeBoundaries) {
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;
}
@@ -27,9 +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).
Uint64 lastUsedFrameBoundary = 0;
// 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;
@@ -41,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
};
@@ -80,9 +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 =
@@ -465,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).
@@ -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;
@@ -231,10 +225,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (resource.image == VK_NULL_HANDLE && resource.view == VK_NULL_HANDLE) {
return;
}
m_deferredRenderbufferReleases.push_back({resource.image, resource.allocation, resource.view, m_frameCounter});
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) {
@@ -249,6 +245,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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);
}
@@ -304,7 +303,47 @@ 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);
// 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),
@@ -314,6 +353,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
: 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()];
const Bool needsCreate =
resource.image == VK_NULL_HANDLE ||
@@ -351,6 +430,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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(
@@ -385,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;
@@ -481,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();
@@ -560,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);
@@ -617,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) {
@@ -674,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()) {
@@ -682,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()) {
@@ -699,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;
@@ -789,8 +930,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
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 = rbResource->format;
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
@@ -825,7 +970,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
.finalLayout = rbDesc.finalLayout,
});
textureResources.emplace_back(nullptr);
attachmentViews.emplace_back(rbResource->view);
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);
@@ -894,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,
@@ -907,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,
});
@@ -976,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);
@@ -994,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;
@@ -1077,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,
});
@@ -1122,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;
@@ -1330,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) {
@@ -1382,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;
@@ -188,8 +193,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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,
@@ -219,6 +238,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// 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 /
@@ -231,6 +257,10 @@ 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 {
@@ -244,6 +274,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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;
@@ -277,12 +310,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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;
Bool HasPendingRenderbufferClear(
const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
@@ -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) {
@@ -607,7 +597,11 @@ 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();
@@ -629,6 +623,7 @@ 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
@@ -659,6 +654,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
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) {
@@ -714,45 +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()) {
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);
// 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;
}
@@ -766,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) {
@@ -814,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);
}
@@ -823,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()) {
@@ -833,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",
@@ -1049,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));
@@ -1076,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;
@@ -1160,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;
}
@@ -1189,6 +1225,8 @@ 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;
}
@@ -1406,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;
@@ -1420,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,
@@ -1473,6 +1532,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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 |
@@ -1485,6 +1552,44 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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()) &&
resource.extent.height == static_cast<Uint32>(texelSize.y()) &&
@@ -1614,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;
@@ -1683,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();
@@ -1884,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;
@@ -1947,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;
@@ -1955,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);
}
@@ -1989,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__);
@@ -28,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;
@@ -64,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;
}
};
@@ -80,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;
}
};
@@ -172,6 +181,13 @@ public:
// 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;
@@ -207,6 +223,7 @@ public:
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);
}
@@ -307,6 +324,21 @@ public:
VkImageLayout newLayout);
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
@@ -364,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);
@@ -394,6 +429,11 @@ 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);
@@ -423,6 +463,23 @@ 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;
@@ -430,7 +487,21 @@ private:
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
@@ -76,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 {
@@ -151,6 +155,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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);
@@ -188,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,
@@ -273,6 +304,15 @@ 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);
// 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
@@ -391,6 +431,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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
@@ -443,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;
@@ -455,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;
@@ -491,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;
+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)
@@ -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;
+241 -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,147 @@ 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);
if (const auto beginXfb = MG_Backend::gBackendFunctionsTable.GL.BeginTransformFeedback) {
beginXfb(primitiveMode);
}
}
// 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) {
// Only Vulkan-order captures need this. A backend that runs the capture on its
// own GL/ES driver (it owns the span, hence the EndTransformFeedback entry) has
// already produced GL's vertex order, and reordering it again would corrupt it.
if (MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback != nullptr) {
return;
}
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();
// Closed while the capture state is still active: a backend that captures
// through its own driver reads the capture program and buffer bindings here.
if (const auto endXfb = MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback) {
endXfb();
}
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)
@@ -22,9 +22,21 @@
namespace MobileGL::MG_Impl::GLImpl {
namespace {
Bool IsActiveBackendDirectVulkan() {
// GL only requires support for framebuffers whose depth and stencil attachments
// are the same image; anything else may be reported GL_FRAMEBUFFER_UNSUPPORTED.
// DirectVulkan cannot form two separate attachments at all, and the real ES
// drivers behind DirectGLES answer UNSUPPORTED for it too - so saying COMPLETE
// and then rendering into a framebuffer the driver refuses produced silently
// empty results (KHR-GL3x.packed_depth_stencil.verify_mixed_attachments).
Bool ActiveBackendRejectsDistinctDepthStencil() {
auto* activeBackend = MG_Backend::pActiveBackendObject.get();
return activeBackend != nullptr && activeBackend->GetBackendType() == BackendType::DirectVulkan;
if (activeBackend == nullptr) {
return false;
}
if (activeBackend->GetBackendType() == BackendType::DirectVulkan) {
return true;
}
return !activeBackend->GetDynamicParameters().SupportsDistinctDepthStencilAttachments;
}
Bool HasDistinctCompleteDepthStencilTextureAttachments(
@@ -45,10 +57,32 @@ namespace MobileGL::MG_Impl::GLImpl {
depthAttachment.GetTextureLevel() != stencilAttachment.GetTextureLevel();
}
Bool IsUnsupportedFramebufferForDirectVulkan(
// 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 HasUnsupportedDistinctDepthStencilAttachments(
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) {
@@ -67,7 +101,14 @@ namespace MobileGL::MG_Impl::GLImpl {
// shared-exponent, SNORM, three-channel norm16/float32/sRGB and three-channel integer formats.
// Desktop GL treats those as texture-only too (not in the GL 3.3 required-renderable list), so
// reporting GL_FRAMEBUFFER_UNSUPPORTED for them is legal.
Bool IsColorInternalFormatRenderable(TextureInternalFormat format) {
//
// `capabilityTargetIndex` is the row of the cache the attachment actually lives in;
// kFormatCapabilityTargetCount asks about the format in general. Asking per target matters
// because a capability recorded for one of them says nothing about the others: DirectGLES
// widens three-channel formats to four channels to keep them renderable as *multisample*
// storage, and a format that survives only through that substitution is still texture-only
// on every ordinary target.
Bool IsColorInternalFormatRenderable(TextureInternalFormat format, SizeT capabilityTargetIndex) {
const SizeT formatIndex = static_cast<SizeT>(format);
if (MG_Backend::pActiveBackendObject && formatIndex < MG_Backend::kFormatCapabilityFormatCount) {
const auto& cache = MG_Backend::pActiveBackendObject->GetFormatCapabilities();
@@ -79,8 +120,11 @@ namespace MobileGL::MG_Impl::GLImpl {
MG_Backend::FormatCapability::Creatable);
}
if (cachePopulated) {
for (SizeT targetIndex = 0; targetIndex < MG_Backend::kFormatCapabilityTargetCount;
++targetIndex) {
const Bool singleTarget = capabilityTargetIndex < MG_Backend::kFormatCapabilityTargetCount;
const SizeT firstTarget = singleTarget ? capabilityTargetIndex : 0;
const SizeT lastTarget =
singleTarget ? capabilityTargetIndex + 1 : MG_Backend::kFormatCapabilityTargetCount;
for (SizeT targetIndex = firstTarget; targetIndex < lastTarget; ++targetIndex) {
if (MG_Backend::HasFormatCapability(cache.FullCaps[targetIndex][formatIndex],
MG_Backend::FormatCapability::FramebufferRenderable) ||
MG_Backend::HasFormatCapability(cache.CaveatCaps[targetIndex][formatIndex],
@@ -129,12 +173,17 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto& attachment = attachments[i];
if (!attachment.IsValid()) continue;
TextureInternalFormat format = TextureInternalFormat::Unknown;
SizeT capabilityTargetIndex = MG_Backend::kFormatCapabilityTargetCount;
if (attachment.IsTexture() && attachment.GetTexture()) {
format = attachment.GetTexture()->GetFormat();
capabilityTargetIndex =
MG_Backend::GetFormatCapabilityTargetIndex(attachment.GetTexture()->GetTarget());
} else if (attachment.IsRenderbuffer() && attachment.GetRenderbuffer()) {
format = attachment.GetRenderbuffer()->GetInternalFormat();
capabilityTargetIndex = MG_Backend::GetRenderbufferFormatCapabilityTargetIndex();
}
if (format != TextureInternalFormat::Unknown && !IsColorInternalFormatRenderable(format)) {
if (format != TextureInternalFormat::Unknown &&
!IsColorInternalFormatRenderable(format, capabilityTargetIndex)) {
return true;
}
}
@@ -147,6 +196,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 +603,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 +640,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 +737,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>(
@@ -1422,8 +1658,8 @@ namespace MobileGL::MG_Impl::GLImpl {
if (HasNonRenderableColorAttachment(*framebufferObject)) {
return GL_FRAMEBUFFER_UNSUPPORTED;
}
if (IsActiveBackendDirectVulkan() &&
IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) {
if (ActiveBackendRejectsDistinctDepthStencil() &&
HasUnsupportedDistinctDepthStencilAttachments(*framebufferObject)) {
return GL_FRAMEBUFFER_UNSUPPORTED;
}
return GL_FRAMEBUFFER_COMPLETE;
@@ -1450,8 +1686,8 @@ namespace MobileGL::MG_Impl::GLImpl {
if (HasNonRenderableColorAttachment(*framebufferObject)) {
return GL_FRAMEBUFFER_UNSUPPORTED;
}
if (IsActiveBackendDirectVulkan() &&
IsUnsupportedFramebufferForDirectVulkan(*framebufferObject)) {
if (ActiveBackendRejectsDistinctDepthStencil() &&
HasUnsupportedDistinctDepthStencilAttachments(*framebufferObject)) {
return GL_FRAMEBUFFER_UNSUPPORTED;
}
return GL_FRAMEBUFFER_COMPLETE;
@@ -1468,20 +1704,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 +1783,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>(
+53 -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;
+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) {
+16 -1
View File
@@ -2820,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);
@@ -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);
+48
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;
@@ -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();
@@ -465,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
@@ -475,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();
@@ -558,5 +596,15 @@ namespace MobileGL::MG_State::GLState {
mutable Uint32 m_backendHashMemoVersion = ~0u;
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.
@@ -162,6 +162,7 @@ namespace MobileGL {
DepthComponent32F,
Depth24Stencil8,
Depth32FStencil8,
StencilIndex8,
DepthComponent,
DepthStencil,
@@ -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) {
+92 -6
View File
@@ -197,13 +197,13 @@ TEST(DirectGLESSanity, AdvertisesDepthTextureForGlmarkShadowScenes) {
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_depth_texture), extensions.end());
}
TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensionsWithoutRaisingGLVersion) {
TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensionsAtExperimentalCTSVersion) {
MobileGL::MG_Backend::DirectGLES::BackendObject_DirectGLES backend;
const auto& rendererInfo = backend.GetRendererInfo().RendererGLInfo;
const auto& extensions = rendererInfo.Extensions;
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 3);
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 3);
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 4);
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 6);
EXPECT_EQ(rendererInfo.TargetGLVersion.Patch, 0);
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_compute_shader),
@@ -397,13 +397,13 @@ TEST(DirectVulkanSanity, RenderPassExtentUsesSwapchainSizeOnlyForDefaultFramebuf
MobileGL::IntVec2(512, 512));
}
TEST(DirectVulkanSanity, AdvertisesVoxyRequiredRenderingExtensionsWithoutRaisingGLVersion) {
TEST(DirectVulkanSanity, AdvertisesVoxyRequiredRenderingExtensionsAtExperimentalCTSVersion) {
MobileGL::MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend;
const auto& rendererInfo = backend.GetRendererInfo().RendererGLInfo;
const auto& extensions = rendererInfo.Extensions;
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 3);
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 3);
EXPECT_EQ(rendererInfo.TargetGLVersion.Major, 4);
EXPECT_EQ(rendererInfo.TargetGLVersion.Minor, 6);
EXPECT_EQ(rendererInfo.TargetGLVersion.Patch, 0);
EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_compute_shader),
@@ -516,6 +516,50 @@ TEST(DirectGLESSanity, PreservesHostPerStageImageUniformLimits) {
EXPECT_EQ(params.MaxComputeImageUniforms, 5);
}
TEST(FragmentInterpolationCapabilities, PlumbsGLESAndBothVulkanPropertyPaths) {
using namespace MobileGL;
MG_External::GLESCapabilities glesCaps;
glesCaps.MinFragmentInterpolationOffset = -0.75f;
glesCaps.MaxFragmentInterpolationOffset = 0.625f;
glesCaps.FragmentInterpolationOffsetBits = 6;
MG_Backend::DirectGLES::BackendObject_DirectGLES glesBackend;
glesBackend.ApplyGLESCapabilitiesForTesting(glesCaps);
EXPECT_FLOAT_EQ(glesBackend.GetDynamicParameters().MinFragmentInterpolationOffset, -0.75f);
EXPECT_FLOAT_EQ(glesBackend.GetDynamicParameters().MaxFragmentInterpolationOffset, 0.625f);
EXPECT_EQ(glesBackend.GetDynamicParameters().FragmentInterpolationOffsetBits, 6);
VkPhysicalDeviceProperties properties{};
// A common Vulkan limit pair: max is one representable 4-bit step below 0.5.
properties.limits.minInterpolationOffset = -0.5f;
properties.limits.maxInterpolationOffset = 0.4375f;
properties.limits.subPixelInterpolationOffsetBits = 4;
MG_External::VulkanCapabilities vkCaps;
MG_Util::BackendLoader::FillInVulkanCapabilities(vkCaps, properties);
EXPECT_FLOAT_EQ(vkCaps.MinFragmentInterpolationOffset, -0.5f);
EXPECT_FLOAT_EQ(vkCaps.MaxFragmentInterpolationOffset, 0.4375f);
EXPECT_EQ(vkCaps.FragmentInterpolationOffsetBits, 4);
vkCaps.MinFragmentInterpolationOffset = -0.875f;
vkCaps.MaxFragmentInterpolationOffset = 0.75f;
vkCaps.FragmentInterpolationOffsetBits = 7;
MG_Backend::DirectVulkan::BackendObject_DirectVulkan vkBackend;
vkBackend.ApplyVulkanCapabilitiesForTesting(vkCaps);
EXPECT_FLOAT_EQ(vkBackend.GetDynamicParameters().MinFragmentInterpolationOffset, -0.875f);
EXPECT_FLOAT_EQ(vkBackend.GetDynamicParameters().MaxFragmentInterpolationOffset, 0.75f);
EXPECT_EQ(vkBackend.GetDynamicParameters().FragmentInterpolationOffsetBits, 7);
// Invalid/zero host data cannot under-advertise the OpenGL 4 minimums.
MG_External::VulkanCapabilities invalidCaps;
invalidCaps.MinFragmentInterpolationOffset = 0.0f;
invalidCaps.MaxFragmentInterpolationOffset = 0.0f;
invalidCaps.FragmentInterpolationOffsetBits = 0;
vkBackend.ApplyVulkanCapabilitiesForTesting(invalidCaps);
EXPECT_LE(vkBackend.GetDynamicParameters().MinFragmentInterpolationOffset, -0.5f);
EXPECT_FLOAT_EQ(vkBackend.GetDynamicParameters().MaxFragmentInterpolationOffset, 0.4375f);
EXPECT_EQ(vkBackend.GetDynamicParameters().FragmentInterpolationOffsetBits, 4);
}
TEST(DirectVulkanSanity, AdvertisesSubgroupOnlyWhenVulkanReportsUsableSupport) {
using namespace MobileGL;
@@ -638,6 +682,48 @@ TEST(GetterSanity, ClampsMaxVertexAttribsToCurrentValueStorageCapacity) {
MG_State::pGLContext.reset();
}
TEST(GetterSanity, ReportsFragmentInterpolationLimitsForFloatAndIntegerQueries) {
using namespace MobileGL;
auto previousContext = Move(MG_State::pGLContext);
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
MG_Backend::DynamicBackendParameters params;
params.MinFragmentInterpolationOffset = -0.75f;
params.MaxFragmentInterpolationOffset = 0.4375f;
params.FragmentInterpolationOffsetBits = 6;
MG_Backend::pActiveBackendObject = MakeUnique<DynamicParameterBackend>(params);
GLfloat floatValue = 0.0f;
MG_Impl::GLImpl::GetFloatv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &floatValue);
EXPECT_FLOAT_EQ(floatValue, -0.75f);
MG_Impl::GLImpl::GetFloatv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &floatValue);
EXPECT_FLOAT_EQ(floatValue, 0.4375f);
MG_Impl::GLImpl::GetFloatv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &floatValue);
EXPECT_FLOAT_EQ(floatValue, 6.0f);
GLint intValue = 0;
MG_Impl::GLImpl::GetIntegerv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &intValue);
EXPECT_EQ(intValue, -1);
MG_Impl::GLImpl::GetIntegerv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &intValue);
EXPECT_EQ(intValue, 0);
MG_Impl::GLImpl::GetIntegerv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &intValue);
EXPECT_EQ(intValue, 6);
GLboolean boolValue = GL_FALSE;
MG_Impl::GLImpl::GetBooleanv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &boolValue);
EXPECT_EQ(boolValue, GL_TRUE);
MG_Impl::GLImpl::GetBooleanv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &boolValue);
EXPECT_EQ(boolValue, GL_TRUE);
MG_Impl::GLImpl::GetBooleanv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &boolValue);
EXPECT_EQ(boolValue, GL_TRUE);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Backend::pActiveBackendObject = Move(previousBackend);
MG_State::pGLContext = Move(previousContext);
}
TEST(GetterSanity, PerStageImageUniformQueriesMatchShaderCompilerLimits) {
using namespace MobileGL;
@@ -9,6 +9,7 @@
#include "Loader.h"
#include "MG_Util/Types.h"
#include <Config.h>
#include <cmath>
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN 1
@@ -823,6 +824,12 @@ namespace MobileGL::MG_Util::BackendLoader {
if (std::strcmp(extension, "GL_EXT_texture_norm16") == 0) {
caps.SupportsNorm16Texture = true;
}
if (std::strcmp(extension, "GL_EXT_render_snorm") == 0) {
caps.SupportsRenderSnorm = true;
}
if (std::strcmp(extension, "GL_EXT_sRGB_write_control") == 0) {
caps.SupportsSrgbWriteControl = true;
}
if (std::strcmp(extension, "GL_EXT_texture_filter_anisotropic") == 0) {
caps.SupportsTextureFilterAnisotropy = true;
}
@@ -838,8 +845,14 @@ namespace MobileGL::MG_Util::BackendLoader {
if (std::strcmp(extension, "GL_NV_shader_noperspective_interpolation") == 0) {
caps.SupportsNoperspectiveInterpolation = true;
}
if (std::strcmp(extension, "GL_OES_shader_multisample_interpolation") == 0) {
caps.SupportsShaderMultisampleInterpolation = true;
}
}
}
caps.SupportsShaderMultisampleInterpolation =
caps.SupportsShaderMultisampleInterpolation || caps.GLESVersion.Major > 3 ||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2);
// Detect optional raster/color-mask entry points by whether they loaded. glColorMaski is GLES
// 3.2 core (no extension string), so pointer presence is the reliable signal for all of these.
@@ -900,6 +913,9 @@ namespace MobileGL::MG_Util::BackendLoader {
GLint maxColorAttachments = 8;
GLint maxClipDistances = 8;
GLint maxViewports = 16;
GLfloat minFragmentInterpolationOffset = -0.5f;
GLfloat maxFragmentInterpolationOffset = 0.4375f;
GLint fragmentInterpolationOffsetBits = 4;
glesFuncs.glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, aliasedLineWidthRange);
glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_RANGE, smoothLineWidthRange);
glesFuncs.glGetFloatv(GL_SMOOTH_LINE_WIDTH_GRANULARITY, &smoothLineWidthGranularity);
@@ -919,6 +935,15 @@ namespace MobileGL::MG_Util::BackendLoader {
glesFuncs.glGetIntegerv(GL_MAX_INTEGER_SAMPLES, &maxIntegerSamples);
glesFuncs.glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
glesFuncs.glGetIntegerv(GL_MAX_SAMPLE_MASK_WORDS, &maxSampleMaskWords);
// MobileGL's sample-mask state only stores a single 32-bit word (see
// RenderState::SampleMaskValue) and SampleMaski_State() rejects any
// maskNumber other than 0. Advertising the real driver's value here (e.g.
// NVIDIA's GLES driver reports 2) makes glSampleMaski(1, ...) - which
// dEQP's per-case gluStateReset always issues up to GL_MAX_SAMPLE_MASK_WORDS -
// raise GL_INVALID_VALUE and aborts the whole glcts process after every
// single test case. 1 is a spec-legal value (the minimum required), so cap
// to what is actually implemented instead of forwarding the raw driver limit.
maxSampleMaskWords = std::min(maxSampleMaskWords, 1);
glesFuncs.glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxTextureImageUnits);
glesFuncs.glGetIntegerv(GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, &maxVertexTextureImageUnits);
glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS, &maxComputeTextureImageUnits);
@@ -950,6 +975,29 @@ namespace MobileGL::MG_Util::BackendLoader {
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORTS, &maxViewports);
glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims);
glesFuncs.glGetIntegerv(GL_VIEWPORT_SUBPIXEL_BITS, &viewportSubpixelBits);
if (caps.SupportsShaderMultisampleInterpolation && glesFuncs.glGetFloatv) {
const auto drainErrors = [&glesFuncs]() {
Bool hadError = false;
if (glesFuncs.glGetError) {
while (glesFuncs.glGetError() != GL_NO_ERROR) hadError = true;
}
return hadError;
};
// Isolate these optional queries from errors raised by preceding capability
// probes, then consume any query error so initialization never leaks it into
// the application's first glGetError call.
drainErrors();
glesFuncs.glGetFloatv(GL_MIN_FRAGMENT_INTERPOLATION_OFFSET, &minFragmentInterpolationOffset);
glesFuncs.glGetFloatv(GL_MAX_FRAGMENT_INTERPOLATION_OFFSET, &maxFragmentInterpolationOffset);
glesFuncs.glGetIntegerv(GL_FRAGMENT_INTERPOLATION_OFFSET_BITS, &fragmentInterpolationOffsetBits);
if (drainErrors()) {
MGLOG_W("Fragment interpolation limit query failed; using OpenGL minimums");
minFragmentInterpolationOffset = -0.5f;
maxFragmentInterpolationOffset = 0.4375f;
fragmentInterpolationOffsetBits = 4;
}
}
// Only legal to query once the extension has been seen in the loop above, hence not batched
// with the unconditional probes: on a driver without it this raises GL_INVALID_ENUM.
if (caps.SupportsTextureFilterAnisotropy) {
@@ -1007,6 +1055,19 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.ViewportBoundsRangeMin = viewportBoundsRange[0];
caps.ViewportBoundsRangeMax = viewportBoundsRange[1];
caps.ViewportSubpixelBits = viewportSubpixelBits;
caps.MinFragmentInterpolationOffset =
std::isfinite(minFragmentInterpolationOffset) && minFragmentInterpolationOffset <= -0.5f
? minFragmentInterpolationOffset
: -0.5f;
caps.MaxFragmentInterpolationOffset = 0.4375f;
caps.FragmentInterpolationOffsetBits = 4;
if (fragmentInterpolationOffsetBits >= 4 && std::isfinite(maxFragmentInterpolationOffset)) {
const Float requiredMaxOffset = 0.5f - std::ldexp(1.0f, -fragmentInterpolationOffsetBits);
if (maxFragmentInterpolationOffset >= requiredMaxOffset) {
caps.MaxFragmentInterpolationOffset = maxFragmentInterpolationOffset;
caps.FragmentInterpolationOffsetBits = fragmentInterpolationOffsetBits;
}
}
MGLOG_I(" GL_ALIASED_LINE_WIDTH_RANGE: [%.3f, %.3f]", caps.AliasedLineWidthRangeMin,
caps.AliasedLineWidthRangeMax);
MGLOG_I(" GL_SMOOTH_LINE_WIDTH_RANGE: [%.3f, %.3f]", caps.SmoothLineWidthRangeMin,
@@ -1031,6 +1031,13 @@ namespace MobileGL {
String GLESShadingLanguageVersionString;
Bool SupportsPersistentMapping = false;
Bool SupportsNorm16Texture = false;
// GL_EXT_render_snorm is present, so the signed-normalized formats are colour-renderable
// (and usable as multisample texture storage) rather than texture-only.
Bool SupportsRenderSnorm = false;
// GL_EXT_sRGB_write_control is present, so GL_FRAMEBUFFER_SRGB can be turned off.
// GLES has no such switch in core: writes into an sRGB attachment are ALWAYS encoded,
// while desktop GL leaves GL_FRAMEBUFFER_SRGB disabled by default and writes raw.
Bool SupportsSrgbWriteControl = false;
// GL_EXT_texture_filter_anisotropic is present, so sampler/texture
// anisotropy may be forwarded without raising GL_INVALID_ENUM in GLES.
Bool SupportsTextureFilterAnisotropy = false;
@@ -1055,6 +1062,9 @@ namespace MobileGL {
// SPIRV-Cross's `#extension ... : require` would fail to compile and MobileGL falls back
// to stripping the NoPerspective decoration (smooth interpolation) via StripNoPerspectivePass.
Bool SupportsNoperspectiveInterpolation = false;
// GLES 3.2 core or GL_OES_shader_multisample_interpolation exposes
// interpolateAtOffset and the three fragment-offset limit queries.
Bool SupportsShaderMultisampleInterpolation = false;
// GL_RENDERER contains "ANGLE".
Bool IsAngleRenderer = false;
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
@@ -1120,6 +1130,9 @@ namespace MobileGL {
Float ViewportBoundsRangeMin = 0.0f;
Float ViewportBoundsRangeMax = 0.0f;
Int ViewportSubpixelBits = 0;
Float MinFragmentInterpolationOffset = -0.5f;
Float MaxFragmentInterpolationOffset = 0.4375f;
Int FragmentInterpolationOffsetBits = 4;
};
} // namespace MG_External
@@ -9,6 +9,7 @@
#include "Loader.h"
#include <Config.h>
#include <cmath>
namespace MobileGL::MG_Util::BackendLoader {
namespace {
@@ -40,6 +41,25 @@ namespace MobileGL::MG_Util::BackendLoader {
return MaxSampleCountFromFlags(commonFlags);
}
void FillFragmentInterpolationLimits(MG_External::VulkanCapabilities& caps,
const VkPhysicalDeviceLimits& limits) {
caps.MinFragmentInterpolationOffset =
std::isfinite(limits.minInterpolationOffset) && limits.minInterpolationOffset <= -0.5f
? limits.minInterpolationOffset
: -0.5f;
caps.MaxFragmentInterpolationOffset = 0.4375f;
caps.FragmentInterpolationOffsetBits = 4;
const Int bits = static_cast<Int>(limits.subPixelInterpolationOffsetBits);
if (bits >= 4 && std::isfinite(limits.maxInterpolationOffset)) {
const Float requiredMaxOffset = 0.5f - std::ldexp(1.0f, -bits);
if (limits.maxInterpolationOffset >= requiredMaxOffset) {
caps.MaxFragmentInterpolationOffset = limits.maxInterpolationOffset;
caps.FragmentInterpolationOffsetBits = bits;
}
}
}
VulkanDynamicFunctions LoadVulkanDynamicFunctions(VkInstance instance) {
VulkanDynamicFunctions loaded{};
if (instance == VK_NULL_HANDLE) {
@@ -171,6 +191,7 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.ViewportBoundsRangeMin = p.limits.viewportBoundsRange[0];
caps.ViewportBoundsRangeMax = p.limits.viewportBoundsRange[1];
caps.ViewportSubpixelBits = static_cast<Int>(p.limits.viewportSubPixelBits);
FillFragmentInterpolationLimits(caps, p.limits);
VkPhysicalDeviceFeatures supportedFeatures{};
vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);
@@ -261,6 +282,7 @@ namespace MobileGL::MG_Util::BackendLoader {
caps.ViewportBoundsRangeMin = properties.limits.viewportBoundsRange[0];
caps.ViewportBoundsRangeMax = properties.limits.viewportBoundsRange[1];
caps.ViewportSubpixelBits = static_cast<Int>(properties.limits.viewportSubPixelBits);
FillFragmentInterpolationLimits(caps, properties.limits);
caps.SupportsWideLines = false;
// This helper only receives properties, not VkPhysicalDeviceFeatures. Leave optional
// stage writes disabled rather than inferring them from descriptor limits alone.
@@ -68,6 +68,9 @@ namespace MobileGL {
Float ViewportBoundsRangeMin = 0.0f;
Float ViewportBoundsRangeMax = 0.0f;
Int ViewportSubpixelBits = 0;
Float MinFragmentInterpolationOffset = -0.5f;
Float MaxFragmentInterpolationOffset = 0.4375f;
Int FragmentInterpolationOffsetBits = 4;
Bool SupportsWideLines = false;
// Storage-image descriptors are limited per stage by
// maxPerStageDescriptorStorageImages, but writes/atomics outside compute additionally
@@ -28,6 +28,7 @@ namespace MobileGL {
bool IsStencilFormatInternalFormat(TextureInternalFormat internalformat) {
switch (internalformat) {
case TextureInternalFormat::StencilIndex8:
case TextureInternalFormat::Depth24Stencil8:
case TextureInternalFormat::Depth32FStencil8:
case TextureInternalFormat::DepthStencil:
@@ -251,6 +251,8 @@ namespace MobileGL {
return TextureInternalFormat::Depth24Stencil8;
case GL_DEPTH32F_STENCIL8:
return TextureInternalFormat::Depth32FStencil8;
case GL_STENCIL_INDEX8:
return TextureInternalFormat::StencilIndex8;
case GL_DEPTH_COMPONENT:
return TextureInternalFormat::DepthComponent;
case GL_DEPTH_STENCIL:
@@ -233,6 +233,8 @@ namespace MobileGL {
return GL_DEPTH24_STENCIL8;
case TextureInternalFormat::Depth32FStencil8:
return GL_DEPTH32F_STENCIL8;
case TextureInternalFormat::StencilIndex8:
return GL_STENCIL_INDEX8;
case TextureInternalFormat::DepthComponent32:
return GL_DEPTH_COMPONENT32;
case TextureInternalFormat::DepthStencil:
@@ -234,6 +234,8 @@ namespace MobileGL {
return "Depth24Stencil8";
case TextureInternalFormat::Depth32FStencil8:
return "Depth32FStencil8";
case TextureInternalFormat::StencilIndex8:
return "StencilIndex8";
case TextureInternalFormat::Red:
return "Red";
case TextureInternalFormat::RG:
@@ -25,6 +25,19 @@ namespace MobileGL {
case GL_TRIANGLE_FAN:
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN;
case GL_LINE_LOOP:
// DrawArrays/DrawElements rewrite line loops into closed indexed
// strips; entry points without that rewrite (instanced/indirect)
// degrade to an open strip, which only misses the closing segment.
MGLOG_W("GL_LINE_LOOP without index rewrite; drawing as LINE_STRIP");
return VK_PRIMITIVE_TOPOLOGY_LINE_STRIP;
case GL_LINES_ADJACENCY:
return VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY;
case GL_LINE_STRIP_ADJACENCY:
return VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY;
case GL_TRIANGLES_ADJACENCY:
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY;
case GL_TRIANGLE_STRIP_ADJACENCY:
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY;
default:
MGLOG_W("Unrecognized primitive topology");
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
@@ -226,6 +226,8 @@ namespace MobileGL {
return VK_FORMAT_D24_UNORM_S8_UINT;
case TextureInternalFormat::Depth32FStencil8:
return VK_FORMAT_D32_SFLOAT_S8_UINT;
case TextureInternalFormat::StencilIndex8:
return VK_FORMAT_S8_UINT;
case TextureInternalFormat::DepthComponent32:
return VK_FORMAT_D32_SFLOAT;
case TextureInternalFormat::DepthStencil:
+20 -5
View File
@@ -18,6 +18,7 @@ namespace MobileGL {
case TextureInternalFormat::Red: // UNorm8 shadow layout
case TextureInternalFormat::R8Snorm:
case TextureInternalFormat::R8I:
case TextureInternalFormat::StencilIndex8:
case TextureInternalFormat::R8UI:
return 1;
@@ -43,8 +44,11 @@ namespace MobileGL {
case TextureInternalFormat::SRGB8:
case TextureInternalFormat::RGB8I:
case TextureInternalFormat::RGB8UI:
case TextureInternalFormat::DepthComponent24:
return 3;
// Canonical depth shadow is a full 32-bit unorm word (see PixelStoreProcessor),
// converted at upload to the image's own 24/32-bit layout.
case TextureInternalFormat::DepthComponent24:
return 4;
case TextureInternalFormat::RGBA2:
case TextureInternalFormat::RGBA4:
@@ -91,6 +95,9 @@ namespace MobileGL {
case TextureInternalFormat::RG32F:
case TextureInternalFormat::RG32I:
case TextureInternalFormat::RG32UI:
// Shadow bytes hold the GL_FLOAT_32_UNSIGNED_INT_24_8_REV wire format
// (float depth + a word whose low 8 bits are stencil), 8 bytes/texel.
case TextureInternalFormat::Depth32FStencil8:
return 8;
case TextureInternalFormat::RGB32F:
@@ -101,7 +108,6 @@ namespace MobileGL {
case TextureInternalFormat::RGBA32F:
case TextureInternalFormat::RGBA32I:
case TextureInternalFormat::RGBA32UI:
case TextureInternalFormat::Depth32FStencil8:
return 16;
case TextureInternalFormat::R11FG11FB10F:
@@ -253,8 +259,10 @@ namespace MobileGL {
case TexturePixelDataType::UnsignedInt101111Rev:
case TexturePixelDataType::UnsignedInt5999Rev:
case TexturePixelDataType::UnsignedInt248:
case TexturePixelDataType::Float32UnsignedInt248Rev:
return 4;
case TexturePixelDataType::Float32UnsignedInt248Rev:
// A 32-bit float depth word followed by a 32-bit word holding stencil.
return 8;
default:
return 0;
}
@@ -501,9 +509,16 @@ namespace MobileGL {
s.Depth = 32;
s.Stencil = 8;
break;
case TextureInternalFormat::StencilIndex8:
s.Stencil = 8;
break;
case TextureInternalFormat::Unknown:
// Queried for attachments that have no storage yet (e.g. framebuffer
// parameter queries on the initial state); every size stays 0.
break;
default:
MOBILEGL_ASSERT(false, "Unimplemented internal format in GetComponentSizesForInternalFormat: %d",
static_cast<Int>(internal));
MGLOG_W("Unimplemented internal format in GetComponentSizesForInternalFormat: %d",
static_cast<Int>(internal));
break;
}
@@ -220,11 +220,14 @@ namespace MobileGL {
auto result = ParseShaderSource(lang, shaderType, source, attrib.flags);
if (result) return result;
// Legacy desktop sources are normalized to "#version 330 core", which parses under
// stricter rules than the 460 they used to be forced to: a shader declaring 330 while
// using e.g. layout(binding=...) without the matching #extension line compiles on real
// drivers but is rejected here. Retry once at 460 before reporting failure; a genuinely
// broken shader fails both attempts and keeps its original diagnostics.
// Legacy desktop sources are normalized to "#version 330 core" (with a marker on the
// directive), which parses under stricter rules than the 460 they used to be forced
// to: a shader declaring 110-150 while using e.g. layout(binding=...) without the
// matching #extension line compiles on real drivers but is rejected here. Retry once
// at 460 before reporting failure; a genuinely broken shader fails both attempts and
// keeps its original diagnostics. Application-declared 330+ sources carry no marker
// and keep their declared version's strict rules (the GL CTS negative-compile cases
// depend on that).
String retrySource = source;
if (!MG_Util::ShaderTranspiler::RetargetLegacyVersionDirectiveTo460(retrySource)) {
return result;
@@ -360,6 +363,86 @@ namespace MobileGL {
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::LowerRectImagesForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
constexpr SizeT kSpirvHeaderWordCount = 5;
// OpTypeImage: [0] opcode/wordcount, [1] result id, [2] sampled type, [3] Dim, ...
constexpr SizeT kTypeImageDimWordIndex = 3;
constexpr SizeT kTypeImageMinWordCount = 9;
outputBinary.clear();
if (inputBinary.size() < kSpirvHeaderWordCount || inputBinary[0] != spv::MagicNumber) {
return false;
}
Vector<SizeT> rectDimWordOffsets;
Vector<SizeT> rectCapabilityWordOffsets;
Bool hasNormalizedCoordinateLookup = false;
for (SizeT offset = kSpirvHeaderWordCount; offset < inputBinary.size();) {
const Uint32 instructionWord = inputBinary[offset];
const SizeT wordCount = instructionWord >> 16u;
const auto opcode = static_cast<spv::Op>(instructionWord & 0xffffu);
if (wordCount == 0 || offset + wordCount > inputBinary.size()) {
return false;
}
if (opcode == spv::Op::OpTypeImage && wordCount >= kTypeImageMinWordCount) {
if (static_cast<spv::Dim>(inputBinary[offset + kTypeImageDimWordIndex]) == spv::Dim::Rect) {
rectDimWordOffsets.push_back(offset + kTypeImageDimWordIndex);
}
} else if (opcode == spv::Op::OpCapability && wordCount >= 2) {
const auto capability = static_cast<spv::Capability>(inputBinary[offset + 1]);
if (capability == spv::Capability::SampledRect ||
capability == spv::Capability::ImageRect) {
rectCapabilityWordOffsets.push_back(offset + 1);
}
} else {
switch (opcode) {
// Everything that takes normalized coordinates. Tracing each one back to
// its image type would let a module mix a normalized 2D lookup with a
// rectangle fetch, but the extra reach is not worth the risk of getting
// the trace wrong: decline the whole module instead.
case spv::Op::OpImageSampleImplicitLod:
case spv::Op::OpImageSampleExplicitLod:
case spv::Op::OpImageSampleDrefImplicitLod:
case spv::Op::OpImageSampleDrefExplicitLod:
case spv::Op::OpImageSampleProjImplicitLod:
case spv::Op::OpImageSampleProjExplicitLod:
case spv::Op::OpImageSampleProjDrefImplicitLod:
case spv::Op::OpImageSampleProjDrefExplicitLod:
case spv::Op::OpImageGather:
case spv::Op::OpImageDrefGather:
case spv::Op::OpImageSparseSampleImplicitLod:
case spv::Op::OpImageSparseSampleExplicitLod:
case spv::Op::OpImageSparseSampleDrefImplicitLod:
case spv::Op::OpImageSparseSampleDrefExplicitLod:
case spv::Op::OpImageSparseGather:
case spv::Op::OpImageSparseDrefGather:
hasNormalizedCoordinateLookup = true;
break;
default:
break;
}
}
offset += wordCount;
}
if (rectDimWordOffsets.empty() || hasNormalizedCoordinateLookup) {
return false;
}
outputBinary.assign(inputBinary.begin(), inputBinary.end());
for (const SizeT dimWordOffset : rectDimWordOffsets) {
outputBinary[dimWordOffset] = static_cast<Uint32>(spv::Dim::Dim2D);
}
// The rectangle capabilities describe types that no longer exist. Shader is always
// declared by a graphics module, so restating it keeps the word count intact
// without leaving a capability SPIRV-Cross would key off.
for (const SizeT capabilityWordOffset : rectCapabilityWordOffsets) {
outputBinary[capabilityWordOffset] = static_cast<Uint32>(spv::Capability::Shader);
}
return true;
}
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
@@ -43,6 +43,16 @@ namespace MobileGL {
// devices lacking GL_NV_shader_noperspective_interpolation. See EmulateNoPerspectivePass.
static bool EmulateNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Rewrites rectangle images (Dim::Rect) to plain 2D so SPIRV-Cross can emit ESSL
// for them at all - it refuses outright ("Rectangle textures are not supported on
// OpenGL ES"), which left the whole program unlinkable. Only valid while every use
// of the image takes integer texel coordinates (texelFetch / textureSize), where a
// rectangle target and a 2D target are indistinguishable; a normalized-coordinate
// lookup would also need its coordinates divided by the texture size, so the pass
// declines those modules instead of emitting something subtly wrong. Returns false
// when it changed nothing or cannot safely convert. DirectGLES only.
static bool LowerRectImagesForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so
// shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
// backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex,
@@ -688,6 +688,10 @@ namespace {
return info;
}
// Stamped onto the normalized directive when a legacy (or absent) desktop
// version was rewritten to 330; consumed by RetargetLegacyVersionDirectiveTo460.
constexpr const char* kNormalizedLegacyMarker = "/*mobilegl-normalized-legacy*/";
MobileGL::String GetNormalizedVersionDirective(const ShaderLanguageInfo& info) {
if (info.profile == MobileGL::ShaderProfile::ES) {
// Preserve the pre-existing behavior for standard lowercase "es" directives. MobileGL's Vulkan
@@ -702,9 +706,23 @@ namespace {
return "#version 460 compatibility\n";
}
// An explicitly declared modern core version keeps its number: the GL CTS
// negative-compile cases (reserved names, layout-qualifier forms, missing
// overloads) rely on the declared version's rules, and raising it would
// silently legalize them. gpu_shader5 opt-ins keep the 460 escalation -
// Vulkan glslang's ARB_gpu_shader5 support is not complete enough alone.
if (info.hasValidVersionDirective && info.version >= 330 && !info.enablesGpuShader5) {
return "#version " + std::to_string(info.version) + " core\n";
}
const bool useLegacyDesktopVersion =
info.version < 400 && !info.enablesGpuShader5;
return useLegacyDesktopVersion ? "#version 330 core\n" : "#version 460 core\n";
// The trailing marker records that this 330 came from a legacy declaration
// (or none at all), so the compile-failure retry may re-raise it to 460.
// An application's own "#version 330" never carries it and keeps strict
// 3.30 semantics.
return useLegacyDesktopVersion ? MobileGL::String("#version 330 core ") + kNormalizedLegacyMarker + "\n"
: "#version 460 core\n";
}
void NormalizeVersionDirective(MobileGL::String& source, const ShaderLanguageInfo& info) {
@@ -1306,12 +1324,123 @@ namespace MobileGL {
// Only the set NormalizeVersionDirective downgraded: desktop core below 400. ES and
// compatibility shaders keep whatever they declared.
if (info.profile != ShaderProfile::Core || info.version >= 400) return false;
// Only rescue MobileGL's own legacy normalization (marked on the directive line).
// An application-declared "#version 330" keeps strict 3.30 semantics: raising it
// would re-legalize the CTS negative-compile cases (reserved names, arrays of
// arrays, missing overloads).
SizeT lineEnd = source.find('\n', info.versionDirectiveStart);
if (lineEnd == MobileGL::String::npos) {
lineEnd = source.size();
}
const SizeT markerPos = source.find(kNormalizedLegacyMarker, info.versionDirectiveStart);
if (markerPos == MobileGL::String::npos || markerPos > lineEnd) {
return false;
}
source.replace(info.versionDirectiveStart, info.versionDirectiveEnd - info.versionDirectiveStart,
"#version 460 core\n");
return true;
}
std::optional<String> FindReservedIdentifierViolation(const String& source) {
// Reserved anywhere; glslang accepts them as plain identifiers.
static constexpr const char* kAlwaysReserved[] = {
"image1DShadow",
"image2DShadow",
"image1DArrayShadow",
"image2DArrayShadow",
};
// Keywords legal only inside a layout(...) qualifier list.
static constexpr const char* kLayoutOnlyKeywords[] = {
"packed",
"row_major",
};
const auto isIdentChar = [](char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
};
const SizeT length = source.size();
SizeT i = 0;
Int layoutParenDepth = 0; // >0 while inside layout(...)
Bool pendingLayoutParen = false; // saw "layout", awaiting its '('
while (i < length) {
const char c = source[i];
// Comments.
if (c == '/' && i + 1 < length && source[i + 1] == '/') {
while (i < length && source[i] != '\n') ++i;
continue;
}
if (c == '/' && i + 1 < length && source[i + 1] == '*') {
i += 2;
while (i + 1 < length && !(source[i] == '*' && source[i + 1] == '/')) ++i;
i = (i + 1 < length) ? i + 2 : length;
continue;
}
// Preprocessor lines stay out of scope (macro names may shadow anything).
if (c == '#' && (i == 0 || source[i - 1] == '\n' ||
source.find_last_not_of(" \t", i - 1) == MobileGL::String::npos ||
source[source.find_last_not_of(" \t", i - 1)] == '\n')) {
while (i < length && source[i] != '\n') {
if (source[i] == '\\' && i + 1 < length && source[i + 1] == '\n') ++i;
++i;
}
continue;
}
if (c == '(') {
if (pendingLayoutParen) {
layoutParenDepth = 1;
pendingLayoutParen = false;
} else if (layoutParenDepth > 0) {
++layoutParenDepth;
}
++i;
continue;
}
if (c == ')') {
if (layoutParenDepth > 0) --layoutParenDepth;
++i;
continue;
}
if (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
++i;
continue;
}
if (isIdentChar(c) && !(c >= '0' && c <= '9')) {
const SizeT start = i;
while (i < length && isIdentChar(source[i])) ++i;
const StringView word(source.data() + start, i - start);
if (word == "layout") {
pendingLayoutParen = true;
continue;
}
pendingLayoutParen = false;
for (const char* reserved : kAlwaysReserved) {
if (word == reserved) {
return String("ERROR: reserved identifier '") + reserved + "' may not be used.";
}
}
if (layoutParenDepth == 0) {
for (const char* keyword : kLayoutOnlyKeywords) {
if (word == keyword) {
return String("ERROR: '") + keyword +
"' is a keyword and may not be used as an identifier.";
}
}
}
continue;
}
if (isIdentChar(c)) { // digit-led token: skip the whole number/identifier tail
while (i < length && isIdentChar(source[i])) ++i;
pendingLayoutParen = false;
continue;
}
pendingLayoutParen = false;
++i;
}
return std::nullopt;
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -40,6 +40,11 @@ namespace MobileGL {
// uses 420-era syntax without the matching #extension line, which real drivers tend to
// accept - can be retried instead of failing to compile.
Bool RetargetLegacyVersionDirectiveTo460(String& source);
// GLSL reserves a few names glslang happily accepts as identifiers ("packed",
// "row_major" outside a layout(...) list, the image*Shadow family). Returns the
// compile-error text for the first violation, or nullopt for a clean source.
std::optional<String> FindReservedIdentifierViolation(const String& source);
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -95,6 +95,7 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
Int32,
Half,
Float32,
UNorm32, // 32-bit fixed-point depth shadow
};
struct InternalShadowLayout {
@@ -123,6 +124,21 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
Bool GetInternalShadowLayout(TextureInternalFormat internal, InternalShadowLayout& out) {
switch (internal) {
// Depth shadows follow TextureFormatProcessor::NormalizePixelFormat: 16-bit
// unorm for DEPTH_COMPONENT16, 32-bit unorm for the 24/32-bit fixed-point
// depths, float for DEPTH_COMPONENT32F.
case TextureInternalFormat::DepthComponent16:
out = {1, ShadowComponent::UNorm16, false};
return true;
case TextureInternalFormat::DepthComponent24:
case TextureInternalFormat::DepthComponent32:
case TextureInternalFormat::DepthComponent:
out = {1, ShadowComponent::UNorm32, false};
return true;
case TextureInternalFormat::DepthComponent32F:
out = {1, ShadowComponent::Float32, false};
return true;
case TextureInternalFormat::R8:
case TextureInternalFormat::Red: out = {1, ShadowComponent::UNorm8, false}; return true;
case TextureInternalFormat::RG8:
@@ -299,8 +315,10 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
case TextureInputFormat::RGBAInteger: out = {{0, 1, 2, 3}, 4, true}; return true;
case TextureInputFormat::BGRA: out = {{2, 1, 0, 3}, 4, false}; return true;
case TextureInputFormat::BGRAInteger: out = {{2, 1, 0, 3}, 4, true}; return true;
// A depth value converts like a single normalized/float channel.
case TextureInputFormat::DepthComponent: out = {{0, -1, -1, -1}, 1, false}; return true;
default:
return false; // depth / stencil / unknown
return false; // stencil / packed depth-stencil / unknown
}
}
@@ -346,8 +364,7 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
out = isInteger ? ShadowComponent::Int16 : ShadowComponent::SNorm16;
return true;
case TexturePixelDataType::UnsignedInt:
if (!isInteger) return false; // no 32-bit normalized shadow layout
out = ShadowComponent::UInt32;
out = isInteger ? ShadowComponent::UInt32 : ShadowComponent::UNorm32;
return true;
case TexturePixelDataType::Int:
if (!isInteger) return false;
@@ -617,6 +634,12 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
case ShadowComponent::Float32:
Memcpy(dst, &v, sizeof(v));
break;
case ShadowComponent::UNorm32: {
const auto out = static_cast<Uint32>(
std::llround(static_cast<double>(std::clamp(v, 0.0f, 1.0f)) * 4294967295.0));
Memcpy(dst, &out, sizeof(out));
break;
}
default:
break; // integer components never reach the float encoder
}
@@ -771,6 +794,64 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
const Int effectiveHeight = (params.ImageHeight > 0) ? params.ImageHeight : height;
const SizeT inputRowStride = CalculateRowStride(effectiveWidth, pixelSize, params.Alignment);
// GL_DEPTH_COMPONENT client data may populate a packed depth-stencil internal
// format (the stencil half becomes zero); the generic channel converter cannot
// express the packed shadow words, so convert here.
const Bool packedDepthStencilInternal = targetInternalFormat == TextureInternalFormat::Depth24Stencil8 ||
targetInternalFormat == TextureInternalFormat::DepthStencil ||
targetInternalFormat == TextureInternalFormat::Depth32FStencil8;
if (!isBitmap && packedDepthStencilInternal && textureInputFormat == TextureInputFormat::DepthComponent &&
(inputDataType == TexturePixelDataType::Float || inputDataType == TexturePixelDataType::UnsignedInt ||
inputDataType == TexturePixelDataType::UnsignedShort)) {
const Bool floatShadow = targetInternalFormat == TextureInternalFormat::Depth32FStencil8;
const SizeT outPixelSize = floatShadow ? 8 : 4;
outSize = static_cast<SizeT>(width) * height * std::max(depth, 1) * outPixelSize;
Uint8* outputPixels = static_cast<Uint8*>(malloc(outSize));
if (!outputPixels) {
outSize = 0;
return nullptr;
}
const Uint8* srcBase = static_cast<const Uint8*>(inputPixels) +
static_cast<SizeT>(params.SkipImages) * static_cast<SizeT>(effectiveHeight) * inputRowStride +
static_cast<SizeT>(params.SkipRows) * inputRowStride +
static_cast<SizeT>(params.SkipPixels) * pixelSize;
Uint8* dst = outputPixels;
for (Int z = 0; z < std::max(depth, 1); ++z) {
for (Int y = 0; y < height; ++y) {
const Uint8* srcRow = srcBase +
static_cast<SizeT>(z) * static_cast<SizeT>(effectiveHeight) * inputRowStride +
static_cast<SizeT>(y) * inputRowStride;
for (Int x = 0; x < width; ++x) {
Float depthValue = 0.0f;
if (inputDataType == TexturePixelDataType::Float) {
Memcpy(&depthValue, srcRow + static_cast<SizeT>(x) * 4, sizeof(depthValue));
} else if (inputDataType == TexturePixelDataType::UnsignedInt) {
Uint32 raw = 0;
Memcpy(&raw, srcRow + static_cast<SizeT>(x) * 4, sizeof(raw));
depthValue = static_cast<Float>(static_cast<double>(raw) / 4294967295.0);
} else {
Uint16 raw = 0;
Memcpy(&raw, srcRow + static_cast<SizeT>(x) * 2, sizeof(raw));
depthValue = static_cast<Float>(raw) / 65535.0f;
}
if (floatShadow) {
const Uint32 stencilWord = 0;
Memcpy(dst, &depthValue, sizeof(depthValue));
Memcpy(dst + 4, &stencilWord, sizeof(stencilWord));
dst += 8;
} else {
const Uint32 depth24 = static_cast<Uint32>(
std::llround(static_cast<double>(std::clamp(depthValue, 0.0f, 1.0f)) * 16777215.0));
const Uint32 word = depth24 << 8;
Memcpy(dst, &word, sizeof(word));
dst += 4;
}
}
}
}
return outputPixels;
}
UnpackConversionSpec conversion{};
const Bool needConversion =
!isBitmap && GetUnpackConversionSpec(targetInternalFormat, textureInputFormat, inputDataType, conversion);
@@ -972,6 +1053,11 @@ namespace MobileGL::MG_Util::PixelStoreProcessor {
Memcpy(&v, p, sizeof(v));
return v;
}
case ShadowComponent::UNorm32: {
Uint32 v;
Memcpy(&v, p, sizeof(v));
return static_cast<Float>(static_cast<double>(v) / 4294967295.0);
}
default:
return 0.0f;
}
@@ -29,11 +29,16 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
case GL_RGB12: // stored as RGB16 (see NormalizePixelFormat)
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16;
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRgb16;
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
break;
case GL_RGB16_SNORM:
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRGB16Snorm;
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16;
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16;
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget;
}
break;
case GL_RGBA16_SNORM:
case GL_RG16_SNORM:
@@ -46,6 +51,9 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm;
break;
case GL_RGB8_SNORM:
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget;
break;
case GL_RG8_SNORM:
case GL_R8_SNORM:
applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8;
@@ -66,7 +74,15 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
switch (internalFormat) {
case GL_DEPTH_COMPONENT32:
if (options & PixelFormatNormalizeOptionBit::NoDepthComponent32) {
*outInternalFormat = GL_DEPTH_COMPONENT;
// The unsized GL_DEPTH_COMPONENT base format is not a legal
// glTexStorage/glRenderbufferStorage internal format on ES, which left
// the attachment with no storage at all (KHR-GL3x.framebuffer_blit's
// GL_DEPTH_COMPONENT32 config then read an incomplete framebuffer).
// GL_DEPTH_COMPONENT24 is the nearest sized ES format that keeps the
// same fixed-point encoding, so the GL_UNSIGNED_INT transfer type below
// still describes the data; GL_DEPTH_COMPONENT32F would need a float
// conversion the upload path does not apply.
*outInternalFormat = GL_DEPTH_COMPONENT24;
break;
}
*outInternalFormat = internalFormat;
@@ -79,6 +95,13 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
*outInternalFormat = internalFormat;
break;
case GL_RGB16:
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
// GL_RGB32F is a legal ES texture format but is not colour-renderable, so
// glTexStorage2DMultisample rejects it and the attachment ends up with no
// storage at all.
*outInternalFormat = GL_RGBA32F;
break;
}
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
(options & PixelFormatNormalizeOptionBit::NoRgb16)) {
*outInternalFormat = GL_RGB32F;
@@ -109,6 +132,14 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
*outInternalFormat = internalFormat;
break;
case GL_RGB16_SNORM:
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
// A half float loses the low bits of a 16-bit SNORM channel, so keep the
// signed-normalized encoding whenever the driver can render to it.
*outInternalFormat = (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget)
? GL_RGBA16F
: GL_RGBA16_SNORM;
break;
}
if ((options & PixelFormatNormalizeOptionBit::NoNorm16) ||
(options & PixelFormatNormalizeOptionBit::NoRGB16Snorm) ||
(options & PixelFormatNormalizeOptionBit::NoSnorm16)) {
@@ -142,6 +173,10 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
*outInternalFormat = internalFormat;
break;
case GL_RGB8_SNORM:
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
*outInternalFormat = GL_RGBA16F;
break;
}
if (options & PixelFormatNormalizeOptionBit::NoSnorm8) {
*outInternalFormat = GL_RGB16F;
break;
@@ -552,6 +587,8 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
*outType = GL_UNSIGNED_INT;
break;
case GL_DEPTH_COMPONENT32:
// Follows the internal-format normalization above: ES only accepts
// GL_FLOAT data for a GL_DEPTH_COMPONENT32F store.
*outType = GL_UNSIGNED_INT;
break;
case GL_DEPTH_COMPONENT32F:
@@ -18,6 +18,17 @@ namespace MobileGL {
NoDepthComponent32 = 1 << 4,
NoRGBA8Snorm = 1 << 5,
NoRGB16Snorm = 1 << 6,
// The target must be colour-renderable and ES has no renderable three-channel
// form of the requested format, so it has to be widened to the four-channel one.
// Only meaningful for multisample textures: those can never be uploaded to, only
// rendered into, so the extra alpha comes from the draw (1.0 for an RGB source)
// and no transfer path has to expand three-channel client data.
NoThreeChannelRenderTarget = 1 << 7,
// Pairs with the bit above: the widened four-channel format has to stay renderable AND
// keep 16-bit signed-normalized precision, which needs both EXT_texture_norm16 and
// EXT_render_snorm. Without them the only renderable widening left is a half float, whose
// 11-bit mantissa cannot represent a 16-bit SNORM channel exactly.
NoSnorm16RenderTarget = 1 << 8,
None = 0,
};
namespace MG_Util::TextureFormatProcessor {
+13 -1
View File
@@ -181,7 +181,19 @@ namespace MobileGL {
explicit BindingSlotRange1D(TargetEnum target, const Range1D& range = Range1D())
: BindingSlot<ObjectType>(target), m_range(range) {}
Range1D GetRange() const { return m_range; }
// The range the binding actually covers right now. A whole-buffer binding
// (glBindBufferBase) does not freeze anything: GL resolves it against the
// object's size at every use, so a glBufferData issued after the bind has to
// be visible here - binding an empty buffer and giving it storage afterwards
// is ordinary application code. Only glBindBufferRange pins a fixed window.
Range1D GetRange() const {
if (!m_hasExplicitRange) {
if (const auto& object = this->GetBoundObject()) {
return Range1D(0, object->GetSize());
}
}
return m_range;
}
Bool HasExplicitRange() const { return m_hasExplicitRange; }
+1 -1
View File
@@ -55,7 +55,7 @@ val releaseSigningReady = signingStoreFile.exists()
val debuggableRelease = (findProperty("mobilegl.debuggableRelease") ?: "false").toString().toBoolean()
val mobileGlVersionMajor = 26
val mobileGlVersionMinor = 7
val mobileGlVersionMinor = 8
val mobileGlGitShortHash = runGit("rev-parse", "--short=7", "HEAD") ?: "nogit"
val mobileGlMonthlyRevision = runGit(
"rev-list",
@@ -1,78 +0,0 @@
# MobileGL POST Format Capability Tables
## Goal
Expose the format-capability results used during MobileGL backend startup in the Android plugin's driver POST screen. The screen must show the exact `Full`, `Caveat`, or `None` result for every backend, target, internal format, and capability without duplicating the backend's detection rules.
## Existing Architecture
- `DriverPost.cpp` probes the device GLES and Vulkan drivers before `MobileGL::Initialize()` and returns a `BackendPostReport` for each backend.
- `DriverPostJni.cpp` serializes those reports to JSON for `PostActivity`.
- `PostActivity` uses platform Android views and already supports collapsible check details and a collapsible raw report.
- Backend startup fills a `FormatCapabilityCache` in the DirectGLES and DirectVulkan `InitCapabilities()` paths. `FullCaps` takes precedence over `CaveatCaps`; an absent bit means `None`.
- The capability matrix contains 12 targets, 75 internal formats, and 14 capability columns per backend.
## Selected Approach
Extract callable format-probe entry points from the existing DirectGLES and DirectVulkan implementations. Backend startup and the POST will call these same functions, so their results cannot drift.
The POST will run each probe while its temporary driver resources are still valid:
- DirectGLES: after the GLES function table and capabilities have been populated, while the 1x1 pbuffer context is current.
- DirectVulkan: after selecting the physical device, while the Vulkan instance and physical device handles remain valid.
The resulting optional `FormatCapabilityCache` will be stored in each `BackendPostReport`. Failure to obtain a format table will not discard the existing POST checks or change their verdict; the UI will instead report that the format table is unavailable.
## JSON Contract
The JNI report will add an optional `formatCapabilities` object to each backend. To avoid repeating tens of thousands of status strings, the object will contain:
- one ordered capability-name array;
- one entry per target;
- one compact row per internal format containing the format name, a Full bitmask, and a Caveat bitmask.
Java resolves each cell in this order:
1. Full bit present: `Full`.
2. Otherwise Caveat bit present: `Caveat`.
3. Otherwise: `None`.
This preserves the backend's current precedence and keeps the raw JSON reasonably small.
## Android UI
Each backend section keeps its existing verdict, renderer string, and check table. A new `Format capabilities` subsection follows it.
- Each of the 11 texture targets and `Renderbuffer` is a separate, initially collapsed table.
- Target headers can be expanded independently.
- Table content is created on expansion and removed when collapsed, preventing the activity from retaining roughly 27,000 status views.
- Each expanded table is placed in a horizontal scroll container.
- The first column contains internal-format names. The remaining columns use the ordered capability names from the JSON report.
- Every status cell displays its status text and uses the conventional color mapping:
- `Full`: green background with white text.
- `Caveat`: yellow background with black text.
- `None`: red background with white text.
- Header and format-name cells use neutral dark backgrounds consistent with the existing POST theme.
- The existing raw-report toggle remains available at the end of the screen.
## Performance and Lifecycle
- The existing single-flight native POST and cached JSON behavior remains unchanged.
- Format tables are lazily materialized and discarded on collapse.
- The JSON carries bitmasks rather than repeated `Full`, `Caveat`, and `None` strings.
- Existing configuration-change handling remains unchanged.
## Validation
1. Run focused source checks and `git diff --check`.
2. Build the Android plugin APK with the repository's current Gradle workflow.
3. If an Android target is connected, install the APK and open `PostActivity`.
4. Verify both backend sections, all target toggles, horizontal scrolling, visible cell text, and the green/yellow/red mapping.
5. Confirm collapsing a table removes its generated content and expanding it recreates the same values.
## Non-Goals
- Changing the meanings of `Full`, `Caveat`, or `None`.
- Changing POST verdict rules.
- Displaying sample-count vectors in this iteration.
- Replacing the existing platform-view UI with Compose, AppCompat, or WebView.
+28 -1
View File
@@ -1,4 +1,31 @@
# Running the OpenGL CTS (VK-GL-CTS / KHR-GL33) against MobileGL on Android
# Running the OpenGL CTS against MobileGL
This directory contains two supported paths:
- Android arm64 / MobileGL EGL: the KHR-GL33 workflow documented below and in
`skills/gl-cts-on-mobilegl/SKILL.md`.
- Windows x64 / MobileGL WGL: the GL30-GL46 pipeline in
`scripts/wgl_glcts_pipeline.py`, documented by
`skills/wgl-gl-cts-on-mobilegl/SKILL.md`.
Windows prerequisites are Git, Python 3.9+, CMake, Visual Studio 2022's Desktop
C++ workload, and a Vulkan SDK visible to CMake. DirectVulkan also needs a
working Vulkan loader plus a GPU-vendor ICD and driver; the SDK alone is not a
GPU driver.
For Windows, start with:
```powershell
python tools\cts\scripts\wgl_glcts_pipeline.py --help
```
The pipeline builds MobileGL as a drop-in `opengl32.dll`, builds or reuses
`glcts.exe`, checks that WGL loaded MobileGL rather than the system driver,
resumes individual suites after crashes/timeouts, and writes Markdown plus JSON
reports below the printed `runs/<first-16-of-run-fingerprint>` directory. Its
manifest records provenance and the runner settings used to validate a resume.
## Android KHR-GL33 workflow
Goal: measure how much of the OpenGL 3.3 core-profile conformance suite MobileGL
passes, separately for each backend (`DirectGLES`, `DirectVulkan`).
+33 -7
View File
@@ -1,5 +1,5 @@
/*-------------------------------------------------------------------------
* dEQP platform port for MobileGL on Android
* dEQP platform port for MobileGL (Android and desktop Linux)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,14 +28,17 @@
* - A real surface is always created. MobileGL rejects EGL_NO_SURFACE with
* EGL_BAD_MATCH, and --deqp-surface-type=fbo asks the platform for
* SURFACETYPE_DONT_CARE, so "no surface" is not an option.
* - Window surfaces are backed by an AImageReader rather than an Activity,
* which is what lets the suite run as a plain adb-shell binary. DirectVulkan
* needs this: its pbuffer path requires VK_EXT_headless_surface, which
* Adreno's Android driver does not expose.
* - On Android, window surfaces are backed by an AImageReader rather than an
* Activity, which is what lets the suite run as a plain adb-shell binary.
* DirectVulkan needs this: its pbuffer path requires VK_EXT_headless_surface,
* which Adreno's Android driver does not expose.
* - On desktop Linux, only pbuffer surfaces are offered. DirectVulkan's
* pbuffer path works there because desktop Vulkan loaders expose
* VK_EXT_headless_surface.
*
* Environment:
* MOBILEGL_CTS_LIB path/soname of the MobileGL library (default libMobileGL.so)
* MOBILEGL_CTS_SURFACE "window" (default) or "pbuffer"
* MOBILEGL_CTS_SURFACE "window" (Android default) or "pbuffer" (desktop default/only)
* MOBILEGL_BACKEND_TYPE read by MobileGL itself; set it before launching
*//*--------------------------------------------------------------------*/
@@ -58,9 +61,11 @@
#include "tcuPlatform.hpp"
#include "tcuRenderTarget.hpp"
#if defined(__ANDROID__)
#include <android/hardware_buffer.h>
#include <android/native_window.h>
#include <media/NdkImageReader.h>
#endif
using std::string;
using std::vector;
@@ -88,13 +93,24 @@ static string getLibraryName(void)
return (env && env[0]) ? string(env) : string("libMobileGL.so");
}
//! Window surfaces default on: they are the only kind DirectVulkan can use.
#if defined(__ANDROID__)
//! Window surfaces default on: they are the only kind DirectVulkan can use
//! on Android (its pbuffer path needs VK_EXT_headless_surface).
static bool useWindowSurface(void)
{
const char *env = std::getenv("MOBILEGL_CTS_SURFACE");
return !(env && string(env) == "pbuffer");
}
#else
//! Desktop: pbuffer only. VK_EXT_headless_surface is available there and no
//! Activity-free native window abstraction exists.
static bool useWindowSurface(void)
{
return false;
}
#endif
#if defined(__ANDROID__)
/*--------------------------------------------------------------------*//*!
* \brief A real ANativeWindow with no Activity behind it.
*
@@ -160,6 +176,12 @@ private:
AImageReader *m_reader;
ANativeWindow *m_window;
};
#else
//! Never instantiated on desktop; keeps EglRenderContext's member deletable.
class ImageReaderWindow
{
};
#endif
class GetProcFuncLoader : public glw::FunctionLoader
{
@@ -352,6 +374,7 @@ EglRenderContext::EglRenderContext(const glu::RenderConfig &config, const tcu::C
if (wantWindow)
{
#if defined(__ANDROID__)
m_window = new ImageReaderWindow(width, height);
eglw::EGLint visualId = 0;
@@ -361,6 +384,9 @@ EglRenderContext::EglRenderContext(const glu::RenderConfig &config, const tcu::C
m_eglSurface = m_egl.createWindowSurface(m_eglDisplay, eglConfig,
(eglw::EGLNativeWindowType)m_window->getWindow(), nullptr);
EGLU_CHECK_MSG(m_egl, "eglCreateWindowSurface()");
#else
throw tcu::NotSupportedError("Window surfaces are not supported by the desktop MobileGL platform");
#endif
}
else
{
+545
View File
@@ -0,0 +1,545 @@
#!/usr/bin/env python
"""Build a GL 3.0--3.3 CTS conformance matrix from dEQP QPA logs.
The report deliberately scores against the unique cases in each supplied
caselist. A case that has not produced a result therefore cannot disappear
from the denominator and make a partial run look conformant.
QPA parsing and crash/hang sidecar handling follow :mod:`qpa_report`:
* a later QPA observation of a case wins;
* ``crashed.txt`` upgrades a missing/incomplete result to ``Crash``;
* ``hung.txt`` upgrades a missing/incomplete/crash result to ``DeviceHang``.
Example::
python cts_matrix_report.py \
--gl30-caselist gl30-main.txt --gl30-results runs/gl30 \
--gl31-caselist gl31-main.txt --gl31-results runs/gl31 \
--gl32-caselist gl32-main.txt --gl32-results runs/gl32 \
--gl33-caselist gl33-main.txt --gl33-results runs/gl33 \
--json runs/cts-matrix.json
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from collections import Counter, defaultdict
from datetime import datetime, timezone
from typing import Iterable, Optional, Sequence
try: # Works both as a directly executed script and as a package import.
from . import qpa_report
except ImportError: # pragma: no cover - exercised by the command-line tests
import qpa_report
VERSIONS = ("gl30", "gl31", "gl32", "gl33")
ACCEPTED_STATUSES = (
"Pass",
"NotSupported",
"QualityWarning",
"CompatibilityWarning",
"Waiver",
)
ACCEPTED = frozenset(ACCEPTED_STATUSES)
CHUNK_QPA = re.compile(r"^chunk(\d+)\.qpa$", re.IGNORECASE)
class ReportInputError(ValueError):
"""An input path cannot be used to construct a meaningful report."""
def _read_non_comment_lines(path: str) -> list[str]:
try:
# Match run_cts_windows.py: Khronos lists are UTF-8 and may carry a BOM.
with open(path, "r", encoding="utf-8-sig", errors="strict") as fh:
return [
line.strip()
for line in fh
if line.strip() and not line.lstrip().startswith("#")
]
except (OSError, UnicodeError) as exc:
raise ReportInputError(f"cannot read {path}: {exc}") from exc
def read_caselist(path: str) -> tuple[list[str], dict[str, int]]:
"""Return unique cases in file order and repeated caselist entries.
The mustpass files consumed by glcts and ``run_cts.py`` are one case per
non-empty, non-comment line, so this intentionally uses the same syntax.
"""
entries = _read_non_comment_lines(path)
counts = Counter(entries)
unique = list(dict.fromkeys(entries))
duplicates = {case: count for case, count in counts.items() if count > 1}
return unique, duplicates
def _collect_qpa_files(paths: Sequence[str]) -> list[str]:
missing = [path for path in paths if not os.path.exists(path)]
if missing:
raise ReportInputError(
"result path(s) do not exist: " + ", ".join(sorted(missing))
)
# qpa_report.collect provides the established directory-recursion rules.
# De-duplicate aliases so specifying the same directory twice does not
# manufacture duplicate observations.
files = qpa_report.collect(paths)
by_identity: dict[str, str] = {}
for path in files:
if not os.path.isfile(path):
raise ReportInputError(f"QPA input is not a file: {path}")
absolute = os.path.abspath(path)
by_identity.setdefault(os.path.normcase(absolute), absolute)
def order_key(value: str) -> tuple[str, str, int, str]:
absolute = os.path.abspath(value)
directory = os.path.normcase(os.path.dirname(absolute))
filename = os.path.normcase(os.path.basename(absolute))
match = CHUNK_QPA.fullmatch(filename)
if match:
# run_cts_windows.py uses a minimum width of four digits, not a
# fixed width. Numeric ordering is therefore required once a run
# reaches chunk10000; lexical ordering would put it before
# chunk9999 and break the later-observation-wins rule.
return directory, "chunk", int(match.group(1)), filename
# Preserve a deterministic, name-based position for foreign/legacy
# QPA files while grouping numeric runner chunks at the lexical
# position occupied by the "chunk" basename.
return directory, filename, -1, filename
return sorted(by_identity.values(), key=order_key)
def _ratio(numerator: int, denominator: int) -> float:
return numerator / denominator if denominator else 0.0
def _sidecar(paths: Sequence[str], name: str) -> set[str]:
"""Load a run_cts.py sidecar with qpa_report-compatible lookup rules."""
return qpa_report.load_sidecar(paths, name)
def build_version_report(
version: str, caselist: str, result_paths: Sequence[str]
) -> dict:
"""Build the serialisable report for one GL mustpass version."""
expected_cases, expected_duplicates = read_caselist(caselist)
expected = set(expected_cases)
qpa_files = _collect_qpa_files(result_paths)
results: dict[str, str] = {}
observation_history: dict[str, list[dict[str, str]]] = defaultdict(list)
for qpa_file in qpa_files:
for case, status in qpa_report.parse_qpa(qpa_file):
observation_history[case].append(
{"file": qpa_file, "status": status}
)
results[case] = status
crashed = _sidecar(result_paths, "crashed.txt")
hung = _sidecar(result_paths, "hung.txt")
explicit_unrun = _sidecar(result_paths, "unrun.txt")
skipped = _sidecar(result_paths, "skipped.txt")
# Keep this order and these guards in lock-step with qpa_report.py.
for case in crashed:
if results.get(case, "Incomplete") == "Incomplete":
results[case] = "Crash"
for case in hung:
if results.get(case, "Incomplete") in ("Incomplete", "Crash"):
results[case] = "DeviceHang"
# A begin/end pair without <Result>, or a QPA truncated mid-case, is not a
# completed observation. Sidecars above may upgrade it to Crash/Hang;
# anything still Incomplete must stay in the expected denominator as unrun.
incomplete_results = {
case for case, status in results.items() if status == "Incomplete"
}
for case in incomplete_results:
del results[case]
expected_results = {
case: status for case, status in results.items() if case in expected
}
unexpected_results = {
case: status for case, status in results.items() if case not in expected
}
# Missing cases are inferred from the caselist even if unrun.txt itself is
# missing or stale. This is the invariant that prevents partial-run rate
# inflation.
unrun_cases = expected - set(expected_results)
declared_not_measured = explicit_unrun | skipped
undeclared_unrun = unrun_cases - declared_not_measured
stale_unrun = (explicit_unrun | skipped) & set(expected_results)
counts = Counter(expected_results.values())
strict_pass = counts["Pass"]
accepted = sum(counts[status] for status in ACCEPTED)
result_count = len(expected_results)
expected_count = len(expected)
crash_count = counts["Crash"]
hang_count = counts["DeviceHang"]
duplicate_cases = {
case: {
"observations": len(history),
"extra_observations": len(history) - 1,
"final_status": results.get(case, "Incomplete"),
"history": history,
}
for case, history in sorted(observation_history.items())
if len(history) > 1
}
duplicate_observations = sum(
item["extra_observations"] for item in duplicate_cases.values()
)
sidecar_unknown = {
name: sorted(cases - expected)
for name, cases in (
("crashed.txt", crashed),
("hung.txt", hung),
("unrun.txt", explicit_unrun),
("skipped.txt", skipped),
)
if cases - expected
}
errors: list[str] = []
warnings: list[str] = []
if not expected_count:
errors.append("caselist has no cases")
if expected_duplicates:
errors.append(
f"caselist has {sum(n - 1 for n in expected_duplicates.values())} "
"duplicate entry/entries"
)
if not qpa_files:
errors.append("no .qpa files found")
if unexpected_results:
errors.append(
f"{len(unexpected_results)} result case(s) are absent from the caselist"
)
if sidecar_unknown:
errors.append("one or more sidecars name cases absent from the caselist")
if undeclared_unrun:
errors.append(
f"{len(undeclared_unrun)} missing result case(s) are not declared by "
"unrun.txt/skipped.txt"
)
if stale_unrun:
warnings.append(
f"{len(stale_unrun)} case(s) declared unrun/skipped also have a result"
)
if duplicate_observations:
warnings.append(
f"{duplicate_observations} duplicate QPA observation(s); last result wins"
)
if incomplete_results:
warnings.append(
f"{len(incomplete_results)} QPA case(s) ended without a final result and were treated as unrun"
)
if errors:
state = "ERROR"
elif unrun_cases:
state = "INCOMPLETE"
else:
state = "OK"
return {
"version": version,
"inputs": {
"caselist": os.path.abspath(caselist),
"result_paths": [os.path.abspath(path) for path in result_paths],
"qpa_files": qpa_files,
},
"expected": expected_count,
"result": result_count,
"pass": strict_pass,
"accepted": accepted,
"crash": crash_count,
"hang": hang_count,
"unrun": len(unrun_cases),
"duplicate": duplicate_observations,
"counts": dict(sorted(counts.items())),
"coverage": {
"numerator": result_count,
"denominator": expected_count,
"rate": _ratio(result_count, expected_count),
},
"rates": {
# These are the report's conformance rates. Expected, not merely
# measured results, is the denominator.
"denominator": "expected",
"strict_pass_only": _ratio(strict_pass, expected_count),
"conformance_accepted": _ratio(accepted, expected_count),
# Useful for comparison with qpa_report.py, whose denominator is
# cases with a result. Never presented as the conformance rate.
"measured_only_strict_pass": _ratio(strict_pass, result_count),
"measured_only_conformance_accepted": _ratio(
accepted, result_count
),
},
"strict_pass_rate": _ratio(strict_pass, expected_count),
"conformance_accepted_rate": _ratio(accepted, expected_count),
"validation": {
"state": state,
"ok": state == "OK",
"errors": errors,
"warnings": warnings,
"invariant_expected_equals_result_plus_unrun": (
expected_count == result_count + len(unrun_cases)
),
"undeclared_unrun": sorted(undeclared_unrun),
"stale_unrun_or_skipped": sorted(stale_unrun),
"sidecar_cases_absent_from_caselist": sidecar_unknown,
},
"cases": {
"results": dict(sorted(expected_results.items())),
"unrun": sorted(unrun_cases),
"unexpected_results": dict(sorted(unexpected_results.items())),
"incomplete_results": sorted(incomplete_results),
"duplicate_results": duplicate_cases,
"duplicate_caselist_entries": dict(sorted(expected_duplicates.items())),
},
}
def build_matrix(suites: dict[str, tuple[str, Sequence[str]]]) -> dict:
"""Build all four version reports and their case-weighted aggregate."""
version_reports = {
version: build_version_report(version, *suites[version])
for version in VERSIONS
}
totals = {
key: sum(report[key] for report in version_reports.values())
for key in (
"expected",
"result",
"pass",
"accepted",
"crash",
"hang",
"unrun",
"duplicate",
)
}
status_counts: Counter[str] = Counter()
for report in version_reports.values():
status_counts.update(report["counts"])
states = {report["validation"]["state"] for report in version_reports.values()}
if "ERROR" in states:
overall_state = "ERROR"
elif "INCOMPLETE" in states:
overall_state = "INCOMPLETE"
else:
overall_state = "OK"
overall = {
**totals,
"counts": dict(sorted(status_counts.items())),
"aggregation": "weighted_by_expected_cases",
"coverage": {
"numerator": totals["result"],
"denominator": totals["expected"],
"rate": _ratio(totals["result"], totals["expected"]),
},
"rates": {
"denominator": "expected",
"strict_pass_only": _ratio(totals["pass"], totals["expected"]),
"conformance_accepted": _ratio(
totals["accepted"], totals["expected"]
),
"measured_only_strict_pass": _ratio(
totals["pass"], totals["result"]
),
"measured_only_conformance_accepted": _ratio(
totals["accepted"], totals["result"]
),
},
"strict_pass_rate": _ratio(totals["pass"], totals["expected"]),
"conformance_accepted_rate": _ratio(
totals["accepted"], totals["expected"]
),
"validation": {
"state": overall_state,
"ok": overall_state == "OK",
"invariant_expected_equals_result_plus_unrun": (
totals["expected"] == totals["result"] + totals["unrun"]
),
},
}
return {
"schema_version": 1,
"generated_at": datetime.now(timezone.utc).isoformat(),
"accepted_statuses": list(ACCEPTED_STATUSES),
"rate_policy": {
"denominator": "unique expected cases from each caselist",
"unrun_cases": "included in the denominator and never accepted",
"duplicate_results": "last QPA result wins, matching qpa_report.py",
},
"versions": version_reports,
"overall": overall,
}
def _percent(numerator: int, denominator: int) -> str:
if not denominator:
return "n/a"
return f"{100.0 * numerator / denominator:.2f}% ({numerator}/{denominator})"
def render_markdown(report: dict) -> str:
"""Render the compact terminal-facing conformance table."""
header = (
"| Suite | Expected | Result | Pass | Accepted | Crash | Hang | Unrun | "
"Duplicate | Coverage | Strict Pass-only | Conformance-accepted | Validation |"
)
separator = (
"|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|:---:|"
)
rows = [header, separator]
for version in VERSIONS:
item = report["versions"][version]
rows.append(
"| {version} | {expected} | {result} | {pass_count} | {accepted} | "
"{crash} | {hang} | {unrun} | {duplicate} | {coverage} | {strict} | "
"{accepted_rate} | {state} |".format(
version=version.upper(),
expected=item["expected"],
result=item["result"],
pass_count=item["pass"],
accepted=item["accepted"],
crash=item["crash"],
hang=item["hang"],
unrun=item["unrun"],
duplicate=item["duplicate"],
coverage=_percent(item["result"], item["expected"]),
strict=_percent(item["pass"], item["expected"]),
accepted_rate=_percent(item["accepted"], item["expected"]),
state=item["validation"]["state"],
)
)
overall = report["overall"]
rows.append(
"| **Overall (weighted)** | **{expected}** | **{result}** | **{pass_count}** | "
"**{accepted}** | **{crash}** | **{hang}** | **{unrun}** | **{duplicate}** | "
"**{coverage}** | **{strict}** | **{accepted_rate}** | **{state}** |".format(
expected=overall["expected"],
result=overall["result"],
pass_count=overall["pass"],
accepted=overall["accepted"],
crash=overall["crash"],
hang=overall["hang"],
unrun=overall["unrun"],
duplicate=overall["duplicate"],
coverage=_percent(overall["result"], overall["expected"]),
strict=_percent(overall["pass"], overall["expected"]),
accepted_rate=_percent(overall["accepted"], overall["expected"]),
state=overall["validation"]["state"],
)
)
rows.extend(
(
"",
"Rates use unique **Expected** caselist cases as the denominator; unrun cases "
"remain in that denominator and are not accepted.",
"Accepted statuses: " + ", ".join(f"`{s}`" for s in ACCEPTED_STATUSES) + ".",
"Duplicate is the number of extra QPA observations; the last observation wins.",
)
)
details: list[str] = []
for version in VERSIONS:
validation = report["versions"][version]["validation"]
messages = validation["errors"] + validation["warnings"]
if messages:
details.append(
f"- **{version.upper()} {validation['state']}**: " + "; ".join(messages)
)
if details:
rows.extend(("", "Validation details:", "", *details))
return "\n".join(rows)
def _write_json(path: str, report: dict) -> None:
parent = os.path.dirname(os.path.abspath(path))
os.makedirs(parent, exist_ok=True)
with open(path, "w", encoding="utf-8", newline="\n") as fh:
json.dump(report, fh, indent=2, sort_keys=True)
fh.write("\n")
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
for version in VERSIONS:
parser.add_argument(
f"--{version}-caselist",
f"--{version}-case-list",
required=True,
help=f"{version.upper()} mustpass caselist",
)
parser.add_argument(
f"--{version}-results",
f"--{version}-result-dir",
f"--{version}-results-dir",
action="append",
required=True,
help=f"{version.upper()} result directory or QPA file (repeatable)",
)
parser.add_argument(
"--json",
dest="json_out",
default="cts_matrix_report.json",
help="JSON output path (default: ./cts_matrix_report.json)",
)
parser.add_argument(
"--allow-incomplete",
action="store_true",
help="return success even when validation is ERROR/INCOMPLETE",
)
return parser
def main(argv: Optional[Iterable[str]] = None) -> int:
args = _parser().parse_args(argv)
suites = {
version: (
getattr(args, f"{version}_caselist"),
getattr(args, f"{version}_results"),
)
for version in VERSIONS
}
try:
report = build_matrix(suites)
_write_json(args.json_out, report)
except (OSError, ReportInputError) as exc:
print(f"cts_matrix_report: {exc}", file=sys.stderr)
return 2
print(render_markdown(report))
print(f"\nJSON: {os.path.abspath(args.json_out)}")
if not args.allow_incomplete and not report["overall"]["validation"]["ok"]:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
+540
View File
@@ -0,0 +1,540 @@
#!/usr/bin/env python
"""Summarise any number of GL CTS suites and MobileGL backends.
Each repeatable suite specification consists of four values: backend, label,
caselist, and result directory. For example::
python cts_multi_report.py \
--suite DirectGLES gl30 gl30-main.txt runs/gles/gl30 \
--suite DirectVulkan gl30 gl30-main.txt runs/vulkan/gl30 \
--markdown cts-summary.md --json cts-summary.json
A compact comma form is accepted as well::
--suite=DirectGLES,gl31,gl31-main.txt,runs/gles/gl31
Per-suite parsing and validation deliberately delegate to
``cts_matrix_report`` so QPA ordering, sidecar upgrades, accepted statuses,
unrun handling, and duplicate-result semantics cannot drift between reports.
All conformance rates use unique expected caselist cases as their denominator.
Backend subtotals and the overall total are therefore case-weighted, not an
unweighted average of suite percentages.
"""
from __future__ import annotations
import argparse
from collections import Counter
from dataclasses import dataclass
from datetime import datetime, timezone
import hashlib
import json
import os
import sys
from typing import Iterable, Optional, Sequence
try: # Direct script execution and package imports are both supported.
from . import cts_matrix_report
except ImportError: # pragma: no cover - covered through CLI-style tests
import cts_matrix_report
SUPPORTED_BACKENDS = ("DirectGLES", "DirectVulkan")
SUM_FIELDS = (
"expected",
"result",
"pass",
"accepted",
"crash",
"hang",
"unrun",
"duplicate",
)
class MultiReportInputError(ValueError):
"""Suite specifications cannot produce an unambiguous report."""
@dataclass(frozen=True)
class SuiteSpec:
backend: str
label: str
caselist: str
result_dir: str
@property
def suite_id(self) -> str:
return f"{self.backend}/{self.label}"
def _ratio(numerator: int, denominator: int) -> float:
return numerator / denominator if denominator else 0.0
def _validation_state(items: Sequence[dict]) -> str:
states = {item["validation"]["state"] for item in items}
if "ERROR" in states:
return "ERROR"
if "INCOMPLETE" in states:
return "INCOMPLETE"
return "OK"
def aggregate_reports(items: Sequence[dict], suite_state_keys: Sequence[str]) -> dict:
"""Return an expected-case-weighted aggregate for suite reports."""
if len(items) != len(suite_state_keys):
raise MultiReportInputError("internal suite/state key count mismatch")
totals = {
field: sum(int(item[field]) for item in items)
for field in SUM_FIELDS
}
status_counts: Counter[str] = Counter()
for item in items:
status_counts.update(item["counts"])
state = _validation_state(items)
expected = totals["expected"]
result = totals["result"]
suite_states = {
key: item["validation"]["state"]
for key, item in zip(suite_state_keys, items)
}
return {
**totals,
"suite_count": len(items),
"counts": dict(sorted(status_counts.items())),
"aggregation": "weighted_by_expected_cases",
"coverage": {
"numerator": result,
"denominator": expected,
"rate": _ratio(result, expected),
},
"rates": {
"denominator": "expected",
"strict_pass_only": _ratio(totals["pass"], expected),
"conformance_accepted": _ratio(totals["accepted"], expected),
"measured_only_strict_pass": _ratio(totals["pass"], result),
"measured_only_conformance_accepted": _ratio(
totals["accepted"], result
),
},
# Keep the convenient aliases used by cts_matrix_report consumers.
"strict_pass_rate": _ratio(totals["pass"], expected),
"conformance_accepted_rate": _ratio(totals["accepted"], expected),
"validation": {
"state": state,
"ok": state == "OK",
"suite_states": suite_states,
"invariant_expected_equals_result_plus_unrun": (
expected == result + totals["unrun"]
),
},
}
def _caselist_fingerprint(path: str) -> tuple[str, int]:
cases, _duplicates = cts_matrix_report.read_caselist(path)
payload = "\n".join(cases).encode("utf-8") + b"\n"
return hashlib.sha256(payload).hexdigest(), len(cases)
def _read_provenance(
spec: SuiteSpec,
require_run_state: bool,
expected_run_identity: Optional[str],
) -> dict:
path = os.path.join(spec.result_dir, "run_state.json")
if not os.path.isfile(path):
if require_run_state or expected_run_identity is not None:
raise MultiReportInputError(
"suite result directory has no run_state.json; invocation provenance "
f"cannot be verified: {spec.result_dir}"
)
return {"state": "UNVERIFIED", "run_state": None}
try:
with open(path, "r", encoding="utf-8") as handle:
state = json.load(handle)
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
raise MultiReportInputError(f"cannot read suite run identity {path}: {exc}") from exc
if not isinstance(state, dict):
raise MultiReportInputError(f"suite run identity must be a JSON object: {path}")
if state.get("backend") != spec.backend:
raise MultiReportInputError(
f"suite {spec.suite_id} is labelled {spec.backend}, but run_state.json "
f"records {state.get('backend')!r}"
)
fingerprint, case_count = _caselist_fingerprint(spec.caselist)
if state.get("caselist_sha256") != fingerprint or state.get("case_count") != case_count:
raise MultiReportInputError(
f"suite {spec.suite_id} run_state.json belongs to a different caselist"
)
invocation_identity = state.get("invocation_identity")
if (
expected_run_identity is not None
and invocation_identity != expected_run_identity
):
raise MultiReportInputError(
f"suite {spec.suite_id} run_state.json belongs to a different CTS invocation"
)
return {
"state": "VERIFIED",
"run_state": os.path.abspath(path),
"invocation_identity": invocation_identity,
}
def _validate_specs(
specs: Sequence[SuiteSpec],
require_run_state: bool,
expected_run_identity: Optional[str],
) -> dict[str, dict]:
if not specs:
raise MultiReportInputError("at least one --suite specification is required")
seen: set[tuple[str, str]] = set()
seen_result_dirs: list[tuple[str, str]] = []
provenance: dict[str, dict] = {}
for spec in specs:
if spec.backend not in SUPPORTED_BACKENDS:
raise MultiReportInputError(
f"unsupported backend {spec.backend!r}; expected one of "
+ ", ".join(SUPPORTED_BACKENDS)
)
if not spec.label.strip():
raise MultiReportInputError("suite label cannot be empty")
identity = (spec.backend, spec.label)
if identity in seen:
raise MultiReportInputError(
f"duplicate suite specification for {spec.backend}/{spec.label}"
)
seen.add(identity)
if not os.path.isdir(spec.result_dir):
raise MultiReportInputError(
f"suite result directory does not exist: {spec.result_dir}"
)
physical_result_dir = os.path.normcase(
os.path.realpath(os.path.abspath(spec.result_dir))
)
for previous_dir, previous_suite in seen_result_dirs:
try:
common_dir = os.path.commonpath(
[previous_dir, physical_result_dir]
)
except ValueError:
continue
if common_dir in (previous_dir, physical_result_dir):
raise MultiReportInputError(
f"suite {spec.suite_id} uses a result directory which overlaps "
f"{previous_suite}: {spec.result_dir}"
)
seen_result_dirs.append((physical_result_dir, spec.suite_id))
provenance[spec.suite_id] = _read_provenance(
spec, require_run_state, expected_run_identity
)
return provenance
def build_report(
specs: Sequence[SuiteSpec],
require_run_state: bool = True,
expected_run_identity: Optional[str] = None,
) -> dict:
"""Build suite, per-backend, and overall serialisable reports."""
provenance = _validate_specs(
specs, require_run_state, expected_run_identity
)
suite_reports: list[dict] = []
backend_order: list[str] = []
for spec in specs:
if spec.backend not in backend_order:
backend_order.append(spec.backend)
item = cts_matrix_report.build_version_report(
spec.label, spec.caselist, [spec.result_dir]
)
# ``version`` is the generic label argument in build_version_report;
# expose explicit multi-report terminology while retaining all of its
# validation and case-level evidence.
item.pop("version", None)
item["backend"] = spec.backend
item["label"] = spec.label
item["suite_id"] = spec.suite_id
item["provenance"] = provenance[spec.suite_id]
if item["provenance"]["state"] == "UNVERIFIED":
item["validation"]["warnings"].append(
"result directory has no run_state.json; backend provenance is unverified"
)
suite_reports.append(item)
backends: dict[str, dict] = {}
for backend in backend_order:
backend_items = [
item for item in suite_reports if item["backend"] == backend
]
labels = [item["label"] for item in backend_items]
aggregate = aggregate_reports(backend_items, labels)
aggregate["backend"] = backend
aggregate["suite_labels"] = labels
backends[backend] = aggregate
overall = aggregate_reports(
suite_reports, [item["suite_id"] for item in suite_reports]
)
overall["backend_count"] = len(backends)
overall["backends"] = backend_order
return {
"schema_version": 1,
"generated_at": datetime.now(timezone.utc).isoformat(),
"accepted_statuses": list(cts_matrix_report.ACCEPTED_STATUSES),
"rate_policy": {
"denominator": "unique expected cases from each suite caselist",
"unrun_cases": "included in the denominator and never accepted",
"backend_aggregation": "weighted by expected cases",
"overall_aggregation": "weighted by expected cases across backend-suite pairs",
"duplicate_results": "last QPA result wins, matching qpa_report.py",
},
"suites": suite_reports,
"backends": backends,
"overall": overall,
}
def _percent(numerator: int, denominator: int) -> str:
if not denominator:
return "n/a"
return f"{100.0 * numerator / denominator:.2f}% ({numerator}/{denominator})"
def _markdown_cell(value: object) -> str:
return str(value).replace("|", r"\|").replace("\r", " ").replace("\n", " ")
def _table_row(backend: str, label: str, item: dict, bold: bool = False) -> str:
values = [
backend,
label,
str(item["expected"]),
str(item["result"]),
str(item["pass"]),
str(item["accepted"]),
str(item["crash"]),
str(item["hang"]),
str(item["unrun"]),
str(item["duplicate"]),
_percent(item["result"], item["expected"]),
_percent(item["pass"], item["expected"]),
_percent(item["accepted"], item["expected"]),
item["validation"]["state"],
]
values = [_markdown_cell(value) for value in values]
if bold:
values = [f"**{value}**" for value in values]
return "| " + " | ".join(values) + " |"
def render_markdown(report: dict) -> str:
lines = [
"# GL CTS multi-suite conformance report",
"",
(
"| Backend | Suite | Expected | Result | Pass | Accepted | Crash | Hang | "
"Unrun | Duplicate | Coverage | Strict Pass-only | Conformance-accepted | Validation |"
),
"|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|:---:|",
]
for backend in report["backends"]:
for item in report["suites"]:
if item["backend"] == backend:
lines.append(_table_row(backend, item["label"], item))
subtotal = report["backends"][backend]
lines.append(
_table_row(backend, f"{backend} weighted subtotal", subtotal, bold=True)
)
lines.append(
_table_row(
"All backends",
"Overall weighted",
report["overall"],
bold=True,
)
)
lines.extend(
[
"",
(
"Rates use unique **Expected** caselist cases as the denominator. "
"Unrun cases remain in the denominator and are not accepted."
),
"Accepted statuses: "
+ ", ".join(
f"`{status}`" for status in report["accepted_statuses"]
)
+ ".",
(
"Duplicate is the number of extra QPA observations; the final "
"observation wins."
),
]
)
details: list[str] = []
for item in report["suites"]:
validation = item["validation"]
messages = validation["errors"] + validation["warnings"]
if messages:
details.append(
f"- **{_markdown_cell(item['suite_id'])} {validation['state']}**: "
+ "; ".join(_markdown_cell(message) for message in messages)
)
if details:
lines.extend(["", "## Validation details", "", *details])
return "\n".join(lines) + "\n"
def _write_text(path: str, contents: str) -> None:
absolute = os.path.abspath(path)
os.makedirs(os.path.dirname(absolute), exist_ok=True)
with open(absolute, "w", encoding="utf-8", newline="\n") as handle:
handle.write(contents)
def _write_json(path: str, report: dict) -> None:
_write_text(path, json.dumps(report, indent=2, sort_keys=True) + "\n")
def _normalise_compact_suite_args(argv: Sequence[str]) -> list[str]:
"""Expand ``--suite=b,l,c,r`` into the four-value argparse form."""
result: list[str] = []
index = 0
while index < len(argv):
token = argv[index]
if token.startswith("--suite="):
compact = token.split("=", 1)[1]
parts = compact.split(",", 3)
if len(parts) != 4:
raise MultiReportInputError(
"compact --suite expects backend,label,caselist,result-dir"
)
result.extend(["--suite", *parts])
index += 1
continue
if token == "--suite" and index + 1 < len(argv) and argv[index + 1].count(",") >= 3:
parts = argv[index + 1].split(",", 3)
result.extend(["--suite", *parts])
index += 2
continue
result.append(token)
index += 1
return result
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--suite",
action="append",
nargs=4,
required=True,
metavar=("BACKEND", "LABEL", "CASELIST", "RESULT_DIR"),
help=(
"suite specification; repeat for every backend/suite pair "
f"(backends: {', '.join(SUPPORTED_BACKENDS)})"
),
)
parser.add_argument(
"--markdown",
default="cts_multi_report.md",
help="Markdown output path (default: ./cts_multi_report.md)",
)
parser.add_argument(
"--json",
dest="json_out",
default="cts_multi_report.json",
help="JSON output path (default: ./cts_multi_report.json)",
)
parser.add_argument(
"--allow-incomplete",
action="store_true",
help="return success even when one or more suites are ERROR/INCOMPLETE",
)
parser.add_argument(
"--adopt-legacy",
dest="allow_unverified_provenance",
action="store_true",
help="accept legacy result directories without run_state.json (provenance remains unverified)",
)
parser.add_argument(
"--allow-unverified-provenance",
dest="allow_unverified_provenance",
action="store_true",
help=argparse.SUPPRESS,
)
parser.add_argument(
"--expected-run-identity",
help="require every suite run_state.json to contain this controller fingerprint",
)
return parser
def _specs_from_args(values: Sequence[Sequence[str]]) -> list[SuiteSpec]:
return [
SuiteSpec(
backend=backend.strip(),
label=label.strip(),
caselist=caselist,
result_dir=result_dir,
)
for backend, label, caselist, result_dir in values
]
def main(argv: Optional[Iterable[str]] = None) -> int:
raw_argv = list(argv) if argv is not None else sys.argv[1:]
try:
normalised = _normalise_compact_suite_args(raw_argv)
except MultiReportInputError as exc:
print(f"cts_multi_report: {exc}", file=sys.stderr)
return 2
args = _parser().parse_args(normalised)
if os.path.normcase(os.path.abspath(args.markdown)) == os.path.normcase(
os.path.abspath(args.json_out)
):
print("cts_multi_report: Markdown and JSON paths must differ", file=sys.stderr)
return 2
try:
report = build_report(
_specs_from_args(args.suite),
require_run_state=not args.allow_unverified_provenance,
expected_run_identity=args.expected_run_identity,
)
markdown = render_markdown(report)
_write_text(args.markdown, markdown)
_write_json(args.json_out, report)
except (
OSError,
MultiReportInputError,
cts_matrix_report.ReportInputError,
) as exc:
print(f"cts_multi_report: {exc}", file=sys.stderr)
return 2
print(markdown, end="")
print(f"\nMarkdown: {os.path.abspath(args.markdown)}")
print(f"JSON: {os.path.abspath(args.json_out)}")
if not args.allow_incomplete and not report["overall"]["validation"]["ok"]:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
+227
View File
@@ -0,0 +1,227 @@
#!/usr/bin/env python
"""Drive a glcts run on the local host, resuming across crashes.
Local-host counterpart of run_cts.py: MobileGL crashes on some cases and glcts
takes the whole process down with it, so a single invocation stops at the first
crash. This runner re-invokes glcts with only the cases that have no result
yet, records the case that was open when the process died as "Crash" (or
"Hang" on a timeout), and repeats until the list is exhausted.
Usage:
python run_cts_local.py --backend DirectVulkan \\
--glcts <path-to-glcts-binary> --lib <path-to-libMobileGL.so> \\
--caselist <mustpass.txt> --outdir <dir> [--env K=V ...]
"""
import argparse
import glob
import os
import re
import resource
import subprocess
import sys
import time
CASE_START = re.compile(r"^#beginTestCaseResult\s+(\S+)")
CASE_END = re.compile(r"^#endTestCaseResult")
CASE_TERM = re.compile(r"^#terminateTestCaseResult")
def completed_cases(qpa_path):
"""Return (finished_case_names, last_started_case_or_None)."""
finished = []
current = None
if not os.path.exists(qpa_path):
return finished, None
with open(qpa_path, "r", encoding="utf-8", errors="replace") as fh:
for line in fh:
m = CASE_START.match(line)
if m:
current = m.group(1)
continue
if current is not None and (CASE_END.match(line) or CASE_TERM.match(line)):
finished.append(current)
current = None
return finished, current
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--backend", required=True, choices=["DirectGLES", "DirectVulkan"])
ap.add_argument("--glcts", required=True, help="path to the glcts binary")
ap.add_argument("--lib", required=True, help="path to libMobileGL.so")
ap.add_argument("--caselist", required=True)
ap.add_argument("--outdir", required=True)
ap.add_argument("--surface", default="fbo", help="--deqp-surface-type value")
# 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 (a 4-sample 16K depth texture alone is 4 GiB).
ap.add_argument("--surface-size", type=int, default=256,
help="--deqp-surface-width/height value")
# With DONT_CARE depth/stencil bits dEQP's FboRenderContext picks the first entry of
# its own format list, GL_DEPTH32F_STENCIL8. framebuffer_blit meanwhile hardcodes
# GL_DEPTH24_STENCIL8 for its own buffers whenever it detects an FBO surface, then
# blits depth between the two - which the spec forbids for mismatched formats, so a
# conformant driver has to fail it. Asking for a config the test agrees with avoids
# the contradiction instead of papering over it.
ap.add_argument("--gl-config-name", default="rgba8888d24s8",
help="--deqp-gl-config-name value (empty string to leave it unset)")
ap.add_argument("--max-rounds", type=int, default=4000)
ap.add_argument("--max-empty-streak", type=int, default=64,
help="abort after this many consecutive chunks that produce no log at all")
ap.add_argument("--chunk-timeout", type=int, default=1800,
help="seconds before killing one glcts invocation (a wedged case never returns)")
# dEQP's watchdog aborts the process when a single case exceeds a hardcoded 30s
# (framework/common/tcuApp.hpp), which is not a hang on a CPU rasterizer - some
# texture_swizzle cases legitimately take ~17s each and cross it once the process is
# warm, so they came back as spurious Timeouts. dEQP's own default is off; the
# chunk-timeout above is what actually rescues a genuinely wedged case.
ap.add_argument("--watchdog", default="disable", choices=["enable", "disable"],
help="--deqp-watchdog value")
ap.add_argument("--skip-file", default=None,
help="file of case names to exclude, e.g. cases known to wedge the host")
ap.add_argument("--waiver-file", default=None,
help="--deqp-waiver-file value, e.g. tools/cts/waivers/mobilegl-fbo-harness.xml")
ap.add_argument("--env", action="append", default=[], metavar="K=V",
help="extra environment variable for glcts (repeatable)")
args = ap.parse_args()
os.makedirs(args.outdir, exist_ok=True)
glcts = os.path.abspath(args.glcts)
lib = os.path.abspath(args.lib)
# glcts resolves its gl_cts data tree relative to the binary's directory.
workdir = os.path.dirname(glcts)
with open(args.caselist, "r", encoding="utf-8") as fh:
remaining = [l.strip() for l in fh if l.strip() and not l.strip().startswith("#")]
skipped = []
if args.skip_file and os.path.isfile(args.skip_file):
with open(args.skip_file, "r", encoding="utf-8") as fh:
skip = {l.strip() for l in fh if l.strip() and not l.strip().startswith("#")}
skipped = [c for c in remaining if c in skip]
remaining = [c for c in remaining if c not in skip]
print(f"[run_cts_local] skipping {len(skipped)} case(s) from {args.skip_file}")
total = len(remaining)
print(f"[run_cts_local] {args.backend}: {total} cases")
env = dict(os.environ)
env["MOBILEGL_BACKEND_TYPE"] = args.backend
env["MOBILEGL_CTS_LIB"] = lib
for kv in args.env:
k, _, v = kv.partition("=")
env[k] = v
crashed = []
hung = []
done = set()
chunk = 0
started = time.time()
empty_streak = 0
# Resume: results in chunk files from an interrupted run still count. The
# case that was open when that run died is re-tried rather than assumed bad.
prior_chunks = sorted(glob.glob(os.path.join(args.outdir, "chunk*.qpa")))
for prior in prior_chunks:
finished, _ = completed_cases(prior)
done.update(finished)
if prior_chunks:
chunk = int(re.search(r"chunk(\d+)\.qpa$", prior_chunks[-1]).group(1)) + 1
remaining = [c for c in remaining if c not in done]
print(f"[run_cts_local] resuming: {len(done)} case(s) already measured, "
f"{len(remaining)} to go")
while remaining and chunk < args.max_rounds:
listfile = os.path.abspath(os.path.join(args.outdir, "remaining.txt"))
with open(listfile, "w", encoding="utf-8", newline="\n") as fh:
fh.write("\n".join(remaining) + "\n")
qpa = os.path.abspath(os.path.join(args.outdir, f"chunk{chunk:04d}.qpa"))
cmd = [
glcts,
f"--deqp-caselist-file={listfile}",
f"--deqp-surface-type={args.surface}",
f"--deqp-surface-width={args.surface_size}",
f"--deqp-surface-height={args.surface_size}",
"--deqp-terminate-on-device-lost=disable",
f"--deqp-watchdog={args.watchdog}",
"--deqp-log-images=disable",
"--deqp-log-shader-sources=disable",
f"--deqp-log-filename={qpa}",
]
if args.gl_config_name:
cmd.append(f"--deqp-gl-config-name={args.gl_config_name}")
if args.waiver_file:
cmd.append(f"--deqp-waiver-file={os.path.abspath(args.waiver_file)}")
timed_out = False
try:
# RLIMIT_CORE=0: MobileGL asserts abort with a core dump, and writing
# a multi-GB glcts core image after every crash dominates wall time.
subprocess.run(cmd, cwd=workdir, env=env, timeout=args.chunk_timeout,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
start_new_session=True,
preexec_fn=lambda: resource.setrlimit(resource.RLIMIT_CORE, (0, 0)))
except subprocess.TimeoutExpired:
timed_out = True
print(f"[run_cts_local] chunk {chunk:04d} timed out after {args.chunk_timeout}s",
file=sys.stderr)
finished, in_flight = completed_cases(qpa)
for c in finished:
done.add(c)
progressed = len(finished)
if progressed > 0:
empty_streak = 0
if in_flight is not None:
if timed_out:
print(f"[run_cts_local] HANG in {in_flight} - quarantining it")
hung.append(in_flight)
else:
crashed.append(in_flight)
done.add(in_flight)
progressed += 1
elif progressed == 0:
empty_streak += 1
if empty_streak >= args.max_empty_streak:
print(f"[run_cts_local] ABORTING: {empty_streak} consecutive chunks produced no "
f"output. Something systemic is wrong; refusing to label the rest of the "
f"suite as crashes.", file=sys.stderr)
break
victim = remaining[0]
label = "Hang" if timed_out else "Crash"
print(f"[run_cts_local] no output at all; recording {victim} as {label}")
(hung if timed_out else crashed).append(victim)
done.add(victim)
progressed = 1
remaining = [c for c in remaining if c not in done]
elapsed = time.time() - started
print(
f"[run_cts_local] chunk {chunk:04d}: +{progressed} (done {len(done)}/{total}, "
f"crashes {len(crashed)}, hangs {len(hung)}, {elapsed / 60:.1f} min)"
)
chunk += 1
with open(os.path.join(args.outdir, "crashed.txt"), "w", encoding="utf-8", newline="\n") as fh:
fh.write("\n".join(crashed) + ("\n" if crashed else ""))
with open(os.path.join(args.outdir, "hung.txt"), "w", encoding="utf-8", newline="\n") as fh:
fh.write("\n".join(hung) + ("\n" if hung else ""))
with open(os.path.join(args.outdir, "unrun.txt"), "w", encoding="utf-8", newline="\n") as fh:
fh.write("\n".join(remaining) + ("\n" if remaining else ""))
if skipped:
with open(os.path.join(args.outdir, "skipped.txt"), "w", encoding="utf-8", newline="\n") as fh:
fh.write("\n".join(skipped) + "\n")
if remaining:
print(f"[run_cts_local] WARNING: {len(remaining)} cases were never run (see unrun.txt)",
file=sys.stderr)
print(f"[run_cts_local] finished: {len(done)}/{total} cases, {len(crashed)} crashes, "
f"{len(hung)} hangs, {chunk} invocations")
print(f"[run_cts_local] qpa chunks in {args.outdir}")
return 0
if __name__ == "__main__":
sys.exit(main())
+878
View File
@@ -0,0 +1,878 @@
#!/usr/bin/env python
"""Run a local Windows glcts executable and resume across process failures.
The desktop CTS normally runs a complete caselist in one process. That is a
poor fit for testing a developing OpenGL implementation: one access violation
or GPU hang prevents every later case from running. This driver gives each
invocation the cases which have not produced a result yet, preserves one QPA
and stdout/stderr pair per invocation, and starts another process after a
crash.
Existing ``chunkNNNN.qpa`` files and ``crashed.txt``/``hung.txt`` sidecars are
read on startup, so invoking the same command and output directory resumes an
interrupted run. A timeout is based on *idle QPA time*, not total process wall
time: a healthy invocation may legitimately run thousands of cases for hours.
Example (values beginning with ``--`` use argparse's ``=`` spelling)::
py run_cts_windows.py \
--exe D:\\glcts\\glcts.exe --workdir D:\\glcts \
--caselist D:\\glcts\\mustpass\\gl30.txt --outdir D:\\results\\gl30 \
--backend DirectVulkan \
--deqp-arg=--deqp-surface-type=window
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import signal
import subprocess
import sys
import tempfile
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Iterable, Optional, Sequence
CASE_START = re.compile(r"^#beginTestCaseResult\s+(\S+)")
CASE_END = re.compile(r"^#endTestCaseResult(?:\s|$)")
CASE_TERM = re.compile(r"^#terminateTestCaseResult(?:\s|$)")
CASE_RESULT = re.compile(r'<Result\s+StatusCode="[^"]+"')
CHUNK_ARTIFACT = re.compile(r"^chunk(\d+)(?:\.|$)", re.IGNORECASE)
CHUNK_META = re.compile(r"^chunk(\d+)\.meta\.json$", re.IGNORECASE)
RECOVERY_SIDECAR_NAMES = frozenset(
{"crashed.txt", "hung.txt", "unrun.txt", "skipped.txt", "remaining.txt"}
)
CONTROLLED_DEQP_OPTIONS = {
"--deqp-caselist-file",
"--deqp-log-filename",
}
ATOMIC_REPLACE_ATTEMPTS = 8
ATOMIC_REPLACE_INITIAL_BACKOFF_SECONDS = 0.025
ATOMIC_REPLACE_MAX_BACKOFF_SECONDS = 0.2
class RunnerError(Exception):
"""A user/configuration error which should not be attributed to a case."""
@dataclass
class QpaProgress:
"""Cases recorded by a QPA and its unterminated tail, if any."""
recorded: list[str]
in_flight: Optional[str]
begin_count: int
@dataclass
class ProcessOutcome:
returncode: Optional[int]
duration_seconds: float
timed_out: bool = False
timeout_reason: Optional[str] = None
interrupted: bool = False
launch_error: Optional[str] = None
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def read_caselist(path: Path) -> list[str]:
"""Read a dEQP text caselist, preserving order and removing duplicates."""
try:
lines = path.read_text(encoding="utf-8-sig", errors="strict").splitlines()
except (OSError, UnicodeError) as exc:
raise RunnerError(f"cannot read caselist {path}: {exc}") from exc
cases: list[str] = []
seen: set[str] = set()
for raw in lines:
case = raw.strip()
if not case or case.startswith("#") or case in seen:
continue
cases.append(case)
seen.add(case)
if not cases:
raise RunnerError(f"caselist contains no test cases: {path}")
return cases
def read_name_set(path: Path) -> set[str]:
if not path.is_file():
return set()
try:
return {
line.strip()
for line in path.read_text(encoding="utf-8-sig", errors="replace").splitlines()
if line.strip() and not line.lstrip().startswith("#")
}
except OSError as exc:
raise RunnerError(f"cannot read recovery file {path}: {exc}") from exc
def _replace_with_retry(source: Path, destination: Path) -> None:
"""Replace a state file, tolerating brief Windows access-denied races.
Antivirus/indexing tools can momentarily open ``remaining.txt`` without
delete sharing. Windows then reports either ``PermissionError`` or a
generic ``OSError`` carrying ``winerror == 5``. Retry only those cases;
disk, path, and programming errors remain immediately visible.
"""
for attempt in range(ATOMIC_REPLACE_ATTEMPTS):
try:
os.replace(source, destination)
return
except OSError as exc:
retryable = isinstance(exc, PermissionError) or getattr(exc, "winerror", None) == 5
if not retryable or attempt + 1 >= ATOMIC_REPLACE_ATTEMPTS:
raise
delay = min(
ATOMIC_REPLACE_INITIAL_BACKOFF_SECONDS * (2**attempt),
ATOMIC_REPLACE_MAX_BACKOFF_SECONDS,
)
time.sleep(delay)
def atomic_write_text(path: Path, text: str) -> None:
"""Replace a small state file without exposing a partially-written copy."""
path.parent.mkdir(parents=True, exist_ok=True)
fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=str(path.parent))
temporary_path = Path(temporary)
try:
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
handle.write(text)
handle.flush()
os.fsync(handle.fileno())
_replace_with_retry(temporary_path, path)
finally:
try:
temporary_path.unlink()
except FileNotFoundError:
pass
def atomic_write_json(path: Path, value: object) -> None:
atomic_write_text(path, json.dumps(value, indent=2, sort_keys=True) + "\n")
def write_case_file(path: Path, cases: Iterable[str]) -> None:
values = list(cases)
atomic_write_text(path, "\n".join(values) + ("\n" if values else ""))
def scan_qpa(path: Path) -> QpaProgress:
"""Return cases with a final result and the unfinished tail, if any.
``#terminateTestCaseResult`` is a completed result (usually Crash or
Timeout). ``#endTestCaseResult`` only completes a case when its XML carried
a ``<Result StatusCode=...>``. A truncated case that already wrote Result is
also recoverable; a case with no Result remains eligible for a retry.
"""
if not path.is_file():
return QpaProgress([], None, 0)
recorded: list[str] = []
current: Optional[str] = None
has_result = False
begin_count = 0
try:
with path.open("r", encoding="utf-8", errors="replace") as handle:
for raw_line in handle:
line = raw_line.lstrip("\ufeff")
match = CASE_START.match(line)
if match:
if current is not None and has_result:
recorded.append(current)
current = match.group(1)
has_result = False
begin_count += 1
continue
if current is not None and CASE_RESULT.search(line):
has_result = True
continue
if current is not None and CASE_TERM.match(line):
recorded.append(current)
current = None
has_result = False
continue
if current is not None and CASE_END.match(line):
if has_result:
recorded.append(current)
current = None
has_result = False
except OSError as exc:
raise RunnerError(f"cannot read QPA {path}: {exc}") from exc
if current is not None and has_result:
recorded.append(current)
current = None
return QpaProgress(recorded, current, begin_count)
def numbered_files(outdir: Path, pattern: re.Pattern[str]) -> list[tuple[int, Path]]:
found: list[tuple[int, Path]] = []
try:
children = list(outdir.iterdir())
except OSError as exc:
raise RunnerError(f"cannot list output directory {outdir}: {exc}") from exc
for path in children:
match = pattern.match(path.name)
if match:
found.append((int(match.group(1)), path))
found.sort(key=lambda item: item[0])
return found
def next_chunk_number(outdir: Path) -> int:
numbers = [number for number, _path in numbered_files(outdir, CHUNK_ARTIFACT)]
return max(numbers, default=-1) + 1
def load_meta_classifications(outdir: Path, expected: set[str]) -> tuple[set[str], set[str]]:
"""Recover an atomic classification written just before sidecar updates."""
crashed: set[str] = set()
hung: set[str] = set()
for _number, path in numbered_files(outdir, CHUNK_META):
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
# A damaged metadata file is diagnostic only. QPA and sidecars are
# authoritative and must still allow recovery.
continue
if not isinstance(value, dict):
continue
case = value.get("classified_case")
classification = value.get("classification")
if not isinstance(case, str) or case not in expected:
continue
if classification == "DeviceHang":
hung.add(case)
elif classification == "Crash":
crashed.add(case)
crashed.difference_update(hung)
return crashed, hung
def result_qpa_files(outdir: Path) -> list[Path]:
"""Return every QPA a directory-based report would consume."""
found: list[Path] = []
try:
for root, directories, names in os.walk(outdir):
directories.sort(key=str.casefold)
for name in sorted(names, key=str.casefold):
if name.casefold().endswith(".qpa"):
found.append(Path(root) / name)
except OSError as exc:
raise RunnerError(f"cannot scan output directory {outdir}: {exc}") from exc
return found
def recover_results(outdir: Path, expected: set[str]) -> tuple[set[str], set[str], set[str]]:
recorded: set[str] = set()
for path in result_qpa_files(outdir):
progress = scan_qpa(path)
recorded.update(case for case in progress.recorded if case in expected)
crashed = read_name_set(outdir / "crashed.txt") & expected
hung = read_name_set(outdir / "hung.txt") & expected
meta_crashed, meta_hung = load_meta_classifications(outdir, expected)
crashed.update(meta_crashed)
hung.update(meta_hung)
crashed.difference_update(hung)
return recorded, crashed, hung
def caselist_fingerprint(cases: Sequence[str]) -> str:
payload = "\n".join(cases).encode("utf-8") + b"\n"
return hashlib.sha256(payload).hexdigest()
def recovery_artifacts(outdir: Path) -> list[Path]:
"""Return prior-run evidence which must not be adopted implicitly."""
found = set(result_qpa_files(outdir))
try:
children = list(outdir.iterdir())
except OSError as exc:
raise RunnerError(f"cannot list output directory {outdir}: {exc}") from exc
found.update(
path
for path in children
if CHUNK_ARTIFACT.match(path.name)
or path.name.casefold() in RECOVERY_SIDECAR_NAMES
)
return sorted(
found,
key=lambda path: str(path.relative_to(outdir)).casefold(),
)
def check_run_identity(
outdir: Path,
backend: str,
cases: Sequence[str],
invocation_identity: Optional[str] = None,
adopt_legacy: bool = False,
) -> None:
"""Refuse to silently mix different suites/backends in one result dir."""
path = outdir / "run_state.json"
fingerprint = caselist_fingerprint(cases)
if path.is_file():
try:
state = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
raise RunnerError(f"cannot read run identity {path}: {exc}") from exc
if not isinstance(state, dict):
raise RunnerError(f"run identity must be a JSON object: {path}")
if state.get("backend") != backend:
raise RunnerError(
f"output directory belongs to backend {state.get('backend')!r}, not {backend!r}: {outdir}"
)
if state.get("caselist_sha256") != fingerprint:
raise RunnerError(f"output directory belongs to a different caselist: {outdir}")
stored_invocation_identity = state.get("invocation_identity")
if (
stored_invocation_identity is not None
or invocation_identity is not None
) and stored_invocation_identity != invocation_identity:
raise RunnerError(f"output directory belongs to a different CTS invocation: {outdir}")
return
legacy_artifacts = recovery_artifacts(outdir)
if legacy_artifacts and not adopt_legacy:
examples = ", ".join(path.name for path in legacy_artifacts[:3])
raise RunnerError(
"output directory contains CTS recovery artifacts but no run_state.json; "
f"refusing to adopt unverified legacy results ({examples}). Re-run with "
"--adopt-legacy only after verifying the backend, caselist, and invocation."
)
atomic_write_json(
path,
{
"version": 1,
"backend": backend,
"case_count": len(cases),
"caselist_sha256": fingerprint,
"invocation_identity": invocation_identity,
"adopted_legacy": bool(legacy_artifacts),
"created_utc": utc_now(),
},
)
def persist_sidecars(
outdir: Path,
ordered_cases: Sequence[str],
crashed: set[str],
hung: set[str],
remaining: Sequence[str],
) -> None:
write_case_file(outdir / "crashed.txt", (case for case in ordered_cases if case in crashed))
write_case_file(outdir / "hung.txt", (case for case in ordered_cases if case in hung))
write_case_file(outdir / "unrun.txt", remaining)
write_case_file(outdir / "remaining.txt", remaining)
def parse_environment(values: Sequence[str]) -> dict[str, str]:
result: dict[str, str] = {}
for value in values:
if "=" not in value:
raise RunnerError(f"--env expects NAME=VALUE, got {value!r}")
name, contents = value.split("=", 1)
if not name or "\x00" in name or "=" in name:
raise RunnerError(f"invalid environment variable name in {value!r}")
result[name] = contents
return result
def validate_deqp_args(values: Sequence[str]) -> None:
for value in values:
option = value.split("=", 1)[0].lower()
if option in CONTROLLED_DEQP_OPTIONS:
raise RunnerError(f"{option} is controlled by this runner and cannot be supplied via --deqp-arg")
def resolve_paths(
exe_value: str,
workdir_value: Optional[str],
caselist_value: str,
outdir_value: str,
) -> tuple[Path, Path, Path, Path]:
launch_dir = Path.cwd()
requested_exe = Path(exe_value).expanduser()
if workdir_value:
workdir = Path(workdir_value).expanduser().resolve()
elif requested_exe.is_absolute():
workdir = requested_exe.resolve().parent
else:
workdir = launch_dir
if requested_exe.is_absolute():
exe = requested_exe.resolve()
else:
in_workdir = (workdir / requested_exe).resolve()
in_launch_dir = (launch_dir / requested_exe).resolve()
exe = in_workdir if in_workdir.is_file() else in_launch_dir
caselist = Path(caselist_value).expanduser().resolve()
outdir = Path(outdir_value).expanduser().resolve()
if not exe.is_file():
raise RunnerError(f"glcts executable does not exist: {exe}")
if not workdir.is_dir():
raise RunnerError(f"working directory does not exist: {workdir}")
if not caselist.is_file():
raise RunnerError(f"caselist does not exist: {caselist}")
return exe, workdir, caselist, outdir
def qpa_signature(path: Path) -> Optional[tuple[int, int]]:
try:
stat = path.stat()
except FileNotFoundError:
return None
except OSError:
# A transient sharing violation must not kill a healthy process. The
# next poll will retry and the idle clock retains its previous value.
return None
return stat.st_size, stat.st_mtime_ns
def kill_process_tree(process: subprocess.Popen[bytes]) -> None:
"""Force-stop the process and descendants, with a parent-only fallback."""
if process.poll() is not None:
return
if os.name == "nt":
# /T is essential: CTS/platform helpers can outlive the top-level
# process, retain the QPA/DLL, and poison the next continuation round.
taskkill = Path(os.environ.get("SystemRoot", r"C:\Windows")) / "System32" / "taskkill.exe"
command = [str(taskkill), "/PID", str(process.pid), "/T", "/F"]
try:
subprocess.run(
command,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=20,
check=False,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
except (OSError, subprocess.TimeoutExpired):
pass
else:
try:
os.killpg(process.pid, signal.SIGKILL)
except (ProcessLookupError, PermissionError, OSError):
pass
try:
process.wait(timeout=10)
return
except subprocess.TimeoutExpired:
pass
try:
process.kill()
except OSError:
pass
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
pass
def run_process(
command: Sequence[str],
workdir: Path,
environment: dict[str, str],
qpa_path: Path,
stdout_path: Path,
stderr_path: Path,
idle_timeout: float,
max_round_seconds: float,
poll_seconds: float,
) -> ProcessOutcome:
"""Run one CTS chunk, killing its tree only after QPA progress stalls."""
started = time.monotonic()
with stdout_path.open("wb") as stdout_handle, stderr_path.open("wb") as stderr_handle:
popen_options: dict[str, object] = {
"cwd": str(workdir),
"env": environment,
"stdin": subprocess.DEVNULL,
"stdout": stdout_handle,
"stderr": stderr_handle,
}
if os.name == "nt":
popen_options["creationflags"] = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
else:
popen_options["start_new_session"] = True
try:
process = subprocess.Popen(list(command), **popen_options) # type: ignore[arg-type]
except OSError as exc:
message = f"failed to launch {command[0]}: {exc}\n"
stderr_handle.write(message.encode("utf-8", errors="replace"))
stderr_handle.flush()
return ProcessOutcome(None, time.monotonic() - started, launch_error=str(exc))
last_signature = qpa_signature(qpa_path)
last_progress = time.monotonic()
timed_out = False
timeout_reason: Optional[str] = None
interrupted = False
try:
while True:
try:
returncode = process.wait(timeout=poll_seconds)
break
except subprocess.TimeoutExpired:
pass
now = time.monotonic()
signature = qpa_signature(qpa_path)
if signature is not None and signature != last_signature:
last_signature = signature
last_progress = now
if idle_timeout > 0 and now - last_progress >= idle_timeout:
timed_out = True
timeout_reason = "qpa-idle"
kill_process_tree(process)
returncode = process.poll()
break
if max_round_seconds > 0 and now - started >= max_round_seconds:
timed_out = True
timeout_reason = "max-round"
kill_process_tree(process)
returncode = process.poll()
break
except KeyboardInterrupt:
interrupted = True
kill_process_tree(process)
returncode = process.poll()
return ProcessOutcome(
returncode=returncode,
duration_seconds=time.monotonic() - started,
timed_out=timed_out,
timeout_reason=timeout_reason,
interrupted=interrupted,
)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Run Windows glcts against MobileGL, resuming across crashes and GPU hangs."
)
parser.add_argument("--exe", required=True, help="path to glcts.exe")
parser.add_argument(
"--workdir",
help="glcts working directory (default: executable directory, or current directory for a relative exe)",
)
parser.add_argument("--caselist", required=True, help="mustpass/caselist text file")
parser.add_argument("--outdir", required=True, help="persistent result directory")
parser.add_argument("--backend", required=True, choices=("DirectGLES", "DirectVulkan"))
parser.add_argument(
"--run-identity",
help="controller fingerprint for executable, data, arguments, and environment",
)
parser.add_argument(
"--adopt-legacy",
action="store_true",
help=(
"adopt existing chunk/sidecar results which predate run_state.json; "
"disabled by default because their provenance cannot be verified"
),
)
parser.add_argument(
"--env",
action="append",
default=[],
metavar="NAME=VALUE",
help="extra child environment variable (repeatable)",
)
parser.add_argument(
"--deqp-arg",
action="append",
default=[],
metavar="ARG",
help="extra glcts argument; repeat and use --deqp-arg=--option=value for leading dashes",
)
parser.add_argument(
"--idle-timeout",
type=float,
default=300.0,
metavar="SECONDS",
help="kill a chunk after this many seconds with no QPA size/mtime change (0 disables; default: 300)",
)
parser.add_argument(
"--max-round-seconds",
type=float,
default=0.0,
metavar="SECONDS",
help="optional total wall limit for one invocation (0 disables; default: 0)",
)
parser.add_argument("--poll-seconds", type=float, default=1.0, help=argparse.SUPPRESS)
parser.add_argument(
"--max-rounds",
type=int,
default=10000,
help="maximum glcts invocations in this runner process (default: 10000)",
)
parser.add_argument(
"--max-empty-streak",
type=int,
default=3,
help="abort after this many invocations record no case at all; no case is blamed (default: 3)",
)
return parser
def execute(args: argparse.Namespace) -> int:
if args.idle_timeout < 0 or args.max_round_seconds < 0:
raise RunnerError("timeout values must be non-negative")
if args.poll_seconds <= 0:
raise RunnerError("--poll-seconds must be greater than zero")
if args.max_rounds <= 0 or args.max_empty_streak <= 0:
raise RunnerError("--max-rounds and --max-empty-streak must be greater than zero")
validate_deqp_args(args.deqp_arg)
extra_environment = parse_environment(args.env)
exe, workdir, caselist_path, outdir = resolve_paths(
args.exe, args.workdir, args.caselist, args.outdir
)
outdir.mkdir(parents=True, exist_ok=True)
cases = read_caselist(caselist_path)
expected = set(cases)
check_run_identity(
outdir,
args.backend,
cases,
args.run_identity,
adopt_legacy=args.adopt_legacy,
)
recorded, crashed, hung = recover_results(outdir, expected)
accounted = recorded | crashed | hung
remaining = [case for case in cases if case not in accounted]
persist_sidecars(outdir, cases, crashed, hung, remaining)
existing_qpas = len(result_qpa_files(outdir))
print(
f"[run_cts_windows] {args.backend}: expected {len(cases)}, recovered {len(accounted)} "
f"({existing_qpas} QPA chunk(s), {len(crashed)} crash, {len(hung)} hang)"
)
if not remaining:
print(f"[run_cts_windows] complete: all {len(cases)} expected cases are accounted")
return 0
environment = os.environ.copy()
environment.update(extra_environment)
# --backend is authoritative even if the inherited or extra environment
# already contains a different value.
environment["MOBILEGL_BACKEND_TYPE"] = args.backend
common_arguments = [
"--deqp-terminate-on-device-lost=disable",
"--deqp-log-images=disable",
"--deqp-log-shader-sources=disable",
]
chunk_number = next_chunk_number(outdir)
rounds = 0
empty_streak = 0
interrupted = False
fatal_launch_error = False
started_all = time.monotonic()
while remaining and rounds < args.max_rounds:
prefix = f"chunk{chunk_number:04d}"
remaining_path = outdir / "remaining.txt"
qpa_path = outdir / f"{prefix}.qpa"
stdout_path = outdir / f"{prefix}.stdout.log"
stderr_path = outdir / f"{prefix}.stderr.log"
meta_path = outdir / f"{prefix}.meta.json"
# The number allocator considers every chunk artifact, so these should
# be new. Refuse to truncate evidence if a foreign file races us.
for artifact in (qpa_path, stdout_path, stderr_path, meta_path):
if artifact.exists():
raise RunnerError(f"refusing to overwrite existing chunk artifact: {artifact}")
write_case_file(remaining_path, remaining)
command = [
str(exe),
f"--deqp-caselist-file={remaining_path}",
f"--deqp-log-filename={qpa_path}",
*common_arguments,
*args.deqp_arg,
]
print(
f"[run_cts_windows] {prefix}: launching {len(remaining)} remaining case(s); "
f"idle timeout {args.idle_timeout:g}s"
)
chunk_started_utc = utc_now()
outcome = run_process(
command,
workdir,
environment,
qpa_path,
stdout_path,
stderr_path,
args.idle_timeout,
args.max_round_seconds,
args.poll_seconds,
)
progress = scan_qpa(qpa_path)
before = set(accounted)
for case in progress.recorded:
if case in expected:
recorded.add(case)
accounted.add(case)
classification: Optional[str] = None
classified_case: Optional[str] = None
in_flight = progress.in_flight if progress.in_flight in expected else None
if not outcome.interrupted and in_flight is not None and in_flight not in accounted:
classified_case = in_flight
if outcome.timed_out:
classification = "DeviceHang"
hung.add(in_flight)
crashed.discard(in_flight)
else:
classification = "Crash"
crashed.add(in_flight)
accounted.add(in_flight)
new_accounted = len(accounted - before)
if new_accounted:
empty_streak = 0
elif progress.begin_count == 0:
# No #begin marker means there is no evidence that the first
# remaining case was reached. Retry the identical caselist, then
# abort rather than manufacturing a string of false Crash results.
empty_streak += 1
else:
# A log containing only already-accounted cases is also no forward
# progress, but it is a different failure mode. Bound it with the
# same guard while retaining the QPA evidence.
empty_streak += 1
remaining = [case for case in cases if case not in accounted]
metadata = {
"version": 1,
"chunk": chunk_number,
"started_utc": chunk_started_utc,
"finished_utc": utc_now(),
"duration_seconds": round(outcome.duration_seconds, 3),
"returncode": outcome.returncode,
"timed_out": outcome.timed_out,
"timeout_reason": outcome.timeout_reason,
"interrupted": outcome.interrupted,
"launch_error": outcome.launch_error,
"qpa_begin_count": progress.begin_count,
"qpa_recorded_count": len(progress.recorded),
"in_flight": progress.in_flight,
"classification": classification,
"classified_case": classified_case,
"new_accounted": new_accounted,
"remaining": len(remaining),
}
# Metadata is committed first. If the runner itself dies between this
# write and the sidecars, recovery can reconstruct the classification.
atomic_write_json(meta_path, metadata)
persist_sidecars(outdir, cases, crashed, hung, remaining)
rounds += 1
elapsed_minutes = (time.monotonic() - started_all) / 60.0
detail = ""
if classification:
detail = f", {classification}={classified_case}"
if outcome.timed_out:
detail += f", timeout={outcome.timeout_reason}"
print(
f"[run_cts_windows] {prefix}: +{new_accounted}, accounted "
f"{len(accounted)}/{len(cases)}, remaining {len(remaining)}{detail} "
f"({elapsed_minutes:.1f} min)"
)
chunk_number += 1
if outcome.interrupted:
interrupted = True
print("[run_cts_windows] interrupted; process tree stopped and state preserved", file=sys.stderr)
break
if outcome.launch_error:
fatal_launch_error = True
print(
f"[run_cts_windows] launch failed; see {stderr_path.name}: {outcome.launch_error}",
file=sys.stderr,
)
break
if empty_streak >= args.max_empty_streak:
print(
f"[run_cts_windows] aborting after {empty_streak} consecutive chunks made no "
"case progress; no unobserved case was labelled Crash/Hang",
file=sys.stderr,
)
break
# Recompute from the persisted evidence so the final completeness claim is
# subject to the exact same recovery path as a later invocation.
final_recorded, final_crashed, final_hung = recover_results(outdir, expected)
final_accounted = final_recorded | final_crashed | final_hung
final_remaining = [case for case in cases if case not in final_accounted]
persist_sidecars(outdir, cases, final_crashed, final_hung, final_remaining)
if not final_remaining and final_accounted == expected:
print(
f"[run_cts_windows] complete: all {len(cases)} expected cases are accounted "
f"({len(final_crashed)} crash, {len(final_hung)} hang, {rounds} new invocation(s))"
)
return 0
print(
f"[run_cts_windows] INCOMPLETE: {len(final_accounted)}/{len(cases)} accounted; "
f"{len(final_remaining)} listed in {outdir / 'unrun.txt'}",
file=sys.stderr,
)
if interrupted:
return 130
if fatal_launch_error:
return 3
return 4
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
try:
return execute(args)
except RunnerError as exc:
print(f"[run_cts_windows] ERROR: {exc}", file=sys.stderr)
return 2
except OSError as exc:
print(f"[run_cts_windows] ERROR: filesystem/process operation failed: {exc}", file=sys.stderr)
return 2
if __name__ == "__main__":
sys.exit(main())
+1
View File
@@ -21,6 +21,7 @@ CTS_TOOLS = os.path.dirname(HERE)
COPIES = [
(os.path.join(CTS_TOOLS, "platform"), "framework/platform/mobilegl", None),
(os.path.join(CTS_TOOLS, "targets"), "targets/mobilegl", ["mobilegl.cmake", "ndk-modern.cmake"]),
(os.path.join(CTS_TOOLS, "targets"), "targets/mobilegl-desktop", ["mobilegl-desktop.cmake"]),
]
+251
View File
@@ -0,0 +1,251 @@
import contextlib
import io
import json
import tempfile
import unittest
from pathlib import Path
try:
from . import cts_matrix_report as report
except ImportError: # Allows `python test_cts_matrix_report.py`.
import cts_matrix_report as report
def qpa_case(case, status):
return (
f"#beginTestCaseResult {case}\n"
f'<Result StatusCode="{status}"/>\n'
"#endTestCaseResult\n"
)
class MatrixReportTests(unittest.TestCase):
def test_utf8_bom_caselist_matches_runner_semantics(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
caselist = root / "cases.txt"
caselist.write_bytes(b"\xef\xbb\xbfcase.a\n")
results = root / "results"
results.mkdir()
(results / "run.qpa").write_text(
qpa_case("case.a", "Pass"), encoding="utf-8"
)
item = report.build_version_report("gl30", str(caselist), [str(results)])
self.assertEqual(1, item["expected"])
self.assertEqual({"case.a": "Pass"}, item["cases"]["results"])
self.assertEqual("OK", item["validation"]["state"])
def test_incomplete_qpa_is_unrun_not_a_completed_result(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
caselist = root / "cases.txt"
caselist.write_text("case.a\n", encoding="utf-8")
results = root / "results"
results.mkdir()
(results / "run.qpa").write_text(
"#beginTestCaseResult case.a\n#endTestCaseResult\n",
encoding="utf-8",
)
(results / "unrun.txt").write_text("case.a\n", encoding="utf-8")
item = report.build_version_report("gl46", str(caselist), [str(results)])
self.assertEqual(0, item["result"])
self.assertEqual(1, item["unrun"])
self.assertEqual(["case.a"], item["cases"]["incomplete_results"])
self.assertEqual("INCOMPLETE", item["validation"]["state"])
def test_incomplete_qpa_is_upgraded_by_crash_sidecar(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
caselist = root / "cases.txt"
caselist.write_text("case.a\n", encoding="utf-8")
results = root / "results"
results.mkdir()
(results / "run.qpa").write_text(
"#beginTestCaseResult case.a\n", encoding="utf-8"
)
(results / "crashed.txt").write_text("case.a\n", encoding="utf-8")
item = report.build_version_report("gl46", str(caselist), [str(results)])
self.assertEqual("Crash", item["cases"]["results"]["case.a"])
self.assertEqual([], item["cases"]["incomplete_results"])
self.assertEqual("OK", item["validation"]["state"])
def test_chunk_numbers_above_four_digits_use_numeric_order(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
caselist = root / "cases.txt"
caselist.write_text("case.a\n", encoding="utf-8")
results = root / "results"
results.mkdir()
(results / "alpha.qpa").write_text("# no results\n", encoding="utf-8")
(results / "chunk9999.qpa").write_text(
qpa_case("case.a", "Fail"), encoding="utf-8"
)
(results / "chunk10000.qpa").write_text(
qpa_case("case.a", "Pass"), encoding="utf-8"
)
(results / "zeta.qpa").write_text("# no results\n", encoding="utf-8")
item = report.build_version_report(
"gl46", str(caselist), [str(results)]
)
self.assertEqual(
["alpha.qpa", "chunk9999.qpa", "chunk10000.qpa", "zeta.qpa"],
[Path(path).name for path in item["inputs"]["qpa_files"]],
)
self.assertEqual("Pass", item["cases"]["results"]["case.a"])
self.assertEqual(1, item["duplicate"])
def test_qpa_sidecars_duplicates_and_expected_denominator(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
caselist = root / "gl30.txt"
caselist.write_text("\n".join("abcdefg") + "\n", encoding="utf-8")
results = root / "results"
results.mkdir()
(results / "chunk0000.qpa").write_text(
qpa_case("a", "Fail") + "#beginTestCaseResult e\n",
encoding="utf-8",
)
(results / "chunk0001.qpa").write_text(
qpa_case("a", "Pass")
+ qpa_case("b", "NotSupported")
+ qpa_case("c", "QualityWarning")
+ qpa_case("d", "Fail"),
encoding="utf-8",
)
(results / "crashed.txt").write_text("e\n", encoding="utf-8")
(results / "hung.txt").write_text("f\n", encoding="utf-8")
(results / "unrun.txt").write_text("g\n", encoding="utf-8")
item = report.build_version_report(
"gl30", str(caselist), [str(results)]
)
self.assertEqual(item["expected"], 7)
self.assertEqual(item["result"], 6)
self.assertEqual(item["pass"], 1)
self.assertEqual(item["accepted"], 3)
self.assertEqual(item["crash"], 1)
self.assertEqual(item["hang"], 1)
self.assertEqual(item["unrun"], 1)
self.assertEqual(item["duplicate"], 1)
self.assertEqual(item["cases"]["results"]["a"], "Pass")
self.assertEqual(item["cases"]["results"]["e"], "Crash")
self.assertEqual(item["cases"]["results"]["f"], "DeviceHang")
self.assertAlmostEqual(item["strict_pass_rate"], 1 / 7)
self.assertAlmostEqual(item["conformance_accepted_rate"], 3 / 7)
self.assertAlmostEqual(
item["rates"]["measured_only_conformance_accepted"], 3 / 6
)
self.assertEqual(item["validation"]["state"], "INCOMPLETE")
self.assertEqual(item["validation"]["errors"], [])
self.assertTrue(
item["validation"]["invariant_expected_equals_result_plus_unrun"]
)
def test_missing_result_is_inferred_and_rejected_when_not_declared(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
caselist = root / "cases.txt"
caselist.write_text("a\nb\n", encoding="utf-8")
results = root / "results"
results.mkdir()
(results / "run.qpa").write_text(qpa_case("a", "Pass"), encoding="utf-8")
item = report.build_version_report(
"gl31", str(caselist), [str(results)]
)
self.assertEqual(item["unrun"], 1)
self.assertEqual(item["cases"]["unrun"], ["b"])
self.assertEqual(item["validation"]["state"], "ERROR")
self.assertEqual(item["validation"]["undeclared_unrun"], ["b"])
self.assertIn("not declared", item["validation"]["errors"][0])
self.assertEqual(item["strict_pass_rate"], 0.5)
def test_cli_emits_markdown_json_and_weighted_overall(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
statuses = {
"gl30": "Pass",
"gl31": "Fail",
"gl32": "NotSupported",
"gl33": None,
}
argv = []
for version, status in statuses.items():
caselist = root / f"{version}.txt"
caselist.write_text(f"{version}.case\n", encoding="utf-8")
result_dir = root / f"{version}-results"
result_dir.mkdir()
qpa = result_dir / "run.qpa"
qpa.write_text(
qpa_case(f"{version}.case", status) if status else "# empty run\n",
encoding="utf-8",
)
if status is None:
(result_dir / "unrun.txt").write_text(
f"{version}.case\n", encoding="utf-8"
)
argv.extend(
[
f"--{version}-caselist",
str(caselist),
f"--{version}-results",
str(result_dir),
]
)
json_path = root / "matrix.json"
argv.extend(["--json", str(json_path)])
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
rc = report.main(argv)
self.assertEqual(rc, 1) # GL33 is explicitly incomplete.
markdown = stdout.getvalue()
self.assertIn("| Suite | Expected | Result", markdown)
self.assertIn("| **Overall (weighted)**", markdown)
payload = json.loads(json_path.read_text(encoding="utf-8"))
overall = payload["overall"]
self.assertEqual(overall["expected"], 4)
self.assertEqual(overall["result"], 3)
self.assertEqual(overall["pass"], 1)
self.assertEqual(overall["accepted"], 2)
self.assertEqual(overall["unrun"], 1)
self.assertEqual(overall["strict_pass_rate"], 0.25)
self.assertEqual(overall["conformance_accepted_rate"], 0.5)
self.assertEqual(overall["aggregation"], "weighted_by_expected_cases")
self.assertEqual(overall["validation"]["state"], "INCOMPLETE")
def test_duplicate_caselist_and_unexpected_result_are_validation_errors(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
caselist = root / "cases.txt"
caselist.write_text("a\na\n", encoding="utf-8")
results = root / "results"
results.mkdir()
(results / "run.qpa").write_text(
qpa_case("a", "Pass") + qpa_case("outside", "Pass"),
encoding="utf-8",
)
item = report.build_version_report(
"gl32", str(caselist), [str(results)]
)
self.assertEqual(item["cases"]["duplicate_caselist_entries"], {"a": 2})
self.assertEqual(item["cases"]["unexpected_results"], {"outside": "Pass"})
self.assertEqual(item["validation"]["state"], "ERROR")
self.assertEqual(len(item["validation"]["errors"]), 2)
if __name__ == "__main__":
unittest.main()
+342
View File
@@ -0,0 +1,342 @@
import contextlib
import io
import json
import tempfile
import unittest
from pathlib import Path
try:
from . import cts_multi_report as report
except ImportError: # Allows `python test_cts_multi_report.py`.
import cts_multi_report as report
def qpa_case(case: str, status: str) -> str:
return (
f"#beginTestCaseResult {case}\n"
f'<Result StatusCode="{status}"/>\n'
"#endTestCaseResult\n"
)
def write_run_state(
caselist: Path,
result_dir: Path,
backend: str,
invocation_identity=None,
) -> None:
fingerprint, case_count = report._caselist_fingerprint(str(caselist))
(result_dir / "run_state.json").write_text(
json.dumps(
{
"version": 1,
"backend": backend,
"case_count": case_count,
"caselist_sha256": fingerprint,
"invocation_identity": invocation_identity,
}
),
encoding="utf-8",
)
def make_inputs(
root: Path,
name: str,
cases: list[str],
qpa: str,
backend: str = "DirectGLES",
):
caselist = root / f"{name}.txt"
caselist.write_text("\n".join(cases) + "\n", encoding="utf-8")
result_dir = root / f"{name}-results"
result_dir.mkdir()
(result_dir / "chunk0000.qpa").write_text(qpa, encoding="utf-8")
write_run_state(caselist, result_dir, backend)
return caselist, result_dir
class MultiReportTests(unittest.TestCase):
def test_backend_aggregate_is_weighted_by_expected_cases(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
small_cases, small_results = make_inputs(
root, "small", ["small.pass"], qpa_case("small.pass", "Pass")
)
large_cases, large_results = make_inputs(
root,
"large",
["large.pass", "large.crash", "large.hang"],
qpa_case("large.pass", "Pass"),
)
(large_results / "crashed.txt").write_text(
"large.crash\n", encoding="utf-8"
)
(large_results / "hung.txt").write_text(
"large.hang\n", encoding="utf-8"
)
payload = report.build_report(
[
report.SuiteSpec(
"DirectGLES", "small", str(small_cases), str(small_results)
),
report.SuiteSpec(
"DirectGLES", "large", str(large_cases), str(large_results)
),
]
)
aggregate = payload["backends"]["DirectGLES"]
self.assertEqual(4, aggregate["expected"])
self.assertEqual(4, aggregate["result"])
self.assertEqual(2, aggregate["pass"])
self.assertEqual(2, aggregate["accepted"])
self.assertEqual(1, aggregate["crash"])
self.assertEqual(1, aggregate["hang"])
self.assertEqual(0, aggregate["unrun"])
# (1 accepted + 1 accepted) / (1 expected + 3 expected), not
# the unweighted mean of 100% and 33.3%.
self.assertEqual(0.5, aggregate["conformance_accepted_rate"])
self.assertEqual("weighted_by_expected_cases", aggregate["aggregation"])
self.assertEqual("OK", aggregate["validation"]["state"])
def test_declared_unrun_is_incomplete_and_cli_returns_nonzero(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
caselist, result_dir = make_inputs(
root, "missing", ["case.a", "case.b"], qpa_case("case.a", "Pass")
)
(result_dir / "unrun.txt").write_text("case.b\n", encoding="utf-8")
markdown_path = root / "report.md"
json_path = root / "report.json"
argv = [
"--suite",
"DirectGLES",
"gl30",
str(caselist),
str(result_dir),
"--markdown",
str(markdown_path),
"--json",
str(json_path),
]
with contextlib.redirect_stdout(io.StringIO()):
returncode = report.main(argv)
self.assertEqual(1, returncode)
self.assertTrue(markdown_path.is_file())
payload = json.loads(json_path.read_text(encoding="utf-8"))
suite = payload["suites"][0]
self.assertEqual(2, suite["expected"])
self.assertEqual(1, suite["result"])
self.assertEqual(1, suite["unrun"])
self.assertEqual("INCOMPLETE", suite["validation"]["state"])
self.assertEqual("INCOMPLETE", payload["overall"]["validation"]["state"])
self.assertEqual(0.5, payload["overall"]["conformance_accepted_rate"])
def test_dual_backend_cli_outputs_markdown_and_json(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
caselist = root / "gl30.txt"
caselist.write_text("gl30.case\n", encoding="utf-8")
gles = root / "gles"
vulkan = root / "vulkan"
gles.mkdir()
vulkan.mkdir()
(gles / "run.qpa").write_text(
qpa_case("gl30.case", "Pass"), encoding="utf-8"
)
(vulkan / "run.qpa").write_text(
qpa_case("gl30.case", "Fail"), encoding="utf-8"
)
write_run_state(caselist, gles, "DirectGLES")
write_run_state(caselist, vulkan, "DirectVulkan")
markdown_path = root / "dual.md"
json_path = root / "dual.json"
argv = [
f"--suite=DirectGLES,gl30,{caselist},{gles}",
"--suite",
"DirectVulkan",
"gl30",
str(caselist),
str(vulkan),
"--markdown",
str(markdown_path),
"--json",
str(json_path),
]
stdout = io.StringIO()
with contextlib.redirect_stdout(stdout):
returncode = report.main(argv)
self.assertEqual(0, returncode)
payload = json.loads(json_path.read_text(encoding="utf-8"))
self.assertEqual({"DirectGLES", "DirectVulkan"}, set(payload["backends"]))
self.assertEqual(1, payload["backends"]["DirectGLES"]["accepted"])
self.assertEqual(0, payload["backends"]["DirectVulkan"]["accepted"])
self.assertEqual(2, payload["overall"]["expected"])
self.assertEqual(1, payload["overall"]["accepted"])
self.assertEqual(0.5, payload["overall"]["conformance_accepted_rate"])
markdown = markdown_path.read_text(encoding="utf-8")
self.assertIn("DirectGLES weighted subtotal", markdown)
self.assertIn("DirectVulkan weighted subtotal", markdown)
self.assertIn("Overall weighted", markdown)
self.assertIn("Markdown:", stdout.getvalue())
def test_duplicate_qpa_result_uses_last_observation(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
caselist, result_dir = make_inputs(
root,
"duplicate",
["case.a"],
qpa_case("case.a", "Fail"),
backend="DirectVulkan",
)
(result_dir / "chunk0001.qpa").write_text(
qpa_case("case.a", "Pass"), encoding="utf-8"
)
payload = report.build_report(
[
report.SuiteSpec(
"DirectVulkan", "gl33", str(caselist), str(result_dir)
)
]
)
suite = payload["suites"][0]
self.assertEqual("Pass", suite["cases"]["results"]["case.a"])
self.assertEqual(1, suite["duplicate"])
self.assertEqual(1, payload["overall"]["duplicate"])
self.assertEqual(1.0, payload["overall"]["strict_pass_rate"])
self.assertEqual("OK", suite["validation"]["state"])
self.assertIn("last result wins", suite["validation"]["warnings"][0])
def test_backend_provenance_mismatch_is_rejected(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
caselist, result_dir = make_inputs(
root,
"provenance",
["case.a"],
qpa_case("case.a", "Pass"),
backend="DirectVulkan",
)
with self.assertRaises(report.MultiReportInputError):
report.build_report(
[report.SuiteSpec("DirectGLES", "gl30", str(caselist), str(result_dir))]
)
def test_missing_provenance_requires_explicit_legacy_opt_in(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
caselist, result_dir = make_inputs(
root, "legacy", ["case.a"], qpa_case("case.a", "Pass")
)
(result_dir / "run_state.json").unlink()
spec = report.SuiteSpec("DirectGLES", "gl30", str(caselist), str(result_dir))
with self.assertRaises(report.MultiReportInputError):
report.build_report([spec])
payload = report.build_report([spec], require_run_state=False)
self.assertEqual("UNVERIFIED", payload["suites"][0]["provenance"]["state"])
def test_expected_run_identity_accepts_match_and_rejects_mismatch(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
caselist, result_dir = make_inputs(
root, "identity", ["case.a"], qpa_case("case.a", "Pass")
)
write_run_state(
caselist, result_dir, "DirectGLES", invocation_identity="identity-a"
)
spec = report.SuiteSpec(
"DirectGLES", "gl30", str(caselist), str(result_dir)
)
payload = report.build_report(
[spec], expected_run_identity="identity-a"
)
self.assertEqual(
"identity-a",
payload["suites"][0]["provenance"]["invocation_identity"],
)
with self.assertRaises(report.MultiReportInputError):
report.build_report([spec], expected_run_identity="identity-b")
def test_expected_identity_rejects_legacy_state_and_missing_state(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
caselist, result_dir = make_inputs(
root, "legacy-identity", ["case.a"], qpa_case("case.a", "Pass")
)
spec = report.SuiteSpec(
"DirectGLES", "gl30", str(caselist), str(result_dir)
)
with self.assertRaises(report.MultiReportInputError):
report.build_report([spec], expected_run_identity="identity-a")
(result_dir / "run_state.json").unlink()
with self.assertRaises(report.MultiReportInputError):
report.build_report(
[spec],
require_run_state=False,
expected_run_identity="identity-a",
)
def test_duplicate_physical_result_directory_is_rejected(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
caselist, result_dir = make_inputs(
root, "duplicate-dir", ["case.a"], qpa_case("case.a", "Pass")
)
with self.assertRaises(report.MultiReportInputError):
report.build_report(
[
report.SuiteSpec(
"DirectGLES", "gl30", str(caselist), str(result_dir)
),
report.SuiteSpec(
"DirectGLES",
"gl31",
str(caselist),
str(result_dir / "."),
),
]
)
def test_ancestor_and_descendant_result_directories_are_rejected(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
caselist = root / "cases.txt"
caselist.write_text("case.a\n", encoding="utf-8")
parent = root / "results"
child = parent / "nested"
child.mkdir(parents=True)
(parent / "chunk0000.qpa").write_text(
qpa_case("case.a", "Pass"), encoding="utf-8"
)
(child / "chunk0000.qpa").write_text(
qpa_case("case.a", "Fail"), encoding="utf-8"
)
write_run_state(caselist, parent, "DirectGLES")
write_run_state(caselist, child, "DirectVulkan")
with self.assertRaises(report.MultiReportInputError):
report.build_report(
[
report.SuiteSpec(
"DirectGLES", "gl30", str(caselist), str(parent)
),
report.SuiteSpec(
"DirectVulkan", "gl30", str(caselist), str(child)
),
]
)
if __name__ == "__main__":
unittest.main()
+401
View File
@@ -0,0 +1,401 @@
import sys
import tempfile
import time
import unittest
from pathlib import Path
from unittest import mock
import run_cts_windows as runner
def qpa_closed(case: str, status: str = "Pass") -> str:
return (
f"#beginTestCaseResult {case}\n"
f'<Result StatusCode="{status}">ok</Result>\n'
"#endTestCaseResult\n"
)
def command_path(command, option):
prefix = option + "="
return Path(next(value[len(prefix) :] for value in command if value.startswith(prefix)))
class AtomicWriteTests(unittest.TestCase):
def test_access_denied_retries_then_replace_succeeds(self):
with tempfile.TemporaryDirectory() as temporary:
target = Path(temporary) / "remaining.txt"
real_replace = runner.os.replace
attempts = 0
def flaky_replace(source, destination):
nonlocal attempts
attempts += 1
if attempts == 1:
raise PermissionError(13, "temporarily denied", str(destination))
if attempts == 2:
error = OSError("temporary WinError 5")
error.winerror = 5
raise error
real_replace(source, destination)
with mock.patch.object(runner.os, "replace", side_effect=flaky_replace), mock.patch.object(
runner.time, "sleep"
) as sleep:
runner.atomic_write_text(target, "case.a\n")
self.assertEqual(3, attempts)
self.assertEqual("case.a\n", target.read_text(encoding="utf-8"))
self.assertEqual(2, sleep.call_count)
self.assertEqual(
[
mock.call(runner.ATOMIC_REPLACE_INITIAL_BACKOFF_SECONDS),
mock.call(runner.ATOMIC_REPLACE_INITIAL_BACKOFF_SECONDS * 2),
],
sleep.call_args_list,
)
self.assertEqual([], list(target.parent.glob(".remaining.txt.*.tmp")))
def test_permanent_access_denied_stops_after_bounded_attempts(self):
with tempfile.TemporaryDirectory() as temporary:
target = Path(temporary) / "remaining.txt"
def always_denied(_source, _destination):
error = OSError("persistent WinError 5")
error.winerror = 5
raise error
with mock.patch.object(
runner.os, "replace", side_effect=always_denied
) as replace, mock.patch.object(runner.time, "sleep") as sleep:
with self.assertRaises(OSError) as raised:
runner.atomic_write_text(target, "case.a\n")
self.assertEqual(5, raised.exception.winerror)
self.assertEqual(runner.ATOMIC_REPLACE_ATTEMPTS, replace.call_count)
self.assertEqual(runner.ATOMIC_REPLACE_ATTEMPTS - 1, sleep.call_count)
self.assertFalse(target.exists())
self.assertEqual([], list(target.parent.glob(".remaining.txt.*.tmp")))
def test_non_access_error_is_not_retried(self):
with tempfile.TemporaryDirectory() as temporary:
target = Path(temporary) / "remaining.txt"
error = OSError(28, "disk full")
with mock.patch.object(
runner.os, "replace", side_effect=error
) as replace, mock.patch.object(runner.time, "sleep") as sleep:
with self.assertRaises(OSError):
runner.atomic_write_text(target, "case.a\n")
self.assertEqual(1, replace.call_count)
sleep.assert_not_called()
class QpaParsingTests(unittest.TestCase):
def test_terminate_is_a_completed_result(self):
with tempfile.TemporaryDirectory() as temporary:
path = Path(temporary) / "chunk0000.qpa"
path.write_text(
"#beginTestCaseResult KHR-GL30.a\n"
"#terminateTestCaseResult Crash\n"
"#beginTestCaseResult KHR-GL30.b\n",
encoding="utf-8",
)
progress = runner.scan_qpa(path)
self.assertEqual(["KHR-GL30.a"], progress.recorded)
self.assertEqual("KHR-GL30.b", progress.in_flight)
self.assertEqual(2, progress.begin_count)
def test_end_without_result_is_not_accounted(self):
with tempfile.TemporaryDirectory() as temporary:
path = Path(temporary) / "chunk0000.qpa"
path.write_text(
"#beginTestCaseResult KHR-GL46.incomplete\n"
"#endTestCaseResult\n"
+ qpa_closed("KHR-GL46.complete"),
encoding="utf-8",
)
progress = runner.scan_qpa(path)
self.assertEqual(["KHR-GL46.complete"], progress.recorded)
self.assertIsNone(progress.in_flight)
def test_result_written_before_truncated_eof_is_recovered(self):
with tempfile.TemporaryDirectory() as temporary:
path = Path(temporary) / "chunk0000.qpa"
path.write_text(
"#beginTestCaseResult KHR-GL46.complete\n"
'<Result StatusCode="Pass">ok</Result>\n',
encoding="utf-8",
)
progress = runner.scan_qpa(path)
self.assertEqual(["KHR-GL46.complete"], progress.recorded)
self.assertIsNone(progress.in_flight)
class RunIdentityTests(unittest.TestCase):
def test_non_object_run_state_is_a_controlled_error(self):
with tempfile.TemporaryDirectory() as temporary:
outdir = Path(temporary)
(outdir / "run_state.json").write_text("null\n", encoding="utf-8")
with self.assertRaises(runner.RunnerError):
runner.check_run_identity(outdir, "DirectVulkan", ["case.a"])
def test_controller_identity_prevents_mixed_invocations(self):
with tempfile.TemporaryDirectory() as temporary:
outdir = Path(temporary)
runner.check_run_identity(
outdir, "DirectVulkan", ["case.a"], invocation_identity="identity-a"
)
runner.check_run_identity(
outdir, "DirectVulkan", ["case.a"], invocation_identity="identity-a"
)
with self.assertRaises(runner.RunnerError):
runner.check_run_identity(
outdir, "DirectVulkan", ["case.a"], invocation_identity="identity-b"
)
def test_controller_identity_cannot_be_downgraded_by_omission(self):
with tempfile.TemporaryDirectory() as temporary:
outdir = Path(temporary)
runner.check_run_identity(
outdir, "DirectVulkan", ["case.a"], invocation_identity="identity-a"
)
with self.assertRaises(runner.RunnerError):
runner.check_run_identity(outdir, "DirectVulkan", ["case.a"])
def test_legacy_artifacts_require_explicit_adoption(self):
with tempfile.TemporaryDirectory() as temporary:
outdir = Path(temporary)
(outdir / "chunk0000.qpa").write_text(
qpa_closed("case.a"), encoding="utf-8"
)
with self.assertRaises(runner.RunnerError):
runner.check_run_identity(
outdir, "DirectVulkan", ["case.a"], invocation_identity="identity-a"
)
self.assertFalse((outdir / "run_state.json").exists())
runner.check_run_identity(
outdir,
"DirectVulkan",
["case.a"],
invocation_identity="identity-a",
adopt_legacy=True,
)
state = runner.json.loads(
(outdir / "run_state.json").read_text(encoding="utf-8")
)
self.assertTrue(state["adopted_legacy"])
def test_foreign_nested_qpa_and_skipped_sidecar_are_legacy_evidence(self):
with tempfile.TemporaryDirectory() as temporary:
outdir = Path(temporary)
nested = outdir / "old"
nested.mkdir()
qpa = nested / "legacy.qpa"
qpa.write_text(qpa_closed("case.a"), encoding="utf-8")
skipped = outdir / "skipped.txt"
skipped.write_text("case.b\n", encoding="utf-8")
self.assertEqual(
{qpa, skipped}, set(runner.recovery_artifacts(outdir))
)
with self.assertRaises(runner.RunnerError):
runner.check_run_identity(
outdir, "DirectVulkan", ["case.a", "case.b"]
)
recorded, crashed, hung = runner.recover_results(
outdir, {"case.a", "case.b"}
)
self.assertEqual({"case.a"}, recorded)
self.assertEqual(set(), crashed)
self.assertEqual(set(), hung)
def test_non_object_or_non_string_meta_classification_is_ignored(self):
with tempfile.TemporaryDirectory() as temporary:
outdir = Path(temporary)
(outdir / "chunk0000.meta.json").write_text("null\n", encoding="utf-8")
(outdir / "chunk0001.meta.json").write_text("[]\n", encoding="utf-8")
(outdir / "chunk0002.meta.json").write_text(
'{"classified_case": [], "classification": "Crash"}\n', encoding="utf-8"
)
self.assertEqual(
(set(), set()), runner.load_meta_classifications(outdir, {"case.a"})
)
class RunnerRecoveryTests(unittest.TestCase):
def run_args(self, root: Path, caselist: Path, outdir: Path, *extra: str):
return [
"--exe",
sys.executable,
"--workdir",
str(root),
"--caselist",
str(caselist),
"--outdir",
str(outdir),
"--backend",
"DirectVulkan",
*extra,
]
def test_crash_tail_is_quarantined_and_next_chunk_resumes(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
caselist = root / "cases.txt"
outdir = root / "results"
caselist.write_text("KHR-GL30.a\nKHR-GL30.b\nKHR-GL30.c\n", encoding="utf-8")
seen_remaining = []
def fake_run(command, workdir, environment, qpa_path, stdout_path, stderr_path, *timeouts):
del workdir, timeouts
seen_remaining.append(
command_path(command, "--deqp-caselist-file")
.read_text(encoding="utf-8")
.splitlines()
)
stdout_path.write_text("fake stdout\n", encoding="utf-8")
stderr_path.write_text("fake stderr\n", encoding="utf-8")
self.assertEqual("DirectVulkan", environment["MOBILEGL_BACKEND_TYPE"])
if len(seen_remaining) == 1:
qpa_path.write_text(
qpa_closed("KHR-GL30.a") + "#beginTestCaseResult KHR-GL30.b\n",
encoding="utf-8",
)
return runner.ProcessOutcome(0xC0000005, 0.1)
qpa_path.write_text(qpa_closed("KHR-GL30.c"), encoding="utf-8")
return runner.ProcessOutcome(0, 0.1)
with mock.patch.object(runner, "run_process", side_effect=fake_run):
result = runner.main(self.run_args(root, caselist, outdir))
self.assertEqual(0, result)
self.assertEqual(
[
["KHR-GL30.a", "KHR-GL30.b", "KHR-GL30.c"],
["KHR-GL30.c"],
],
seen_remaining,
)
self.assertEqual("KHR-GL30.b\n", (outdir / "crashed.txt").read_text(encoding="utf-8"))
self.assertEqual("", (outdir / "hung.txt").read_text(encoding="utf-8"))
self.assertEqual("", (outdir / "unrun.txt").read_text(encoding="utf-8"))
def test_existing_qpa_and_sidecar_are_recovered(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
caselist = root / "cases.txt"
outdir = root / "results"
outdir.mkdir()
caselist.write_text("KHR-GL31.a\nKHR-GL31.b\nKHR-GL31.c\n", encoding="utf-8")
(outdir / "chunk0000.qpa").write_text(qpa_closed("KHR-GL31.a"), encoding="utf-8")
(outdir / "crashed.txt").write_text("KHR-GL31.b\n", encoding="utf-8")
seen_remaining = []
def fake_run(command, workdir, environment, qpa_path, stdout_path, stderr_path, *timeouts):
del workdir, environment, stdout_path, stderr_path, timeouts
seen_remaining.extend(
command_path(command, "--deqp-caselist-file")
.read_text(encoding="utf-8")
.splitlines()
)
qpa_path.write_text(qpa_closed("KHR-GL31.c"), encoding="utf-8")
return runner.ProcessOutcome(0, 0.1)
with mock.patch.object(runner, "run_process", side_effect=fake_run):
result = runner.main(
self.run_args(root, caselist, outdir, "--adopt-legacy")
)
self.assertEqual(0, result)
self.assertEqual(["KHR-GL31.c"], seen_remaining)
self.assertTrue((outdir / "chunk0001.qpa").is_file())
def test_repeated_no_output_aborts_without_false_case_blame(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
caselist = root / "cases.txt"
outdir = root / "results"
caselist.write_text("KHR-GL32.a\nKHR-GL32.b\n", encoding="utf-8")
seen_remaining = []
def fake_run(command, workdir, environment, qpa_path, stdout_path, stderr_path, *timeouts):
del workdir, environment, stdout_path, stderr_path, timeouts
seen_remaining.append(
command_path(command, "--deqp-caselist-file")
.read_text(encoding="utf-8")
.splitlines()
)
qpa_path.write_text("#sessionInfo releaseName fake\n", encoding="utf-8")
return runner.ProcessOutcome(
1, 0.1, timed_out=True, timeout_reason="qpa-idle"
)
with mock.patch.object(runner, "run_process", side_effect=fake_run):
result = runner.main(
self.run_args(root, caselist, outdir, "--max-empty-streak", "2")
)
self.assertEqual(4, result)
self.assertEqual(
[["KHR-GL32.a", "KHR-GL32.b"], ["KHR-GL32.a", "KHR-GL32.b"]],
seen_remaining,
)
self.assertEqual("", (outdir / "crashed.txt").read_text(encoding="utf-8"))
self.assertEqual("", (outdir / "hung.txt").read_text(encoding="utf-8"))
self.assertEqual(
"KHR-GL32.a\nKHR-GL32.b\n",
(outdir / "unrun.txt").read_text(encoding="utf-8"),
)
class ProcessTimeoutTests(unittest.TestCase):
def test_qpa_activity_prevents_idle_timeout(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
qpa = root / "active.qpa"
helper = (
"import pathlib,sys,time\n"
"path=pathlib.Path(sys.argv[1])\n"
"for size in range(1, 9):\n"
" path.write_text('x' * size, encoding='utf-8')\n"
" time.sleep(0.08)\n"
)
outcome = runner.run_process(
[sys.executable, "-c", helper, str(qpa)],
root,
dict(runner.os.environ),
qpa,
root / "stdout.log",
root / "stderr.log",
idle_timeout=0.2,
max_round_seconds=0,
poll_seconds=0.03,
)
self.assertFalse(outcome.timed_out)
self.assertEqual(0, outcome.returncode)
def test_idle_timeout_really_stops_process(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
started = time.monotonic()
outcome = runner.run_process(
[sys.executable, "-c", "import time; time.sleep(30)"],
root,
dict(runner.os.environ),
root / "never-created.qpa",
root / "stdout.log",
root / "stderr.log",
idle_timeout=0.2,
max_round_seconds=0,
poll_seconds=0.05,
)
elapsed = time.monotonic() - started
self.assertTrue(outcome.timed_out)
self.assertEqual("qpa-idle", outcome.timeout_reason)
self.assertLess(elapsed, 10)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,294 @@
import argparse
import json
from pathlib import Path
import struct
import tempfile
import unittest
import wgl_glcts_pipeline as pipeline
def write_fake_pe(path: Path, machine: int = pipeline.PE_MACHINE_AMD64, payload: bytes = b"") -> None:
data = bytearray(0x88)
data[0:2] = b"MZ"
struct.pack_into("<I", data, 0x3C, 0x80)
data[0x80:0x84] = b"PE\0\0"
struct.pack_into("<H", data, 0x84, machine)
path.write_bytes(bytes(data) + payload)
class ArgumentTests(unittest.TestCase):
def test_versions_accept_gl_and_dotted_spellings(self):
self.assertEqual("30", pipeline.normalize_version("GL30"))
self.assertEqual("46", pipeline.normalize_version("4.6"))
with self.assertRaises(argparse.ArgumentTypeError):
pipeline.normalize_version("4.7")
def test_environment_assignment_validation(self):
self.assertEqual(("MOBILEGL_TEST", "a=b"), pipeline.parse_assignment("MOBILEGL_TEST=a=b"))
with self.assertRaises(argparse.ArgumentTypeError):
pipeline.parse_assignment("9BAD=value")
def test_windows_environment_keys_are_canonical_and_last_wins(self):
self.assertEqual(
{"FOO": "x", "PATH": "second"},
pipeline.canonicalize_windows_environment(
[("Path", "first"), ("FOO", "x"), ("pAtH", "second")]
),
)
class CommandTests(unittest.TestCase):
def test_visual_studio_configure_commands_are_x64_and_wgl_default(self):
mobilegl = pipeline.mobilegl_configure_command(
Path("C:/src/MobileGL"), Path("D:/work/mg"), "Visual Studio 17 2022", "x64", []
)
self.assertIn("-A", mobilegl)
self.assertIn("x64", mobilegl)
self.assertIn("-DMOBILEGL_BUILD_TEST=OFF", mobilegl)
cts = pipeline.cts_configure_command(
Path("D:/src/VK-GL-CTS"), Path("D:/work/cts"), "Visual Studio 17 2022", "x64", []
)
self.assertIn("-DDEQP_TARGET=default", cts)
self.assertNotIn("-DDEQP_TARGET=mobilegl", cts)
def test_runner_command_contains_identity_preserving_wgl_flags(self):
command = pipeline.runner_command(
Path("runner.py"),
Path("runtime/glcts.exe"),
Path("cts/modules"),
Path("gl46-main.txt"),
Path("results/gl46"),
"DirectVulkan",
300,
0,
10000,
{"MOBILEGL_LOG_FILE_PATH": "result/mobilegl.log"},
pipeline.DEFAULT_DEQP_ARGS,
"run-fingerprint",
)
self.assertIn("--backend", command)
self.assertIn("DirectVulkan", command)
self.assertIn("--deqp-arg=--deqp-gl-context-type=wgl", command)
self.assertIn("--deqp-arg=--deqp-surface-type=fbo", command)
self.assertIn("--env", command)
self.assertIn("MOBILEGL_LOG_FILE_PATH=result/mobilegl.log", command)
self.assertIn("--run-identity", command)
self.assertIn("run-fingerprint", command)
def test_report_command_requires_the_pipeline_run_identity(self):
command = pipeline.report_command(
Path("report.py"),
[("DirectVulkan", "gl46", Path("gl46.txt"), Path("results/gl46"))],
Path("summary.md"),
Path("summary.json"),
False,
"run-fingerprint",
)
self.assertIn("--expected-run-identity", command)
self.assertIn("run-fingerprint", command)
class RuntimeTests(unittest.TestCase):
def test_pe_machine_rejects_non_x64(self):
with tempfile.TemporaryDirectory() as temporary:
path = Path(temporary) / "x86.dll"
write_fake_pe(path, machine=0x14C)
self.assertEqual(0x14C, pipeline.pe_machine(path))
with self.assertRaises(pipeline.PipelineError):
pipeline.require_x64_pe(path, "test DLL")
def test_runtime_is_hash_keyed_and_copies_only_declared_files(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
glcts = root / "source-glcts.exe"
mobilegl = root / "source-MobileGL.dll"
write_fake_pe(glcts, payload=b"glcts")
write_fake_pe(mobilegl, payload=b"mobilegl")
sources = {"glcts.exe": glcts, "opengl32.dll": mobilegl}
fingerprint, hashes = pipeline.runtime_fingerprint(sources)
runtime = pipeline.assemble_runtime(root / "work", sources, fingerprint, hashes)
self.assertEqual(fingerprint[:16], runtime.name)
self.assertEqual(hashes["glcts.exe"], pipeline.sha256_file(runtime / "glcts.exe"))
self.assertEqual(hashes["opengl32.dll"], pipeline.sha256_file(runtime / "opengl32.dll"))
manifest = json.loads((runtime / "manifest.json").read_text(encoding="utf-8"))
self.assertEqual(fingerprint, manifest["fingerprint"])
self.assertFalse((runtime / "libEGL.dll").exists())
def test_run_fingerprint_changes_with_execution_semantics(self):
base = pipeline.run_fingerprint(
"runtime", "data", {"runner": "tool"}, {"30": "caselist"}, ["--deqp-surface-type=fbo"], {"FLAG": "1"}
)
self.assertEqual(
base,
pipeline.run_fingerprint(
"runtime", "data", {"runner": "tool"}, {"30": "caselist"}, ["--deqp-surface-type=fbo"], {"FLAG": "1"}
),
)
self.assertNotEqual(
base,
pipeline.run_fingerprint(
"runtime", "data", {"runner": "tool"}, {"30": "caselist"}, ["--deqp-surface-type=window"], {"FLAG": "1"}
),
)
self.assertNotEqual(
base,
pipeline.run_fingerprint(
"runtime", "data", {"runner": "tool"}, {"30": "different"}, ["--deqp-surface-type=fbo"], {"FLAG": "1"}
),
)
self.assertNotEqual(
base,
pipeline.run_fingerprint(
"runtime", "different-data", {"runner": "tool"}, {"30": "caselist"}, ["--deqp-surface-type=fbo"], {"FLAG": "1"}
),
)
self.assertNotEqual(
base,
pipeline.run_fingerprint(
"runtime", "data", {"runner": "different-tool"}, {"30": "caselist"}, ["--deqp-surface-type=fbo"], {"FLAG": "1"}
),
)
timeout_baseline = pipeline.run_fingerprint(
"runtime",
"data",
{"runner": "tool"},
{"30": "caselist"},
["--deqp-surface-type=fbo"],
{"FLAG": "1"},
{"idle_timeout_seconds": 300.0, "max_round_seconds": 0.0},
)
idle_changed = pipeline.run_fingerprint(
"runtime",
"data",
{"runner": "tool"},
{"30": "caselist"},
["--deqp-surface-type=fbo"],
{"FLAG": "1"},
{"idle_timeout_seconds": 1.0, "max_round_seconds": 0.0},
)
max_round_changed = pipeline.run_fingerprint(
"runtime",
"data",
{"runner": "tool"},
{"30": "caselist"},
["--deqp-surface-type=fbo"],
{"FLAG": "1"},
{"idle_timeout_seconds": 300.0, "max_round_seconds": 60.0},
)
self.assertNotEqual(timeout_baseline, idle_changed)
self.assertNotEqual(timeout_baseline, max_round_changed)
def test_tracked_environment_is_case_insensitive_and_narrow(self):
ambient, effective = pipeline.tracked_run_environment(
{
"Path": "ambient-path",
"mobilegl_debug": "0",
"LibGL_Driver": "ambient-libgl",
"vK_iCd_fIlEnAmEs": "ambient-icd",
"ANGLE_DEFAULT_PLATFORM": "vulkan",
"Egl_Test": "1",
"D3D_Feature": "1",
"DxVk_Config": "ambient-dxvk",
"HOME": "ignored",
"PATH_EXTRA": "ignored",
"MOBILEGL": "ignored",
},
{"pAtH": "explicit-path", "vk_icd_filenames": "explicit-icd", "CUSTOM": "kept"},
)
self.assertEqual("ambient-path", ambient["PATH"])
self.assertNotIn("HOME", ambient)
self.assertNotIn("PATH_EXTRA", ambient)
self.assertNotIn("MOBILEGL", ambient)
self.assertEqual("explicit-path", effective["PATH"])
self.assertEqual("explicit-icd", effective["VK_ICD_FILENAMES"])
self.assertEqual("kept", effective["CUSTOM"])
def test_reserved_environment_names_cannot_hide_behind_case(self):
overrides = pipeline.canonicalize_windows_environment(
[("mobilegl_backend_type", "DirectGLES")]
)
self.assertEqual(
{"MOBILEGL_BACKEND_TYPE"},
pipeline.CONTROLLED_ENVIRONMENT_NAMES & set(overrides),
)
def test_directgles_requires_complete_angle_runtime(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
glcts = root / "glcts.exe"
mobilegl = root / "MobileGL.dll"
write_fake_pe(glcts)
write_fake_pe(mobilegl)
with self.assertRaises(pipeline.PipelineError):
pipeline.runtime_source_files(glcts, mobilegl, ["DirectGLES"], root / "angle")
angle = root / "angle"
angle.mkdir()
for name in pipeline.ANGLE_REQUIRED_DLLS:
write_fake_pe(angle / name, payload=name.encode("ascii"))
files = pipeline.runtime_source_files(glcts, mobilegl, ["DirectGLES"], angle)
self.assertEqual(
{"glcts.exe", "opengl32.dll", *pipeline.ANGLE_REQUIRED_DLLS}, set(files)
)
class CaselistTests(unittest.TestCase):
def test_preflight_prefers_small_buffer_case(self):
with tempfile.TemporaryDirectory() as temporary:
caselist = Path(temporary) / "gl30-main.txt"
caselist.write_text(
"KHR-GL30.api.coverage\nKHR-GL30.buffer_objects.gen_buffers\n",
encoding="utf-8",
)
self.assertEqual("KHR-GL30.buffer_objects.gen_buffers", pipeline.choose_preflight_case(caselist))
class PreflightTests(unittest.TestCase):
def write_identity(self, root: Path, renderer: str, version: str = "4.6"):
qpa = root / "chunk0000.qpa"
qpa.write_text(
'#sessionInfo vendor "MobileGL-Dev"\n'
f'#sessionInfo renderer "{renderer}"\n'
'#sessionInfo commandLineParameters "--deqp-gl-context-type=wgl --deqp-surface-type=fbo"\n',
encoding="utf-8",
)
log = root / "mobilegl.log"
log.write_text(f"Target OpenGL Version: {version}\n", encoding="utf-8")
return qpa, log
def test_identity_accepts_both_mobilegl_renderers(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
qpa, log = self.write_identity(root, "Magma (MobileGL Core)")
identity = pipeline.parse_preflight_identity([qpa], log, "DirectVulkan", (4, 6))
self.assertEqual("4.6", identity["target_gl_version"])
qpa, log = self.write_identity(root, "Espryt (MobileGL Core)")
identity = pipeline.parse_preflight_identity([qpa], log, "DirectGLES", (3, 3))
self.assertIn("Espryt", identity["renderer"])
def test_identity_rejects_system_driver_or_low_version(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
qpa, log = self.write_identity(root, "NVIDIA GeForce RTX", version="4.6")
qpa.write_text(
'#sessionInfo vendor "NVIDIA Corporation"\n'
'#sessionInfo renderer "NVIDIA GeForce RTX"\n'
'#sessionInfo commandLineParameters "--deqp-gl-context-type=wgl"\n',
encoding="utf-8",
)
with self.assertRaises(pipeline.PipelineError):
pipeline.parse_preflight_identity([qpa], log, "DirectVulkan", (4, 6))
qpa, log = self.write_identity(root, "Magma (MobileGL Core)", version="4.5")
with self.assertRaises(pipeline.PipelineError):
pipeline.parse_preflight_identity([qpa], log, "DirectVulkan", (4, 6))
if __name__ == "__main__":
unittest.main()
+914
View File
@@ -0,0 +1,914 @@
#!/usr/bin/env python
"""Build MobileGL's Windows WGL shim and run Khronos OpenGL CTS suites.
The pipeline intentionally keeps the build, runtime, results, and reports in an
explicit work root. Each run is keyed by the hashes of glcts.exe, opengl32.dll,
and (for DirectGLES) the ANGLE runtime, so resuming can never silently combine
results from different binaries.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import struct
import subprocess
import sys
from datetime import datetime, timezone
from typing import Iterable, Mapping, Optional, Sequence
SUPPORTED_VERSIONS = ("30", "31", "32", "33", "40", "41", "42", "43", "44", "45", "46")
SUPPORTED_BACKENDS = ("DirectGLES", "DirectVulkan")
ANGLE_REQUIRED_DLLS = ("libEGL.dll", "libGLESv2.dll", "d3dcompiler_47.dll")
ANGLE_OPTIONAL_DLLS = ("dxcompiler.dll", "dxil.dll")
PE_MACHINE_AMD64 = 0x8664
TRACKED_ENVIRONMENT_NAMES = frozenset({"PATH"})
TRACKED_ENVIRONMENT_PREFIXES = (
"MOBILEGL_",
"LIBGL_",
"VK_",
"ANGLE_",
"EGL_",
"D3D_",
"DXVK_",
)
CONTROLLED_ENVIRONMENT_NAMES = frozenset(
{"MOBILEGL_BACKEND_TYPE", "MOBILEGL_LOG_FILE_PATH"}
)
DEFAULT_DEQP_ARGS = (
"--deqp-gl-context-type=wgl",
"--deqp-surface-type=fbo",
"--deqp-gl-config-name=rgba8888d24s8",
"--deqp-surface-width=64",
"--deqp-surface-height=-1",
"--deqp-base-seed=3",
"--deqp-visibility=hidden",
"--deqp-watchdog=enable",
"--deqp-crashhandler=enable",
)
SESSION_VENDOR = re.compile(r'^#sessionInfo vendor "([^"]*)"', re.MULTILINE)
SESSION_RENDERER = re.compile(r'^#sessionInfo renderer "([^"]*)"', re.MULTILINE)
SESSION_COMMAND_LINE = re.compile(r'^#sessionInfo commandLineParameters "([^"]*)"', re.MULTILINE)
TARGET_GL_VERSION = re.compile(r"Target OpenGL Version:\s*(\d+)\.(\d+)")
class PipelineError(RuntimeError):
"""A configuration, build, or identity error."""
def repository_root() -> Path:
return Path(__file__).resolve().parents[3]
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def normalize_version(value: str) -> str:
normalized = value.strip().lower().removeprefix("gl").replace(".", "")
if normalized not in SUPPORTED_VERSIONS:
supported = ", ".join(f"gl{version}" for version in SUPPORTED_VERSIONS)
raise argparse.ArgumentTypeError(f"unsupported GL suite {value!r}; choose one of: {supported}")
return normalized
def gl_version_tuple(version: str) -> tuple[int, int]:
return int(version[0]), int(version[1])
def parse_assignment(value: str) -> tuple[str, str]:
name, separator, setting = value.partition("=")
if not separator or not name or "\x00" in value:
raise argparse.ArgumentTypeError(f"expected NAME=VALUE, got {value!r}")
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name):
raise argparse.ArgumentTypeError(f"invalid environment variable name {name!r}")
return name, setting
def canonicalize_windows_environment(
items: Iterable[tuple[str, str]],
) -> dict[str, str]:
"""Canonicalize environment keys using Windows' case-insensitive rules."""
result: dict[str, str] = {}
for name, value in items:
result[name.upper()] = value
return result
def tracked_run_environment(
inherited: Mapping[str, str], overrides: Mapping[str, str]
) -> tuple[dict[str, str], dict[str, str]]:
"""Return tracked ambient values and the effective values used for identity."""
canonical_inherited = canonicalize_windows_environment(inherited.items())
ambient = {
name: value
for name, value in canonical_inherited.items()
if name in TRACKED_ENVIRONMENT_NAMES
or name.startswith(TRACKED_ENVIRONMENT_PREFIXES)
}
effective = dict(ambient)
effective.update(canonicalize_windows_environment(overrides.items()))
return dict(sorted(ambient.items())), dict(sorted(effective.items()))
def command_text(command: Sequence[object]) -> str:
return subprocess.list2cmdline([str(part) for part in command])
def run_command(
command: Sequence[object],
*,
cwd: Optional[Path] = None,
env: Optional[Mapping[str, str]] = None,
check: bool = True,
capture: bool = False,
) -> subprocess.CompletedProcess[str]:
rendered = command_text(command)
location = f" (cwd={cwd})" if cwd else ""
print(f"[wgl_glcts_pipeline] $ {rendered}{location}", flush=True)
completed = subprocess.run(
[str(part) for part in command],
cwd=str(cwd) if cwd else None,
env=dict(env) if env else None,
text=True,
capture_output=capture,
check=False,
)
if check and completed.returncode != 0:
detail = ""
if capture:
detail = f"\nstdout:\n{completed.stdout}\nstderr:\n{completed.stderr}"
raise PipelineError(f"command failed with exit code {completed.returncode}: {rendered}{detail}")
return completed
def require_file(path: Path, label: str) -> Path:
resolved = path.expanduser().resolve()
if not resolved.is_file():
raise PipelineError(f"{label} does not exist or is not a file: {resolved}")
return resolved
def require_directory(path: Path, label: str) -> Path:
resolved = path.expanduser().resolve()
if not resolved.is_dir():
raise PipelineError(f"{label} does not exist or is not a directory: {resolved}")
return resolved
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def sha256_directory(path: Path) -> str:
digest = hashlib.sha256()
files = sorted(
(candidate for candidate in path.rglob("*") if candidate.is_file()),
key=lambda candidate: candidate.relative_to(path).as_posix(),
)
if not files:
raise PipelineError(f"directory contains no files to fingerprint: {path}")
for candidate in files:
relative = candidate.relative_to(path).as_posix()
digest.update(relative.encode("utf-8"))
digest.update(b"\0")
digest.update(sha256_file(candidate).encode("ascii"))
digest.update(b"\n")
return digest.hexdigest()
def pe_machine(path: Path) -> int:
with path.open("rb") as stream:
if stream.read(2) != b"MZ":
raise PipelineError(f"not a PE executable: {path}")
stream.seek(0x3C)
offset_bytes = stream.read(4)
if len(offset_bytes) != 4:
raise PipelineError(f"truncated PE header: {path}")
pe_offset = struct.unpack("<I", offset_bytes)[0]
stream.seek(pe_offset)
if stream.read(4) != b"PE\0\0":
raise PipelineError(f"invalid PE signature: {path}")
machine_bytes = stream.read(2)
if len(machine_bytes) != 2:
raise PipelineError(f"truncated PE COFF header: {path}")
return struct.unpack("<H", machine_bytes)[0]
def require_x64_pe(path: Path, label: str) -> None:
machine = pe_machine(path)
if machine != PE_MACHINE_AMD64:
raise PipelineError(f"{label} must be an x64 PE (machine 0x8664), got 0x{machine:04x}: {path}")
def git_snapshot(path: Path) -> dict[str, object]:
snapshot: dict[str, object] = {"path": str(path)}
try:
head = run_command(
["git", "-C", path, "rev-parse", "HEAD"], check=True, capture=True
).stdout.strip()
status = run_command(
["git", "-C", path, "status", "--porcelain"], check=True, capture=True
).stdout
snapshot.update({"head": head, "dirty": bool(status.strip())})
except (OSError, PipelineError):
snapshot.update({"head": None, "dirty": None})
return snapshot
def generator_arguments(generator: str, architecture: str) -> list[str]:
arguments = ["-G", generator]
if generator.lower().startswith("visual studio"):
arguments.extend(["-A", architecture])
return arguments
def mobilegl_configure_command(
repo_root: Path,
build_dir: Path,
generator: str,
architecture: str,
extra: Iterable[str],
) -> list[str]:
return [
"cmake",
"-S",
str(repo_root),
"-B",
str(build_dir),
*generator_arguments(generator, architecture),
"-DMOBILEGL_BUILD_TEST=OFF",
"-DMOBILEGL_BUILD_BENCHMARK=OFF",
"-DMOBILEGL_BUILD_TRACE_REPLAY=OFF",
"-DMOBILEGL_ENABLE_TRACY=OFF",
"-DMOBILEGL_FORCE_RELEASE_OPT=ON",
*extra,
]
def cts_configure_command(
cts_source: Path,
build_dir: Path,
generator: str,
architecture: str,
extra: Iterable[str],
) -> list[str]:
return [
"cmake",
"-S",
str(cts_source),
"-B",
str(build_dir),
*generator_arguments(generator, architecture),
"-DDEQP_TARGET=default",
"-DDEQP_SUPPORT_DRM=OFF",
*extra,
]
def build_command(build_dir: Path, configuration: str, target: str, jobs: int) -> list[str]:
command = ["cmake", "--build", str(build_dir), "--config", configuration, "--target", target]
if jobs > 0:
command.extend(["--parallel", str(jobs)])
return command
def verify_mobilegl_sources(repo_root: Path) -> None:
required = (
repo_root / "CMakeLists.txt",
repo_root / "MobileGL" / "MG_Impl" / "WGLImpl" / "WGLImpl.cpp",
repo_root / "3rdparty" / "glslang" / "CMakeLists.txt",
repo_root / "3rdparty" / "SPIRV-Cross" / "CMakeLists.txt",
repo_root / "3rdparty" / "Vulkan-Headers" / "CMakeLists.txt",
)
missing = [str(path) for path in required if not path.is_file()]
if missing:
raise PipelineError(
"MobileGL source/submodules are incomplete:\n "
+ "\n ".join(missing)
+ f"\nRun: git -C {repo_root} submodule update --init --recursive"
)
def verify_cts_sources(cts_source: Path) -> None:
required = (
cts_source / "CMakeLists.txt",
cts_source / "external" / "openglcts" / "CMakeLists.txt",
)
missing = [str(path) for path in required if not path.is_file()]
if missing:
raise PipelineError(
"VK-GL-CTS source/external packages are incomplete:\n "
+ "\n ".join(missing)
+ f"\nRun: {sys.executable} {cts_source / 'external' / 'fetch_sources.py'}"
)
def discover_mobilegl_dll(build_dir: Path, configuration: str) -> Path:
preferred = (
build_dir / configuration / "opengl32.dll",
build_dir / "MobileGL" / configuration / "opengl32.dll",
build_dir / "opengl32.dll",
)
for candidate in preferred:
if candidate.is_file():
return candidate.resolve()
candidates = sorted({path.resolve() for path in build_dir.rglob("opengl32.dll") if path.is_file()})
if len(candidates) == 1:
return candidates[0]
if not candidates:
raise PipelineError(f"MobileGL build produced no opengl32.dll under {build_dir}")
raise PipelineError("multiple opengl32.dll candidates; pass --mobilegl-dll explicitly:\n " + "\n ".join(map(str, candidates)))
def discover_glcts_exe(build_dir: Path, configuration: str) -> Path:
preferred = (
build_dir / "external" / "openglcts" / "modules" / configuration / "glcts.exe",
build_dir / "external" / "openglcts" / "modules" / "glcts.exe",
)
for candidate in preferred:
if candidate.is_file():
return candidate.resolve()
candidates = sorted({path.resolve() for path in build_dir.rglob("glcts.exe") if path.is_file()})
if len(candidates) == 1:
return candidates[0]
if not candidates:
raise PipelineError(f"CTS build produced no glcts.exe under {build_dir}")
raise PipelineError("multiple glcts.exe candidates; pass --glcts-exe explicitly:\n " + "\n ".join(map(str, candidates)))
def default_cts_modules_dir(cts_build_dir: Path) -> Path:
return cts_build_dir / "external" / "openglcts" / "modules"
def find_caselist_root(cts_modules_dir: Path, cts_source: Path) -> Path:
relative = Path("gl_cts/data/mustpass/gl/khronos_mustpass/main")
candidates = (cts_modules_dir / relative, cts_source / "external" / "openglcts" / "modules" / relative)
for candidate in candidates:
if candidate.is_dir():
return candidate.resolve()
raise PipelineError("Khronos GL mustpass directory was not found; checked:\n " + "\n ".join(map(str, candidates)))
def caselist_for(caselist_root: Path, version: str) -> Path:
return require_file(caselist_root / f"gl{version}-main.txt", f"GL{version} mustpass caselist")
def runtime_source_files(
glcts_exe: Path,
mobilegl_dll: Path,
backends: Sequence[str],
angle_dir: Optional[Path],
) -> dict[str, Path]:
files = {"glcts.exe": glcts_exe, "opengl32.dll": mobilegl_dll}
if "DirectGLES" in backends:
if angle_dir is None:
raise PipelineError("--angle-dir is required when DirectGLES is selected")
angle_dir = require_directory(angle_dir, "ANGLE runtime directory")
for name in ANGLE_REQUIRED_DLLS:
files[name] = require_file(angle_dir / name, f"ANGLE {name}")
for name in ANGLE_OPTIONAL_DLLS:
candidate = angle_dir / name
if candidate.is_file():
files[name] = candidate.resolve()
return files
def runtime_fingerprint(files: Mapping[str, Path]) -> tuple[str, dict[str, str]]:
hashes = {name: sha256_file(path) for name, path in sorted(files.items())}
digest = hashlib.sha256()
for name, file_hash in hashes.items():
digest.update(f"{name}\0{file_hash}\n".encode("utf-8"))
return digest.hexdigest(), hashes
def run_fingerprint(
runtime_hash: str,
cts_data_hash: str,
tool_hashes: Mapping[str, str],
caselist_hashes: Mapping[str, str],
deqp_args: Sequence[str],
environment: Mapping[str, str],
result_semantics: Optional[Mapping[str, object]] = None,
) -> str:
identity = {
"version": 2,
"runtime_fingerprint": runtime_hash,
"cts_data_sha256": cts_data_hash,
"tool_hashes": dict(sorted(tool_hashes.items())),
"caselist_hashes": dict(sorted(caselist_hashes.items())),
"deqp_args": list(deqp_args),
"environment": dict(sorted(environment.items())),
"result_semantics": dict(sorted((result_semantics or {}).items())),
}
return hashlib.sha256(
json.dumps(identity, sort_keys=True, separators=(",", ":")).encode("utf-8")
).hexdigest()
def assemble_runtime(
work_root: Path,
sources: Mapping[str, Path],
fingerprint: str,
hashes: Mapping[str, str],
) -> Path:
runtime_dir = work_root / "runtime" / fingerprint[:16]
runtime_dir.mkdir(parents=True, exist_ok=True)
for name, source in sources.items():
require_x64_pe(source, name)
destination = runtime_dir / name
if source.resolve() != destination.resolve():
shutil.copy2(source, destination)
if sha256_file(destination) != hashes[name]:
raise PipelineError(f"runtime copy hash mismatch: {destination}")
manifest = {
"version": 1,
"created_utc": utc_now(),
"fingerprint": fingerprint,
"files": {
name: {"source": str(source), "sha256": hashes[name]}
for name, source in sorted(sources.items())
},
}
write_json(runtime_dir / "manifest.json", manifest)
return runtime_dir
def write_json(path: Path, value: object) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(path.name + ".tmp")
temporary.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
os.replace(temporary, path)
def read_cases(caselist: Path) -> list[str]:
cases: list[str] = []
for raw_line in caselist.read_text(encoding="utf-8-sig").splitlines():
line = raw_line.strip()
if line and not line.startswith("#"):
cases.append(line)
if not cases:
raise PipelineError(f"caselist contains no cases: {caselist}")
return cases
def choose_preflight_case(caselist: Path) -> str:
cases = read_cases(caselist)
preferred_suffixes = (
".buffer_objects.gen_buffers",
".CommonBugs.CommonBug_GetProgramivActiveUniformBlockMaxNameLength",
)
for suffix in preferred_suffixes:
for case in cases:
if case.endswith(suffix):
return case
return cases[0]
def runner_command(
runner: Path,
runtime_exe: Path,
workdir: Path,
caselist: Path,
outdir: Path,
backend: str,
idle_timeout: float,
max_round_seconds: float,
max_rounds: int,
environment: Mapping[str, str],
deqp_args: Sequence[str],
run_identity: Optional[str] = None,
) -> list[str]:
command = [
sys.executable,
str(runner),
"--exe",
str(runtime_exe),
"--workdir",
str(workdir),
"--caselist",
str(caselist),
"--outdir",
str(outdir),
"--backend",
backend,
"--idle-timeout",
str(idle_timeout),
"--max-round-seconds",
str(max_round_seconds),
"--max-rounds",
str(max_rounds),
]
if run_identity is not None:
command.extend(["--run-identity", run_identity])
for name, value in sorted(environment.items()):
command.extend(["--env", f"{name}={value}"])
command.extend(f"--deqp-arg={argument}" for argument in deqp_args)
return command
def parse_preflight_identity(
qpa_files: Sequence[Path],
mobilegl_log: Path,
backend: str,
minimum_version: tuple[int, int],
) -> dict[str, object]:
if not qpa_files:
raise PipelineError(f"{backend} preflight produced no QPA file")
qpa_text = "\n".join(path.read_text(encoding="utf-8", errors="replace") for path in qpa_files)
vendors = SESSION_VENDOR.findall(qpa_text)
renderers = SESSION_RENDERER.findall(qpa_text)
command_lines = SESSION_COMMAND_LINE.findall(qpa_text)
if not vendors or "MobileGL" not in vendors[-1]:
raise PipelineError(f"{backend} preflight did not load MobileGL (vendor={vendors[-1] if vendors else None!r})")
expected_renderer = "Espryt" if backend == "DirectGLES" else "Magma"
if not renderers or expected_renderer not in renderers[-1]:
raise PipelineError(
f"{backend} preflight renderer mismatch: expected {expected_renderer!r}, "
f"got {renderers[-1] if renderers else None!r}"
)
if not command_lines or "--deqp-gl-context-type=wgl" not in command_lines[-1]:
raise PipelineError(f"{backend} preflight did not record a WGL context")
if not mobilegl_log.is_file():
raise PipelineError(f"{backend} preflight did not create MobileGL log: {mobilegl_log}")
log_text = mobilegl_log.read_text(encoding="utf-8", errors="replace")
versions = [(int(major), int(minor)) for major, minor in TARGET_GL_VERSION.findall(log_text)]
if not versions:
raise PipelineError(f"{backend} preflight log contains no target OpenGL version")
actual_version = versions[-1]
if actual_version < minimum_version:
raise PipelineError(
f"{backend} reports GL {actual_version[0]}.{actual_version[1]}, "
f"but selected suites require at least {minimum_version[0]}.{minimum_version[1]}"
)
return {
"backend": backend,
"vendor": vendors[-1],
"renderer": renderers[-1],
"target_gl_version": f"{actual_version[0]}.{actual_version[1]}",
"qpa_files": [str(path) for path in qpa_files],
"mobilegl_log": str(mobilegl_log),
}
def run_preflight(
*,
runner: Path,
runtime_exe: Path,
cts_modules_dir: Path,
caselist: Path,
preflight_root: Path,
backend: str,
idle_timeout: float,
environment: Mapping[str, str],
deqp_args: Sequence[str],
minimum_version: tuple[int, int],
run_identity: str,
) -> dict[str, object]:
outdir = preflight_root / backend.lower()
outdir.mkdir(parents=True, exist_ok=True)
case_file = outdir / "case.txt"
case_file.write_text(choose_preflight_case(caselist) + "\n", encoding="utf-8")
log_path = outdir / "mobilegl.log"
child_environment = dict(environment)
child_environment["MOBILEGL_LOG_FILE_PATH"] = str(log_path)
command = runner_command(
runner,
runtime_exe,
cts_modules_dir,
case_file,
outdir,
backend,
min(idle_timeout, 120.0) if idle_timeout > 0 else 120.0,
180.0,
1,
child_environment,
deqp_args,
run_identity,
)
# A developing driver may fail the chosen case or terminate during deinit.
# Identity is the gate: the QPA and MobileGL log must prove which WGL driver ran.
run_command(command, cwd=repository_root(), check=False)
identity = parse_preflight_identity(sorted(outdir.glob("chunk*.qpa")), log_path, backend, minimum_version)
print(
f"[wgl_glcts_pipeline] preflight {backend}: {identity['renderer']} | "
f"GL {identity['target_gl_version']}"
)
return identity
def report_command(
reporter: Path,
suites: Sequence[tuple[str, str, Path, Path]],
markdown: Path,
json_out: Path,
allow_incomplete: bool,
expected_run_identity: Optional[str] = None,
) -> list[str]:
command = [sys.executable, str(reporter)]
for backend, label, caselist, results in suites:
command.extend(["--suite", backend, label, str(caselist), str(results)])
command.extend(["--markdown", str(markdown), "--json", str(json_out)])
if expected_run_identity is not None:
command.extend(["--expected-run-identity", expected_run_identity])
if allow_incomplete:
command.append("--allow-incomplete")
return command
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Build MobileGL WGL, assemble a local glcts runtime, run GL30-GL46, and report results."
)
parser.add_argument("--repo-root", type=Path, default=repository_root(), help="MobileGL worktree root")
parser.add_argument("--cts-source", type=Path, required=True, help="VK-GL-CTS source checkout")
parser.add_argument("--work-root", type=Path, required=True, help="build/result root (kept outside source)")
parser.add_argument("--angle-dir", type=Path, help="x64 ANGLE directory for DirectGLES")
parser.add_argument("--backends", nargs="+", choices=SUPPORTED_BACKENDS, default=list(SUPPORTED_BACKENDS))
parser.add_argument("--versions", nargs="+", type=normalize_version, default=list(SUPPORTED_VERSIONS))
parser.add_argument("--configuration", default="Release")
parser.add_argument("--generator", default="Visual Studio 17 2022")
parser.add_argument("--architecture", default="x64")
parser.add_argument("--jobs", type=int, default=max(1, os.cpu_count() or 1))
parser.add_argument("--mobilegl-build-dir", type=Path)
parser.add_argument("--cts-build-dir", type=Path)
parser.add_argument("--mobilegl-dll", type=Path, help="reuse an existing MobileGL/opengl32 DLL")
parser.add_argument("--glcts-exe", type=Path, help="reuse an existing glcts.exe")
parser.add_argument("--cts-modules-dir", type=Path, help="glcts working directory containing gl_cts data")
parser.add_argument("--skip-mobilegl-build", action="store_true")
parser.add_argument("--skip-cts-build", action="store_true")
parser.add_argument("--skip-preflight", action="store_true")
parser.add_argument("--skip-run", action="store_true")
parser.add_argument("--skip-report", action="store_true")
parser.add_argument("--allow-incomplete-report", action="store_true")
parser.add_argument("--continue-on-suite-error", action="store_true")
parser.add_argument("--idle-timeout", type=float, default=300.0)
parser.add_argument("--max-round-seconds", type=float, default=0.0)
parser.add_argument("--max-rounds", type=int, default=10000)
parser.add_argument("--env", action="append", type=parse_assignment, default=[], metavar="NAME=VALUE")
parser.add_argument(
"--deqp-arg", action="append", default=[], metavar="ARG", help="extra glcts option; use --deqp-arg=--x=y"
)
parser.add_argument(
"--mobilegl-cmake-arg", action="append", default=[], metavar="ARG", help="extra MobileGL configure option"
)
parser.add_argument("--cts-cmake-arg", action="append", default=[], metavar="ARG", help="extra CTS configure option")
return parser
def execute(args: argparse.Namespace) -> int:
if os.name != "nt":
raise PipelineError("this pipeline builds and exercises the Windows WGL target and must run on Windows")
if shutil.which("cmake") is None and (not args.skip_mobilegl_build or not args.skip_cts_build):
raise PipelineError("cmake was not found on PATH")
if args.jobs < 0 or args.max_rounds <= 0:
raise PipelineError("--jobs must be >= 0 and --max-rounds must be > 0")
repo_root = require_directory(args.repo_root, "MobileGL worktree")
cts_source = require_directory(args.cts_source, "VK-GL-CTS checkout")
work_root = args.work_root.expanduser().resolve()
work_root.mkdir(parents=True, exist_ok=True)
versions = list(dict.fromkeys(args.versions))
backends = list(dict.fromkeys(args.backends))
minimum_version = max(gl_version_tuple(version) for version in versions)
extra_environment = canonicalize_windows_environment(args.env)
reserved_environment = CONTROLLED_ENVIRONMENT_NAMES & set(extra_environment)
if reserved_environment:
raise PipelineError("the pipeline controls these environment variables: " + ", ".join(sorted(reserved_environment)))
configuration = args.configuration
mobilegl_build_dir = (args.mobilegl_build_dir or work_root / f"mobilegl-build-{configuration.lower()}").resolve()
cts_build_dir = (args.cts_build_dir or work_root / f"cts-build-wgl-{configuration.lower()}").resolve()
if not args.skip_mobilegl_build:
verify_mobilegl_sources(repo_root)
mobilegl_build_dir.mkdir(parents=True, exist_ok=True)
run_command(
mobilegl_configure_command(
repo_root, mobilegl_build_dir, args.generator, args.architecture, args.mobilegl_cmake_arg
),
cwd=repo_root,
)
run_command(build_command(mobilegl_build_dir, configuration, "MobileGL", args.jobs), cwd=repo_root)
mobilegl_dll = (
require_file(args.mobilegl_dll, "MobileGL DLL")
if args.mobilegl_dll
else discover_mobilegl_dll(mobilegl_build_dir, configuration)
)
if not args.skip_cts_build:
verify_cts_sources(cts_source)
cts_build_dir.mkdir(parents=True, exist_ok=True)
run_command(
cts_configure_command(cts_source, cts_build_dir, args.generator, args.architecture, args.cts_cmake_arg),
cwd=cts_source,
)
run_command(build_command(cts_build_dir, configuration, "glcts", args.jobs), cwd=cts_source)
glcts_exe = (
require_file(args.glcts_exe, "glcts executable")
if args.glcts_exe
else discover_glcts_exe(cts_build_dir, configuration)
)
cts_modules_dir = require_directory(
args.cts_modules_dir or default_cts_modules_dir(cts_build_dir), "CTS modules/working directory"
)
cts_data_dir = require_directory(cts_modules_dir / "gl_cts" / "data", "CTS gl_cts data directory")
cts_data_hash = sha256_directory(cts_data_dir)
caselist_root = find_caselist_root(cts_modules_dir, cts_source)
caselists = {version: caselist_for(caselist_root, version) for version in versions}
runtime_sources = runtime_source_files(glcts_exe, mobilegl_dll, backends, args.angle_dir)
fingerprint, hashes = runtime_fingerprint(runtime_sources)
runtime_dir = assemble_runtime(work_root, runtime_sources, fingerprint, hashes)
runtime_exe = runtime_dir / "glcts.exe"
runner = require_file(repo_root / "tools" / "cts" / "scripts" / "run_cts_windows.py", "Windows CTS runner")
reporter = require_file(repo_root / "tools" / "cts" / "scripts" / "cts_multi_report.py", "CTS reporter")
matrix_reporter = require_file(
repo_root / "tools" / "cts" / "scripts" / "cts_matrix_report.py", "CTS matrix reporter"
)
qpa_reporter = require_file(repo_root / "tools" / "cts" / "scripts" / "qpa_report.py", "QPA parser")
tool_hashes = {
"wgl_glcts_pipeline.py": sha256_file(Path(__file__).resolve()),
"run_cts_windows.py": sha256_file(runner),
"cts_multi_report.py": sha256_file(reporter),
"cts_matrix_report.py": sha256_file(matrix_reporter),
"qpa_report.py": sha256_file(qpa_reporter),
}
deqp_args = [*DEFAULT_DEQP_ARGS, *args.deqp_arg]
caselist_hashes = {version: sha256_file(path) for version, path in caselists.items()}
ambient_environment, identity_environment = tracked_run_environment(
os.environ, extra_environment
)
result_semantics = {
"idle_timeout_seconds": args.idle_timeout,
"max_round_seconds": args.max_round_seconds,
}
execution_settings = {
**result_semantics,
"max_rounds": args.max_rounds,
"continue_on_suite_error": args.continue_on_suite_error,
}
execution_fingerprint = run_fingerprint(
fingerprint,
cts_data_hash,
tool_hashes,
caselist_hashes,
deqp_args,
identity_environment,
result_semantics,
)
run_root = work_root / "runs" / execution_fingerprint[:16]
report_root = run_root / "reports"
manifest = {
"version": 1,
"created_utc": utc_now(),
"run_fingerprint": execution_fingerprint,
"runtime_fingerprint": fingerprint,
"runtime_hashes": hashes,
"tool_hashes": tool_hashes,
"cts_data": {"path": str(cts_data_dir), "sha256": cts_data_hash},
"runtime_dir": str(runtime_dir),
"mobilegl": git_snapshot(repo_root),
"vk_gl_cts": git_snapshot(cts_source),
"configuration": configuration,
"generator": args.generator,
"architecture": args.architecture,
"backends": backends,
"versions": versions,
"caselists": {version: {"path": str(path), "sha256": caselist_hashes[version]} for version, path in caselists.items()},
"deqp_args": deqp_args,
"environment_overrides": extra_environment,
"ambient_environment": ambient_environment,
"identity_environment": identity_environment,
"result_semantics": result_semantics,
"execution_settings": execution_settings,
}
write_json(run_root / "manifest.json", manifest)
print(f"[wgl_glcts_pipeline] runtime fingerprint: {fingerprint}")
print(f"[wgl_glcts_pipeline] run fingerprint: {execution_fingerprint}")
print(f"[wgl_glcts_pipeline] run root: {run_root}")
identities: list[dict[str, object]] = []
if not args.skip_preflight:
first_caselist = caselists[versions[0]]
preflight_root = run_root / "preflight"
for backend in backends:
identities.append(
run_preflight(
runner=runner,
runtime_exe=runtime_exe,
cts_modules_dir=cts_modules_dir,
caselist=first_caselist,
preflight_root=preflight_root,
backend=backend,
idle_timeout=args.idle_timeout,
environment=extra_environment,
deqp_args=deqp_args,
minimum_version=minimum_version,
run_identity=execution_fingerprint,
)
)
manifest["preflight"] = identities
write_json(run_root / "manifest.json", manifest)
suites: list[tuple[str, str, Path, Path]] = []
suite_errors: list[dict[str, object]] = []
for backend in backends:
for version in versions:
label = f"gl{version}"
result_dir = run_root / "results" / backend.lower() / label
suites.append((backend, label, caselists[version], result_dir))
if args.skip_run:
continue
result_dir.mkdir(parents=True, exist_ok=True)
child_environment = dict(extra_environment)
child_environment["MOBILEGL_LOG_FILE_PATH"] = str(result_dir / "mobilegl.log")
command = runner_command(
runner,
runtime_exe,
cts_modules_dir,
caselists[version],
result_dir,
backend,
args.idle_timeout,
args.max_round_seconds,
args.max_rounds,
child_environment,
deqp_args,
execution_fingerprint,
)
completed = run_command(command, cwd=repo_root, check=False)
if completed.returncode != 0:
suite_errors.append({"backend": backend, "suite": label, "returncode": completed.returncode})
if not args.continue_on_suite_error:
break
if suite_errors and not args.continue_on_suite_error:
break
report_returncode: Optional[int] = None
if not args.skip_report:
report_root.mkdir(parents=True, exist_ok=True)
completed = run_command(
report_command(
reporter,
suites,
report_root / "gl-cts-summary.md",
report_root / "gl-cts-summary.json",
args.allow_incomplete_report or bool(suite_errors),
execution_fingerprint,
),
cwd=repo_root,
check=False,
)
report_returncode = completed.returncode
manifest["suite_errors"] = suite_errors
manifest["report_returncode"] = report_returncode
manifest["finished_utc"] = utc_now()
write_json(run_root / "manifest.json", manifest)
if suite_errors:
print(f"[wgl_glcts_pipeline] {len(suite_errors)} suite runner(s) incomplete; see manifest/report", file=sys.stderr)
returncodes = {int(item["returncode"]) for item in suite_errors}
if 130 in returncodes:
return 130
if 2 in returncodes:
return 2
if 3 in returncodes:
return 3
return 4
if report_returncode is not None and report_returncode != 0:
print(f"[wgl_glcts_pipeline] report validation failed with exit code {report_returncode}", file=sys.stderr)
return report_returncode
return 0
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
try:
return execute(args)
except PipelineError as exc:
print(f"[wgl_glcts_pipeline] ERROR: {exc}", file=sys.stderr)
return 2
except OSError as exc:
print(f"[wgl_glcts_pipeline] ERROR: filesystem/process operation failed: {exc}", file=sys.stderr)
return 2
if __name__ == "__main__":
sys.exit(main())
+1
View File
@@ -16,3 +16,4 @@ Each skill is a self-contained package, matching the layout used by
| Skill | What it does |
| --- | --- |
| [gl-cts-on-mobilegl](gl-cts-on-mobilegl/SKILL.md) | Build VK-GL-CTS `glcts` as a standalone Android arm64 binary against MobileGL's own EGL, run KHR-GL33, and report a per-backend OpenGL 3.3 core conformance rate. |
| [wgl-gl-cts-on-mobilegl](wgl-gl-cts-on-mobilegl/SKILL.md) | Build MobileGL's Windows x64 WGL drop-in, run GL30-GL46 core CTS against DirectGLES and DirectVulkan, resume safely, and emit validated reports. |
@@ -0,0 +1,143 @@
---
name: wgl-gl-cts-on-mobilegl
description: Build MobileGL as a Windows x64 WGL drop-in opengl32.dll, build or reuse VK-GL-CTS glcts, run Khronos GL30 through GL46 core suites against DirectGLES and DirectVulkan, resume after crashes or idle timeouts, and produce validated Markdown/JSON conformance reports. Use when Codex needs to compile MobileGL's WGL target, connect it to desktop OpenGL CTS on Windows, rerun selected GL core mustpass lists, or verify that CTS loaded MobileGL instead of the system OpenGL driver.
---
# WGL OpenGL CTS on MobileGL
Use `tools/cts/scripts/wgl_glcts_pipeline.py` as the single entry point. It
configures both Visual Studio builds, assembles a private runtime, verifies the
loaded WGL implementation, calls the crash-resuming runner, and generates the
multi-suite report.
## Prerequisites
- Run on Windows x64 with Git, Python 3.9 or newer, CMake, Visual Studio 2022's
Desktop C++ workload, and a Vulkan SDK visible to MobileGL's CMake configure.
- Use the already selected MobileGL worktree. Inspect `git status` first and do
not create another worktree unless the user explicitly asks.
- Initialize MobileGL submodules:
```powershell
git submodule update --init --recursive
```
- Prepare a VK-GL-CTS checkout at a citable release tag, then fetch its pinned
externals:
```powershell
git -C D:\VK-GL-CTS checkout opengl-cts-4.6.8.1
python D:\VK-GL-CTS\external\fetch_sources.py
```
- For DirectGLES, provide one x64 ANGLE directory containing matching
`libEGL.dll`, `libGLESv2.dll`, and `d3dcompiler_47.dll`. DirectVulkan does not
need ANGLE.
- For DirectVulkan, provide a working Vulkan loader plus a GPU-vendor ICD and
driver. The Vulkan SDK alone does not provide a usable GPU device.
## Run the full matrix
From the MobileGL worktree root:
```powershell
python tools\cts\scripts\wgl_glcts_pipeline.py `
--cts-source D:\VK-GL-CTS `
--work-root D:\MobileGL-WGL-CTS `
--angle-dir C:\path\to\angle-x64 `
--backends DirectGLES DirectVulkan `
--versions gl30 gl31 gl32 gl33 gl40 gl41 gl42 gl43 gl44 gl45 gl46
```
The defaults deliberately reproduce the proven Windows setup:
- Visual Studio 17 2022, x64, Release;
- VK-GL-CTS `DEQP_TARGET=default`, which selects desktop WGL on Windows;
- MobileGL copied beside `glcts.exe` as `opengl32.dll`;
- WGL context, FBO surface, `rgba8888d24s8`, hidden window, watchdog and crash
handler enabled;
- `--deqp-terminate-on-device-lost=disable` supplied by the underlying runner.
Do not replace the FBO surface with the default framebuffer when comparing
backends; it changes the readback path and invalidates comparison with the
established runs.
## Mandatory preflight
Leave preflight enabled for a new binary. It must prove all of the following
before the full suite starts:
- QPA vendor contains `MobileGL`;
- DirectGLES renderer contains `Espryt`, or DirectVulkan contains `Magma`;
- QPA command line records `--deqp-gl-context-type=wgl`;
- MobileGL's log reports a GL version at least as high as the highest selected
suite.
Treat any preflight failure as a hard stop. It commonly means `glcts.exe` loaded
the system `opengl32.dll`, ANGLE DLLs have the wrong architecture, or the driver
still reports too low a GL version.
## Common variants
Run DirectVulkan only, without ANGLE:
```powershell
python tools\cts\scripts\wgl_glcts_pipeline.py `
--cts-source D:\VK-GL-CTS --work-root D:\MobileGL-WGL-CTS `
--backends DirectVulkan --versions gl43 gl44 gl45 gl46
```
Build and preflight without starting a multi-hour CTS run:
```powershell
python tools\cts\scripts\wgl_glcts_pipeline.py `
--cts-source D:\VK-GL-CTS --work-root D:\MobileGL-WGL-CTS `
--angle-dir C:\path\to\angle-x64 `
--skip-run --skip-report
```
Reuse previously built binaries with `--skip-mobilegl-build`,
`--skip-cts-build`, `--mobilegl-dll`, `--glcts-exe`, and
`--cts-modules-dir`. Continue to use a modules directory containing the
`gl_cts` data tree; the executable directory alone is insufficient.
Pass extra dEQP options with the equals form so argparse does not consume the
leading dashes:
```powershell
--deqp-arg=--deqp-log-images=enable
```
## Resume and artifacts
Repeat the exact command and `--work-root` to resume. The runner recovers
completed QPA cases plus `crashed.txt` and `hung.txt`, then schedules only
unaccounted cases.
The pipeline prints full SHA-256 fingerprints and uses their first 16
hexadecimal characters as directory names: `runtime/<runtime-prefix>` and
`runs/<run-prefix>`. The run fingerprint covers the CTS data tree,
runner/report tools, caselists, dEQP arguments, explicit `--env` values, tracked
ambient GL/Vulkan environment, and the timeout settings that determine
Crash/Hang classification. The manifest records those inputs plus
`max_rounds`, suite-error continuation policy, source commits and dirty state,
preflight identity, suite errors, and report status.
The runner refuses to attach a new `run_state.json` to old QPA or sidecar files
by default. Invoke `run_cts_windows.py --adopt-legacy` directly only after
verifying that those artifacts match the backend, caselist, binaries, and dEQP
arguments. Pipeline reports require every suite state to match the current run
fingerprint.
Read the final outputs at:
```text
<work-root>/runs/<run-prefix>/reports/gl-cts-summary.md
<work-root>/runs/<run-prefix>/reports/gl-cts-summary.json
```
Use the exact run root printed by the pipeline.
Do not claim completeness when the report validation is incomplete or when the
manifest records suite errors. Keep `NotSupported` separate from hard failures
when prioritizing implementation work.
@@ -0,0 +1,4 @@
interface:
display_name: "WGL GL CTS on MobileGL"
short_description: "Build MobileGL WGL and run GL30-GL46 CTS on Windows"
default_prompt: "Use $wgl-gl-cts-on-mobilegl to build MobileGL's WGL DLL and run the selected OpenGL CTS suites on Windows."
+29
View File
@@ -0,0 +1,29 @@
#-------------------------------------------------------------------------
# VK-GL-CTS target: MobileGL on desktop Linux
#
# Builds glcts as a normal host executable that reaches OpenGL exclusively
# through libMobileGL.so, loaded at runtime via the eglw dynamic wrapper.
# Nothing here links libEGL or libGL: a conformance result must be
# unambiguously MobileGL's, never the system GL stack's.
#
# The platform port only offers pbuffer surfaces on desktop; DirectVulkan's
# pbuffer path works because desktop Vulkan exposes VK_EXT_headless_surface.
#-------------------------------------------------------------------------
message("*** Using MobileGL desktop target")
set(DEQP_TARGET_NAME "MobileGL")
# EGL comes from libMobileGL.so via the eglw dynamic wrapper, so the support
# flag is on but no import library is supplied.
set(DEQP_SUPPORT_EGL ON)
set(DEQP_EGL_LIBRARIES)
set(DEQP_GLES2_LIBRARIES)
set(DEQP_GLES3_LIBRARIES)
set(TCUTIL_PLATFORM_SRCS
mobilegl/tcuMobileGLPlatform.cpp
mobilegl/tcuMobileGLPlatform.hpp
)
list(APPEND TCUTIL_PLATFORM_LIBS dl pthread)
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<waiver_list>
<!--
Waivers for known false failures caused by this local test harness's
fbo-surface-type mode (deqp-surface-type=fbo), not by MobileGL itself.
-->
<waiver vendor="MobileGL-Dev*" url="local-only: fbo-surface-type wrapper-FBO artifact, not a MobileGL conformance bug">
<description>
dEQP's fbo surface-type mode wraps rendering in its own
application level framebuffer object (a real, non-zero-named FBO,
not framebuffer 0). ApiCoverageTestCase's ReadBuffer coverage
sub-test captures GL_READ_BUFFER while that wrapper FBO is bound
(a valid GL_COLOR_ATTACHMENTn value there), 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. Restoring the captured
GL_COLOR_ATTACHMENTn value against the true default framebuffer
correctly raises GL_INVALID_ENUM per spec (only FRONT/BACK style
tokens are valid there), so the test fails with a fully
spec conformant driver. This can only happen when the harness's
"default framebuffer" is a real dEQP created FBO instead of a
genuine window/pbuffer backed framebuffer 0, which is unique to
this local fbo surface-type setup. Confirmed by tracing
framebuffer bindings/external indices across the test's
execution (2026-08-01).
</description>
<renderer_list>
<r>Magma*</r>
<r>Espryt*</r>
</renderer_list>
<t>KHR-GL30.api.coverage</t>
<t>KHR-GL31.api.coverage</t>
<t>KHR-GL32.api.coverage</t>
<t>KHR-GL33.api.coverage</t>
</waiver>
</waiver_list>