Commit Graph
366 Commits
Author SHA1 Message Date
swung0x48 6ea948779e [Feat, Test] (MG_Impl, MG_State, MG_Util): opt-in MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS - shader compile status/log answer without joining, latched per compile 2026-08-10 02:06:17 -04:00
BZLZHH d8d7530011 [Fix, Test] (MG_Backend/DirectGLES, MG_Util, MG_Impl): widen three-channel render targets wherever the driver refuses them
Complementary Reimagined would not load through Espryt on Mali: Iris got
GL_FRAMEBUFFER_UNSUPPORTED building its composite framebuffer, because
colortex1 is RGB8_SNORM and colortex2 is RGB16F - three-channel formats
that no real ES driver can render to (EXT_render_snorm covers R/RG/RGBA
only, and the float extensions exclude the RGB forms). The frontend's
probe cache diagnosed this correctly and then had nothing to offer: the
NoThreeChannelRenderTarget widening machinery existed but was gated to
multisample targets alone. llvmpipe turns out to refuse most of the same
attachments - CI retrace stayed green only because a replay never
branches on glCheckFramebufferStatus - so this was never a desktop-vs-
device split, just an unlit path.

The widening now applies to every color-attachable image, renderbuffers
included, riding the driver-probe branch so the native format is still
tried first and substituted only on refusal. One ThreeChannelWidening
table owns the widened (internalformat, format, type) triple per source
format - the previous per-case branches disagreed with each other and
could emit an unuploadable (RGBA16F, GL_RGB, GL_BYTE) combination or
widen into another three-channel format the driver refuses just the
same. Uploads repack three-component client data to four with the
format's own one in the alpha channel (127 is not 1 for RGB8I - the
integer arms carry integer ones); readback drops the synthetic alpha,
derived from the actual image being read, not the bound framebuffer,
so glGetTexImage through a scratch FBO cannot be confused by an
unrelated widened attachment.

Stored alpha on a widened attachment is now an invariant 1.0 rather
than an accident: the color-mask sync clears the alpha bit per draw
buffer (glColorMaski for MRT mixes), and clears route through
glClearBufferfv with alpha substituted on widened slots only -
scissored clears inherit the discipline for free, integer color
buffers keep their explicit integer-clear path, and glGet still
answers the application's own mask. GL_DST_ALPHA blending, blits and
readback therefore all see 1.0 without further interception.

DriverPost grows the rows this bug earned: EXT_color_buffer_float
detection (previously unreferenced anywhere) with a FAIL row when
absent, the missing EXT_render_snorm row, and a three-channel-
attachment row that reports one representative per widening class -
graded so a half-float-only driver warns about the 32-bit float gap
instead of being declared unsupported.

Gates: 606/606 unit at default and with the async kill switch; full
retrace, both backends - the complementary fixtures now run with the
widening ACTIVE on llvmpipe and pass with a slightly better SSIM than
before; ext caselist DirectGLES holds 3914/4867 with zero set drift
while 54 cases move from NotSupported to genuinely passing; on the
Mali-G77 device, Complementary Reimagined builds its pipeline and
renders in-world through Espryt (md5-verified build), BSL still green.
A new ThreeChannelAttachmentScenario pins the frontend answer -
COMPLETE where it used to say UNSUPPORTED - on the real driver.
2026-08-09 15:28:34 -04:00
BZLZHH 2e6fc1ffc0 [Feat] (MG_Util, MG_Test): enable asynchronous shader compilation by default (P1 stage 7)
kAsyncShaderCompileDefault flips to true, which also advertises
GL_KHR_parallel_shader_compile by default on both backends. Unset
MOBILEGL_ASYNC_SHADER_COMPILE now resolves to ON; =0 remains the complete kill
switch (reverts the threading and withdraws the extension together).

The gate behind the flip (headless Mesa - llvmpipe for Espryt, lavapipe for
Magma - at c6299f75): GL30/31/32/33/40 mustpass plus the
KHR-GL46.parallel_shader_compile group, both backends, async=1 with the
extension advertised. 58,344 case-runs, 8 failures - and every one of the 8
also fails standalone at async=0, in the full async=0 suite arms with an
identical per-case failure set, and under the pre-P1-stage-6 library. Zero
async-attributable deltas; the 8 are Mesa-upgrade drift (4 unique signatures:
Espryt GL40 transform_feedback.draw_xfb{,_feedbackk}_test, Magma
texture_size_promotion.functional + packed_pixels rgb9_e5_format_red on
GL32/33/40), recorded for separate follow-up.

Validation under the flipped default (no env var): 553/553 unit at the default
AND with the kill switch; parallel_shader_compile 3/3 on both backends proving
the default advertises; 44+44 integration scenarios; 71/72 CI trace-replay
fixtures (the one failure is the pre-existing create-indirect lavapipe crash,
identical under the pre-fix library). The lifecycle test's contract updates
with the default: AsyncIsOnByDefaultAndTheOverrideDecidesEitherWay.

Not covered by this gate and deliberately left open: SSO/DSA suites (GL41+,
separate follow-up per review), and real-driver confirmation on NVIDIA/Mali -
the Mali-G77 on-device sweep runs separately as a report-only pass.
2026-08-09 02:04:59 -04:00
BZLZHH bd0def6133 [Feat] (MG_Impl, MG_State, MG_Util): the GL_KHR_parallel_shader_compile surface (P1 stage 5)
GL_COMPLETION_STATUS_KHR in both object getters, reading the non-joining
node-direct state - the one query that must never block is asserted never
to reach a join gate. glMaxShaderCompilerThreadsKHR/ARB share one
implementation: a zero count suspends async FIRST and then joins every
outstanding compile and link this context owns (suspend-before-join is the
only order whose post-condition is 'nothing in flight'), a nonzero count
restores; the suspension is a process latch the extension controls, kept
distinct from the configuration flag that gates the ADVERTISEMENT - an app
that turned threading off has not made the extension disappear.
GL_MAX_SHADER_COMPILER_THREADS_KHR reports the thread count. DriverPost
gains the MobileGL-side async row (PASS/INFO naming the env knob) and an
informational host-driver row backed by a new GLES capability probe.

The extension string itself lands per backend in the two follow-up
commits, keeping this one green stand-alone.
2026-08-08 13:17:58 -04:00
BZLZHH 6f8b7fbc40 [Feat] (MG_State, MG_Util): async program linking on the job graph (P1 stage 4)
glLinkProgram with the flag on snapshots its inputs in a GL-thread prologue
(stage-sorted shaders with their compile nodes taken without joining, env,
explicit locations/fragdata/xfb, draw-buffer count), then runs the whole
link body - glslang link/mapIO, SPIR-V, reflection, routing tables - as a
ProgramLinkTask that auto-posts when its last compile dependency settles
(+1-guarded countdown; no worker ever waits on another job). The publish is
one move of the LinkArtifacts block at the join, with the second version
bump so nothing memoized during the pending window survives.

The consume-once TShader claim moved onto the shared compile node as a CAS:
two link jobs racing for one shader resolve to winner-takes-the-parse,
loser re-parses the preprocessed source against the node's own env -
identical SPIR-V pinned by test for 2 and for 12 sharing programs.

Two deliberate corrections to the design's cancel matrix, both test-proven:
attach/detach do NOT cancel a pending link (the snapshot isolates it, and
glCreateShaderProgramv's link-then-detach would otherwise discard its own
result before anyone read it); and a compile node a pending link depends on
is pinned against the orphan-name sweep - the ordinary LWJGL teardown
compile/attach/link/detach/delete used to cancel the dependency and turn a
must-pass link into GL_FALSE.

Continuations are now throw-contained per-item (a stage-3 leftover made
load-bearing by the first real continuation), and the review's deadlock
find is fixed: the dispatch loop no longer cancels a node while holding the
pool mutex, since that cancel can run OnDepSettled -> Post -> same mutex.

Explicit joins: the draw path (GetProgramForDraw, both the pipeline stage
loop and the plain-UseProgram half) and the composite-link site; destroy
paths cancel-not-join; COMPLETION_STATUS readers stay non-joining.

Gates: 506/506 unit both flag states; AsyncCompile/AsyncLink/AsyncTeardown
suites x10 repeats clean both states (teardown with 128 jobs in flight,
then re-Initialize); full NVIDIA DirectGLES retrace flag on twice - result
sets identical to flag off, zero new deltas. Compile-phase prefix-diff,
flag on vs off: complementary-reimagined 5.21s -> 2.16s, BSL 1.72s ->
0.90s - past the design's final acceptance targets before the KHR
extension is even advertised. Default remains OFF until stage 5+7.
2026-08-08 11:58:38 -04:00
BZLZHH e5fb57f7eb [Feat] (MG_State, MG_Util): async shader compilation behind the default-off flag (P1 stage 3)
glCompileShader with MOBILEGL_ASYNC_SHADER_COMPILE=1 snapshots its inputs on
the GL thread (source SharedPtr, CompileEnv, cache handle) and runs the whole
pure pipeline - preprocess, validators, extractors, glslang parse - as a
ShaderCompileTask on the worker pool, returning immediately. Every read of
compile-produced state joins through the single Compiled() gate; links stay
synchronous this stage and join their attached shaders at the top of the
body. Flag off, the path is the same code run inline.

Mechanics: the job node owns all its inputs (no back-pointer, no lifetime
tie to the shader object), so re-sourcing or deleting a pending shader is
cancel-and-drop, never a wait; glslang worker hygiene is a TLS-allocator
scope guard plus GL-thread builtin prewarm (gated on the flag, latch reset
on Destroy so re-initialization re-warms); worker-side diagnostics defer
through the job and replay on the GL thread at the join, enforced by
IsPoolThread asserts in RecordError and an empty-deferred-errors tripwire.
A body that throws publishes a COMPLETE failed compile (status false, real
info log) rather than an abandoned node, and never memoizes away the retry;
a failed enqueue (OOM) cancels the node instead of stranding the joiner -
including inside the dispatch loop, where the in-flight slot is repaid.
The pool StopAndDrains from an atexit sentinel too: workers still inside
glslang parse while exit() ran static destructors was a real 2-in-5 SIGSEGV,
reproduced and fixed (15/15 clean after).

Backend-internal shader objects (default FS, DirectVulkan blit/mipmap) are
cache-less and always compile inline - compile-and-read-in-one-breath needs
no round trip.

Gates: unit suite 488/488 with the flag off AND on (x5); AsyncCompileTest
(12 e2e cases: pending re-source/delete/recompile, byte-identical failure
logs across modes, 48-compile cache stress) x10 repeats clean both modes;
full NVIDIA DirectGLES retrace identical result sets flag off/on (zero new
deltas); compile-phase timing flat as designed (links still serial - the
parallel win arrives with stage 4's async link + stage 5's
KHR_parallel_shader_compile).
2026-08-08 10:33:50 -04:00
BZLZHH c93e5fa409 [Refactor] (MG_State, MG_Util): join-by-construction link/compile artifacts (P1 stage 2)
Still fully synchronous - EnsureLinkJoined()/EnsureCompileJoined() are empty
inline no-ops (verified to fold away at every one of the ~1200 call sites;
this project builds without LTO) - but every read of link- or compile-produced
state now goes through a private accessor the compiler enforces, so when
stage 4 moves the bodies onto pool workers, 'which reads must join' is a
type-system fact instead of a 400-line audit.

- ProgramObject: the 31 fields ResetLinkArtifacts clears plus the 5 link
  outputs it forgot (infoLog, linkedFragData{Location,Index}, the geometry
  strip-capture pair) move into a nested LinkArtifacts behind Artifacts().
  ResetLinkArtifacts is now a worker-safe pure clear; the link-observable
  version bumps (backendState/link/uboContent) move to a GL-thread-only
  BumpLinkObservableVersions() called once from Link()'s prologue and from
  glProgramBinary's mandated failure - the link body never writes them, so
  a stage-4 worker cannot lose an invalidation against the draw path.
- ShaderObject: compile artifacts (TShader, preprocessed source, side-channel
  maps, status/log, consume-once flag) behind Compiled(); the P0b layer-1
  memo trio deliberately stays outside as the future non-joining
  COMPLETION_STATUS_KHR fast path.
- CompileEnv (new): a GL-thread snapshot of everything the compile pipeline
  used to read live from the backend mid-parse - compute limits (the
  GetIntegeri_v reach-back is gone from the worker path), advertised
  extensions, device quirks, TBuiltInResource inputs. Captured lazily per
  backend activation; the consume-once re-parse now runs against the same
  env as the original parse.
- The GL-thread prologue / worker-body boundary is marked in Link() where
  the stage sort ends; everything below is a pure function of the snapshot.

Public getter signatures unchanged - MG_Impl and both backends compile
untouched. Unit 476/476, Program suites 117/117, DirectGLES retrace 38/39 on
llvmpipe (the one failure is the known pre-existing non-CI iterationrp case;
the NVIDIA userspace driver was updated out from under the running kernel
module mid-session, so GLX there is down until a reboot).
2026-08-08 07:12:37 -04:00
BZLZHH 8191075133 [Feat] (MG_Util): the async-compile pool skeleton behind a default-off flag (P1 stage 1)
Standalone Asio (submodule, asio-1-38-2 @ 8806a680, ASIO_STANDALONE +
ASIO_NO_DEPRECATED, header-only - no linked artifact) and the job machinery
the async shader pipeline will run on: JobNode (state machine with deferred
errors, continuations firing exactly once, dependency counters, cancel
semantics split into request vs outcome) and ShaderCompilePool
(asio::thread_pool behind a pimpl so no header leaks asio; big-core count
via cpufreq at >=85% of peak clamped to [1,4]; lazily constructed, so with
the flag off no worker thread ever exists; StopAndDrain leads DestroyImpl).

MOBILEGL_ASYNC_SHADER_COMPILE / _THREADS config knobs, default OFF. Nothing
in the GL pipeline references the pool yet - grep-verified; the full
DirectGLES retrace and compile benches are byte- and time-identical. 25
threaded unit tests, clean across 20x gtest_repeat.
2026-08-08 05:28:51 -04:00
BZLZHH d6caed7822 [Fix] (MG_Util, MG_State): five latent frontend bugs the async work made load-bearing
- SpvcSession's move constructor and move assignment dropped the parsed
  metadata, so a moved-to session silently reported empty reflection.
- ParseComputeLocalSize used std::stoull, whose std::out_of_range escaped
  glCompileShader on an oversized local_size literal; now std::from_chars
  saturating to UINT_MAX, pinned by a regression test that reproduced the
  escaping exception.
- The compute local_size std::regex was rebuilt on every compile; hoisted.
- LinkProgram dumped every shader's full source through MGLOG_D per link.
- glslang::FinalizeProcess ran before the GL context tore down, leaving the
  context's live TShaders pointing at freed builtin symbol tables.
2026-08-08 05:28:28 -04:00
BZLZHH 2406e2d219 [Perf, Fix] (MG_Util): preprocessing cleanups - dead scanners, quote-mask bug, one version inspection per compile
Three scoped changes to ShaderSourceProcessor, none altering any transform's
output (pinned by a byte-stability test across the legacy-shader anchor path):

- Delete BlankBlockComments and RemoveDefineForIdentifier - dead since their
  callers left; the former's newline-terminated quote handling moves into
  MaskCommentsAndQuotedText (below) together with its rationale comment.

- Fix MaskCommentsAndQuotedText treating a quote as running past end-of-line.
  GLSL has no multi-line literals, but a stray apostrophe in a directive or
  comment tail ("#pragma message can't") blanked the REST OF THE FILE for
  every masked consumer - the tokenizer, the version inspection, and the P0a
  explicit-location/binding extractors silently lost everything after it.

- Inspect the shader language once per PreprocessShaderSource run instead of
  up to five times: NormalizeVersionDirective now takes the already-computed
  ShaderLanguageInfo, and the two after-version injections share one
  AfterVersionAnchor instead of re-running a full masked sweep each
  (FindAfterVersionDirective -> InspectShaderLanguage) to find the same spot.

Compile-phase timings hold (BSL 1.848s, complementary-reimagined ~5.7s);
retraces and the 435-test unit suite unchanged.
2026-08-08 03:43:56 -04:00
BZLZHH b228f813c0 [Perf] (MG_Util): replace the builtin-shadowing string scans with one tokenize and a SPIR-V OpName pass
RenameBuiltinShadowingFunction probed the whole source ten times per compile
(5 names x mask + scan, each a full-text pass) and still had two blind spots:
a 5-name list and single-line-definition-only detection. On Complementary-scale
packs (4.5MB of sources) that was ~68% of the compile phase.

The rename is now split by FAILURE LAYER, both halves sharing one name table
header so they cannot drift:

- A SPIR-V OpName pass in SanitizeAndOptimizeBinary covers the full ESSL 3.20
  builtin set (~146 names). Renaming a function id is safe by construction:
  builtin calls are GLSL.std.450 instructions and can never resolve to a user
  OpFunction, overloads are distinct ids (a helper overload delegating to the
  real builtin keeps working), dead preprocessor branches never reach SPIR-V,
  and macro-expanded definitions are covered. ESSL 3.x is the only consumer
  that forbids the redefinitions, and this pass runs before its transpile.

- A lexical pass covers only the 5 names whose exact-signature redefinitions
  glslang's relaxed parse rejects outright (never producing SPIR-V for the
  backstop): the historical fma/max3/min3/round/tanh. One TokenizeCode pass;
  definition detection requires brace depth 0, a type-identifier previous
  token that is neither a statement keyword nor a directive tail, and skips
  files whose token-level braces do not balance (preprocessor-asymmetric
  arms) - over-detection is unrecoverable, so every ambiguity falls through
  to the backstop.

Measured on the compile phase (prefix-diff, 3-run medians, Espryt/NVIDIA):
complementary-reimagined 20.0s -> 5.5s, BSL 2.14s -> 1.85s. bliss (the pack
that ships from-scratch fma/tanh helpers) stays at SSIM 0.999962.

Tests: end-to-end ESSL assertions for the multiline-definition and
new-overload shapes, the three adversarial-review reproductions (statement-
keyword call under asymmetric braces, dead-#if compat shim, overload
delegating to the shadowed builtin), and a source-level assertion pinning
the lexical half specifically.
2026-08-08 03:11:38 -04:00
BZLZHH 0d0527192a [Perf] (MG_State, MG_Util): compile shaders with a single relaxed parse
glCompileShader used to parse every source twice: once under the GL client
(reflection only) and once under the relaxed Vulkan client (SPIR-V + the
plain-uniform global UBO), with GenerateBinary re-preprocessing, re-parsing
and re-linking every attached shader on every glLinkProgram. The GL-client
pass is gone: Compile() performs the one link-compatible relaxed parse and
the linked TProgram serves reflection and codegen both. Measured on the BSL
shaderpack compile phase: Espryt 2.80s -> 2.14s, Magma 3.78s -> 3.07s.

What the relaxed parse cannot provide is restored explicitly:
- explicit layout(location/binding) qualifiers on default-block uniforms and
  samplers are extracted lexically at Compile() (the relaxed parse strips
  them) and merged per link with cross-stage conflict checks;
- uniforms the relaxed parse sweeps into MGL_GLOBAL_UBO but no stage reads
  are filtered from the GL reflection surface through GL<->TProgram index
  translation maps (dead uniforms stay inactive, the synthesized block stays
  hidden, builtins reflect under their GL spellings);
- SPIR-V is generated BEFORE buildReflection touches the program (its
  live-variable analysis perturbs GlslangToSpv output - generated modules
  stay bit-identical to the old pipeline's), while the glUniform*-to-scratch
  routing tables are built strictly AFTER reflection, whose results size and
  key them;
- a TShader feeds exactly one link (mapIO mutates the intermediate); relinks
  and multi-program attachments re-parse the stored preprocessed source.

Validated: DirectGLES retrace suite green (two pre-existing local-driver
failures unchanged old vs new), KHR-GL30 877/878 on Espryt/NVIDIA (the one
failure pre-exists this change), unit tests green, per-module SPIR-V hashes
identical across a full DirectVulkan replay.
2026-08-08 01:25:54 -04:00
BZLZHH 867fe3e0ef [Feat] (MG_Util, MG_IntegrationTest): POST rows for the Espryt multi-draw tier, and the scenario that pins it
Three DriverPost rows per the POST rule, since the ladder took on two
new driver dependencies: glDrawElementsBaseVertex (WARN when absent -
every base-vertex draw then costs a CPU index rewrite and an upload) and
compute shaders (INFO - the default tiers never use them). The third
names the tier that will actually run, with the full set the driver
supports, resolved by the same function the backend calls so the two can
not drift. The existing "Multi-draw base vertex" row stopped saying the
fallback is a per-draw loop, which is no longer the whole truth.

Scenario D asserts the one contract every tier shares: a multi-draw
paints exactly what the unrolled single draws paint. The reference side
is a loop of glDrawElementsBaseVertex and never enters the emulation, so
a tier cannot make itself look right by breaking both sides alike, and a
blank-frame pair is rejected outright - drawing nothing is the failure
mode this path actually has.

Nine cases, chosen for the shapes the Minecraft retraces contain none
of: narrow index types, a base vertex past the index type's range,
primitive restart inside a strip on two index types, client-memory index
arrays, and a batch with zero-count sub-draws (whose prefix sums the
flattening tier's binary search has to skip). Each of the six tiers
passes all nine on NVIDIA, and ext/auto/compute also pass on Mesa where
the ext tier is reachable.

The suite is falsifiable, not merely green: rewriting the rebase the way
MobileGlues does it - truncate to the source width, no restart
passthrough - fails exactly three cases on the drawelements tier (both
restart cases and the out-of-range base vertex) and leaves basevertex,
which rewrites nothing, passing. That control is also what turned up the
restart hole in the flattening tier fixed in the previous commit.
2026-08-07 08:16:08 -04:00
BZLZHH 23b880c8be [Feat] (MG_Config, MG_Util): a tier knob and two capability flags for Espryt multi-draw
MOBILEGL_ESPRYT_MULTIDRAW_MODE=ext|multiindirect|indirect|basevertex|
drawelements|compute|auto names the DirectGLES emulation tier for
glMultiDrawElements(BaseVertex). Same contract as the Magma knob: a
preference, not a demand, clamped at resolution time to what the driver
actually has, and invalid values keep auto. Nothing reads it yet.

The two capability flags the ladder selects on are new because neither
existed in the shape the choice needs. SupportsDrawElementsBaseVertex is
the weaker sibling of SupportsMultiDrawElementsBaseVertex - ES 3.2 core
or EXT/OES_draw_elements_base_vertex, with no GL_EXT_multi_draw_arrays
requirement - and it decides whether a batch can replay its sub-draws
with their own base vertices or has to fold them into rewritten indices.
SupportsComputeShader is ES 3.1 core plus the dispatch, barrier and
shader-object entry points. Both keep the house rule the multi-draw
flags already follow: the extension/version check is what proves
support, the resolved pointer only confirms it, because
eglGetProcAddress may hand back a live-looking stub for a function the
context does not implement.
2026-08-07 08:15:31 -04:00
BZLZHH 231d5c90e4 [Feat] (MG_Config, MG_Util): a preference knob and POST rows for Magma's multi-draw tiers
MOBILEGL_MAGMA_MULTIDRAW_MODE=ext|indirect|unroll|auto selects the
DirectVulkan multi-draw dispatch tier, clamped to what the device
supports with one INFO line when it falls back; auto (and unset) picks
the best supported tier. Invalid values keep auto. Magma-only: the
variable has no effect on DirectGLES. Note for the escape hatch:
mode=unroll also forces the GL indirect multi-draw paths onto their
per-command loop, where gl_DrawID reads 0 for every sub-draw -
Flywheel-style content that keys on flw_drawId renders accordingly.

Three DriverPost rows per the POST rule: VK_EXT_multi_draw
(PASS/INFO), the multiDrawIndirect feature (WARN downgraded to INFO -
there is always a fallback tier), and the resolved dispatch tier with
the full chain. drawIndirectFirstInstance gains a row too, since the
indirect tier's legality check now relies on it.
2026-08-07 06:41:21 -04:00
BZLZHH ac3a83b207 [Fix] (MG_Util, MG_Test): never take a non-null eglGetProcAddress result as support
On GLVND Linux eglGetProcAddress returns a non-NULL trampoline for ANY
name - including a fabricated one - so pointer-nullness can never
signal driver support. The three EXT multi-draw entry points were
registered as required (spurious error logs on drivers without them)
and their pointers were trusted; the NVIDIA ES driver hands back a
stub for glMultiDrawElementsBaseVertexEXT that SILENTLY DROPS draws,
which once made a "77% faster" multi-draw batch that rendered nothing.

The three entries are optional now, and two extension-derived
capability flags follow the established Supports* pattern - each is an
extension-string check AND a resolved pointer, so a flag alone is
sufficient at a call site:
  SupportsMultiDrawIndirect: GL_EXT_multi_draw_indirect + both entry
  points resolved.
  SupportsMultiDrawElementsBaseVertex: (GL_EXT or
  GL_OES_draw_elements_base_vertex) + GL_EXT_multi_draw_arrays + the
  entry point resolved. The multi_draw_arrays conjunct is the registry
  fact the stub exploited: glMultiDrawElementsBaseVertexEXT exists only
  in interaction with GL_EXT_multi_draw_arrays, and this NVIDIA driver
  advertises everything else EXCEPT that one - so the entry point is
  genuinely unsupported while eglGetProcAddress still "resolves" it.

Two DriverPost rows report both capabilities (INFO when absent - a
fallback always exists). Unit tests pin the stub shape, the exact
NVIDIA shape, the supported shape and extension-without-pointer.

Proven load-bearing: forcing the old pointer-only condition on the
NVIDIA ES driver reproduces the silent drop exactly (sodium retrace
SSIM 1.000000 -> 0.329522, no crash, no GL error); with the gate the
same run is a literal 1.000000. Unit suite 423/423 (two new tests),
retrace subset 10/10, integration suite 52/52.
2026-08-07 06:00:54 -04:00
BZLZHH d524330032 [Test] (MG_Benchmark, MG_Util): model four more Minecraft frame patterns in the driver bench
The captured traces contain per-frame patterns the bench did not exercise, and
first measurements show two of them are now the worst remaining multipliers -
which is exactly what the missing cases were hiding.

mc_pass_switch: the 26.2 snapshot switches render targets 132 times a frame and
re-declares draw buffers 198 times. Render-target churn is where a Vulkan
backend pays for render-pass breaks and where a tiler pays most on device, and
no case measured it. mc_state_toggle: Blaze3D brackets batches with blend
toggles - 46 enable/disable pairs and 28 blend-func changes per vanilla frame.
mc_tex_param: 26.2 re-sets texture parameters 612 times a frame, almost always
to the value already in place, so this measures redundant-parameter filtering.
mc_use_program: Sodium switches programs 62 times a frame with a mat4 upload on
each, roughly one switch per multi-draw.

All four live in the shared case file at the measured per-frame rates, so the
desktop harness, the on-device harness and the POST screen's Run Bench report
comparable numbers. First desktop measurements (ns/op, native / Espryt / Magma):
pass_switch 8877 / 18502 / 13896, state_toggle 1182 / 8526 / 8305,
tex_param 42 / 102 / 197, use_program 2182 / 10648 / 5096. The state-toggle
multiplier - 7x on both backends - is the largest newly exposed gap and the next
optimization target.

Unit tests 421/421; the Android JNI translation unit compiles against the
extended case set.
2026-08-06 09:39:02 -04:00
BZLZHH 57aeeec053 [Perf] (MG_State, MG_Impl, MG_Backend): stop paying per draw and per upload for work already known
A per-draw CPU profile of a real Minecraft frame (perf on the render thread,
which sits at 100% of one core on both backends) said the deficit is translation
overhead, not the GPU, and named where it goes. This removes the largest items
it found, on both backends and in the shared frontend they both feed.

The single biggest one was not translation at all: IsBackendContextCurrentOnThisThread
called eglGetCurrentContext on every invocation, and glvnd answers that with a
getpid() fork check - a real syscall. The predicate sits two and three deep in
every draw (the deferred-release drain, the global-UBO ring availability check,
and the ring allocation), so it accounted for 16.3% of the render thread. EGL is
still the ground truth, but re-verifying it once per thread per frame catches an
external migration at the next frame boundary rather than the next call, which
recovers the same bookkeeping.

Texture uploads now carry a dirty region instead of a per-level flag. Minecraft
animates atlas sprites with 16x16 glTexSubImage2D calls into a 1024x512 atlas
and respecifies the lightmap every frame; a per-level flag turned each of those
into a full-level re-upload - about 3.6 MB a frame of texels nobody changed.
MipmapStorage accumulates the written box, Espryt uploads it with
UNPACK_ROW_LENGTH striding into the level shadow, and Magma stages just that box.
The box is a union, not a range list: repeated writes to one level widen it and
it degrades to exactly the old whole-level upload, which is the honest worst case.

glBufferData(NULL) is the orphaning idiom, and the backend was answering it by
uploading the stale CPU shadow - turning a rename the driver does for free into
a full synchronized upload. BufferObject now records that a NULL respecify leaves
the store undefined, and the upload is skipped until content is actually written.

The rest are smaller and of a kind: the deferred-release queue is probed without
taking its mutex, the UBO ring waits on the frame fence that frees the space it
needs instead of draining the whole pipeline with glFinish at the size cap, VAO
binds go through a shadow so a draw's second bind of the same object does not
reach the driver, the per-draw clean-texture probe short-circuits on the content
version before rebuilding shape info, glUniform drops byte-identical writes
(which otherwise dirty the whole UBO for the next draw), re-binding the texture
or VAO a slot already holds no longer bumps the generation counters a backend
fast path is keyed on, and the texture validators stopped taking shared_ptr by
value.

On Magma: descriptor-set reuse keeps four entries instead of one, because draws
alternating between two programs - the chunk/entity ping-pong - thrashed a single
slot into a full re-allocate and re-write every draw; a DynamicDraw buffer whose
contents survive two frame boundaries is promoted to resident storage instead of
being re-copied into the per-frame arena forever; and sampled-read barriers name
only the shader stages whose device feature is enabled, which also removes a
latent VUID violation (ALL_GRAPHICS names geometry and tessellation stages a
device need not have).

Measured with the Minecraft rig (render distance 32, p50 fps, same machine,
single sample each): vanilla 1.21.1 Espryt 10.8 -> 36.3 and Magma 31.3 -> 44.6;
26.2 snapshot Magma 114.5 -> 210.5. Fabric+Sodium moved inside noise on Magma
(854 -> 766) with the native baseline itself moving 838 -> 1031 between the two
sessions, so treat that cell as unresolved rather than a regression measured.
Unit tests 421/421. The CTS A/B was not run: these numbers and the test suite are
the whole of the evidence, and a conformance regression would not have been
caught here.
2026-08-06 06:24:37 -04:00
BZLZHH 9c0144d24a [Test] (MG_Benchmark, MG_Util, MG_Backend, android-plugin): run the driver benchmark on a phone
The Minecraft-shaped driver benchmark could only be run from a desktop shell
against a desktop driver, which is the wrong machine: MobileGL exists to run on
mobile GPUs, and nothing said what its translation costs there. This puts the
same cases on an Android device, both in the plugin's POST screen and from a
shell, and adds the native-driver baseline they have to be read against.

The cases move into DriverBenchCases.inc so both harnesses run byte-identical
bodies - the desktop program resolving entry points from one EGL provider, and
DriverBenchJni.cpp calling MobileGL's frontend in-process. The JNI file binds
every gl*/egl* name to MG_Impl by macro rather than by linkage: this library
legitimately has the platform libEGL and libGLESv3 in its own lookup scope, and
a benchmark that quietly measured the device driver instead of the translation
layer would have looked like very good news.

Frames are now closed with a fence wait instead of glFinish. MobileGL implements
glFinish and glFlush as no-ops, so the old loop timed submit-plus-GPU on a native
driver and submit-only on a MobileGL backend, and the two numbers did not
describe the same work.

To measure a device's own driver the cases needed to be expressible in GLES:
ESSL 3.20 twins of the four shaders (chosen at runtime from GL_VERSION, since
MobileGL is deliberately still fed desktop GLSL - translating it is the thing
under test), a multi-draw hook that loops DrawElementsBaseVertex where the
multi-draw entry point does not exist, and an EGL bootstrap that falls back from
desktop GL to GLES 3. The binary cross-compiles for arm64 unchanged.

BenchService hosts each run in its own process and exits afterwards. That is not
caution: the backend is latched from MOBILEGL_BACKEND_TYPE at initialization, so
Espryt and Magma can never share a process, and Espryt's teardown terminates the
process-default EGL display, which would take the POST activity's own EGL
objects with it.

Running it found that Magma could not create a windowless context on Mali at
all - CreateInstance required VK_EXT_headless_surface, which no mobile driver
here exposes, and aborted the process. The Xlib path already probes and falls
back to a hidden window for the same reason on NVIDIA; Android now probes too
and hands the WSI an AImageReader's ANativeWindow, a real producer surface
attached to no display whose images are never acquired. DriverPost reports the
extension's absence as a WARN so the fallback is visible rather than silent.

Measured on a Mali-G77 MC9 (native / Espryt / Magma, ns per operation):
5495 chunk draws 14397 / 36934 / 33763, the 26.2 per-draw uniform-range pattern
13710 / 31205 / 21252, sodium-style multi-draw 256956 / 238389 / 209527. The
translation costs about 2.4x per draw here against 5-9x on the desktop, because
the mobile driver's own per-call cost dwarfs it - and both backends beat the
native driver on multi-draw, which it has to emulate.

Desktop unit tests 421/421; the POST screen and both Run Bench buttons verified
on the device.
2026-08-06 06:13:34 -04:00
BZLZHH ba81ee114e [Feat] (MG_Backend, MG_Impl, MG_Util): attach one layer of any layered texture on DirectVulkan
Whether a backend can attach a single layer of a texture to a framebuffer was one
Bool, so it could only give the most conservative answer any target needed.
DirectVulkan therefore declined every layer of every target and
direct_state_access.framebuffers_texture_layer_attachment failed with 542
messages across four targets.

The three ways a GL layer maps onto Vulkan are independent capabilities, so the
flag becomes a per-TextureTarget mask. A 2D or 2D multisample array layer IS a
VkImage array layer and needed nothing but the gate opened. A cube map array is
one 2D image with arrayLayers = 6 * cubeCount and CUBE_COMPATIBLE, which is a
shape VkTextureManager simply did not have - it is declined softly when the depth
is not a whole number of cubes or the level is not square, because that function's
Bool return exists for unrepresentable shapes and asserting there would abort on
ordinary input, GL_PROXY_TEXTURE_CUBE_MAP_ARRAY above all. A 3D texture's layer is
a z slice, which needs a 2D-array-compatible image and a per-slice clear, because
vkCmdClearColorImage cannot address a subset of a 3D image's slices - a render
pass whose only content is its LOAD_OP_CLEAR can, since its attachment is a 2D
view over that one slice.

VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is asked for per format and withdrawn per
format, mirroring the MUTABLE_FORMAT pattern already in this file: the capability
is per format+usage, so a single global probe answers a different question than
the one the frontend goes on to ask. Losing it costs per-slice attachment for that
format; failing creation would lose the texture.

Three things found on the way that are not the headline:

glFramebufferTextureLayer, the non-DSA twin, had no gate at all and additionally
refused cube map arrays that GL 4.5 requires it to accept. GL 4.6 core 9.2.8 makes
the two entry points equivalent, so they now decline in the same places - leaving
one ungated is what let an unrepresentable attachment reach the renderer.

ComputeFullMipLevelCount takes max(x, y, z), and for every array shape z is the
layer count rather than a mip-able axis, so a 4x4 array with 192 layers asked for
six mip levels on an image whose legal maximum is three
(VUID-VkImageCreateInfo-mipLevels-00958). Only the image's own extent can bound
it. lavapipe had been letting that through.

A layered GL clear queues layerCount = depth, which is illegal for a
VK_IMAGE_TYPE_3D image (VUID-vkCmdClearColorImage-baseArrayLayer-01472 pins it to
0/1, read as the whole mip level) and the old code passed it straight through.

Takes framebuffers_texture_layer_attachment green on DirectVulkan, so the whole
direct_state_access suite is 371/371 there; Espryt stays 370/371, the remaining
case being the fp64 one it declines by design.

Known and deliberately not fixed here, with a FIXME at the site:
KHR-GL44/45/46.geometry_shader.layered_framebuffer.clear_call_support now fails on
DirectVulkan - a layered clear of a 3D texture reads back zeros. Those cases exist
only in the GL44+ lists, above the 4.0 this backend reports. An A/B of a 6935-case
subset (cube map array, texture storage, framebuffer, 3D, the full DSA suite and
the GL33 texture group) is otherwise clean on both backends: 16 cases fixed and
none broken on Espryt, 15 fixed and those 2 broken on Magma, and zero difference
anywhere at GL 4.0 or below. The FIXME records which causes were already ruled out
by bisection so the next reader does not repeat them.
2026-08-05 12:07:04 -04:00
BZLZHH c8c7b19579 [Feat] (MG_Backend, MG_Util): give DirectVulkan GL's provoking vertex
Vulkan's built-in convention is "provoking vertex first"; GL's default is
LAST_VERTEX_CONVENTION, and GL derives both flat shading and the transform
feedback vertex order from it. DirectVulkan had no way to say so, which is why
direct_state_access.queries_functional failed on a value with nothing in its log
- the primitives came back counted against a strip recorded in the wrong vertex
order.

VK_EXT_provoking_vertex is now enabled when present, and the mode is a hashed
field of the pipeline payload rather than dynamic state, because it is baked into
VkPipelineRasterizationStateCreateInfo: two draws differing only in it must not
collide on one cached VkPipeline, or whichever mode built first would stick for
the rest of the frame. The pNext is chained only when the mode is not Vulkan's
default, so a device without the extension produces a byte-identical
VkGraphicsPipelineCreateInfo to before.

Two carve-outs, both measured rather than reasoned:

A geometry shader already emits its triangles in GL's vertex order, so asking for
LAST rotates them a second time and transform_feedback.geometry reads back the
wrong vertices. The mode is one pipeline bit and the input-assembler path wants
the opposite, so the two cannot both be satisfied: a program that runs a geometry
shader and captures transform feedback keeps Vulkan's own convention. That test
is read off the program's own shader list, not
programObj.rasterizationProducerStage - the latter is filled by the clip-fixup
analysis, which does not run for every program and reads Unknown for exactly the
programs this guard exists to catch. Both halves are link-time facts folded into
programObj.hash, so no pipeline memo can hand back one built for the other mode;
keying on IsTransformFeedbackActive() instead would be a live bug, since neither
memo key moves on glBeginTransformFeedback.

transformFeedbackPreservesProvokingVertex is deliberately not requested. It buys
nothing here - the capture order queries_functional needs comes from
provokingVertexLast alone - and leaving it off keeps
VUID-VkGraphicsPipelineCreateInfo-topology-04884 disarmed, so a TRIANGLE_FAN
pipeline may take LAST on any device.

The blit pipeline routes through the same selector: it has no flat varying and no
capture, but on a device without provokingVertexModePerPipeline a blit left on
FIRST inside a render pass whose draws are LAST is an illegal mix.

Per the POST rule the new extension gets rows for provokingVertexLast and for the
two properties that change what MobileGL can promise.

Fixes queries_functional on Magma (370/371). An A/B over a 976-case transform
feedback / geometry shader / layered rendering subset of GL30-GL45 is otherwise
identical on both backends and additionally takes 14 geometry_shader rendering
and layered_rendering cases from failing to passing on Magma.
2026-08-05 10:04:30 -04:00
BZLZHH 0e7692251d [Feat] (MG_State, MG_Impl, MG_Util): store a compressed texture image and hand it back
glCompressedTexImage2D rejected every internalformat with GL_INVALID_ENUM, so
direct_state_access.textures_get_image threw at its first compressed call and
reported InternalError with nothing in the log at all - the uncompressed half of
the case had already passed.

The compressed bytes are now kept verbatim, in a side-channel beside the texel
shadow rather than in place of it. That placement is the load-bearing decision:
both backends pair MapMipmapData with GetMipmapByteSize while sizing their copy
regions from GetMipmapTexelSize, and DirectGLES additionally divides the byte
size by the texel count to recover bytes-per-texel, so putting 16 bytes where a
4x4 RGBA8 extent says 64 would be an out-of-bounds read on both. The texel
storage therefore stays uncompressed and correctly sized - the image samples as
zeros, which is the same deviation the RGTC/BPTC/ETC2 arms of
ConvertGLEnumToTextureInternalFormat already document - while
glGetCompressedTexImage returns the image *as stored*, which GL 4.6 core 8.11
requires and which no re-encode could satisfy byte for byte. Nothing ever hands
the compressed bytes to GLES or Vulkan, so the shadow is authoritative rather
than potentially stale, which is why the readback never asks a backend.

The accepted set is exactly the RGTC/BPTC/ETC2-EAC formats core GL requires, and
it is deliberately the same set ConvertGLEnumToTextureInternalFormat can back
with uncompressed storage, so the upload can never accept a format whose texel
shadow it cannot allocate. imageSize is checked against the block arithmetic,
which is also what keeps the copy in bounds.

Three things the shape depends on. AllocateStorage clears the compressed tag, so
a glTexImage2D or glTexStorage2D over the level un-compresses it - without that,
textures_compressed_subimage would flip branches and start asking for data
MobileGL cannot produce. GL_TEXTURE_COMPRESSED and
GL_TEXTURE_COMPRESSED_IMAGE_SIZE are answered per level rather than per texture,
because a compressed internalformat handed to glTexImage2D resolves to
uncompressed storage and must keep reading as uncompressed. And
GL_TEXTURE_INTERNAL_FORMAT now reports the compressed token for such a level, or
it would claim GL_RGBA8 while GL_TEXTURE_COMPRESSED said true.

Still rejected on purpose: glCompressedTexImage1D/3D and every
glCompressedTexSubImage*, which caps the blast radius.

Fixes textures_get_image on both backends (Espryt 370/371, Magma 369/371). A/B
over a 1210-case compressed/texture-storage/texture-view/buffer-storage subset of
KHR-GL45 is identical before and after on both backends but for
get_texture_sub_image.errors_test, which stops throwing and fails on a value
instead.
2026-08-05 09:40:29 -04:00
BZLZHH 34f09291da [Feat] (MG_State, MG_Backend, MG_Util): feed a 64-bit vertex attribute on DirectVulkan
glVertexAttribLFormat validated its arguments and then refused unconditionally
with "64-bit vertex attributes are not supported", so
direct_state_access.vertex_arrays_attribute_format failed every GL_DOUBLE
subcase on both backends - the format never landed, the draw fetched whatever
the attribute held before, and the captured values came back as reinterpreted
garbage.

The attribute is now real state. IsLong is its own bit rather than being
inferred from Float64, because glVertexAttribFormat(GL_DOUBLE) also reads
doubles - it just asks for them converted to float - so the type alone cannot
tell the two apart. It participates in the format comparison, so an L-format
call over a plain one still bumps the version, and glVertexAttribPointer clears
it inside the mutation block so the clear and the bump stay atomic.
GL_VERTEX_ATTRIB_ARRAY_LONG stops being hardcoded false, and the pname is now
accepted by the attribute queries at all.

Support is detected, never assumed. SupportsFloat64VertexAttributes comes from
VkPhysicalDeviceFeatures::shaderFloat64 on DirectVulkan and is false on
DirectGLES - not a driver question there and never will be, since ES has no
GL_DOUBLE vertex format and ESSL has no fp64 type to consume one with. A backend
without it declines in the entry point, with the GL error and a log line naming
the reason, rather than accepting state no draw could honour. Both cases get a
DriverPost row so the loss is named at startup instead of at draw setup.

On DirectVulkan the attribute deliberately does not use VK_FORMAT_R64*_SFLOAT:
those are optional and lavapipe advertises zero features for all four of them.
It is fetched as its 32-bit word pair (R32G32_UINT / R32G32B32A32_UINT) and
bitcast back to double in the shader by a new SPIR-V pass, which is bit-exact
and needs no format capability at all. The pass re-declares the input as uvec2 /
uvec4, demotes the original variable to a Private global and seeds it once at
the top of the entry point, so every existing load keeps its id and its double
type and no other instruction is rewritten. Both halves branch on nothing but
"is this attribute long", so they cannot disagree - and if the pass ever fails,
the assertion fires rather than letting a UINT format sit under a double input.
The pointer types are all created before any variable that names them and the
demoted variable is moved after them, since the types-and-variables section may
not forward-reference a type.

dvec3/dvec4 are declined rather than fetched wrong: six or eight uint32
components have no single VkFormat, and GL spreads such an input over two
attribute locations, which the location-per-index model here does not express.

Fixes vertex_arrays_attribute_format on Magma (369/371). On Espryt it stays
failing, now as a detected and explained decline rather than a blanket refusal.
2026-08-05 08:49:23 -04:00
BZLZHH 5545d31c37 [Feat] (MG_Backend, MG_Util): give a cube map array real storage on DirectGLES
TextureCubeMapArray was missing from every storage and upload switch in the
DirectGLES texture sync, so a cube map array reached the driver with no storage
at all - and from the glFramebufferTextureLayer branch, so attaching one of its
layers fell through to glFramebufferTexture2D and raised INVALID_ENUM. Every
GL_TEXTURE_CUBE_MAP_ARRAY colour check in
direct_state_access.framebuffers_texture_layer_attachment read nothing.

ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly like a 2D
array whose depth is six times the cube count, so each switch gains the case
beside Texture2DArray and nothing else changes. 1D arrays join the layer branch
for the same reason - their backend image is a 2D array.

Per the POST rule the new GLES dependency gets a capability
(SupportsTextureCubeMapArray, ES 3.2 core or EXT/OES_texture_cube_map_array) and
a DriverPost row saying what a user loses without it.

Takes framebuffers_texture_layer_attachment from failing to passing on Espryt. It
still fails on DirectVulkan, which declines a layered attachment outright.
2026-08-05 06:58:00 -04:00
BZLZHH 394d1ce748 [Feat] (MG_Impl, MG_Util): copy into 1D and 3D textures, and accept the BPTC and ETC2 enums
Two unrelated texture gaps.

glCopyTextureSubImage1D and 3D validated their arguments and then did nothing:
CopyTexSubImage1D_State and CopyTexSubImage3D_State were empty TODOs and no
backend exposes anything but a 2D blit. But a texture's contents live in its CPU
storage - the backends sync from it - so the copy does not need a blit at all.
CopyReadFramebufferIntoMipmapRegion reads the region out of the read framebuffer
through the existing ReadPixels path, in the destination's own canonical client
layout so the bytes need no second conversion, and writes them straight into the
level. GL 4.6 core 8.6 says the copy ignores pixel-store state and any bound pack
buffer, which the borrowed readback does not, so both are neutralised for the
duration and restored after. A cube map destination addresses its faces as
separate upload targets, so its zoffset picks the target rather than a slice.

ConvertGLEnumToTextureInternalFormat had arms for the six generic compressed
formats and the four RGTC ones, all resolving to uncompressed storage, but none
for BPTC or ETC2/EAC - so glTexImage2D with one of those fourteen enums answered
INVALID_ENUM, which was never a legal reply for formats core GL has required
since 4.2 and 4.3. They follow the same deviation for the same reason: nothing in
this stack can compress them, and uncompressed storage is the trade the RGTC
formats already take.

Takes textures_compressed_subimage from failing to passing on both backends and
textures_copy on Espryt. textures_copy still fails on Magma, where the readback
of a layered attachment does not yet resolve the attached layer.
2026-08-05 05:50:32 -04:00
BZLZHH dd60ff39ce [Feat] (MG_State, MG_Impl, MG_Backend, MG_Util): make the border colour real sampler state
glGetSamplerParameterfv(sampler, GL_TEXTURE_BORDER_COLOR) raised INVALID_ENUM,
because MobileGL kept the border colour on the texture object and
GetSamplerParam_State had no case for it at all. That is the first thing
direct_state_access.samplers_defaults asks, so the case threw before reaching
any of the defaults it was written to check.

GL 4.6 core table 23.18 lists TEXTURE_BORDER_COLOR as sampler state, so it moves
to SamplerParameters and TextureObjectBase reaches it through the SamplerObject
it already owns - one source of truth, and a sampler object bound over a texture
now supplies its own border colour, which is what GL says should happen. The
texture params version still moves on a write, because the DirectGLES texture
sync memoises on it. glSamplerParameter{fv,Iiv,Iuiv} and their getters read and
write all four components in whichever representation the caller used, and the
three representations are kept in step so any getter has an answer. The bogus
[0,1] and [0,255] range checks are gone: GL clamps a border colour when a
fixed-point format is sampled, it does not reject it.

DirectVulkan's ResolveVkBorderColor now reads the sampler rather than the
texture. DirectGLES gained a glSamplerParameterfv in its sampler sync, and both
that and the pre-existing glTexParameterfv are gated on a new
SupportsTextureBorderClamp capability - ES 3.2 core, or EXT/OES_texture_border_clamp
before it - since without the extension every such call is INVALID_ENUM on the
driver. DriverPost gains the matching row per the POST rule, saying what a user
actually loses when it is missing.

Takes direct_state_access.samplers_defaults from failing to passing on both
backends.
2026-08-05 04:45:55 -04:00
BZLZHH 534ec65dda [Fix] (MG_Util): ask the ES driver for the texture buffer offset alignment
The DirectGLES capability probe queried GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT with a bare
glGetIntegerv while every other query in the same function goes through glesFuncs. A bare
call resolves to MobileGL's own exported entry point, which answers that pname out of the
capability table this code is in the middle of filling in, so the value read back was the
default it started from and the driver's real alignment never arrived.

The backend therefore advertised an alignment of 1. An application that trusts that -
which is the only thing it can do - passes glTextureBufferRange an offset the ES driver
cannot honour, and the driver produces a texture that reads as zeros with no error
anywhere. The alignment llvmpipe actually wants is 16.

Takes direct_state_access.textures_buffer_* from 3 to 30 of 30 on DirectGLES, and the
whole DSA group from 66.85% to 74.12%. DirectVulkan was unaffected: its alignment comes
from a Vulkan device limit and was already right.
2026-08-05 02:26:50 +00:00
BZLZHH bb582203d9 [Feat] (MG_Impl, MG_State, MG_Util): attach a buffer texture to a range of its buffer
glTexBufferRange, glTextureBuffer and glTextureBufferRange were all stubs, so a
buffer texture could only ever be attached through glTexBuffer -- by binding, and
always to the whole buffer.

Give the buffer texture the window it is supposed to address. The non-range forms
record it as offset 0 with a whole-buffer sentinel rather than the size the buffer
happens to have, so a later respecify keeps being followed instead of freezing the
texture at yesterday's size. All four entry points now share one attach path,
differing only in how they name the texture: by binding for the target forms, by
name for the DSA ones.

Both backends honour the window: DirectVulkan offsets and clamps the buffer view,
DirectGLES uses glTexBufferRange when the texture names a sub-range and keeps
plain glTexBuffer for the whole-buffer case, which also works on a driver without
the range entry point.

GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT reported 0 with a comment explaining that the
range entry points were stubbed. It now reports what the device actually requires
-- minTexelBufferOffsetAlignment on Vulkan, the driver's own value on GLES -- and
the range entry points enforce it. Zero was never a legal answer; the minimum is
1, and an application that trusted it would have built unaligned offsets.
2026-08-04 20:53:53 -04:00
BZLZHH cb2ba71feb [Feat] (DirectVulkan): run the tessellation stages
The backend already turned a tessellation control/evaluation shader into the
right VkShaderStage, but nothing downstream knew what to do with it: GL_PATCHES
had no topology, so it fell through to the triangle-list default, and the
pipeline carried no tessellation state at all. A GL_PATCHES draw therefore ran
the vertex and fragment stages over raw triangles.

Map GL_PATCHES to VK_PRIMITIVE_TOPOLOGY_PATCH_LIST, carry GL_PATCH_VERTICES into
the pipeline as patchControlPoints (part of the key, since two patch sizes are
two pipelines), attach VkPipelineTessellationStateCreateInfo for a patch topology
only, and enable the tessellationShader device feature.

POST reports the feature, because without it a program with a tessellation stage
cannot build a pipeline at all and GL_PATCHES draws render nothing.
2026-08-04 20:03:24 -04:00
BZLZHH cff959b2e8 [Feat] (DirectVulkan, MG_Util): honour a glVertexAttribDivisor other than 1
Vulkan's VK_VERTEX_INPUT_RATE_INSTANCE advances an attribute once per instance and has
no way to say anything else, so every non-zero divisor collapsed to 1: an attribute the
application asked to change every three instances changed every one, and
KHR-GL40.draw_indirect.basic-drawArrays-instancing and its elements sibling drew the
wrong colours from instance one onward.

VK_EXT_vertex_attribute_divisor is exactly this state, so it is enabled when the device
has it and the per-binding divisors ride into the pipeline through
VkPipelineVertexInputDivisorStateCreateInfoEXT. Only divisors other than 1 are listed -
1 is what the plain input rate already means - and they join the layout hash, so two
layouts that differ only in a divisor no longer share a pipeline.

POST reports the feature either way, because without it the failure is silent and looks
like a shader bug: the attribute is fetched, just from the wrong instance. The GLES side
gains the two checks this session's other work made load-bearing for the same reason -
glPatchParameteri (without it GL_PATCH_VERTICES stays at the driver's 3 and a patch draw
of any other size renders nothing) and the transform feedback object entry points
(without them a second object cannot open a capture while the first is paused).

KHR-GL40.draw_indirect on Magma: 70/70 but for the arbitrary primitive-restart index,
which Vulkan cannot express at all.
2026-08-04 19:25:32 -04:00
BZLZHH 28d0af6f04 [Feat] (MG_Util, DirectGLES, DirectVulkan): normalize rectangle coordinates in the module
Neither target API has GL_TEXTURE_RECTANGLE: ESSL has no rectangle sampler, and
Vulkan's SPIR-V environment does not allow Dim::Rect. Both emulate it on a plain 2D
texture, and the two differ in exactly one way - a rectangle lookup addresses texels
where a 2D one addresses [0,1].

That one difference now lives in one SPIR-V pass, so neither backend has to know about
it: every lookup taking normalized coordinates gets its coordinate divided by the size
the texture reports, and the image type is then rewritten to 2D. Magma had no rectangle
handling at all - it fed Dim::Rect straight to Vulkan, which read the texel coordinates
as normalized and sampled the edge, so all fifteen KHR-GL40.texture_gather.*-2drect
cases came back holding the clear colour.

This replaces the ESSL text rewrite that did the same divide for DirectGLES only. Doing
it in the module instead is both shorter and stricter: the pass resolves an operation's
image type through the sampled-image and pointer wrappers rather than matching a
sampler name in generated source, so it cannot be fooled by an expression where it
expected an identifier, and it needs no help from the frontend reflection to know which
samplers were rectangles.

Still declined, as before: the Dref *sample* forms, whose coordinate carries the compare
value in its last component, and the projective ones, where the divide would have to
happen after the perspective divide. texelFetch is deliberately untouched - integer
texel coordinates mean the same thing on both targets.

KHR-GL40.texture_gather: Magma 66 failures -> 2, Espryt stays at 75/75.
2026-08-04 13:25:40 -04:00
BZLZHH b95fcb7bca [Feat] (MG_State, MG_Impl, DirectGLES): implement glPatchParameteri
GL_PATCH_VERTICES decides how many vertices one tessellation patch consumes, and
glPatchParameteri was a stub - so the value stayed at the driver's default of 3 no
matter what the application asked for. KHR-GL40.texture_gather.gather-tesselation-shader
sets it to 1 and then draws a single patch: with the request dropped the draw had too
few vertices for one patch, produced nothing at all, and the case read back the clear
colour.

The value is context state on both sides and ES 3.2 spells the entry point exactly the
same way, so it is stored in the render state (where glGetIntegerv(GL_PATCH_VERTICES)
now finds it) and forwarded. Validation needs the real bound, so GL_MAX_PATCH_VERTICES
and GL_MAX_TESS_GEN_LEVEL are probed off the host driver alongside the other limits and
answered from there too; the defaults are the GL 4.0 core minimums.

KHR-GL40.texture_gather is now 75/75.
2026-08-04 10:58:14 -04:00
BZLZHH 5437947240 [Feat] (DirectGLES, MG_Util): normalize the coordinates of a rectangle lookup
A rectangle texture is emulated on an ES 2D texture, and LowerRectImagesForEssl
rewrites the image type in the SPIR-V to match. That is exact only where the lookup
addresses texels directly, which is why the pass declined any module containing a
lookup that takes normalized coordinates - the whole KHR-GL40.texture_gather 2drect
set among them.

The missing half is one divide: a rectangle lookup's coordinate is in texels and the
2D lookup it becomes wants [0,1], so the coordinate has to be divided by the texture's
size. It goes in on the ESSL the transpiler produces, next to the LOD-bias emulation
that already rewrites lookup arguments there, and reads the size back with
textureSize() rather than plumbing a uniform down - the emulated texture is a real ES
2D texture, so the shader can ask it directly.

Only the forms whose argument 1 is the bare coordinate are rewritten - texture,
textureOffset and the three textureGather flavours, which covers the Dref gathers too
because those carry the compare value in a separate argument. texelFetch is
deliberately left alone: its coordinates are integer texels on both targets. The
SPIR-V pass keeps declining everything else, so a projective lookup or a Dref sample
(where the compare value rides in coord.z) still refuses the module instead of
producing something subtly wrong.

Which samplers were declared rectangle is no longer visible in the transpiled source -
they are plain sampler2D by then - so the names come from the frontend program's
reflection.
2026-08-04 10:38:01 -04:00
BZLZHH 00534d8bbc [Fix] (MG_Impl): report the draw-indirect binding and the buffer access state
Three pieces of queryable buffer state were missing, all of them read by the
KHR-GL40.draw_indirect basic-binding-* and basic-buffer-* cases:

- GL_DRAW_INDIRECT_BUFFER_BINDING had no case in glGetIntegerv, so it raised
  GL_INVALID_ENUM and left the caller's variable untouched (the test read back its own
  -9999 sentinel). GL_DISPATCH_INDIRECT_BUFFER_BINDING right next to it was already
  handled; this is the same two lines against BufferTarget::DrawIndirect. Because
  glGetBooleanv/glGetFloatv/glGetDoublev all widen from the integer path, one case
  fixes all four getters.
- GL_BUFFER_ACCESS answered 0 for an unmapped buffer. Its initial value is
  GL_READ_WRITE and glUnmapBuffer restores it (GL 4.6 core table 6.2); 0 is not a legal
  value of that state at all, and the test threw on the unrecognised enum.
- GL_BUFFER_ACCESS_FLAGS was not implemented, so it fell through to the invalid-pname
  arm. It is the MapBufferRange bitfield verbatim, which the mapping access flags
  already hold in normalised form - glMapBuffer's access enum is converted on the way
  in - so it converts straight back out, and reads zero while unmapped.
2026-08-04 09:52:40 -04:00
BZLZHH 3dc6a1b6db [Fix] (MG_Impl, DirectGLES): answer the texture-gather offset limit queries
glGetIntegerv(GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET) and its GL_MAX_ counterpart fell
through to the default arm of the getter and raised GL_INVALID_ENUM, leaving the
caller's variable untouched - KHR-GL40.texture_gather.api-enums read back the
uninitialised 32764 that happened to be on its stack and failed on the error alone.

Both are core state from GL 4.0 (table 23.53) and from ES 3.1 (table 20.40), so the
value is simply the host driver's, probed alongside the other limits in
FillInGLESCapabilities and carried to the getter through DynamicBackendParameters.
The probe result is widened to the -8/+7 core minimums rather than trusted blindly:
a driver that leaves the out-parameter alone (no ES 3.1, or an enum it ignores) would
otherwise hand us a range narrower than GL 4.0 requires MobileGL to advertise, and
the shaders the CTS builds assume the guaranteed range regardless.
2026-08-04 09:40:15 -04:00
BZLZHH e45f7ae5d4 [Fix] (DirectGLES, MG_Util): keep 16-bit SNORM precision through the 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 43a43c1180 [Fix] (DirectGLES, MG_Util): raw framebuffer writes while GL_FRAMEBUFFER_SRGB is off
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 65dbfa6f26 [Fix] (DirectGLES, MG_Util): 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 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 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 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 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 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 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 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 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 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 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