Commit Graph
462 Commits
Author SHA1 Message Date
swung0x48 15dbe78ce6 [Fix] (DirectVulkan): give each served session its own renderer and Magma state 2026-10-02 05:52:38 -04:00
swung0x48 fc415aee76 [Fix] (Magma, P8-SV): the wire arm's 3D native mip blit uses one layer with the depth in its z offsets, the depth/stencil mip decline gets its WireDeclineTally row (MipmapDepthStencilAspect), the depth shader mip refuses 1D/1D-array/3D view types before the chain grows, and glDispatchComputeIndirect is issued as vkCmdDispatchIndirect from the wire store (P8-D's barrier rule, wdsp counter, two named declines) 2026-09-30 03:44:03 -04:00
swung0x48 d639e54616 [Perf] (P8-D, Magma): the wire arm issues glDraw*Indirect / glMultiDraw*Indirect[Count] as vkCmdDraw*Indirect[Count] from the wire store instead of expanding them on the CPU behind a GPU-idle wait (create-indirect split: 321 waits / ~8 s -> 0), with an INDIRECT_COMMAND_READ barrier once per shader write; windr[] PipeStats counters, WireIndirectDrawScenario on both backends x every arm, notes/p8/D.md 2026-09-29 22:32:49 -04:00
swung0x48 7cde55b1c1 [Feat] (Magma, P11 B2): import a T0 store through VK_ANDROID_external_memory_android_hardware_buffer as a wire buffer (external VkBufferObject), serial-gated release, done-no-bytes readback, and the sustained-lock POST on the renderer's device 2026-09-29 17:50:50 -04:00
swung0x48 c371599811 [Merge] (P11): dev@c14283ee - scattered sub-image writes upload exactly their texels, Magma submits a frame's recording every 16384 draws with backpressure, and Magma grows descriptor pools geometrically and trims a quiet slot whole; the handle arm notes driver-side writes by handle and the wire arm splits recordings and keeps the trim's epoch peak across descriptor rewinds 2026-09-29 12:11:37 -04:00
swung0x48 d20c2d01ae [Fix] (Magma): a frame's recording is submitted every 16384 draws/dispatches and continues on a fresh command buffer, with at most frames-in-flight + 1 submissions outstanding - rd12's 981k-draw loading frame no longer grows one command buffer until Adreno's 16 KiB driver mappings exhaust vm.max_map_count (MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER, 0 = unbounded) 2026-09-29 09:44:58 -04:00
swung0x48 e0102bad8c [Merge] (P11): dev@c60639db - the Espryt image-bindable re-mint reads GPU levels back on the monolith arm too, partial writes into converted or reshaped levels upload only their region on every arm, Magma orders an upload after the recording's writes and queued clears, and Magma's eglSwapInterval reaches the swapchain 2026-09-29 06:15:56 -04:00
swung0x48 c60639db1d [Fix] (Magma): eglSwapInterval reaches the swapchain - 0 picks IMMEDIATE, else MAILBOX, else FIFO; >= 1 FIFO; < 0 FIFO_RELAXED, else FIFO; rebuilt after the next present when the mode changes (MC 26.3 vsync on: 190-290 fps -> 120) 2026-09-29 05:51:18 -04:00
swung0x48 b1d60523e4 [Fix] (Magma): an upload into an image the open recording already wrote, or that still has a queued clear, is submitted after that work - a glTexSubImage after a clear or a draw into the level no longer ends up under it 2026-09-29 04:28:55 -04:00
swung0x48 74636c8432 [MG_Backend, MG_Test] (P7): the wire arm's shader colour blit samples an unscaled blit through its nearest sampler, so a 1:1 GL_LINEAR glBlitFramebuffer into the default framebuffer is an exact copy 2026-09-23 18:59:20 -04:00
swung0x48 9eb798a60e [MG_Backend, MG_Test, MG_IntegrationTest, scripts] (P7): a private image placeholder is keyed by (shape, unit) and capped, and the depth-resolve identity is built by a tested helper 2026-09-23 17:45:57 -04:00
swung0x48 e1b47dc2f6 [MG_Backend, MG_Test] (P7): the depth/stencil resolve probe's verdict is memoized per device identity (vendor, device, driver version, pipeline-cache UUID), not once per process 2026-09-23 17:04:13 -04:00
swung0x48 a7848f0a03 [MG_Backend, MG_IntegrationTest, scripts] (P7): an invalid or empty image unit on the wire arm binds a null storage descriptor (VK_EXT_robustness2 nullDescriptor), else a placeholder private to its (binding, unit) 2026-09-23 17:04:13 -04:00
swung0x48 431bda67dc [MG_Backend, MG_IntegrationTest] (P7): a real-probe negative control proves the depth-resolve measurement can see a broken render-pass arm on the host 2026-09-23 15:20:17 -04:00
swung0x48 e83581c3c4 [MG_Backend, MG_Test, MG_IntegrationTest, scripts] (P7): choose the multisample depth/stencil resolve arm by a device probe with a shader-arm control instead of the Qualcomm vendor id 2026-09-23 15:13:44 -04:00
swung0x48 23be55e477 [MG_Backend, MG_IntegrationTest, scripts] (P7): pin the Qualcomm depth/stencil resolve arm order on the split and spawn arms 2026-09-23 15:13:44 -04:00
swung0x48 53397e6e1e [MG_Backend, MG_Test] (P7): resolve multisample depth/stencil with the shader arm first on Qualcomm 2026-09-23 15:13:44 -04:00
swung0x48 3ef5c61e96 [MG_Backend, MG_Test] (P7): a PH-5 unit control that discriminates the element charge, @Ph-declined on PH-2's eight Fatal sites, and the mip-shadow comment made true 2026-09-23 13:44:37 -04:00
swung0x48 caaedd7abe [MG_Remote, MG_Pipe, MG_Backend, MG_Test] (P7): repair F2 ID-49, byteless-level and R8 lane regressions 2026-09-23 13:44:37 -04:00
swung0x48 b19297d5e5 [MG_Backend, MG_Test] (P7): prove aggregate waits with a single-fence control 2026-09-23 03:53:37 -04:00
swung0x48 a4c1bc8b1e [MG_Backend, MG_Remote] (P7): draft B4 aggregate waits and staged extent reset 2026-09-23 03:53:37 -04:00
swung0x48 3ce5b9913e [docs, MG_Backend] (P7): record W6 device evidence and land M3 on review 2026-09-23 01:59:32 -04:00
swung0x48 9fd768d493 [MG_Backend, MG_IntegrationTest] (P7): bound wire descriptor sets within long frames 2026-09-23 01:35:20 -04:00
swung0x48 cba0c3a110 [MG_Backend, MG_IntegrationTest] (P7): the busy predicate's comments name the floor as the guarantee and the submission term as a probe, and the term is consulted and re-stamped wherever the serial is
Review ID-P7-34, item 3. 02bb5789 presented the submission term as the fix
and still carried the every-8th-drain mechanism that magma-b3.md §2.4
refutes (the probe saw one successful drain and zero boundary works across
the OpenRA replay). The term is not sound alone: lastUseSubmitIndex is
stamped at AcquireWireSlice, but a mid-draw flush (BindProgramUniformBuffers
-> SyncWireTextureShape's preserve path -> FlushWirePendingCommandsFor-
TextureUpdate) can submit the stamped index without the draw, so the stamp
names S1 while the draw rides S2. The guarantee is the floor in
OnSubmitsCompletedUpTo; the term is a defence and a device probe.

- WriteWireBuffer's block comment, the WireBufferResource field comment,
  the AcquireWireSlice stamp comment, the StreamedBufferSubData CMake
  comment and the F1WireScenario case comment are rewritten on that
  footing; the drain story is gone. The drain probe's comment now says it
  refuted the drain as the mechanism.
- WaitForWireBufferHostAccess's early return also requires the submission
  term complete, so a caller with the serial term idle cannot skip a wait
  the term demands.
- ReadWireBuffer re-stamps lastUseSubmitIndex beside lastUseSerial after
  its wait, preserving the pending draw's reservation in both halves.

All disaggregated-only code. OpenRA DirectVulkan SPLIT / SPAWN retraces
ssim 1.000000, 0 px, zero MGWIRE-FLOOR lines; integration-magma-split 90/90,
-spawn 69/69, -tcp 70/70.
2026-09-22 20:25:11 -04:00
swung0x48 511c1f1641 [MG_Backend] (P7): the completed-frame-serial floor only advances to a serial nothing in flight still carries
THE OpenRA DEVICE DIVERGENCE. "Frame-serial completion piggybacks on submission completion"
holds only while a frame serial has ONE submission. On the wire arm it has two, measured on this
trace: 73 submissions over 32 serials on inproc and on spawn, 30 over 32 on monolith.

S1 is the per-frame palette glTexImage2D, which reaches UploadPendingWireLevels ->
FlushWirePendingCommandsForTextureUpdate -> FlushPendingCommands at the first palette-sampling
draw and submits under a POOLED fence. S2 is Present, and it carries the frame's draws AND the
barriered vkCmdCopyBuffer that StagedWireRangeCopy records for every streamed glBufferSubData.
RegisterSubmit stamps both with the serial current at submit time, so they share it.

S1 is tiny and signals almost immediately. RefreshCompletedSubmits retires it alone, and
OnSubmitsCompletedUpTo called NotifyFrameSerialComplete per retired record - advancing the floor
to N-1 while S2(N-1), the submission actually carrying frame N-1's buffer copies, was still
executing. GetCompletedSerial then returned N-1, so frame N's first write to that buffer failed
WriteWireBuffer's busy predicate and took the unordered host memcpy at :268 - and S2(N-1)'s
queued copies landed on top of it afterwards.

That is why the wrong pictures are a PREFIX TEAR of one draw's vertex range rather than a missing
draw: 318 vertices / 6 = 53 quads at 11448/53 = 216 bytes each, and 6696 = exactly 31 of them.
6696 and 11448 are both real write sizes of that same buffer (26 and 5 occurrences in this
replay), so the damage is the previous frame's write list replayed over the new bytes, and its
length is the largest write in whatever suffix of that list had not executed yet. It also
explains the device's FRAMESINFLIGHT=4/8 result (6/6 whole-write loss): more frames in flight
puts the GPU further behind, so the pending suffix contains the larger writes.

The floor now advances only to a serial NO remaining in-flight submission still carries.
NotifyFrameSerialComplete is monotone and already refuses the still-recording serial, so this can
only advance the floor later, never further; the disaggregated build's monolith arm is
behaviour-identical (one submission per serial, and its counter reads zero either way). The #else
branch is the pull build's statement verbatim, which is what keeps G1 byte-identical.

The counter is the red-once's reading, per lens C: an advance naming a serial some remaining
submission still carries. Base 26 on inproc, 26 on spawn, 0 on monolith; zero on all three after
this commit. It is logged per event rather than dumped at teardown because this trace's renderer
is never Shutdown() - the process exits - so a teardown dump reports the start-up context's zero.
2026-09-22 18:44:07 -04:00
swung0x48 76bbb6c300 [MG_Backend] (P7): every silent exit on the Magma wire draw path has a name, a tally and a decline line
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.
2026-09-22 18:44:07 -04:00
swung0x48 3c2867d34d [MG_Backend, scripts, docs] (P7): in a disaggregated build the monolith arm blits and mips with the baked modules, and the ratchet's p7-magma bucket falls to 88
CONTRACT-P7 §5.2's (B'), which §4.2 needs.

WHAT MOVES. In a disaggregated build the monolith arm now drives the SAME baked
modules the wire arm does:
  * InitializeBlitResources / InitializeDepthMipmapResources return true - there is
    nothing to build on any transport;
  * TryBlitToDefaultFramebufferWithShader turns its two BlitImageBindings into
    WireImages and calls BlitWireColorToDefault (WireColorBlit.inc);
  * GenerateDepthMipmapWithShader loops GenerateWireDepthMipLevel
    (WireDepthMipmap.inc) and hands the image back in the finalLayout its caller
    promised;
  * GetOrCreateBlitPipeline, the three GLSL strings, the eight kHidden* ids and the
    frontend members of BlitResources / DepthMipmapResources compile out.
The `#else` is the pull build's code, statement for statement.

WHY AN `#if` AND NOT A RUNTIME BRANCH - this is the whole of §4.2. P5f fv already
gave both initializers a non-monolith early return, and the ratchet counted the
symbols anyway, because a reference is a reference whether or not the branch runs.
And the MEMBER has to go with the construction: a SharedPtr member's destructor is
itself a reference to ~ProgramObject.

THE BUCKET TABLE (scripts/link_ratchet.py --bucket, build-split):

  bucket             before   after   a6
  p6-core                11      11    6
  p7-magma              101      88  101
  p3b-p4b-espryt         13      18   14
  both-backends          61      56   60
  total                 186     173  184

p3b-p4b-espryt's +5 is not a regression: those five MOVED out of both-backends
because DirectVulkan stopped referring to them and only DirectGLES still does.
Thirteen symbols went away outright, and they are exactly the three classes §4.2
names: ProgramObject::{AllocateLifetimeId, AttachShader, Link, ~ProgramObject},
SamplerObject::{SetLodRange, SetWrapR, SetWrapS, SetWrapT} and
ShaderObject::{Compile, DropCompileNode, JoinPendingCompile, ReleaseCompileNode,
SetShaderSource}. Re-baselined in this commit with --write-baseline; the 7 `# P13`
annotations carried over.

§4.2's "down to 0" DOES NOT HOLD, and the gap is not (B') left half-done - it is
that 88 of the 101 were never about the hidden programs. 62 come from
UniformManager.cpp.o and 14 more from VkSamplerManager.cpp.o: the MONOLITH
descriptor binder and sampler cache reading the application's own GL texture and
sampler objects (TextureObjectBase 35, TextureObjectWithOneMipmap 20 with its
vtable and typeinfo, SamplerObject's fourteen getters). Under a transport
SetupDraw branches to SetupWireDraw and not one of those lines runs - but that
branch is a RUNTIME one, which ID-P7-5 deliberately leaves to the optimisation
phase. notes/p7/magma-b2.md §4.3 names all eleven referring-object groups and what
each is.

NONE OF THEM IS ANNOTATED `# P13`, on purpose: that mark is for a symbol that
cannot fall without moving pull `.text`, and nothing above has shown that.
UniformManager's and VkSamplerManager's monolith arms are pull-build code, and
`#if`-ing them is the same operation this commit performed on VulkanRenderer.cpp.
They are wave 3's work, not P13's boundary problem, and marking them P13 early
would record an untried thing as a tried-and-impossible one.

Gates: integration-gpu 1705 (every monolith `DirectVulkan.` entry is in there),
integration-magma-split 93, -spawn 72, -tcp 74, integration-magma-full-split 524,
ctest -L unit 2423, all green; fatal_census 79 abort sites / 44 family words / 0
unmarked; spawn_lane_parity green; link_ratchet --assert-monotone unchanged at 173
against the re-baselined file, and --self-test OK.

RETRACE, which is this slice's load-bearing reading because the monolith arm
changed implementation: OpenRA.DirectVulkan (monolith) ssim 1.000000,
mismatchPixels 0, twice; OpenRA.DirectVulkan.SPLIT ssim 1.000000, 0. The whole
DirectVulkan matrix is 39/40 monolith and 38/39 split, and both reds are the SAME
case, minecraft-1.21.4-fabric-iris-iterationrp-in-world (ssim 0.027915), which is
PRE-EXISTING - it fails identically on ~/w7/p7-magma-b (this branch's parent),
because it needs CONTRACT-P7 §2.4's three iterationRP knobs and those are exported
by the CI job rather than by the ctest entry.

G1 pull .text 0xa52203, nm --defined-only identical to p7-before/pull-syms.txt
(30570 lines, 0 diffs).

Device debt: the monolith arm in the Android disaggregated package now runs the
baked blit, and its ROTATE_90/180/270 geometry has never executed - every surface
on this host is IDENTITY. notes/p7/magma-b2.md §5 lists it with the package's three
other device items.
2026-09-22 18:29:22 -04:00
swung0x48 a50d50df60 [MG_Backend, MG_Impl] (P7): the two Magma allocator debt scopes go, because the objects they were about are never built off the monolith arm
CONTRACT-P7 §5.2's "the two MagmaP7AllocatorDebtScope are deleted with it (prove
first, with a temporary assert, that they never arm under a non-monolith
transport)".

WHAT THE DEBT WAS. The hidden blit program, its two samplers and the hidden depth
-mipmap program are FRONTEND objects. Under an active transport the server backend
created them on the apply thread and destroyed them on it, where their destructors
run the client death helper and its lifetime-id probe - so P5c gave the two
teardowns a named exemption and P5e ruling 12 renamed it after the debt.

WHAT RETIRES IT IS NOT AN EXEMPTION BUT AN ABSENCE. P5f fv already gave
InitializeBlitResources and InitializeDepthMipmapResources an early return for
every non-monolith transport, so off the monolith arm neither resource ever holds
anything and `m_blitResources = {}` destroys nothing. The scopes have been
markers over a path that cannot run since that day.

PROVED, NOT ARGUED. A temporary probe in both teardowns failed the session by name
if either resource was non-null under a non-monolith transport. With it armed:
integration-magma-split 93, -spawn 72, -tcp 74, integration-magma-full-split 524,
-full-spawn 524, integration-split 187 and integration-magma-buffers 21 all green -
it never fired. The negative control is what makes that a measurement: dropping the
probe's transport test killed EVERY monolith `DirectVulkan.` entry at teardown
(8/8 aborted, after the case itself passed) while the split entries stayed green.
integration-magma-full-tcp's one red under the probe is the documented StreamLink x
Magma exposure texel (CONTRACT-P7 §2.5's named tcp entry,
IterationRPProgram203Scenario), not a probe death - it fails, it does not abort.

THE COUNT AGAINST P5e RULING 12. That ruling named FOUR Magma sites
(VulkanRenderer.cpp x3 and ResourceTracker.h x1). Two had already gone before P7
began - `git log -S` puts the ResourceTracker.h site's removal in P5f fv (4f3d2d6b)
and the third VulkanRenderer site's in P5f fm (036c3a3d) - so the P7 baseline
78b7d6be carried TWO, which are the two this commit deletes. Magma's apply-thread
allocator debt is now ZERO sites.

THE TYPE STAYS, and its comment now says why: MG_Test/Wire/RemoteClientTest's
RemoteGuards.BarrieredLegacyScopesCannotExemptAllocator and
.BarrieredFrontendRegistryMembersRefuseBothLegacyScopes construct it on the apply
thread and assert the allocator refuses ANYWAY. That is a negative control for the
property P5f (fr) established, and deleting the class would delete the only case
that pins it. No backend use remains.

Gates: integration-magma-split 93, -spawn 72, -tcp 74, integration-magma-full-split
524, ctest -L unit 2423, all green (one InitialCapsStartup death test flaked once
under -j 4 beside a sibling worktree's build and is green on re-run and on a second
full lane); fatal_census 79 abort sites / 44 family words / 0 unmarked;
link_ratchet --assert-monotone unchanged at 186; G1 pull .text 0xa52203, nm
--defined-only identical to p7-before/pull-syms.txt (30570 lines, 0 diffs).
2026-09-22 18:29:22 -04:00
swung0x48 51fa513d73 [MG_Backend, MG_IntegrationTest, MG_Test] (P7): a multisample depth or stencil buffer resolves on a device with no resolve extension, and the sample-count shapes with no arm decline
CONTRACT-P7 §3.2's `multisample-blit-aspect@P7`, the last `@P7` refusal in the tree.
Two halves again, and neither is the other's fallback.

THE HALF WITH AN ARM. vkCmdResolveImage is COLOUR ONLY. The one way Vulkan
resolves a depth or stencil aspect without a shader is a render pass with a
depth/stencil resolve attachment, which needs VK_KHR_create_renderpass2 and
VK_KHR_depth_stencil_resolve - and a device with neither ended the session at
`Magma:multisample-depth-resolve-capability` on the first
glBlitFramebuffer(GL_DEPTH_BUFFER_BIT) out of a multisampled read framebuffer,
which is what every multisample depth resolve in GL is. §3.2 names the second arm
and this is it: WireMultisampleResolve.inc, three baked modules (one vertex stage,
one fragment stage per aspect), fetching SAMPLE ZERO - the same sample
VK_RESOLVE_MODE_SAMPLE_ZERO_BIT picks, so the two arms answer identically and the
lane can assert that.

The pass carries nothing: the scratch has the source's extent and coordinate
space, so the fragment stage indexes by gl_FragCoord and the rectangle is the
scissor. No push constants, no varyings, no frontend object. The stencil half
needs VK_EXT_shader_stencil_export to exist at all (a fragment shader cannot write
the stencil aspect otherwise); where it is absent the arm declines stencil rather
than leaving the aspect undefined, and the module is not even created, because
handing vkCreateShaderModule SPIR-V with an unsupported capability is undefined
rather than an error.

The two capability tests are deliberately not the same test: the subpass resolves
every aspect the FORMAT has and needs a mode for each, while the pass writes only
the aspect this blit asked for - which is safe because the copy-out reads exactly
that aspect.

THE HALF WITH NO ARM. Sample counts that differ with the DESTINATION multisampled:
one sample fanned out to N, or N reduced to M. vkCmdResolveImage only ever writes
a single-sample destination, vkCmdBlitImage refuses a multisampled side, and
vkCmdCopyImage needs the counts equal. Rule I (a): name both counts, record
INVALID_OPERATION, leave the destination alone.

Lanes. DepthStencilReadbackMatrixScenario.AResolvedMultisampleDepth… is PROMOTED
from the informational tier into the three gated Magma arms - it already owns the
observable (clear a 4x DEPTH24_STENCIL8, resolve with glBlitFramebuffer, read both
aspects back, check that a depth-only resolve left the destination's stencil alone
and vice versa), it just had no gated entry. Two entries over it per arm: plain,
and under MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE, which is the only way the new
arm is reachable here at all - lavapipe carries BOTH resolve extensions, so the
extension arm answers every resolve on this host. The new F1 case is the decline.
It does NOT skip on this host: the shader arm resolves depth and stencil, because
lavapipe also carries VK_EXT_shader_stencil_export.

Three rows join MOBILEGL_BAKED_INTERNAL_SHADERS (8 -> 11).

Red once (R-16, executed and reverted), three times:
  * restore MagmaWireFatal("multisample-depth-resolve-capability") for the
    non-extension arm -> DirectVulkan.Split.MsResolve1.… dies by that name while
    MsResolve0 stays green, which is also the proof that the knob routes;
  * restore MagmaWireFatal("multisample-blit-aspect@P7") ->
    DirectVulkan.Split.MsBlit.…FromASingleSampleSourceDeclines… dies by that name;
  * fetch sample 1 instead of 0 in WireMultisampleStencilResolve.frag (no rebuild -
    the gate reads the source at run time) -> WireMultisampleStencilResolveFragment
    reds at word 158, naming the symbol, the header and the regeneration command.
Reverted, 15/15 green on the three arms and 11/11 on the freshness gate.

`grep -rn "@P7" MobileGL/ --include=*.cpp --include=*.inc --include=*.h` now
returns COMMENTS ONLY - 19 lines, every one of them prose about a retirement or a
red-once. No refusal in the tree carries the mark any more.

Gates: integration-magma-split 93 (+3), -spawn 72 (+3), -tcp 74 (+3),
integration-magma-full-split 524 (+1), ctest -L unit 2423 (+3), all green;
bake_internal_shaders.py --check reports all five headers fresh; fatal_census 79
abort sites / 44 family words / 0 unmarked; link_ratchet --assert-monotone
unchanged at 186; G1 pull .text 0xa52203, nm --defined-only identical to
p7-before/pull-syms.txt (30570 lines, 0 diffs) - every line lives under
`#if MOBILEGL_BUILD_DISAGGREGATED` or in the test tree.

Device debt: the shader arm is the arm the PHONE takes wherever
VK_KHR_depth_stencil_resolve is missing, and this host can only reach it through
the knob. Its geometry needs one look on the Redmi.
2026-09-22 18:29:22 -04:00
swung0x48 76b7469389 [MG_Backend, MG_IntegrationTest, docs] (P7): the wire readback waits for the frame it is reading, instead of inferring who wrote it
Exit gate 3, OpenRA on the Redmi. The mechanism, in two lines of this tree:

  WireFramebuffer.inc, ReadWirePixels: `if (HasPendingRecordedWork() &&
  !FlushPendingCommands())` - FlushPendingCommands (VulkanRenderer.cpp:13534-13598)
  puts the frame's draws on the queue under a POOLED FENCE OF ITS OWN and returns
  without waiting; there is no vkWaitForFences on its success path. The
  vkCmdCopyImageToBuffer that follows is recorded into a fresh command buffer and
  becomes a LATER submission.

  VulkanRenderer.cpp:10707-10748, SubmitReadbackCommandsAndWait: it waited ONE fence,
  its own, and then called OnSubmitsCompletedUpTo(frame.lastSubmitIndex) - which
  RECORDED the earlier submission as complete and retired its fence, so afterwards
  nothing could observe that it had not been waited for. The comment beneath it said
  "the wait proved every submission complete". That is true of the monolith arm, which
  never flushes before a readback and therefore has its draws and its copy in one
  submission under one fence. It was not true of the wire arm.

The only thing tying the two submissions together was the pipeline barrier that the
readback's TransitionWireImage records - and that barrier derives its SOURCE scope from
the image's tracked layout, through GetImageTransitionSourceState
(VulkanRenderer.cpp:1784-1825). Two of its arms, VK_IMAGE_LAYOUT_UNDEFINED and
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR (:1787-1791), return VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT
with no access bits, which is Vulkan's spelling of an EMPTY first scope: wait for
nothing. A just-presented default framebuffer is in the second of those layouts. The
slower pipeline creation is, the longer the draws' submission runs, and the more of the
frame's LAST-drawn objects the copy can miss - which is what the device's diff image
shows.

Measured on this host (OpenRA, DirectVulkan, inproc), at the moment the copy is
recorded: submit=72 completed=71. The submission carrying the whole frame's draws is in
flight and unwaited. That is structural, not a coin flip, and it reproduces every run.

The fix is two halves, both wire-only:

 - RecordWireReadbackWriteBarrier (WireFramebuffer.inc, new): the readback records its
   own ALL_COMMANDS -> TRANSFER, MEMORY_WRITE -> TRANSFER_READ|MEMORY_READ barrier
   before the copy rather than inferring from a layout who it should wait for. A
   barrier's first scope is every command earlier in SUBMISSION order on the queue, so
   it reaches across the submission boundary.
 - SubmitReadbackCommandsAndWait now waits every in-flight fence at or below its own
   submission index. Under #if MOBILEGL_BUILD_DISAGGREGATED: the pull build has no wire
   arm, cannot flush before a readback, and is the image G1 pins - its single-fence wait
   is unchanged. The cost is nothing a readback does not already pay.

THE COORDINATOR'S REPRODUCTION PATH DOES NOT WORK ON THIS HOST, and that is recorded
rather than glossed. MOBILEGL_TEST_PIPELINE_CREATE_DELAY_MS is added as asked
(split-only, inert unless set, on the pipeline-creation MISS path in
GetOrCreatePipelineWithInput) and it does bite - the OpenRA DirectVulkan SPLIT retrace
goes from 1.88 s to 4.53 s at 25 ms - but ssim stays 1.000000. The reason is in the
probe above: this host's default framebuffer is tracked as VK_IMAGE_LAYOUT_GENERAL at
the readback, which takes GetImageTransitionSourceState's `default:` arm (ALL_COMMANDS /
MEMORY_WRITE) and orders the copy by accident. The device's is not GENERAL.

Red once (executed, reverted) is therefore a COUNT, not a picture, and it is
deterministic: instrumenting how many fences the readback actually waits on gives
fences=2 upTo=73 completed=71 with the fix (submission 72, the draws, plus its own 73)
and fences=1 without it, with completed=71 either way - i.e. 72 provably outstanding and
provably unwaited before.

Lane: three readback-after-work cases (VertexIdSamplerAndScalarUniformPixels,
ComputeImageStoreFramebufferPixels, GenerateMipmapPixels) registered on the three Magma
arms under MOBILEGL_TEST_PIPELINE_CREATE_DELAY_MS=25, prefix .PipelineDelay. They are a
REGRESSION GATE, not the reproduction: they pin that a readback still returns the draw's
pixels when every pipeline creation on the path costs 25 ms.

Gates: integration-magma-split 84 (+3), -spawn 63 (+3), -tcp 65 (+3), 0 red;
ctest -L unit 2410; informational integration-magma-full-split 516, 0 red, 59 skip;
fatal_census green; link_ratchet --assert-monotone unchanged at 186, p7-magma 101;
G1 pull .text 0xa52203, defined-symbol name list identical (30570); OpenRA DirectVulkan
SPLIT retrace ssim 1.000000 with and without the delay, two runs each.

Owed to the device: the three cold-cache repeats. If they still diverge, the next two
suspects are named in notes/p7/magma-b.md §5.6 - the eleven silent `return false` drop
sites in SetupWireDraw (one of which is `pipeline == VK_NULL_HANDLE`, on the same path
as the device log's three "handing vkCreateShaderModule an INVALID module" errors), and
SetImageContentDefined being set BEFORE the draw is recorded on the wire arm while the
monolith sets it from EndRenderPass, which silences the "no write is recorded" warning
exactly when it should fire. Neither is in this commit's scope.
2026-09-22 16:37:54 -04:00
swung0x48 4f857aa5c6 [MG_Backend, MG_IntegrationTest, docs] (P7): a same-image copy takes one GENERAL transition instead of the session
CONTRACT-P7 §3.2's `copy-image-in-place@P7`, retired.

glCopyImageSubData between two subresources of ONE texture is ordinary, defined GL -
4.6 core 18.3.2 spells out that a texture and a view of it are two objects over one
image and may be copied between. The wire arm took the session down for it, and the
reason it gave was true of the code beneath it and of nothing else: the
TRANSFER_SRC/TRANSFER_DST pair cannot describe one image, because both endpoints share
one tracked VkImageLayout and the second transition undoes the first.

VK_IMAGE_LAYOUT_GENERAL can describe it - both sides of vkCmdCopyImage accept it. So a
same-image copy now takes ONE transition of the whole image to GENERAL (the source level
and the destination level are both inside it), GENERAL on both sides of the command, and
ONE restore, with an access mask carrying both directions because that one barrier is
the edge for both. The monolith arm still declines every in-place copy a few lines up,
so the wire arm is the one that performs this - which is what §3.2 asks for rather than
parity. `inPlace` is constexpr false in the pull build so every branch it guards folds:
this function is in the monolith image and G1 pins that image's .text.

The one shape that stays a decline is same image + same level + overlapping slices +
overlapping rectangle, all three at once. GL leaves that undefined, and GENERAL makes
the command legal to RECORD rather than meaningful to execute - vkCmdCopyImage orders
nothing between its own reads and writes inside a region, so a tiler produces whichever
half of each texel it reached first. Level 0 to level 1, disjoint layers, and disjoint
rectangles of one level are all defined, and all performed.

Two split-only cases on the three Magma arms, over one 8x8x2 two-level array texture
whose every touched subresource is a different colour - a flat fill would let "copied to
the wrong level", "copied to the wrong layer" and "did not copy" all read as correct.
CopyImageInPlaceAcrossLevelsAndLayers does level 0 -> level 1 and then layer 0 -> layer 1
and checks the untouched subresources too; CopyImageInPlaceOverlapDeclinesAndLeavesTheLevelAlone
copies (0,0)-(4,4) onto (2,2) and pins every texel unchanged - an executed copy would
turn the texel at (4,4) from yellow to red, so the assertion can tell.

Red once (executed, reverted): with MagmaWireFatal("copy-image-in-place@P7") restored,
both entries die with Fatal{UnmigratedVerb, "Magma:copy-image-in-place@P7"}; without it,
2/2 green.

Gates: integration-magma-split 79 (+2), -spawn 58 (+2), -tcp 60 (+2), 0 red;
ctest -L unit 2410; informational integration-magma-full-split 515, 0 red, 59 skip;
fatal_census green (79 sites / 43 words / 0 unmarked); link_ratchet --assert-monotone
unchanged at 186, p7-magma 101; G1 pull .text 0xa52203 and the defined-symbol name list
identical (30570) - the constexpr-false branches fold as intended.

This tree's integration TCP endpoint moved to 127.0.0.1:40913: a sibling worktree's
libMobileGLServer was already listening on pipe's default 40613 and took the whole tcp
lane down at TcpServer.Start. It is a local configure option, not a lane semantic.
2026-09-22 16:37:54 -04:00
swung0x48 fdd611e83d [MG_Backend, MG_Test, MG_IntegrationTest, scripts, docs] (P7): the depth mip chain is baked, and every baked header in the tree now has to match its source
CONTRACT-P7 §5.1's (A) and (D), and (D) is what makes (A) safe to have.

(A) WireDepthMipmap.{vert,frag,inc}. VK_FORMAT_FEATURE_BLIT_DST is OPTIONAL for depth
formats, so a depth chain through a shader is the ordinary case and not the exotic
one - the monolith arm has carried such a fallback since before the split. The wire
arm had none, so a depth texture the device would not blit died on the first
disjunction of `mipmap-shader-format-or-shape`, and a DEPTH|STENCIL one died a guard
earlier as `depth-stencil-mipmap@P7` whether or not it could be blitted.

The monolith's fallback is not reusable, which is the whole of (A): it builds a GL
ProgramObject, two ShaderObjects and a GL sampler, binds through the uniform manager's
global UBO and writes uSrcTexelSize as a default-block uniform - four frontend objects
and a GL compiler on the server's apply thread. The baked program is the same shader
with the same arithmetic on a push constant and its own descriptor set. The fragment
stage is a PORT and not a rewrite: §3.2 retires a refusal by making the wire arm
observably the monolith's, so the 2x2 box lands where the monolith's lands, including
the half texel its ivec2(texCoord * srcTexelSize) sits off the destination centre.

`depth-stencil-mipmap@P7` therefore splits. DEPTH-only retires into the baked program.
DEPTH|STENCIL declines - MGLOG_E_ONCE and return, before the chain is grown, so a level
this verb will not fill is not created and then recorded as defined. A decline rather
than a Fatal because the monolith does no better: its fallback asserts depth-only, and
an assert is nothing in the release build both arms ship as. Nothing in GL promises a
stencil mip either, and this pass writes gl_FragDepth - it would leave the stencil
aspect of the level it just produced undefined.

(D) MOBILEGL_BAKED_INTERNAL_SHADERS. A baked header is a compiler output committed with
its input beside it and nothing connecting the two: edit the source and the build stays
green while the shader that runs is the one from before the edit. MG_Test's
BakedInternalShadersTest recompiles all eight arrays - WireColorBlit x2,
WireDepthMipmap x2, the iterationRP witness, the three probe stages - from their
committed GLSL with the in-tree glslang and compares word for word.

THE GATE IS THE BAKER. scripts/bake_internal_shaders.py compiles nothing; it runs this
binary with MOBILEGL_BAKE_INTERNAL_SHADERS and splices the words it emits. A gate that
used the glslang LIBRARY while the baker shelled out to a glslangValidator BINARY would
be two compilers with no rule about which is right, and the day their defaults diverge
the gate reds on a header nobody can regenerate.

What the first run measured, which is the one thing nobody could know beforehand:
WireColorBlitSpirv.h and all three arrays of PrimitivesGeneratedNoXfbProbeSpv.h were
already fresh - the hand-run glslangValidator words reproduce exactly. The iterationRP
witness was stale, as its own prose said; it is regenerated here (2142 -> 2143 words),
and the witness magic 0x50323033 is still there because it is a constant in the .comp
rather than a property of the words. All three provenance comments now point at the
script and the gate instead of at a tool on somebody's PATH.

Scenarios, on the three Magma arms: GenerateMipmapDepthWithoutNativeBlitPixels runs
DEPTH_COMPONENT24 and DEPTH_COMPONENT32F under MGITEST_MAGMA_FORCE_SHADER_MIPMAP=1 and
reads level one back through a real sampled attachment, asserting per half and leaving
row 3 - the straddle band - unpinned, because pinning it would assert the half-texel
offset rather than the mip. GenerateMipmapDepthStencilDeclinesAndKeepsTheSession takes
no knob (the decline is on the aspect, before blit support is asked about) and pins that
the session survives and level zero still holds what was cleared into it.

Red once, three of them, executed and reverted:
 - a one-character edit to WireDepthMipmap.frag (no rebuild - the gate reads the source
   at runtime) reds the WireDepthMipmapFragment row at word 279;
 - with the depth arm disabled, the DepthMip entries die with Fatal{UnmigratedVerb,
   "Magma:mipmap-shader-format-or-shape format=125 ... aspect=0x2 ...
   missing=COLOR_ATTACHMENT"} - format 125 is X8_D24_UNORM_PACK32, which slice 1's
   diagnostic names for us;
 - with the Fatal restored, the decline case dies as
   Fatal{UnmigratedVerb, "Magma:depth-stencil-mipmap@P7"}.

Gates: integration-magma-split 77 (+2), -spawn 56 (+2), -tcp 58 (+2), 0 red;
ctest -L unit 2410 (+8, the gate's rows); informational integration-magma-full-split
513, 0 red, 59 skip (base 510/0/57); fatal_census green (79 abort sites, 43 words, 0
unmarked); link_ratchet --assert-monotone unchanged at 186, p7-magma 101.

G1 holds and it was not obvious: the witness header is compiled into the pull build
too, and its word count changed. Pull .text is still 0xa52203 and the defined-symbol
name list is identical to ~/w7/p7-before/pull-syms.txt line for line (30570) - the
change lands in .rodata and widens no instruction encoding.

VulkanRenderer.h is edited outside this package's file list: the new pass needs four
private declarations next to the colour blit's. Flagged for the integrator.
2026-09-22 16:37:54 -04:00
swung0x48 e1ec69341a [MG_Pipe, MG_Backend, MG_Remote, MG_Test, scripts] (P7): the Magma wire fatals and the two Staged aborts go through Session::Fail
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.
2026-09-22 14:01:06 -04:00
swung0x48 5264f9a966 [MG_Backend] (DirectGLES/DirectVulkan): the monolith readback and resolve arms get the depth/stencil fixes the wire arm already had
f5234b9e taught the WIRE depth/stencil readback to scale a depth into the signed
integer widths' own range and to answer GL_HALF_FLOAT; 7b926429 taught the WIRE
depth/stencil resolve to copy only the requested aspect. Both landings stopped at
the wire entry points, so with MOBILEGL_TRANSPORT=monolith the same seven
DepthStencilReadbackMatrixScenario cases still failed on the same assertions.

DirectGLES: IsSupportedDepthStencilReadPixelsPair turned GL_BYTE/GL_SHORT/GL_INT/
GL_HALF_FLOAT depth and GL_HALF_FLOAT stencil away before the helper that can serve
them, and glGetTexImage had no GL_STENCIL_INDEX branch at all, so the read fell
through to a native ES glReadPixels the driver refuses and nothing was written.
ReadPixelsDepthComponent and ReadPixelsStencilViaNative now answer the signed
widths out of their own ranges and encode the half-float widths.

DirectVulkan: ReadDepthStencilImageToClient carried its own copy of the packing
and had drifted - GL_SHORT was scaled into 65535 (a 0.5 depth came back as
0x8000, i.e. -32768), GL_BYTE/GL_UNSIGNED_BYTE emitted the stencil byte for a
DEPTH_COMPONENT read, and GL_HALF_FLOAT was rejected outright. The packing moves
into one PackDepthStencilTight that both arms call, so they cannot disagree again.

DirectVulkan blit: a single-aspect depth/stencil copy of a packed format went out
as a whole-image vkCmdCopyImage, which moves the packed native word and therefore
replaced the destination's other aspect with the source's - a depth-only
multisample resolve overwrote the destination's stencil. Same-format,
same-size, single-layer single-aspect copies now stage that one aspect through a
device-local buffer, exactly as ResolveWireDepthStencil does.
2026-09-20 21:32:25 -04:00
swung0x48 64182b76dc [MG_Backend] (Magma): the indirect client-array staging block is monolith-only 2026-09-20 20:58:18 -04:00
swung0x48 e550d10339 [MG_Backend] (Magma): an indirect draw's client-memory arrays are staged from the commands
The commands live in GPU memory, so the draw is issued natively and the vertex
range was left unknown (vertexCount = 0, instanceCount = 1). That is exactly
right for a buffer-backed array and wrong for a client-memory one: its bytes must
be in the store before the GPU reads the command, and the range the upload is
bounded by came out as one element - so the quad the command named was never
staged and the draw painted nothing.

The words are now resolved on the CPU for exactly that case, with the shadow
reconciled first (they may be shader-written), and the draw stays native.
2026-09-20 20:53:57 -04:00
swung0x48 cf7366276e [MG_Backend, MG_Impl] (DirectGLES, Magma): the monolith arm's client fetch bounds
DirectGLES: the client snapshot's baseInstance shift belongs to the backend's
emulation, not to the element set. A driver that applies baseInstance itself
(UseNativeBaseInstance) must not also receive a shifted pointer - doing both
moved instance 0 from element 1 to element 2 and left the instance that reads
element 1 outside the snapshot. EmulatedFetchBaseInstance is not declared this
early in the file, so the wrapper states the same answer inline.

Magma: ResolveIndirectCommandBytes reconciles a GPU-written command block before
the callers read it. Every one of them reads the words on the CPU - the
per-command expansion, the baseInstance the shader emulation is fed, and the
range a client-memory vertex array's upload is bounded by - so a command a
compute shader wrote this frame was expanded as the poison the shadow still
held, which is no draw at all for glDrawArraysIndirect.

Magma: a client-memory INSTANCE-RATE binding is bounded by its instances
(baseInstance + (instanceCount - 1) / divisor), not by the vertex range; the
upload now covers the union of the two, which is what instances shifted off the
front of the array needed.
2026-09-20 20:51:09 -04:00
swung0x48 fd30b5c30a [MG_Backend] (Magma): keep a view's mip window and readback inside the layers it names
Magma's generate path (VulkanRenderer::GenerateMipmap) took its window from the
BOUND object and its blits from the STORAGE image, and the two do not coincide
for a texture view: glTextureView gives a view its own BASE_LEVEL/MAX_LEVEL and
layer slice over somebody else's image. Three things followed from the mix-up,
all visible through a two-layer view over a four-layer 2D array:

  * EnsureGenerateMipmapStorageAllocated allocated the chain THROUGH the bound
    object from that object's OWN base extent, so the storage owner's level was
    resized to the view's layer count - the storage behind its other layers was
    gone - and the levels outside the view's window were redefined with it.
    Immutable storage and views already own every level generation can write,
    so neither allocates anything now, and a mutable owner gets only the levels
    its window is missing (AllocateStorage RESIZES the level it is handed, so
    an existing level keeps the shadow bytes its own upload path wrote).
  * The level the chain starts at was the view's, applied to the storage image:
    a view whose MIN_LEVEL is 1 generated from the storage's level 1 instead of
    its level 2. The window now comes from the frontend's own plan
    (MipmapGenerationPlan.h - the same numbers MGPMipPlan::LevelCount publishes)
    and is shifted into the storage's numbering before any blit or barrier.
  * Every layer of the storage was written instead of the view's window. The
    blits carry the view's baseArrayLayer and layerCount now; every layer
    outside that window belongs to the storage owner and is left as it was.

The end of the window follows MAX_LEVEL for the same reason the frontend's plan
does: a chain MAX_LEVEL cuts short is no longer generated past it.

The readback had the same shape of bug pointing the other way.
VulkanRenderer::GetTextureImage sized its copy by the storage image's array
layer count while starting it at the caller's layer origin, so a two-layer view
of a four-layer array copied four layers - two outside the window, and the last
one past the end of the image. glGetTexImage carries no destination size, while
the size glGetTextureImage validates is built from the VIEW's own extents, so a
caller that sized its buffer for the view had it overrun by exactly the layers
outside its window. The copy covers the caller's own layers now.

TextureViewScenario.GenerateMipmapThroughAViewLeavesTheOwnerShapeAlone runs on
both backends now. Its DirectVulkan skip named the reallocation above, and the
sub-image readback the skip blamed as well is not broken: a control run of the
same scenario with the generate removed reads every seeded level and layer of a
2D array back, mip 2 and 3 included, through both glGetTexImage and a per-layer
FBO attachment - the two mismatches that control reported are the colours the
generate was supposed to write. GetTexImageThroughAViewReturnsOnlyItsOwnLayers
is new and pins the readback window with canary slices past the view's last
layer; it fails on the reverted readback and passes on DirectGLES too.
2026-09-20 19:48:16 -04:00
swung0x48 f1d3fa8f1a [MG_Backend] keep the default framebuffer on the image GL last rendered into across a swap
DirectVulkan resolved every default-framebuffer role against
m_imageIndexAcquired. Present() advances that to the image it acquires for
the frame that is about to start, and a present record is kWaitPresent - the
client does not wait for the apply - so a readback belonging to the frame
being presented (the retrace snapshot, taken at the swap call) can be applied
after the acquire and then resolve an image nothing had written. That is the
pure-black sundial-lite DirectVulkan retrace frame; DirectGLES is immune
because the driver keeps handing back the buffer the app drew into.

Split the GL-visible index out (VulkanRenderer::m_defaultFramebufferImageIndex)
and keep it out of the present/acquire path: it only re-points at the acquired
image when something WRITES the default framebuffer (DefaultFramebufferWriteIndex,
called at the FBO-0 render-pass creation, the deferred-clear-free direct writes
and the wire draw/clear/blit resolutions) and is reset on RecreateSwapchain.
Reads - glReadPixels both arms, the parked-clear materialization, a blit or copy
SOURCED from FBO 0, ResolveWireImage's read role (carrying the resolved index in
WireImage) - resolve DefaultFramebufferReadIndex instead, i.e. whatever that is.

A readback that resolves a swapchain image with no recorded write now says so
once (MGLOG_W_ONCE, both arms) instead of silently handing back raw pixels.

New integration scenario DirectVulkan|DirectGLES.DefaultFramebufferAcrossSwap
pins both halves: a readback after a swap still sees the drawn frame, and the
first draw after a swap lands in the image the readback then resolves.
2026-09-20 18:52:08 -04:00
swung0x48 0cb0e9af9b [MG_Backend] (Disaggregated): resolve multisample depth and stencil through renderpass2 2026-09-20 12:44:37 -04:00
swung0x48 bb816906a7 [Magma] resolve copy-image renderbuffer endpoints from wire storage 2026-09-20 12:07:28 -04:00
swung0x48 7875e28cd8 [Magma] read texture images into server-owned tight reply bytes 2026-09-20 11:43:49 -04:00
swung0x48 d0c6fdbc14 [Magma] consume wire-owned transform feedback capture spans 2026-09-20 11:15:58 -04:00
swung0x48 70fb6689ce [Magma] key wire pipelines by exact render-pass compatibility identities 2026-09-20 08:42:47 -04:00
swung0x48 0ece468e14 [Magma] reuse wire framebuffer objects within fenced frame slots 2026-09-20 08:28:55 -04:00
swung0x48 194382c96a [MG_Backend, CI] (Disaggregated): reclaim abandoned wire objects only after texture uploads are idle 2026-09-20 05:24:25 -04:00
swung0x48 4f762262a0 [MG_Backend] (Disaggregated): make Magma descriptors and present barriers Vulkan-valid 2026-09-20 05:17:21 -04:00
swung0x48 3c97ea9cfc [MG_Backend] (Disaggregated): validate wire framebuffer identity without frontend getters 2026-09-20 04:38:40 -04:00
swung0x48 b3e4b7b78b [MG_Backend] (Disaggregated): retire the final wire pass at present submission 2026-09-20 04:17:18 -04:00
swung0x48 51e61ddabe [MG_Backend] (Disaggregated): order Magma GPU work and defer wire object retirement 2026-09-20 04:11:36 -04:00