Commit Graph
379 Commits
Author SHA1 Message Date
gold ead7a10b68 [Feature] (disaggregated): the default framebuffer on a host-framed surface is the host's frame
Three things this path needs that the server-owned one never did, each because the pixels are not
in an EGL surface this time:

 - The client's DEFAULT framebuffer has to resolve to the frame.  Four places bind the driver's 0
   for the default framebuffer; they now ask g_hostFrameFramebufferId, which is 0 everywhere except
   while a host-framed surface is current, where it is the frame's framebuffer.  Without this every
   draw would land in the pbuffer the context was made with - a surface nobody ever sees.

 - eglMakeCurrent binds the surface's own default framebuffer behind the engine's binding shadow, so
   the frame has to be put back after every make-current, with the shadow invalidated first: a bind
   the shadow thinks already happened never reaches the driver.

 - The bind itself goes through FramebufferImpl::BindFramebufferId, not a raw glBindFramebuffer, for
   the same reason - a raw bind leaves the shadow claiming the previous framebuffer.

Frame lifecycle is otherwise unchanged: the frame becomes the draw target when it arrives, and is
dropped after the host has been answered and has presented it.

Compiles and links (libMobileGL.so, libMobileGLServer.so, exit 0), deployed to the device, and the
container's client is rebuilding against it.  The run that shows Plasma through it is next.
2026-10-01 01:14:51 +08:00
swung0x48 0222852b94 [Fix] (Espryt, P8-SE): a spawn / tcp server publishes the default framebuffer's depth/stencil format (it has no frontend placeholder; the case leaves pending-fix for the three arms); the server CPU mip arm declines by name (generate-mipmap-store-declined) instead of filtering a store the driver wrote over, and E's refused-readback knob reaches it (CpuMipRefused. entries); WireIndirectDrawScenario case 3 runs on the Espryt split arms; copy-image-shadow-mirror goes and unmigrated_emulation_sites.py holds PipeCatalogueTest's list to the call sites 2026-09-30 03:44:29 -04:00
swung0x48 39cd8fb73b [Merge] (P8): integrate package E - a texture readback the Espryt driver refuses is answered from the split server's staged store, which now follows glCopyImageSubData (FollowCopy, client-box merge, sticky driver-written mark, named decline texture-readback-store-declined); CopyImageStoreReadbackScenario on both backends x every arm plus the extent-scoped StoreRead knob; the dead CopyImageSubData+RENDERBUFFER latch goes (census 79 -> 78) 2026-09-30 02:17:51 -04:00
swung0x48 9441071567 [Merge] (P8): integrate package C - the Espryt split server refreshes its staged copy of a buffer a shader wrote before a CPU reader consumes it (storage/atomic/buffer-image marks, XFB captures into the copy; restart rewrite, multi-draw rebase, *IndirectCount, CPU-fed indirect fields, XFB scatter and fp64 narrowing read through SplitHostBytesForCpuRead); GpuWrittenDrawInputScenario (12 cases) on the Espryt split arms 2026-09-30 00:45:16 -04:00
swung0x48 f823422bfa [Fix] (Espryt, P8-E): a texture readback the driver refuses is answered from the server's staged store, which now follows glCopyImageSubData (FollowCopy, client-box merge, sticky driver-written mark, named decline); CopyImageStoreReadbackScenario on both backends x four arms plus the extent-scoped store-read knob; the dead CopyImageSubData+RENDERBUFFER latch goes (census 79 -> 78) 2026-09-29 22:43:02 -04:00
swung0x48 e52a997f34 [Fix] (Espryt, P8-C): the split server refreshes its staged copy of a buffer a shader wrote before a CPU reader consumes it - storage/atomic/buffer-image writes mark the server twin, XFB captures land in the copy, the restart rewrite, multi-draw rebase, *IndirectCount, CPU-fed indirect fields, XFB scatter and fp64 narrowing read through SplitHostBytesForCpuRead; GpuWrittenDrawInputScenario (12 cases) on the split arms 2026-09-29 22:38:33 -04:00
swung0x48 8dac1fd8a4 [Fix] (Espryt, P8-B): glGenerateMipmap on the server - the record's plan is the generated window (B1) and RGB16F/RGB32F generate natively or on the server's CPU from its own level (B2), retiring both split generate-mipmap names; GenerateMipmapServerScenario on both backends x monolith/Split/Spawn/Tcp plus the forced-CPU arm 2026-09-29 22:22:55 -04:00
swung0x48 2a69f61c88 [Feat] (Espryt, P11 B2): import a T0 store with glBufferStorageExternalEXT as the adopted twin - no staged copy, server writes queued GPU-ordered, CPU readers on the import's coherent map, fence-gated release, re-import after a context loss, and the sustained-lock POST in a throwaway context 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 9524c688ca [Fix] (Espryt, Magma): scattered glTexSubImage writes upload exactly their texels - MipmapStorage keeps an exact dirty footprint beside its rect list (a write log once a merge or a fold would widen the list past the writes), Magma always uploads it, Espryt uploads it box by box (through the unpack ring too) once the driver texture may hold GPU writes and merges it at the image-bindable re-mint; the union box, the 3/4 cut-off, the 96-rect fold and a level-spanning union no longer put the stale shadow over GPU-written texels (two 2x2 writes over a GPU clear: 41 of 64 texels red -> 0) 2026-09-29 09:06:07 -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 6c2f8a560c [Fix] (Espryt, P9 W2): a texture re-mint never needs the client - read back only levels the driver holds, replay the staged level when the readback is refused, count trp on the server path, and retire the texture-remint-pull markers and OnTexturePullRequest (callbacks 9 -> 8) 2026-09-29 03:23:54 -04:00
swung0x48 568090f09f [Fix] (Espryt): the image-bindable re-mint reads every level back from the driver texture before replaying it, so GPU-written levels survive the first glBindImageTexture - iris-derivative SSIM 0.996196552 -> 0.996376198 2026-09-29 03:06:45 -04:00
swung0x48 7680b78621 [MG_Backend] (P12 review): an ended server session's swap interval is forgotten, so the next session in the in-process display server starts at the driver's default
DirectGLES keeps the requested eglSwapInterval in a process global on purpose (a surface re-created mid-session re-applies it), and every server process used to serve one session. The in-process display server serves the next session in the same process, which then silently inherited the previous client's interval. The server backend's destruction under a transport now calls DirectGLES::ForgetRequestedSwapInterval() beside DropEveryTwinForEndedServerSession (split build only; the pull build is unchanged, G1).
2026-09-24 14:03:47 +00:00
swung0x48 29a82617e0 [MG_Remote, MG_Backend] (P12 review): a server-owned surface resizes the server's window and reports its real size, every server-owned surface keeps its geometry, and Espryt reports a window's extent only for the server's own window
- Client resize: eglResize on a headless client's server-owned surface only rewrote the backend's record of the window - the phone's SurfaceView kept its size - and the client's EGL state answered the size it asked for. The server now recognises the surface as its own (ServerLoop::m_serverOwnedSurfaces, per session) and resizes it as a geometry request (the lease re-taken by its holder, the display waited for), resizes the backend surface to the window's real extent and replies with it; BackendObject_Remote adopts that extent (ServerResizeServerOwnedWindowSurface).
- Size tracking: the client held ONE server-owned surface, so a second one took the first's geometry updates and releasing it left the first with none. ClientSession now tracks every server-owned surface and applies an extent to all of them (they share the server's one window).
- Size reports without the knob: Espryt's new extent publish fired for every window surface of a spawn/tcp session, including windows a client named itself without MOBILEGL_IPC_SURFACE=server, switching their consumer to the reallocating arm. ServerLoop now names its own leased window to the backend (MGPipeServerSetOwnedWindow, split-only) and Espryt publishes the extent only for that window.
2026-09-24 14:03:47 +00:00
swung0x48 2fa4714fc9 [MG_Backend] (P12): Espryt publishes a remote session's window-surface extent with its default-framebuffer format (split build only; the pull build is unchanged, G1) 2026-09-23 23:55:23 -04:00
swung0x48 1c36987e84 [MG_Backend, MG_Pipe, MG_Impl] (P7): D-K2's dependency rule has one statement, and every reader reads it
P3b/P4b wave 2-D package D3, slice 2 - D2's two loose ends, now in-partition because D1 has
landed on feat/disaggregated and MG_Backend/DirectGLES/ is free.

THE STALE PROSE, in the header of the file that implements the refusal.
MG_Backend/DirectGLES/Managers.h's resolver block said "THREE OF THEM CARRY A DEPENDENCY"
(there are six rows; bits 8 and 13 were the two it did not know about) and listed
"10 without 11" among the mirror pairs that are FINE - when 10-without-11 is precisely D-K2's
fourth row (ID-14/ID-15), refused twenty lines below by ResolveTextureResourceSubsystemArm and
withheld by the client. MGPipe.h lost the identical pair of defects with R-5; this copy was
left behind because the file belonged to a package in flight. It does not restate the rows
again: the file that states them is the file the code reads.

BOTH READERS NOW READ MG_Pipe/SubsystemDeps.def. More than the two one-line changes D2
estimated, because the rule had six statements and the estimate counted two:

  PipeFill.cpp P4aFamilyDependencyBits   its own four-row kMGPipeP4aFamilyDependencies[]
                                         -> MGPipeSubsystemRequires(); table deleted
  PipeFill.cpp P5eFamilyIsLive           hand-coded (pushMask & bit 7) == 0
                                         -> MGPipeSubsystemDependenciesAreSet(bit 13, mask)
  Managers.cpp PipeSubsystemDependencyMissing   took a literal dependency BIT per call site
                                         -> takes the FAMILY and asks the table
  Managers.cpp the four resolvers        five calls, five hand-written sentences
                                         -> four calls, one per family, naming only the family
  Managers.cpp ResolveVertexInputSubsystemArm    hand-rolled bitSet && !resourcesBitSet
                                         -> the table
  DirectGLES.cpp ResolveBufferBindingSubsystemArm  the same hand-rolled pair -> the table

MGPipe.h gains one accessor, MGPipeSubsystemDependencyWhy(), so a reader that refuses prints
the ROW'S OWN sentence instead of a copy of it. The .def has carried a Why column since R-5
and nothing read it; its "WHO READS IT" section is rewritten because "THEY ARE NOT SWITCHED
OVER IN THIS COMMIT" is no longer true of anything.

THE CLIENT'S ARGUMENT IS MASKED TO kMGPipeP4aFamilySubsystems, and that mask is the whole
difference between a rewrite and a behaviour change. The .def carries SIX rows; the table
deleted from PipeFill.cpp carried FOUR, and that predicate is wants()'s fourth conjunct for
P4a's families only. Reading all six unmasked makes the CLIENT withhold bit 8's vertex-input
emissions at a mask with bit 7 clear, which is P3a's own rule and not this gate's - caught,
not reasoned about, by TextureEmitTest's
EveryDKTwoDependencyRowGatesItsOwnFamilyAndTheMirrorPairsStayLive going red on exactly that
assertion ("bit 8 alone, with bit 7 clear: P3a's own rule, which this table must not touch").
Bit 13 is not narrowed there either; it asks the table for itself in P5eFamilyIsLive, because
bit 13's conjunct is bit 13's own. The ROWS come from one place; WHO each gate speaks for is
still each gate's.

ONE BEHAVIOUR IMPROVEMENT FALLS OUT rather than being designed in: the texture family is the
only row with two required bits and used to be two guarded calls, so a mask missing bit 11 but
carrying bit 7 was reported with the bit-7 sentence. One call now computes required & ~mask
from the row, so the refusal names whichever bit is actually missing.

WHAT THIS COSTS, said out loud. SubsystemDepsTest used to compare two COPIES of the rule; it
now compares two READERS of one table and cannot catch a typo in a row. It is kept and the
.def says why: both sides still fold in reasons of their own - the server's own family bit and
D-C3's wire-caps refusal, the client's consumer and wired conjuncts - and those can still
disagree at a mask. The compile-time half of D2's red-once (drop the fourth row from the .def
-> MGPipe.h:225/:230 fail) is untouched and is now the load-bearing one.

SubsystemDepsTest 5/5, TextureEmit's D-K2 case green, unit lane 2418/2418.
G1: .text 0xa52203, nm --defined-only 30570 symbols, 0 added / 0 removed, -D 0/0. Every line
is inside #if MOBILEGL_PIPE_PUSH or #if MOBILEGL_BUILD_DISAGGREGATED, and MGPipe.h's new
accessor is inline constexpr and unreferenced in the pull build.
2026-09-22 18:10:56 -04:00
swung0x48 ba81453f19 [MG_Backend, MG_IntegrationTest, docs] (P7): the readback family runs on the split arms, and it was already green
P3b/P4b espryt D1 slice 4, landing under the P7 program (notes/p7/PLAN-PH-P34B-P7.md
§1.2 row "回读 / pack state", §3 wave 2-D).

(a) THE CENSUS, AND IT CAME BACK CLEAN. Twenty-two cases per arm now run on the three split
arms: the two F1 pack-state cases, PixelStoreSweepScenario, FramebufferChurnScenario,
PackedWordReadbackScenario, LayeredTextureReadbackScenario and
DepthStencilReadbackMatrixScenario. The row expected reds and said so ("Expect reds - that
is the point"); the first run was 65/65 before any of this commit's code changes. That is
the finding: the 回读 half was assumed open because nothing had ever run it under a
transport, and the assumption was what was wrong - ScatterTightReadbackIntoPackState and the
server's neutral-pack read were already carrying it. No case needed a fix and no case needed
a named deferral.

DepthStencilReadbackMatrixScenario gets a per-arm block of its own, because the registration
macro cannot carry MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1 and without that pin the
scenario is unfalsifiable here: ES has no core depth/stencil readback, Mesa accepts the
reads anyway, and on llvmpipe every case goes green through a path the device does not have.
Its SeparateDepthAndStencilAttachmentsAreBothReadable SKIPS on all three arms and on monolith
too, naming the driver reason itself - a driver fact, not a transport one.

(b) THE TWO std::abort EMULATIONS, checked rather than assumed. copy-image-shadow-mirror
ALREADY cannot abort: MirrorCopyImageIntoDestinationShadow returns under a transport before
reaching MGPipeUnmigratedEmulation, and has since P5b package i1. get-tex-image-shadow is
retired here, and the reason is that its own premise did not hold: P4a wrote "a split server
holds no shadow to convert", but the level storage this converts IS the server's own, fed by
the staged texture store. When MapMipmapData answers, the conversion reads bytes that exist
on this side. So it converts when the shadow is there and DECLINES BY NAME when it is not
(rule I: MGLOG_E_ONCE + GL_INVALID_OPERATION, destination untouched). std::abort inside a
server answering a client's readback takes the session down for a level it happens not to
hold, and bypasses Session::Fail, which is the funnel the census gate watches. The funnel
keeps its teeth for the other four sites - generate-mipmap-storage,
generate-mipmap-cpu-fallback, generate-mipmap-cpu-filter, texture-remint-pull - which really
do reach into a client address space; fatal_census is unchanged at 79 abort sites because
the abort lives in MGPipeUnmigratedEmulation and that function stays. Stated plainly in the
notes: no case in the armed census reaches this site under a transport (the server answers
GetTextureImage with ReadTextureImageWire, never through the backend's GetTexImage slot), so
the change is defence in depth and has no integration red-once of its own.

(c) THE CASE NOTHING COVERED, and this slice's red-once:
PixelStoreSweepScenario.ReadPixelsIntoAPackBufferHonoursPackStateAndLeavesTheGapsAlone -
glReadPixels into a bound GL_PIXEL_PACK_BUFFER at a non-zero buffer offset with
PACK_ALIGNMENT/ROW_LENGTH/SKIP_ROWS/SKIP_PIXELS all non-default, read back with
glMapBufferRange. It is the only thing that drives EmitTables.cpp's `if (pbo)` branch, the
client servicing the pack PBO itself after the reply lands; the existing sweep case reads
into a client pointer, which is the other half and has unit cover. Every number makes a term
matter: ROW_LENGTH 5 at 4 bpp is 20, which ALIGNMENT 8 rounds up to a 24-byte stride;
SKIP_ROWS 1 and SKIP_PIXELS 2 put the first pixel at image byte 32; the read is issued at
buffer offset 16, so it lands at 48. The whole buffer starts 0xCD and the case asserts both
halves - the rows land where the pack state says, and every other byte is still poison.

Red-once (R-16): dropping just the buffer-offset term from EmitTables.cpp's `start` reds the
three split entries with "readback[at] Which is: '\xCD' (205)" - the destination still poison
because the rows landed 16 bytes early - while the ambient monolith entries pass, since the
driver services its own PBO there. EmitTables.cpp is not part of this commit.

(d) SplitReadbackPackBuffer's comment claimed "The client refuses PACK_BUFFER before
emission". It does not: it KEEPS the PBO and services it after the reply, and a refusal would
have been observable as a GL error. The conclusion (the server must see no pack buffer) was
right and the reason was backwards; rewritten to ID-49's actual split of the work.

Gates: integration-split 225/225 (base 186), integration-spawn 141/141 (base 102),
integration-tcp 144/144, ctest -L unit 2408/2408, integration-magma-split 73/73,
spawn_lane_parity / fatal_census (79 sites, 0 unmarked) / link_ratchet (186, unchanged)
green, G1 .text 0xa52203 with both nm lists byte-identical to ~/w7/p7-before.
2026-09-22 16:41:54 -04:00
swung0x48 96fa569250 [MG_Backend, MG_Pipe, MG_Remote, MG_IntegrationTest, docs] (P7): the XFB writeback event is sliced, so a large capture stops killing the server
P3b/P4b espryt D1 slice 2, landing under the P7 program (notes/p7/PLAN-PH-P34B-P7.md
§1.2 row "XFB scatter", §3 wave 2-D).

Both XFB writeback producers posted ONE unbounded OnBufferWriteback covering the whole
bound range - ScatterCapturedRecords from its staged vector, ReadbackCapturedRanges from a
live glMapBufferRange mapping. ServerOnBufferWriteback turns each into one SEG_EVENT record
of sizeof(EventBufferWritebackHead) + bytes, and RingProducer::Reserve refuses any record
above Capacity()/2 outright - not "when full" but always, because above half a capacity a
record is placeable at some head offsets and not at others and waiting for room would be a
hang. EventRingBytes defaults to 256 KiB, so any capture target wider than ~128 KiB was
Fatal{EventRingOverflow} and the SERVER PROCESS DIED, on a buffer size, with nothing else
wrong. The producers' own Fatal text already said what to do: raise MOBILEGL_IPC_EVENT_KB
or slice the event at its producer, "as the writeback path already does" - which was true
of the client's request path and of nothing on this side.

MG_Pipe now carries the one number, published the way gMGPipeSegmentResolver is published:
gMGPipeEventRingCapacityBytes, installed by ServerSession::Accept beside the four reverse
producers and released at Close only if it is still ours, and
MGPipeBufferWritebackSliceBytes() which turns it into a per-record width - a quarter of the
ring with a 4 KiB floor, the same number and the same reasoning as the client-linkage
BufferWritebackSliceBytes one ring over. The backend can reach neither MG_Remote::Client
(wrong linkage) nor ServerSession (wrong layer), so the hook is the seam; 0 means "do not
slice", which is monolith, where the callback is a direct call with no ring under it.

Both producers loop against ONE helper, XfbWritebackSliceStep, because a second spelling of
the width is how one of them ends up still killing the server. The readback site slices
inside its mapping and still unmaps once; the scatter site slices the staged vector and
keeps the single glBufferSubData whole, since the ES store has no ring and cutting it would
change how many backend calls one application call makes. In-order delivery makes the last
slice's landing imply every earlier one, so the client needs nothing new.

Red-once (R-16): XfbAfterClipDistanceScenario.{AScatteredCapture,APlainCapture}
IntoAMegabyteRangeReachesTheApplication - a 1 MiB bound range, about eight times what one
record can ever be, with the same six capture records; glBindBufferBase over the whole
store is what makes the mirrored range a megabyte. Two cases for the two producers. With
the slicing reverted all six split entries go red on Fatal{EventRingOverflow}
("kEventBufferWriteback needs 1048600 bytes and SEG_EVENT caps ONE record at 131072") -
aborting the test process under inproc, losing the peer under spawn and tcp - while both
monolith controls stay green.

XfbAfterClipDistanceScenario and XfbRepeatedCaptureScenario now join the three split arms
in full; slice 1 could register only OrphanedCaptureTarget* because the rest could not
survive the unsliced event. XfbRepeatedCaptureScenario's gl_NextBuffer compacted-target
shape is the first thing outside the census to exercise the archive-served stride and
varyings (ProgramArchive->Link's xfb fields) on the server, and it passed on all three arms
with no further change.

Gates: integration-split 202/202, integration-spawn 118/118, integration-tcp 121/121,
ctest -L unit 2408/2408, the monolith and split XFB families 120/120, spawn_lane_parity /
fatal_census (79 sites, 0 unmarked) / link_ratchet (186, unchanged) green, G1 .text
0xa52203 with both nm lists byte-identical to ~/w7/p7-before.
2026-09-22 16:41:54 -04:00
swung0x48 faf0a1f902 [MG_Backend, MG_IntegrationTest, docs] (P7): an orphaned XFB capture target keeps its varyings
P3b/P4b espryt D1 slice 1, landing under the P7 program (notes/p7/PLAN-PH-P34B-P7.md
§1.2 row "XFB scatter", §3 wave 2-D).

ScatterCapturedRecords is a read-modify-write of the capture destination's pre-capture
bytes - a gl_SkipComponents hole has to come back holding whatever the application left
there - and under a transport those bytes are the server's staged shadow rather than a
frontend MappedData(). The split arm read a null shadow as "this target has no
pre-capture bytes, discard it". A null shadow is not an error: MGPResourceDesc's
HasDefinedContent == 0 is the store the application ORPHANED with glBufferData(size,
NULL), and Managers.cpp's own M-3 comment says every byte it has not staged since is
undefined by its own declaration. So the continue threw a whole capture away for the
ordinary streaming idiom (orphan, upload part of it, capture into it), leaving the
application's buffer holding its pre-draw bytes under GL_NO_ERROR. The partly uploaded
shape did not even reach the continue: it has a shadow, so it asked RequireStagedCoverage
for the whole bound range and died on Fatal{StageSnapshotTooNarrow,
"xfb_scatter_pre_capture"}.

Monolith has always been right here and is right by construction: `Vector<Uint8>
staged(rangeBytes)` is value-initialised, so scattering over an unreadable source scatters
over zeroes and uploads them, which is byte for byte what a fresh MappedData() hands back.
The split arm now skips only the Memcpy, never the target. Discard-with-log is kept for
splitResource == nullptr alone, which really is impossible and has no backendId to upload
into either. RequireStagedCoverage is guarded by ResourceContentIsDeclared, exactly as
Managers.cpp's pool-reuse ladder guards its own whole-store MGL_SERVER_STAGED_REQUIRE: a
declared store still Fatals on a coverage gap, an orphaned one does not.

ResourceContentIsDeclared moves out of Managers.cpp's anonymous namespace to namespace
scope beside RequireStagedCoverage and is declared in Managers.h. The two callers must not
be able to answer "did the application declare these bytes" differently. Both live inside
#if MOBILEGL_BUILD_DISAGGREGATED, so the pull build compiles neither.

Red-once (R-16): XfbAfterClipDistanceScenario.OrphanedCaptureTarget{WithAPartialUpload,
WithNoUploadAtAll}KeepsItsVaryings, registered on the three split arms under the Xfb.
prefix. With the fix reverted the six split entries go red - the partial-upload three on
Fatal{StageSnapshotTooNarrow, "xfb_scatter_pre_capture"} ([0, 696) of a buffer whose
staged coverage does not include it), the whole-orphan three on a captured varying that is
still zero - while the two ambient monolith entries pass, which is what makes it a
transport fact. Details and the lane numbers in docs/Disaggregated/notes/p34b/espryt-d1.md.

The narrow filter is deliberate: the rest of XfbAfterClipDistanceScenario is a DirectVulkan
allocation-pattern regression that has never run under a transport, and it joins the arms
in slice 2 once the writeback event it posts is sliced.

Gates: integration-split 188/188 (186 + 2), integration-spawn 104/104, integration-tcp
107/107, ctest -L unit 2408/2408, the monolith DirectGLES XFB + readback families 66/66,
spawn_lane_parity / fatal_census / link_ratchet (186, unchanged) all green, G1 .text
0xa52203 with both nm lists byte-identical to ~/w7/p7-before.
2026-09-22 16:41:54 -04:00
swung0x48 e3bb6d8b82 [MG_Pipe, MG_Backend, MG_Impl] (P6/sm, P6/st): four guards that were compiled in and could never arm, and the crash behind one of them
CONTRACT-P6 3.2 (D1c), 3.3 (D13) and 3.4 (D10).

Every guard below asked a question a SERVER PROCESS answers wrongly - "is a
ClientSession active" (never, there) or "am I on the apply thread" (every
thread is, there) - so all four were present, compiled, and permanently
disarmed in the one shape they exist for. That is worse than absent: it reads
as coverage.

D13 FIRST, THEN THE WIDENING. MGPipeRoleSplitActive() meant "the REHEARSAL
knob is on AND the transport is real", so widening it in place would have left
a narrow name describing a wide predicate. It is renamed to
MGPipeRoleSplitRehearsalActive() across its 12 sites - including the
DualBlockScenario assertion message that embeds it - and the wider predicate
is introduced BESIDE it.

MGPipeBlocksAreDistinct() IS TRUE FOR TWO DIFFERENT REASONS, which is the
whole point: under the rehearsal the two blocks are distinct objects in one
process; under SPAWN they are distinct because they are in different ADDRESS
SPACES, and no knob is involved. The spawn retrace runs with
role-split-state=0 - measured off the lane's own log - so every guard gated on
the rehearsal was off there.

THE CRASH. MGPipeServerBlockNoteIdentity early-returned on the rehearsal, so a
spawn server's gPipeInputs.ContextIdentity() stayed nullptr for the life of the
process. DirectGLES' framebuffer binding-slot memo compares that identity FIRST
and with NO generation, against a cache variable that also starts nullptr - so
nullptr == nullptr reads as a CACHE HIT and the function returns a slot pointer
nothing ever filled. `sm` widens the gate; `st` lands the named Fatal
PipeInputs.h had already asked for, so a reopening says which memo and which
identity instead of corrupting memory three frames later. Guarded on
MOBILEGL_PIPE_PUSH: in a pull build that identity is the live GLContext's
address and is never null, so the test would be dead code and G1 measures that.

DISJUNCTS, NOT SUBSTITUTIONS - and this was measured the hard way. The first
attempt REPLACED OnApplyThread() with MGPipeServerArm() and
ClientSession::Active() with MGPipeSessionLive(). 14 RemoteGuards and
RemoteClientControls cases went green by omission: a ServerLoop fixture with no
client session has only the original facts, so swapping them out disarmed every
test that drives the apply thread directly. The originals stay; the new
predicates are added beside them.

AND THE PROBE HAD TO BE THE RIGHT ONE. Installing
ClientSession::ApplyThreadIsInsideApplier as the apply-thread probe made every
GL-thread call a role violation whenever the applier happened to be busy - the
inproc retrace died on Fatal{RoleViolation, "MGPipeSlots"} raised from the GL
thread. "Am I the apply thread" and "is the apply thread inside the applier"
are different questions; the second is R-1's mutual-exclusion probe and is
about another thread entirely. ServerLoop::OnApplyThread is the one the guards
mean.

Three of D1c's six predicates land here, the three the four guards need. The
other three (MGPipeSplitActive, MGPipeShouldDial, MGPipeBackendIsLocal) have no
D10 site and are not added as dead code; their call sites are ContextEpoch and
ActiveXfbState, which this commit does not touch.

D14 holds: the non-disaggregated build gets constexpr false definitions, not
runtime-false predicates, so every consult folds at compile time. G1
re-measured - 0 symbol diff, .text unchanged at 0xa52203.

Two test names added, both falsifiable by construction: the spawn case asserts
the blocks are distinct with the rehearsal knob OFF, and the monolith case
asserts the predicate is not simply a constant. Unit suite 2353 tests, one
pre-existing environmental failure. All six OpenRA arms green.
2026-09-21 19:38:19 -04:00
swung0x48 b3b9c2ee12 [MG_Backend] (DirectGLES): the guard comment cites the visibility block it means
Two comments from the previous commit, corrected. The CMakeLists.txt reference in
DirectGLES.cpp pointed at the link libraries instead of the Debug/Release
visibility preset it names (707-719, not 629-641). The Managers.h note said the
overload is "reached by the pull, verify and push build" through "the three
wrappers", which reads as if all three called it; only
VertexArrayImpl::SyncClientSideVertexArraysForFetch does, and what those flavors
compile is the declaration, which is the point being made.

No code change: the four flavors' libraries are byte-identical to the previous
commit's (comment-only).
2026-09-21 04:40:08 -04:00
swung0x48 3e3405e3a8 [MG_Backend] (DirectGLES): the monolith client-array snapshot compiles in every flavor
The indexed and indirect client-memory upload added by 62bfe461 landed inside
`#if MOBILEGL_BUILD_DISAGGREGATED`, and that guard was wrong in the one way a
guard can be wrong without a test saying so: the code exists FOR the monolith
arm - every entry point in the family early-returns when `MG_Config::Transport
!= Monolith` - and the monolith arm is what the pull, verify and push builds
run, not a split-arm fallback. Only the split lane was building the change, so
only the split lane could compile it.

Both CI jobs that build without MOBILEGL_BUILD_DISAGGREGATED were red on the
same twenty diagnostics: `build explicit pull runtime control` (the pull
library retrace's monolith control measures) and `build-linux-verify`
(MOBILEGL_PIPE_VERIFY=ON). DirectGLES.cpp:8437/8455/8521/8537 are the two
indirect executors and :9225/9251/9623/9637/9675/9700 the indexed family,
every one of them in an entry point that compiles in all four flavors, calling
names the pull build had no declaration for:

  error: no member named 'SyncClientSideVertexArraysForIndirectFetch' in
  namespace 'MobileGL::MG_Backend::DirectGLES::VertexArrayImpl'
  ... and SyncClientSideVertexArraysForIndexedFetch, likewise.

The block moves out of the guard rather than being duplicated per flavor, so
the split build's preprocessed text and behaviour are unchanged: the three
wrappers keep their `Transport != Monolith` early return, and in the
transport-free flavors that comparison folds to a compile-time false
(Config.h:566) exactly as it does in every other monolith-only body in this
file.

What the guard also covered, and moves with it:

  * ClientFetchPlan.h - no longer behind `MOBILEGL_BUILD_DISAGGREGATED &&
    MOBILEGL_PIPE_PUSH`. The plan IS the monolith arm's arithmetic (the file's
    own header says so: "the monolith arm asks it to size the per-draw upload"),
    so a pull build that cannot see it cannot answer the question.
  * BackendVertexArrayObject::SyncClientSideAttributesForDraw - declaration and
    definition, the callee of SyncClientSideVertexArraysForFetch.
  * The `#include <MG_Impl/Pipe/ClientFetchPlan.h>` in Managers.h, which had
    been inside the same `#if MOBILEGL_PIPE_PUSH` as the two P3a/P4a includes
    it was appended to.

G1: the three wrappers and the byte reader take internal linkage (`static`),
the spelling the sibling SyncClientSideVertexArraysForDrawArrays already uses
at DirectGLES.cpp:2422 for exactly this reason. Verified on the pull build
(Release/INFO, LTO off, library target only, the monolith-symbol-report job's
own flags): 0 added / 0 removed / 0 resized / 0 renamed, .text +0, and every
one of the new names is LOCAL (`t`/`b`) with 0 of them in the dynamic table.
Had they kept external linkage they would have been 0 in the pull symbol delta
anyway (CXX_VISIBILITY_PRESET hidden) and 4 in a Debug build's dynamic table,
which is the configuration CMakeLists.txt:629-641 switches to `default`.

Verified: build-pull rc=0, build-push rc=0, build-verify
(MOBILEGL_PIPE_VERIFY=ON, DISAGGREGATED=OFF) rc=0, build-split rc=0; split
regressions green under both MOBILEGL_TRANSPORT=monolith and =inproc for
ClientVertexArrayScenario (18/18 each) and DrawRangeElements|MultiDraw (119/119
each), so the client-array behaviour 62bfe461..cb64b62b repaired is not
regressed.
2026-09-21 04:28:33 -04:00
swung0x48 a7d7e2bc44 [MG_Backend] (DirectGLES/DirectVulkan): the stencil-only scratch attachment and the aspect staging buffer's lifetime
Three follow-ups the first pass needed:

DirectGLES glGetTexImage has to attach a STENCIL_INDEX texture at
GL_STENCIL_ATTACHMENT. It went through the colour branch, which left the scratch
READ framebuffer incomplete for a stencil-only texture, so the readback bailed out
before the new stencil branch could run. ScratchFBOImpl::EnsureDepthAttachment2D
gains the stencil-only point; the shadow remembers which of the three points it
holds so a later depth attach is not deduped against it.

DirectVulkan's aspect staging buffer is recorded into the current frame's command
buffer and outlives the glBlitFramebuffer call that records it, so it retires with
the frame. VkBufferManager::DeferRelease is the manager's own deferred-release
entry point and moves to its public section for that.
2026-09-20 21:34:14 -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 cb64b62b57 [MG_Backend] (DirectGLES): both client-snapshot wrappers are monolith-only
The indexed wrapper reads the bound VAO to tell a client index array from an
element-buffer offset, and the indirect one reconciles the command buffer's
shadow - both frontend reads, and under a transport the entry points run on the
apply thread where the client owns those bytes (Fatal{RoleViolation}, which the
split lane aborted on).
2026-09-20 21:03:16 -04:00
swung0x48 92fe12a2f4 [MG_Backend] (DirectGLES): the indexed client snapshot is monolith-only
The backend draw entry points run on the apply thread when a transport is live,
where MGB_CTX->GetBoundVertexArray() is a frontend read the server role must not
make (Fatal{RoleViolation}) - which is what the split lane aborted on. With a
transport the CLIENT owns those bytes and snapshots them from the GL thread, so
the server has nothing to stage.
2026-09-20 20:58:10 -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 ae418e80a0 [MG_Backend] (DirectGLES): the client snapshot's baseInstance shift is the emulated one
The element set a divisor'd client array fetches starts at GL's raw
baseInstance, but the pointer shift that carries it belongs to this backend's
emulation: when the driver applies baseInstance itself (UseNativeBaseInstance),
EmulatedFetchBaseInstance answers 0 and shifting the pointer as well would move
the fetch twice - instance 0 read element 2 instead of element 1, and the
instance that used element 1 read past the snapshot and vanished.

Also drops the two 'continue' statements the indirect executors' lambdas cannot
carry (a lambda is not a loop; 'return' leaves the pass).
2026-09-20 20:50:11 -04:00
swung0x48 62bfe4615d [MG_Impl, MG_Backend] (Disaggregated): the monolith arm fetches the client arrays a draw names
The wire arm snapshots the elements a draw fetches out of the application's own
vertex/index memory (beba0256's MGPipeOwnedDrawInputs). The monolith arm's upload
answered only the non-indexed family's contiguous (first, count) run, so an
INDEXED or INDIRECT draw over a client-memory array staged nothing: the enabled
array kept whatever the ES context last held for it - normally disabled, i.e. every
vertex reading the generic current value - and the draw painted nothing with no GL
error anywhere.

ClientFetchPlan.h is that question factored out once (the fetches, the referenced
vertices, the per-divisor element sets) so both arms answer it from one place;
OwnedDrawInputs now calls it and keeps its behaviour byte for byte.

DirectGLES: SyncClientSideAttributesForDraw stages the sparse snapshot for a draw
whose elements the (first, count) run does not describe, driven by the plan; the
indexed entry points and both indirect executors ask for it, and the arrays path
learns the baseInstance shift a divisor'd client array needs.

W3: DirectGLES' six monolith failures are the four indexed/indirect shapes.
2026-09-20 20:47:50 -04:00
swung0x48 73b37bbe83 [MG_Backend] (DirectGLES): clip view mip generation to the view's layer window
glGenerateMipmap's native arm carries BASE_LEVEL/MAX_LEVEL of the view's own
ES name, so the LEVEL axis was already clipped the way the view describes, but
the layer set it writes is every layer of the storage the view aliases. A
generation through a windowed texture view therefore rewrote the owner's layers
outside the window (F1WireScenario
.GenerateMipmapThroughViewKeepsOwnerLevelsAndLayersOutsideItsWindow).

Both arms now read the window and generate the chain with per-layer blits
between the owner's levels MinLevel + L, over layers
[MinLayer, MinLayer + NumLayers), and leave the native call for the shapes it
already gets right: no view, a view whose layer window covers the whole
storage, a multisample view, and aspects the layer blits do not carry. The
record arm reads the window from the sampler view CSO with the same validation
SyncTextureViewToBackendByRecord uses (Fatal{ProtocolCorruption} on a stale or
foreign record); the monolith arm reads the same numbers off the frontend view
object. ScratchFBOImpl gains EnsureDepthAttachmentLayer for the depth-only
aspect.
2026-09-20 18:47:26 -04:00
swung0x48 c3a5d2f58e [MG_Pipe] preserve image-unit slots and publish named storage-binding programs 2026-09-20 12:41:29 -04:00
swung0x48 39e1692861 [MG_Remote, Espryt] copy image regions through server-owned renderbuffer handles 2026-09-20 12:03:47 -04:00
swung0x48 c1bf7e15c3 [MG_Backend] (Disaggregated): preserve server-owned texture contents during GLES image promotion 2026-09-20 11:11:50 -04:00
swung0x48 a59f005aa4 Merge commit 'bf5ad470' into codex/p5f-final 2026-09-20 00:03:57 -04:00
swung0x48 35ad51d8f7 [MG_Remote, MG_Test] (Disaggregated): P5f fs - keep applier diagnostics within each role 2026-09-19 23:49:45 -04:00
swung0x48 ddf0b5317b [MG_Backend] (Disaggregated): P5f fs - retain separate scratch for paused capture identities 2026-09-19 23:44:59 -04:00
swung0x48 12a9b13894 [MG_Backend, MG_Remote, MG_Test] (Disaggregated): P5f fs - isolate context epochs and server liveness 2026-09-19 23:44:03 -04:00
swung0x48 eeaf14c1f7 P5f fr: refuse frontend identity on every registry apply 2026-09-19 23:35:35 -04:00
swung0x48 203f59ccad [Docs, MG_Backend] (Disaggregated): P5f fe - report carrier retirement and verified gates 2026-09-19 22:20:57 -04:00
swung0x48 31e95b0794 [MG_Backend] (Disaggregated): P5f fe - clarify readback pack-buffer boundary 2026-09-19 22:16:02 -04:00
swung0x48 63c2a331a9 [MG_Remote] (Disaggregated): P5f fe - document handle consumers and capture ordering 2026-09-19 22:14:38 -04:00
swung0x48 19a4400ae8 [MG_Pipe] (Disaggregated): P5f fe - retain live capture snapshots across make-current 2026-09-19 22:13:14 -04:00
swung0x48 a0004db9cb [MG_Pipe, MG_Test] (Disaggregated): P5f fe - derive scalar forwards and ratchet Espryt fatals 2026-09-19 22:02:23 -04:00
swung0x48 881e050e2c [MG_Pipe, MG_Test] (Disaggregated): P5f fe - verify XFB snapshot fields and preserve spans 2026-09-19 21:58:27 -04:00
swung0x48 07bf8d7ce0 [MG_Pipe, MG_Remote, MG_Backend] (Disaggregated): P5f fe - carry XFB capture snapshots across Begin 2026-09-19 21:57:45 -04:00
swung0x48 85f3104731 [MG_Backend, MG_Remote] (Disaggregated): P5f fe - retire existing-carrier Espryt pulls 2026-09-19 21:52:35 -04:00
swung0x48 3c13fa4b4a [MG_Backend] (Disaggregated): P5e ID-124 - the pull and push flavours compile again
Three sites, all pre-existing at 7c6f6886, all the same disease: something guarded by
MOBILEGL_BUILD_DISAGGREGATED used from code that is not. Nothing in the gate built either
flavour, so nobody knew.

- Managers.cpp: StagedTextureTargetForPipeTarget is a pure MGPipeResourceTarget -> TextureTarget
  switch with no wire, no record and no role in it, but it sat inside a nested
  #if MOBILEGL_BUILD_DISAGGREGATED while its only caller MGB_TEXPARAM_TARGET is guarded by
  #if MOBILEGL_PIPE_PUSH alone. Let out of the inner block without moving any text. The narrower
  fix - guarding the macro block too - is WRONG and was tried: that block also carries
  TextureDiagName and MGB_TEXTURE_RECORD_ARM_SELECTED, which the push flavour does need, and
  removing them broke it differently.
- DirectGLES.cpp:5575: the storage-block-signature clause called
  ComputeShaderStorageBlockBindingSignatureOf, declared only under MOBILEGL_PIPE_PUSH, from an
  UNGUARDED condition list. The clause is push-only in substance too - the signature it compares
  is over the override map the wire carries.
- DirectGLES.cpp BindCurrentTextures: keysMatch was declared inside
  #if MOBILEGL_BUILD_DISAGGREGATED while the ternary's else-arm and every use sat outside it.
  The legacy arm becomes a LAZY lambda, not an eager Bool: it reads currentProgram, which is
  null on the handle arm after pa, so the laziness the ternary gave for free is load-bearing
  (ID-81 / ID-110 - the arm decides, and the other arm's reads must not happen at all).

Gate: flavours rc=0 - pull 915 TUs (push define absent), push 929 TUs (push=1), both arms
asserted against compile_commands.json rather than against the flags, because the first version
of this step passed -DMOBILEGL_PIPE_PUSH=OFF beside an option that implies it and reported a
clean 982-TU "pull" build that was a second copy of the split build. The cache entry still read
OFF. Split build unaffected.
2026-09-18 12:07:45 -04:00
swung0x48 d8b9848003 Merge branch p5e/pa into feat/disaggregated (P5e pa) 2026-09-18 12:07:22 -04:00
swung0x48 53b83469e6 [MG_Backend, MG_Remote] (Disaggregated): P5e pa - the draw and dispatch program rows leave the frontend
- PrepareForDraw's single GetProgramForDraw() and PrepareForCompute's
  GetProgramForDispatch() are the 68 red entries this package owns (61 + 7 in the
  base census; 62 + 7 once the PersistentMapArm entry is re-run under its own lane
  environment). Merely CALLING the accessor is what trips MGP_INPUT_CHECK, so the
  row retires by not making the call.
- DrawProgramFromRecords() states the arm ONCE and as a CONJUNCTION:
  ProgramHandleArm() AND TextureImpl::UnitTexturesByHandle(). The hoisted value
  serves four callees and the arm has to name every one of them - the second
  conjunct is BindCurrentTextures', whose legacy memo key and whose
  sampledTargetForUnit lambda both read a real object. The family bits are
  independent A/Bs, so the
2026-09-18 11:47:00 -04:00