- M2's defer path destroys a wire store MID-FRAME (the serial-0 park, the sweep's prefix walk, the watermark); before it a wire store only died at the boundary that also clears UniformManager's memos (BeginFrame from Present or a drain). Those memos are keyed on the VkBuffer HANDLE - m_descriptorReuseMemo hashes the VkDescriptorBufferInfo words, FastRebindMemo compares uboBuffer - and on the wire arm the direct-bind UBO and the SSBO descriptor name the store's own handle. A store destroyed at a park, its handle value re-minted by the next Create (a heap pointer under lavapipe, LIFO out of glibc's tcache), and a draw resolving the same (handle, range) reused a set whose descriptor still pointed at the dead store's memory: silent wrong bytes, no Fatal (ID-P7-43, the fable review's must-fix).
- VkBufferManager gains m_wireStoreDestroyEpoch, bumped on every path that destroys a wire store's VkBuffer (DeferWireRelease's immediate destroy, SweepDeferredWireReleases once per sweep that freed anything, DestroyAllDeferredWireReleases, Shutdown's record teardown) and never reset, with GetWireStoreDestroyEpoch(). UniformManager mixes it into the descriptor-reuse signature and records it in FastRebindMemo, refusing the fast rebind when it moved; all under MOBILEGL_BUILD_DISAGGREGATED. Deliberately not m_sliceEpochCounter, which every WriteWireBuffer bumps and would defeat the memo on every glBufferSubData. On the wire arm only the signature is reached (SetupWireDraw passes no sampler hint), the FastRebindMemo field is the defensive half.
- Two comments from the same review: SweepDeferredWireReleases no longer claims GetSyncPointSubmitIndex() never decreases - it steps back by one across an abandoned recording (RecreateSwapchain, the minimized Present), and the prefix walk is conservative across that, not wrong; and the header's "floor is not a proof" keeps its decision on its first reason alone, the second was a stale citation of a fix that is on the integration tree.
- Section 9 partition exception for UniformManager.{h,cpp} granted by the integrator for this fix.
Rule I admits two shapes for a refusal on the wire arm, a decline and a named Fatal. The draw
path had a third one the rule does not allow: roughly forty bare `return false` statements with
no log, no counter and no census entry, so a dropped draw left no trace anywhere - which is how
the OpenRA device divergence survived four device windows with client and server logs that were
line-identical between a passing and a failing run.
WireDeclines.def names every one of them; WireDeclineTally.h expands that into a counter table
plus MGL_WIRE_DECLINE_AT (the tally always moves, the log is once per site), in the shape of
PipeApplier.h's ServerVerbSink tallies and for the same reason (R-16: a probe may not arm
against a stub). Covered: SetupWireDraw and DispatchWireCompute (25), the three exits of
PrepareWireTextureResources, the four of SyncTextureResourceByHandle, and UploadPendingWireLevels'
five false exits PLUS the two that only `continue` - those report SUCCESS while leaving the entry
pending, i.e. a draw that samples a level which was never uploaded, so they are counted apart.
Also the pending-upload gauges (entries, bytes, peak bytes), sampled at the funnel, because a
refused upload does not consume its entry and the level shadow behind it stays alive.
Shutdown dumps the tally and is silent when it is all zero: a green lane stays quiet and a lane
that dropped a draw cannot. Plus the drain probe behind MOBILEGL_MAGMA_WIREBUF_PROBE, which is
what measured that a host replay reaches the mid-frame frame-boundary drain zero times.
notes/p7/magma-a.md: the per-site table (retired / declined / kept-fatal with the
reason), every red-once with its captured output, the gate readings, and what is
handed on.
The three places package A's own prose still named `uniform-buffer-byte-tail@P7`
lose the marker. They are comments, not refusal sites, and leaving the marker in
would have made a tree-wide `grep @P7` count them against packages B and C. The
census now reads 15 -> 7, and all seven are real: WireFramebuffer.inc x4 and
VulkanRenderer.cpp x1 (package B), WireDraw.inc x2 (package C).
Three findings worth the integrator's attention, all in the note:
1. Most of this cluster was unreachable, and that is provable rather than
assumed. The frontend validates buffer-range offsets against the very caps the
backend then re-checks - GL_{UNIFORM,SHADER_STORAGE,TEXTURE}_BUFFER_OFFSET_ALIGNMENT
and GL_MAX_UNIFORM_BLOCK_SIZE are all published from the same Vulkan limits -
so five of the eight sites could only ever have fired on a malformed peer. The
two that CAN fire are `uniform-buffer-byte-tail` (GL constrains a range's
offset but not its size) and `unaligned-atomic-buffer-range` (GL gives
GL_ATOMIC_COUNTER_BUFFER no queryable alignment, so the frontend only enforces
offset % 4 while the backend needs minStorageBufferOffsetAlignment).
2. fatal_census does not move for any of this, and no re-baseline is due. Wave 0
already routed these funnels through MGPipeSessionFailHook, so the abort lives
in FatalFunnel.cpp and retiring a WireDescriptorFatal call site changes no
census number: 79 abort sites / 43 family words, unchanged, RC=0. The brief
expected the count to fall because the sites were bare aborts before wave 0.
3. DirectVulkan.Split.Full.IterationRPProgram203Scenario.FixedCompleteInputProducesFixedCompleteGoldenOutput
is red on this worktree's build-split and is NOT ours: it reproduces 3/3 with
package A's backend diff fully reverted, and the mismatch bits are identical
to the S1 exposure texel in magma-two-process-first-run.md. The contract books
that one against the tcp arm, and ~/w7/pipe's own build-split passes it, so
whether it is a split-arm regression between 460a3a72 and 610fa281 or a
configure-dependent S1 needs the integrator.
Gates on the finished branch: integration-magma-split 75/75, -spawn 54/54, -tcp
56/56, -buffers 21/21, full-split 512 entries 454 PASS / 57 SKIP / 1 red (the
pre-existing one above; base was 453/57), ctest -L unit 2408/2408,
spawn_lane_parity RC=0, link_ratchet unchanged at 186, fatal_census RC=0, and G1
pull .text = a52203 with nm --defined-only identical to p7-before/pull-syms.txt
(added=0 removed=0).
`texel-buffer-native-format@P7` becomes a decline (rule I (a)), and with it the
last refusal site in UniformManager.cpp is gone. GL's buffer-texture format list
is wider than what Vulkan mandates for texel buffers - GL_RGB32{F,I,UI} and the
16-bit UNORM members are the realistic misses on a real driver - so this is a
device capability gap, not anything the application did wrong and nothing it
could have sent differently.
The placeholder is the right answer here where it was not for the unaligned range
in 7c373d60, and the difference is whether a correct answer exists at all. There
the bytes are addressable and only the descriptor cannot name them, so a
placeholder would have hidden a fixable gap behind silently dropped writes. Here
the device cannot represent this format as a texel buffer in any form, so the
honest observable is monolith's for a missing view: the fetch reads zeros and the
draw survives. AcquireUnboundTexelBufferView already falls back to the R32 member
of the shader's numeric class, whose texel-buffer support is mandatory, so the
fallback cannot itself fail for want of device features.
red-once (R-16). lavapipe supports every format the buffer-texture cases use, so
the site is not reachable on the host - the red is the same case with the device
support check forced to fail, both shapes built and run back to back:
DirectVulkan.{Split,Spawn}.Full.BufferTextureScenario.VertexStageTexelFetchReadsTheBufferAndTracksItsUpdates
unforced 2/2 Passed (the format IS supported here)
before (7c373d60 + forced) Subprocess aborted -
Fatal{UnmigratedVerb, "Magma:texel-buffer-native-format@P7"}
after (this commit + forced) ***Failed on the texel data and NOT on a Fatal:
the fetch read the zero placeholder, the session lived
restored 2/2 Passed, no force markers left
Which format actually trips this is a device question, not a host one. Adreno is
where GL_RGB32F buffer textures are expected to miss, so the decline's real
observable wants a device pass; notes/p7/magma-a.md lists it.
Lanes: integration-magma-split 75/75, -spawn 54/54, -tcp 56/56,
integration-magma-buffers 21/21. `grep -n '@P7'` on this file now returns one
line, and it is the comment that records the A.1 retirement - no refusal sites
remain.
`unaligned-atomic-buffer-range@P7`, `unaligned-storage-buffer-range@P7` and
`unaligned-texel-buffer-range@P7` become named declines (rule I (a)). The two
offset-overflow guards next to them keep their named Fatal under their own words:
those are arithmetic that cannot happen, not shapes an application can bind.
The atomic-counter one is the only member of this cluster an application can
actually reach, and it is a real live gap, not a hypothetical. GL 4.6 core 6.1.1
gives GL_ATOMIC_COUNTER_BUFFER no queryable offset alignment, so
ValidateBufferRangeOffsetAndSize only enforces offset % 4 on it - while glslang
lowers every atomic_uint onto a synthesized storage block whose descriptor offset
must be a multiple of minStorageBufferOffsetAlignment (16 on lavapipe, 64 on
Adreno). `glBindBufferRange(GL_ATOMIC_COUNTER_BUFFER, 0, buf, 4, 4)` is a legal
GL bind that this backend cannot express, and it used to take the session with
it. The plain SSBO and texel halves are unreachable by comparison:
GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT and GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT
are published from the same Vulkan limits, so the frontend already answers
GL_INVALID_VALUE.
DECLINED RATHER THAN COPIED, and this is the package's one real design choice.
Copying the window into an aligned transient slice reads correctly and then
silently DROPS every shader write. This site cannot tell a writable storage block
from a read-only one - there is no readonly bit in the storage-block reflection,
an atomic counter is written by definition, and MarkWireBufferGpuWritten right
below already assumes any binding here may be written. A correct copy needs a
post-dispatch copy-back from the transient slice to the unaligned source, ordered
with a barrier, and the place to record that is the draw/dispatch tail in
VulkanRenderer.cpp / WireDraw.inc - wave-2 packages B and C, not this file.
Returning stale counters with no error is worse than losing the dispatch with a
named line in the role log. notes/p7/magma-a.md carries the recipe for whoever
holds those files.
The texel half keeps the draw where it safely can: a read-only buffer texture
falls back to the same placeholder view an unbound one gets (zeros, which is what
a missing view gives in monolith), while a writable imageBuffer declines - the
placeholder is shared by every unbound binding, so absorbing real stores into it
would alias other bindings as well as lose the writes. Copying is not available
there either: the transient arena carries no {UNIFORM,STORAGE}_TEXEL_BUFFER
usage, so no VkBufferView can be made over it, and widening a shared per-frame
allocation to buy an unreachable path is the wrong trade.
red-once (R-16), a reachable case this time, all three Magma two-process arms:
DirectVulkan.{Split,Spawn,Tcp}.Buffers.F1WireScenario.UnalignedAtomicCounterRangeDeclinesAndTheSessionLives
before Subprocess aborted - Fatal{UnmigratedVerb, "Magma:unaligned-atomic-buffer-range@P7"}
after 3/3 Passed, and the role log carries
"block 'gl_AtomicCounterBlock_0' is bound at offset 4, which is not a
multiple of this device's minStorageBufferOffsetAlignment (16);
atomic-counter ranges cannot be expressed as a descriptor ... DECLINED
(unaligned-atomic-buffer-range)"
The case pins the decline's whole observable: the bind raises no GL error, the
dispatch does not run, every byte of the counter buffer is unchanged, and the
context still serves ordinary work afterwards. A future copy-back turns one
EXPECT into seed[1] + 1 and leaves the rest standing.
Lanes: integration-magma-split 75/75, -spawn 54/54, -tcp 56/56,
integration-magma-buffers 21/21.
`uniform-block-native-range@P7` and `storage-buffer-native-range@P7` both become
clamps with an MGLOG_E_ONCE (rule I (a)). Neither is reachable from a conformant
program: GL_MAX_UNIFORM_BLOCK_SIZE is published straight from the same
maxUniformBufferRange and GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT /
GL_MAX_SHADER_STORAGE_BLOCK_SIZE from the same storage limits
(BackendLoaders/Vulkan/Loader.cpp), so a block this big fails to link long before
a draw. Refusing was a Fatal guarding a shape nothing can produce.
The monolith arm is the argument. It binds whatever the GL range asked for and
never consults maxStorageBufferRange at all (ResolveStorageBufferDescriptor's
resident path), so the wire arm was inventing a death the other backend does not
have. If the two limits ever disagree, the honest answer is the bytes the device
CAN name: everything past the clamp is outside the descriptor and reads back as
zero under robustBufferAccess, which is inside GL's "undefined" for a block the
implementation never promised to hold. Clamping keeps the range constant across
draws, so the descriptor-set reuse hash is unaffected.
red-once (R-16). Neither site is reachable from a test, so the red is the same
two cases with the device limit forced down to 16 bytes, both shapes built and
run back to back on the Split and Spawn arms:
uniform-block-native-range (reflected block 32, limit forced to 16)
before Subprocess aborted - Fatal{UnmigratedVerb, "Magma:uniform-block-native-range@P7"}
after BOTH ARMS PASSED. The clamp keeps every byte the descriptor can name,
so the case's own pixel is unchanged, and the role log carries
"reflected uniform block 0 is 32 bytes, past this device's
maxUniformBufferRange of 16; the binding is clamped to the limit
and the remainder reads zero"
storage-buffer-native-range (block 'Output' 24 bytes, limit forced to 16)
before Subprocess aborted - Fatal{UnmigratedVerb, "Magma:storage-buffer-native-range@P7"}
after ***Failed on the data and NOT on a Fatal - the session lives, 8 of the
24 bytes are genuinely past what the forced limit can name, and the
role log carries "block 'Output' bound 24 bytes, past this device's
maxStorageBufferRange of 16; the binding is clamped to the limit"
restored (this commit as committed): 4/4 green, no force markers left.
Lanes: integration-magma-split 74/74, -spawn 53/53, -tcp 55/55,
integration-magma-buffers 20/20.
`uniform-buffer-dynamic-offset@P7` becomes a decline (rule I (a)). A Vulkan
dynamic offset is a uint32_t, so a transient uniform ring that grew past 4 GiB
inside one frame has nowhere to put the padded block - which is our ring's
problem, not a protocol fault, and nothing the peer could have sent differently.
Killing the session for it was the wrong shape twice over.
The monolith arm has the same ceiling and truncates silently on the way out
(ResolveDynamicUboDescriptor's static_cast<Uint32>), i.e. it binds a WRONG window
rather than none. Declining is the strictly better observable of the two: one
binding is skipped, the draw is lost, the session keeps running, and the next
frame's rewound ring resolves it.
red-once (R-16). The site is not reachable from a test - a 4 GiB transient arena
inside one frame is not something an integration case can build - so the red is
the same case with the ceiling condition forced true, both shapes built and run
back to back on the Split and Spawn arms:
before (fb25ed6b + forced condition)
DirectVulkan.Split.Buffers.F1WireScenario.ShortUniformBufferRangePadsMissingBytes
DirectVulkan.Spawn.Buffers.F1WireScenario.ShortUniformBufferRangePadsMissingBytes
Subprocess aborted - MGPipe: Fatal{UnmigratedVerb, "Magma:uniform-buffer-dynamic-offset@P7"}
after (this commit + forced condition)
both ***Failed on the pixel, NO Fatal and NO abort, and the role log carries
ResolveWireUniformBufferPayload: the transient uniform ring reached offset 0,
past the 4 GiB a Vulkan dynamic offset can name; uniform binding point 3 is
declined for this draw
restored (this commit as committed): both green, no force markers left.
Lanes: integration-magma-split 74/74, -spawn 53/53, integration-magma-buffers
20/20. -tcp not read this round: port 40613 is held by a concurrently running
lane in another worktree, which is a fixture collision and not a result.
`uniform-buffer-byte-tail@P7` is retired. It was the live boundary the Redmi
Minecraft run reported (notes/p5f/magma-inproc-fix.md #5) and, since wave 0, a
named Fatal that took the session with it.
Only the SIZE of a uniform range can land off a word. glBindBufferRange already
forces the offset onto GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT and records
GL_INVALID_VALUE otherwise (GL_Buffer.cpp ValidateBufferRangeOffsetAndSize), and
every device reports that as a multiple of four - but GL 4.6 core 6.1.1 puts no
such rule on size, so a 19-byte bind is legal and 19 % 4 is not 0. The sub-word
start is only reachable protocol-side; the new arm handles it anyway rather than
leaving a second shape to trip over later.
CopyWireBufferSubWordRangeToSlice rounds the read of the APPLICATION's store OUT
to whole words, clamped to the store's end so the widened window can never touch
a byte the application does not own, and lands it in a transient staging slice.
Only the staging -> padded-block shift is sub-word, and that happens inside our
own arena; vkCmdCopyBuffer places no alignment rule on a region's offsets or size
(unlike vkCmdUpdateBuffer / vkCmdFillBuffer), so the shift is a plain legal copy.
The block still starts at the padded slice's own offset, which is what lets the
descriptor keep a minUniformBufferOffsetAlignment-aligned dynamic offset.
red-once (R-16), all three Magma two-process arms, before the backend half:
DirectVulkan.Split.Buffers.F1WireScenario.SubWordUniformBufferRangePadsMissingBytes
DirectVulkan.Spawn.Buffers.F1WireScenario.SubWordUniformBufferRangePadsMissingBytes
DirectVulkan.Tcp.Buffers.F1WireScenario.SubWordUniformBufferRangePadsMissingBytes
Subprocess aborted - MGPipe: Fatal{UnmigratedVerb, "Magma:uniform-buffer-byte-tail@P7"}
and green after it. The case is the sibling of
ShortUniformBufferRangePadsMissingBytes with a 19-byte range; the byte just past
the range is poisoned, so a rounding that leaks it reads tail.x as 0.5f and the
red channel comes back 127 instead of 0.
Lanes: integration-magma-split 74 (was 73), -spawn 53 (52), -tcp 55 (54),
integration-magma-buffers 20 (19), all green. fatal_census unchanged at 79 abort
sites / 43 family words - these refusals stopped being bare aborts in wave 0, so
a retirement moves no census number and needs no re-baseline. link_ratchet
unchanged at 186.
Two corrections to the two commits before this one.
The widening's docstring was written against the 83 sites it found, and the commit that
routed five of them through Session::Fail did not revisit the prose. It now describes the
79-site floor the baseline actually records, says which five went and where, and states that
the enforceable artefact is the baseline's per-file map rather than the counts in the prose -
so a reader who finds the two disagreeing knows which one to believe. The three buckets are
restated too: `(ii)` is no longer "owned by a Ph / P7 row", because with the Magma wire arms
routed what is left there is the peer-reachable deaths in the applier and the two backends,
which is exactly the set ID-P7-1 scopes to Ph's fuzz arm 2 AFTER P7.
And the routing commit's own comments spelled the at-sign refusal marker while explaining
what routes through them, which put SEVEN comment lines into the retirement census: `grep
@P7` went from 15 sites in 4 files to 22 in 6, and that grep is how wave 2 will measure which
refusals it has retired. The prose says the word instead, and the count is 15/4 again.
Five deaths in the split server image raised their own std::abort(). Three of them are
funnels - MagmaWireFatal (Renderer/WireFramebuffer.inc), WireDescriptorFatal
(Renderer/UniformManager.cpp) and WireBufferLegacyFatal (Renderer/WireDraw.inc) - and the
fifteen `@P7` refusals on the tree (8 in UniformManager.cpp, 4 in WireFramebuffer.inc, 2 in
WireDraw.inc, 1 in VulkanRenderer.cpp; plan section 1.3's count confirmed) all die through
two of them. Every one of those deaths published NO SessionFault frame, so the peer read a
bare EOF; bumped NO SessionFaultCount(), so exit gate S8 could neither confirm nor falsify
it; and carried a hand-typed family word with no path to FatalFamilies.def's projection.
MG_Backend may not name an MG_Remote symbol on its own account, so the route is a seam in
MG_Pipe with MGP_TRIP_WIRE_REPORT's shape (PipeApply.cpp:66, and the new code sits beside
it): MG_Pipe/PipeSessionFail.h declares MGPipeSessionFail, PipeApply.cpp defines it inside
`#if MOBILEGL_BUILD_DISAGGREGATED`, the DEFAULT behaviour is byte-identical to what the
three funnels did on their own, and MG_Remote::InstallPipeSessionFailHook() points it at
SessionFail. The install is in InitServerRoleCommon (MG_Backend/Init.cpp), step 0, because
that is the one function BOTH server roles run - the inproc role and the spawn/TCP session
child - so there is no per-transport install to get wrong.
MGPipeFatalFamily is a two-value boundary enum, NOT MGFatalFamily cast to an integer: the
.def's enumerators are positional and a row inserted in its middle would silently re-point
every hook call at a different family. FatalFunnel.cpp's adapter is the one place the two
vocabularies meet, as a switch with a named default.
The family word stays UnmigratedVerb rather than becoming a new MagmaWireRefusal. Three
reasons: the message strings are required to stay byte-identical (5.2), and the retrace
refusal censuses recorded since P5b count these deaths by that word - a rename invalidates
every `@P7` measurement taken against them; `Magma:` in the detail already separates the
Magma arm from the GLES UnmigratedVerb deaths, so a new word adds no discrimination a grep
does not have; and its wire projection would be ProtocolCorruption, exactly UnmigratedVerb's.
Recorded for the integrator.
Ph slice (2) in the same commit: Server/StagedShadow.h's RequireCovered and
Server/StagedTextureStore.h's RequireLevelBytes were MGLOG_F + bare abort. Both are inside
MG_Remote already, so they call SessionFail directly, message verbatim;
check_include_closure.py's four probes do not reach either header and the gate is green.
Census: 83 abort sites -> 79 over 20 files (five sites retired, one gained for the seam's
no-hook default, which is argued into FUNNEL_SITES), baseline rewritten in this commit - the
downward ratchet's notice is what asked for it. Two gate bugs the widening exposed and this
commit fixes: the SessionFail call regex also matched `MGPipeSessionFail(` (a lookbehind
now), and a funnel's anchor needed a wider window than a marker's (kFunnelAnchorWindow).
Red-once, three ways. Pointing MagmaWireFatal back at its own abort reds the census naming
WireFramebuffer.inc 0 -> 1. Making the seam skip the hook reds both new death cases -
`TheMagmaWireVerbDeathReachesSessionFailAndBumpsTheFaultCount` on the exit code, because it
reads SessionFaultCount() out of a SIGABRT handler and gets 0 instead of 1, and
`TheSeamHandsTheInstalledHookItsFamilyAndItsLineVerbatim` on the line the hook echoed.