Audit of every memoization implementation; sixteen verified defects fixed:
DirectGLES backend:
- Broadcast draw-buffer memo: cleared at MakeCurrent/DestroyEGLContext like its
sibling shadows; its identity+version key is only monotonic within one GLContext,
so a library teardown + re-init could false-hit on a recycled FBO address.
- Backend texture id re-mint (RecreateBackendTexture) now bumps an attachment
generation that the SyncCurrentFBO gate and every FBO twin compare, so driver
FBOs re-attach instead of keeping the deleted texture name; the attachment walk
re-enters until the generation is quiescent (a walk itself can re-mint).
- Buffer id re-mint (persistent-map adoption, immutable-store retire) now bumps a
generation the VAO twin sync compares, forcing a full re-emit of the baked
glVertexAttribPointer / element-array bindings that frontend versions cannot see.
- VAO element-array sync memo: bound-object identity joins the wrapping Uint16
slot version (same pairing the ResolvedDrawBuffers IBO memo already uses).
DirectVulkan backend:
- EBO slice memo gains the mapped-buffer guard its vertex-binding sibling has: a
shadow-backed persistent map mutates with no epoch bump, so a hit must decline.
- VkClearManager::MergeClearPayload keeps colorEncoding/colorInt/colorUint with
the color, so deferred glClearBufferiv/uiv no longer degrade to all-zero float.
- GetOrCreateComputePipeline no longer memoizes a failed creation (same contract
as PipelineFactory): a transient driver failure was permanently disabling every
dispatch of that program.
- Explicit-LOD-0 verdict memo keys on the sampling-resolution generation; sampler
filter/aniso/LOD setters bump only that counter, so the old key served a stale
verdict (wrong SPIR-V variant) after glTexParameter/glSamplerParameter changes.
- SetupDraw fast path declines instead of re-arming on a moved sampling-resolution
generation (the snapshot bakes the LOD verdict into its pipeline), and
recomputes the XfbCapture bit so the first draw after glBeginTransformFeedback
cannot bind the undecorated variant and silently capture nothing.
- VertexInputStateFactory eviction epoch is drawn from a process-wide source: VAO
state-pointer memos outlive the factory across renderer recreation, and a fresh
factory restarting at epoch 1 would dereference a dead factory's entry.
- Cached render passes re-read the live renderbuffer clear payload at begin (the
clear VALUE is not in the pass hash; the entry's inline snapshot replayed the
creation-time color and dropped the newly queued one).
- FramebufferObject gains a never-reused lifetime id, keyed into the render-pass
fast-path memo and the SetupDraw snapshot beside the raw pointer + Uint16
version pair, which address reuse plus fresh version counts could equal.
- SyncTextureResource's preserved-content image goes through the deferred-release
ring on both failure paths instead of a synchronous destructor under the GPU.
MG_State frontend:
- Layer-1 compile memo is env-disciplined like layers 2/3: a node computed against
a dead CompileEnv (e.g. pre-capability fallback limits) no longer answers
glCompileShader forever once the environment's content changes.
- Pipeline composite cache rebuilds from each stage program's last-link shader
snapshot (new LinkedShaderRef list + pinned link inputs) instead of the live
attach list and current compile nodes: post-link glAttachShader/glCompileShader
must not leak into the composite while the (lifetimeId, linkVersion) signature
still hits - GL's "as last linked" rule.
A destroyed VertexArrayObject's heap address is handed straight back by the
next allocation of its size, and so is a destroyed BufferObject's. DirectVulkan
keyed its per-VAO draw memo on the VAO POINTER and folded the bound buffer's
ADDRESS into the content hash that validates the memoised bindings, so a
delete/recreate pair under a byte-identical attribute layout reproduced both
the key and its validating hash at once. The successor VAO then inherited the
dead one's resolved bindings and the draw fetched from a destroyed VkBuffer.
Both stated defences failed together, because both reduce to the content hash
and the hash's buffer-identity component was itself a recycled address.
VertexArrayObject and BufferObject now carry a globally-unique, never-reused
GetLifetimeId() - the same contract as ProgramObject's, minted from an atomic
starting at 1 so a zero-initialised slot can never name a live object.
VaoDrawMemo matches on (address, lifetime id) and stores the id on recycle,
SetupDrawSnapshot's "the VAO did not move" test compares the id alongside the
config version, and VertexInputStateFactory::ComputeHash hashes the bound
buffer's id instead of its pointer (0 for client memory).
Proven: the use-after-free reproduces at 100% incidence headless on lavapipe,
including a SEGV whose backtrace is the driver dereferencing a destroyed vertex
buffer inside lvp_queue_submit, and it is gone with the fix. New coverage -
MG_Test/State/ObjectLifetimeIdTest (deterministic, GPU-free, no context: it
waits for the real allocator to repeat an address and asserts the id differs,
and skips loudly rather than passing quietly if it never gets the chance), and
MG_IntegrationTest XfbAfterClipDistanceScenario, registered for DirectGLES,
DirectVulkan, and a third DirectVulkan run with async shader compilation pinned
on because that is a second allocation pattern. Gates: 553/553 unit green at
async=0 and async=1; the scenario 5/5 headless at both flag states; 71/72 CI
trace-replay fixtures over both backends, the one failure a pre-existing
lavapipe crash proven not a regression (identical SIGSEGV at the identical
call number under the pre-fix library).
Pending NVIDIA/X11 confirmation: the KHR-GL{32,40} transform_feedback failures
that opened this investigation never reproduced on lavapipe - the -2/-101
pre-fill signature appears in zero pre-fix runs there - so whether this clears
them is UNPROVEN and must be re-measured on the NVIDIA rig against a freshly
re-run pre-fix baseline. The residual suspect is deliberately untouched here:
m_xfbCounterSlotByObject keys its counter slot on the raw GL transform-feedback
name, so a recycled name whose generation check happens to pass would RESUME
instead of BEGIN. That path was never exercised on lavapipe and is neither
confirmed nor exonerated.
The draw fast path still paid for its own proofs: the hottest single
load (20% of TrySetupDrawFastPath) was chasing the cold
VertexInputStateFactory heap entry just to answer "same vertex-input
layout?". That answer now comes from the frontend VAO's config-guarded
aux memo (layout hash + attribute masks), and a VAO-cycling stream with
a stable layout skips the pre-flight AND pipeline re-resolution
entirely. The VkProgramObject* is memoized on the snapshot behind a new
ProgramFactory cache-structure epoch (bumped on every insert/erase; use
is re-stamped so the idle sweep can never evict a live entry). A
render-state version move no longer forces the full path: the pipeline
value hash is refreshed in place and the 8-entry memo probed directly
(the GL_BLEND-toggle case).
The resolved-vertex-bindings memo now revalidates all-resident unmapped
entries ACROSS frames via per-binding slice epochs - minted from a
process-lifetime counter so a recycled address can never revalidate,
with every mutation path funnelled through BumpSliceEpoch - while
stamping each resource's GPU-use serial exactly as the skipped acquire
would, preserving the busy-tracking that glBufferSubData's
host-write-vs-staged-copy choice depends on. Resident index buffers get
the same treatment through an EBO slice memo.
The six-part dynamic-state tail (viewport/scissor/blend constants/depth
bias/line width/stencil) is gated behind one render-state-parameters
version + pass-geometry compare per command buffer. GetSlice is inlined;
SampledBindingsUnchanged walks only the program's declared bindings.
Quiet-box 6-round order-alternating A/B (on top of the frontend
VAO-bind commit): vanilla_draw -20.8% (790 -> 626 ns/op, 3.4x native to
2.5x), sampler_churn -28.2%, ubo_range -8.4%, state_toggle -3.8%;
tex_param's matrix flag (+10%) was adjudicated by an isolated
alternating re-run at +1.0% - position bias, not regression. Unit tests
421/421.
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.
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.
glVertexAttribPointer now accepts the GL 3.3 Core packed types
GL_INT_/GL_UNSIGNED_INT_2_10_10_10_REV and the GL_BGRA size, clearing the
two long-standing "// TODO: implement GL_BGRA support" markers. Adds the
format end to end across the frontend, VAO state, and both backends.
- DataType: add Int2101010Rev / Uint2101010Rev with GLToMG / MGToGL /
MGToStr converter cases.
- Validation (ValidateVertexAttribFormat): the full glVertexAttribPointer /
glVertexAttribIPointer error table -- size is 1..4 or GL_BGRA (else
INVALID_VALUE, which takes precedence); a packed type requires size 4 or
GL_BGRA (else INVALID_OPERATION); GL_BGRA requires GL_UNSIGNED_BYTE or a
packed type AND normalized == GL_TRUE (else INVALID_OPERATION); the
integer path rejects packed types (INVALID_ENUM) and GL_BGRA size
(INVALID_VALUE).
- VAO: store GL_BGRA as size 4 plus a new IsBgra flag (reset on the
binding-format path).
- DirectVulkan: map the packed/BGRA formats to
VK_FORMAT_A2B10G10R10_* (normal) and VK_FORMAT_A2R10G10B10_* /
VK_FORMAT_B8G8R8A8_UNORM (BGRA reversed), fold IsBgra into the pipeline
hash, and size packed/BGRA elements as one 4-byte word via
GetAttributeByteSize. (Vulkan *_SNORM decodes with the GL 4.2 symmetric
rule, a documented deviation from the 3.3 signed formula.)
- DirectGLES: round-trip the packed enum through the loader, pass GL_BGRA
as the driver size argument, and size client uploads with the packed
4-byte word.
Tests: 4 VertexArrayTest cases covering packed/BGRA storage and the full
float/integer error table; the packed-size hard-fail is mutation-verified.
VertexArrayTest 42/42, SanityTest 30/30, library builds clean.
GL 3.3 Core: a shader input whose generic attribute array is disabled reads that
attribute's current value (per-context state, default (0,0,0,1)). Four defects made
that path non-conformant, three of them silently.
* Out-of-bounds current-value reads. m_currentVertexAttributes held 16 entries while
the DirectVulkan draw path walked shader input locations 0..31 and GL_MAX_VERTEX_ATTRIBS
was advertised straight from the device (commonly 32). The only guard was MOBILEGL_ASSERT,
which expands to nothing outside debug builds. Grow the storage capacity to 32, advertise
min(device limit, capacity), validate against that dynamic limit, and give the accessors
real runtime bounds checks. Replace the literal 32 loops with the constant, and pin
MAX_VERTEX_ATTRIBS to the Uint32 mask width and to vertexInputTypes' bound with
static_asserts so the two can no longer drift apart -- that drift was the bug.
* DirectGLES never fed current values to the driver. Values were stored in MG_State only,
so a disabled attribute always rendered as the ES driver's own untouched (0,0,0,1) while
DirectVulkan rendered it correctly: identical GL code, different pixels per backend.
Add SyncCurrentVertexAttributeValues() to the draw prologue, and hoist the
glType -> (base type, component count) dispatch into MG_State::GLState so both backends
resolve the semantics from one place instead of it living inside VulkanRenderer.
* Enabled arrays the backend could not map were silently demoted to the current value.
ToVkVertexFormat had no DataType::Float16 case, so a GL_HALF_FLOAT array fell to
VK_FORMAT_UNDEFINED, dropped out of the vertex input state, and became indistinguishable
from a disabled array: the geometry rendered a constant colour with GL_NO_ERROR. Add the
Float16 mapping, track an unsupportedAttribMask, and hard-fail the draw before pipeline
creation so no synthetic attribute is baked into a cached VkPipeline.
* glGetVertexAttrib{fv,iv,Iiv,Iuiv}(GL_CURRENT_VERTEX_ATTRIB) returned before any index
validation, reading past the array instead of raising GL_INVALID_VALUE.
Also resolve ProgramObject::DoReflection's "TODO: get from backend" 16-location clamp,
which capped the new DirectGLES sync at locations 0..15; report GL_MAX_VERTEX_ATTRIBS
through the same helper the validators use, so the clamp cannot be bypassed; and bound
vertex binding indices by the same dynamic limit, since the default attribute -> binding
mapping is the identity.
Add a "Vertex attributes" driver POST row to both backends: FAIL below the GL 3.3 Core
minimum of 16, WARN above MobileGL's storage capacity (clamped, extra attributes unusable),
PASS in between -- making the driver/host mismatch that caused the out-of-bounds read
visible instead of silently swallowed.
Covered by 7 new regression tests (each verified to fail against the previous behaviour).
- Memoize the program content hash on ProgramObject (keyed by the backend
state version + compile flags; relinking and binding changes invalidate
it) and the vertex-input hash on VertexArrayObject (keyed by a new
aggregate config version bumped by every attribute mutation). Full-SPIRV
XXH64 hashing fell from 13.7% to 1.4% of the render thread.
- ProgramObject also gains a link version and a global-UBO content version
(bumped by uniform writes and on relink, wrap-safe around the backends'
"never uploaded" sentinel) for backends to gate uploads and link caches.
- Reuse member scratch vectors in SetupDraw, UploadAndBindVertexBuffers,
GetOrCreatePipeline and BindProgramUniformBuffers instead of allocating
per draw (~12% of render-thread time was in the allocator).
- Replace hot-path dynamic_cast with AsMipmapTexture (storage-type tag +
static_cast); TextureObjectMipmap is the only Mipmap-tagged branch.
- Register/prune texture aliases only when a new (texture, lifetimeId)
identity appears instead of scanning the entire alive map on every
sampled-texture sync.
- Make the fallback VkPresentModeKHR log strings report the actual mode.
Vanilla render-thread share of libMobileGL dropped from 48% to 35% on
DirectVulkan (simpleperf, Adreno 830).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>