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 bug: DirectVulkan.cpp::MultiDrawElements had its entire body
commented out - plain glMultiDrawElements on Magma recorded NOTHING,
no error, no pixels (readback shows the deferred clear never even
materialized). It now shares the tuned base-vertex implementation, and
both plain entries are pixel-proven by a 4-sub-draw harness.
The feature: every CPU-side multi-draw form dispatches through three
tiers after round-9's contiguous-run merge (restructured to merge into
a span BEFORE dispatch, so every tier consumes the shrunken array):
1. VK_EXT_multi_draw: one vkCmdDrawMulti(Indexed)EXT, chunked by
maxMultiDrawCount; per-draw vertexOffset rides in the struct. The
extension is requested only when enumerated and its feature bit
confirmed, entry points via vkGetDeviceProcAddr, demoted if
missing.
2. multiDrawIndirect: the param span uploads DIRECTLY as a transient
INDIRECT-usage buffer - DrawIndexedCmdParam is layout-identical
to VkDrawIndexedIndirectCommand and DrawCmdParam's head is a
legal 24-byte-stride VkDrawIndirectCommand, both static_asserted,
so no repacking - then one vkCmdDraw(Indexed)Indirect per
maxDrawIndirectCount chunk. firstInstance!=0 additionally
requires drawIndirectFirstInstance or the batch drops a tier.
3. The byte-identical unroll.
gl_DrawID: tiers 1-2 are spec-correct (0,1,2,3 across a probe's
sub-draws); the unroll tier keeps the pre-existing always-0 contract.
The default tiers strictly improve DrawID correctness.
Adversarially verified: the five real DirectVulkan retrace images are
BIT-IDENTICAL (md5) across auto/ext/indirect/unroll; zero validation
VUIDs on every tier; a simulated no-EXT device resolves to indirect
and renders the same bytes; the known-red create-indirect fixture
crashes at the identical call before and after (not worse, not fixed).
Unit suite 423/423 on the rebased tree, retrace subset 10/10. Bench:
mc_sodium_multidraw's contiguous shape merges 32->1 before dispatch,
so no bench delta - the tiers' beneficiaries are non-contiguous real
streams (the sodium RETRACE pushes ~58-sub-draw batches, in=out
243101 with zero merges) and mobile drivers. A reproducible +3-4%
code-layout drift on mc_use_program (zero shared code, I-cache
displacement from +400 lines) stays under the action gate and is
booked here rather than hidden.
Two per-draw costs from the round-10 profiles. A per-program sampled-set
epoch inside UniformManager skips the per-binding descriptor proof walk
when no texture or sampler API ran since that program's previous draw -
the mc_sampler_churn/mc_tex_param pattern. The pass-switch path stops
re-deriving render-pass state that its own value hash already pins.
Load-gated 6-round order-alternating A/B (medians): magma tex_param
-13.3%, pass_switch -10.9%, state_toggle -5.4%; espryt untouched and
unmoved. The two matrix flags (sodium +7.5%, tex_stream +6.2%) reversed
under 10-pair isolated alternating re-runs (-5.5% and +2.5%) - the same
position-bias artifact every previous round's flags showed. Unit tests
421/421; retrace subset and the 52-entry integration suite pass.
Landing note: this diff was authored by a round-10 agent whose session
died before adjudication; the A/B data survived (r10bmag_ab_raw.csv)
and the flags were adjudicated before landing. Its relink also exposed
the pre-existing exit-teardown SIGSEGV fixed in the previous commit.
Two correctness holes from the round-7/8 fast-path work, found by
bisecting the retrace matrix after corruption reports on device.
Cross-frame slice trust: the vertex-binding and EBO memos skipped the
acquire - the frame's content-sync point - whenever their recorded
slice epochs still matched, trusting the BumpSliceEpoch inventory to
cover every way a buffer's GPU copy can go stale. At least one mutation
path escapes it: journeymap and common-mods retraces shipped visibly
corrupted, and Sodium on an Adreno device rendered random triangles
from stale vertex data. A memo recorded in an earlier frame now
declines, so the first draw of each (VAO, frame) re-runs the full
acquire; the same-frame paths (layout memo, factory-chase elimination,
one-compare rescue) are untouched. The cross-frame idea can return once
the bump-site inventory is proven complete against exactly these traces.
Transform-flags memo key: GetShaderTransformFlags reads the swapchain
pre-transform AND whether the bound draw framebuffer is the default one
- only a presenting pass gets the Y-flip/rotation bits. The memo
declared it pure in the pre-transform, so after any render-to-texture
pass the next default-framebuffer pass inherited the FBO's unflipped
flags: 1.17-main-menu retraced as a perfectly rendered, perfectly
upside-down frame (SSIM 0.052, deterministic), and cloud passes
flickered on device. The memo now keys on (preTransform, isDefaultFbo).
DirectVulkan retraces for 1.17-main-menu, journeymap, common-mods,
sodium and xaero-world-map all pass on lavapipe; unit tests 421/421.
61% of mc_sodium_multidraw's steady-state CPU sat inside the driver
encoding one vkCmdDrawIndexed per sub-draw. MultiDrawElements now
collapses contiguous runs: merge only when the topology is a list
(POINTS/LINES/TRIANGLES), the accumulated count sits on a primitive
boundary, primitive restart is off, baseVertex/instanceCount/
firstInstance are identical and firstIndex is adjacent, with a
count-overflow guard - the bench's 132x32 sub-draws become 132x1.
Dangling-index discard semantics for list topologies are what the GL
spec already mandates per draw. No new Vulkan feature, so no DriverPost
gate; VK_EXT_multi_draw stays a gated follow-up.
The draw fast path's single SetupDraw snapshot died on every program
ping-pong (use_program's A/B pattern sent every other draw down the
full path, CollectSampledTextures alone 6.2% self). A 4-entry
program-keyed snapshot table (MRU by program lifetime id, per-entry
sampled-set copies, per-entry invalidation on decline or full-path
start, all entries still cleared at command-buffer boundary, pipeline
age-out and swapchain recreate) keeps all cycling programs hot.
Load-gated 6-round order-alternating A/B, sha1-fingerprinted pair:
sodium_multidraw -41.3%, use_program -23.9%, pass_switch -12.7%,
tex_param -3.8%, vanilla -2.1%; the one flag (tex_stream +5.4%)
reversed to -0.5% across 10 isolated alternating pairs. Espryt
untouched and unmoved. Unit tests 421/421.
Consume MipmapStorage's new dirty-rect list: pack each rect tightly
into the staging block and issue ONE vkCmdCopyBufferToImage with N
regions instead of staging the whole union box. Offsets are computed
identically in the pack and copy loops; disjoint rects mean no
overlapping copy destinations; the combined depth-stencil and
RGB-expand/depth-convert paths keep their single-box route (gated to
the color-aspect, no-conversion case).
54% of mc_tex_stream's steady-state CPU was the one shadow->staging
memmove of the union box; staged bytes drop to 4.8% (~2MB -> ~95KB per
frame) and the case improves ~-49% (5945 -> 3048 ns/op, ~2.2x native
to ~1.2x). Zero validation-layer findings on the 95-region copy. Unit
tests 421/421.