Two things land together: the plan and contract draft P6 needs before it starts, and the
working notes that until now existed on exactly one machine.
1. P6's plan and contract draft (neither started; not one line of code)
docs/Disaggregated/P6-SPAWN-PLAN.md the package plan
docs/Disaggregated/P6-CONTRACT-DRAFT.md the contract draft (moves to
MG_Remote/CONTRACT-P6.md when c6 lands)
The three findings that mattered, all written into the documents:
a. P6 is SMALLER than its roadmap cell suggests. The transport primitives are already in
the tree and already tested: SocketDoorbell including hangup detection and the two
lost-wakeup fences (Doorbell.cpp:211-360, four cases in FdPassingTest), SCM_RIGHTS fd
passing, ShmSegment::Adopt, and the SurfaceOp / WindowKind schema already defined in
protocol.fbs. inproc uses a real second shm mapping rather than an alias, and
SessionRings.h:22-28 says that was done so today is not the first time it runs. P6 is
assembly plus a control plane plus a process, not "write a socket transport".
b. The evidence for "P6 is only a transport swap" has EXPIRED. It was bought by P5c, whose
audit found 59 direct memory accesses in a tree that already looked clean. P5e changed
the tree again. So the first package, a6, is a read-only audit that writes no code, and
every contract row that depends on it is marked PENDING a6 rather than decided in
advance.
c. P6 does not get a game running on the phone. ANativeWindow* is meaningless in the child,
and protocol.fbs:186-196's own comment says Android transfers the window out of band -
that band is P12. P6 lands pbuffer / surfaceless / offscreen, which is what HeadlessGL,
the split-equivalent lane and trace replay need, and which is exactly what P6's own exit
gate measures. A real window reaching the server is a named refusal. This gets its own
section because "P6 is done" would otherwise read as "the game runs now".
The contract adds rule G: a role may not name a process-local handle. It extends P5c's
rule E (a role may not name the other role's memory) from memory to identity - EGLDisplay /
EGLSurface / EGLContext / ANativeWindow* / fd number / pid / mapped address either cross as
a token this contract defines or they do not cross, and a descriptor crosses only through
SCM_RIGHTS, where the kernel renumbers it.
What P5e left for P6 gets its own section. The latch semantics are already designed
(ARCHITECTURE.md:504) and are copied unchanged. What is new is that after run-ahead the
client no longer waits while the peer process can DIE: the client holds records that were
published and will never be applied, and Android's freezer suspends the child without
closing anything. So slow and dead must be distinguishable WITHOUT a timeout threshold.
That is P6's only new design; everything else is carriage. It shares a predicate with E1's
outstanding re-specification (ID-122), and the two should be designed together.
Two gates carried over from P5e's lessons: G2 requires integration-spawn's test-name set to
match integration-split's name for name, because a differing set means the lane is quietly
running less; and the arm-proof gate requires every entry to record the child pid and
transport=spawn in its OWN private log - ID-124 established that this kind of false green
really happens, and a spawn lane that actually ran monolith would pass every other gate.
S2's shape must be settled before it is written, and the document says why: E1 is the
cautionary case.
2. The working notes move into the repository (docs/Disaggregated/notes/, 415 .md files)
ROADMAP / CURRENT_STAGE_PROGRESS / MEASUREMENTS cite ruling numbers, audit line numbers and
the provenance of gate figures from ~/w7/notes/, a directory that exists on one machine and
is under no version control. A record cited hundreds of times that lives only locally is not
a record, so it moves in whole, with its directory structure intact.
Only .md moves. Scripts, run logs, census and gate result JSON, patches, APKs and nm dumps
stay where they are: they are evidence DATA rather than documents - large, regenerated per
run, and mostly carrying absolute paths and device serials that mean something only on the
machine that produced them. The mapping and this split are written in
docs/Disaggregated/notes/README.md.
Two exceptions are recorded there too: P6's BRIEF has been promoted to a first-class
document and keeps no copy under notes/, so the two cannot drift apart; and recovered/ holds
material salvaged from failed workflow runs, which may duplicate the like-named phase
directories and is kept because a few of those were the only complete version at the time.
CI's citation lint takes docs/Disaggregated/*.md only (non-recursive), and notes/ is
deliberately outside it: those notes' file:line citations were written against their own
code heads, and resolving them against today's HEAD would fail wholesale - which is not an
error, it is their correct state as a historical record. To check one, use the revision it
declares (--rev).
3. Inconsistencies fixed in passing
- README.md's status line still said P5e was in progress while the stage table already said
closed.
- CURRENT_STAGE_PROGRESS's next-step item 1 still listed the exit gate as pending, though
(a) and (b) were run in the previous commit; rewritten to closed plus the one item that
actually remains (E1).
- ROADMAP's P6 row still had the pre-phase plan in three of its cells; it now carries the
package order, the scope boundary and the two new gates.
Checked with check_doc_citations.py: 125 citations across docs/Disaggregated/*.md, 0 problems.
36 KiB
P4a fable fix round - fablefix-v1 (2026-09-08)
Branch refs/heads/p4a/fablefix, worktree ~/w7/p4a-fablefix, base refs/heads/feat/disaggregated =
4678519f (the complete P4a tree: contract c0..c0g, wire, clientsp v3, clientfb v2, esprytobj v2/v3,
esprytdraw v3 + patches, gates v3). HEAD 6035c9d7, seven commits, not pushed, working tree clean.
Integrator grant across package boundaries for this round (ID-45): the files touched are A's Tracker.h /
PipeMutation.h / DirtySurface.def / scripts/gen_pipe_dirty_surface.py, B's TextureObject.cpp /
RenderbufferObject.cpp, D's Managers.{h,cpp}, E's DirectGLES.cpp, and F's
MG_IntegrationTest/CMakeLists.txt (two added lines - one scenario file, one harness TU; section 4). No F
scenario file was edited; the new scenarios live in Scenarios/P4aSeamAuditScenario.cpp with their own peek
TU Harness/P4aSeamPeek.{h,cpp} (F's PipeApplyPeek.cpp is untouched). The census tool
~/w7/notes/tools/wsl_p4a_refusal_census.sh is edited in place (not a tree file).
0. Verdict in brief
- All four proven seams (F-3, F-1/F-1b, F-2, F-4) and E's SD-4 are fixed, each in its own commit with a
scenario that was RED on the unfixed tree and is GREEN after (section 2 has the red/green transcript per
case; every red was taken on
4678519f+ the tests with the eight behavioural files stashed). - SD-4 is not a reflection gap - it is F-2. A probe (2.5) shows
EmitShaderImagesnever ran with the buffer-image program current: bit 14's plain arm mixed the per-program image-unit COUNTER alone, 0 against no program and 0 against the program, so the null -> program transition moved nothing. The 2D/3D kinds were rescued by an unrelated frontend generation moving between the bind and the dispatch; a buffer texture moves none, which is why only that kind reached E's I2 line. F-2's fix closes it: the five itest cases E named no longer carry the line (census 0 lines / 0 tests on the final tree). - F-1b's wrong picture reproduces exactly as the audit described (black quad, 144/144 pixels, Espryt at
0x1fff; Magma and the legacy arms green) once the sequence has a verb between the bind and the switch and the first program is in use before the first verb. The F-1 fix alone closes it; E's epoch is NOT widened with the bind generation (3.2). - F-3 was wrong on BOTH arms, and the audit's "the legacy arm comes out fresh" held only for a respecify
that re-mints the driver id. A same-carrier redefinition (
GL_SRGB8->GL_SRGB8_ALPHA8on a driver that widens the first) regenerates mutable storage on the same id, which moves nothing the pre-handle FBO memo reads either. Commit 1 fixes the handle arm at the SETTER (the framebuffer aggregate); commit 6 fixes the pre-handle arm for textures (an in-place regeneration takes the generation a re-mint takes); commit 7 compiles that bump underMOBILEGL_PIPE_PUSHbecause G1 (pull library byte-identical to the P4a baseline) caught it as 0/0/2/0 - so every arm of a push build has the fix, the pull build keeps the pre-P4a hole and the texture cases decline by name there. A renderbuffer re-storage on the pre-handle arm is D-D2's documented hole and its case asserts on the handle arm only (2.1, 3.4). - Gates on
6035c9d7: G1 0/0/0/0 (.text10806323 -> 10806323), both untouched-region scripts rc 0 (+ self-test, 8 controls), gen_pipe--checkup to date, dirty-surface--checkand--self-testrc 0 (27 controls), include closure rc 0 (4 probes, both invocations), unit 1772 x 3, G2 2863 == 2863, G14 0 removed / +275, integration-gpu 1091/1091 at 0x1fff, 0x1ff, 0 and on the pull build (DirectVulkan lane 546/546), verify 896/896 with 0 Fatal, retrace 79/79, retrace census 0 on all thirteen needles over 79 logs, itest default-arm census 0 lines / 0 tests on all ten rows and three stems (section 6).
1. Commits (branch p4a/fablefix, from 4678519f)
| # | hash | what |
|---|---|---|
| 1 | dcc31e95 |
F-3 (handle arm): the framebuffer aggregate moves from TextureObjectBase::PipePublishDescriptor / RenderbufferObject::PipePublishDescriptor; + the scenario file with the three F-3 cases, its peek TU, two CMake lines, two TrackerAggregates unit cases |
| 2 | 0d01405c |
F-1 / F-1b: bit 12 mixes opaqueUnits (lifetime id x link version x backend state version; stageLinks x per-stage backend state under a pipeline) and the params aggregate; DirtySurface.def's UseProgram row gains NEW_SAMPLER_VIEWS (+ mark); gen_pipe_dirty_surface.py control 21 reads the mark list; two TrackerWalk unit cases; the F-1 scenario |
| 3 | e4579ee8 |
F-2 / SD-4: bit 14's plain arm mixes shader (lifetime id x link version) into programImages; UseProgram row gains NEW_SHADER_IMAGES (+ mark); one TrackerWalk unit case; the F-2/SD-4 scenario |
| 4 | ccb7b6b3 |
F-4: SamplerImpl::ResolveSamplerCsoTwin (Managers.{h,cpp}); the record arm and the program pass bind it (DirectGLES.cpp); SyncToBackend accepts a CSO twin (null object + handle); the F-4 scenario |
| 5 | 72b4c91a |
docs: the record-field -> setter -> shutter table at Tracker.h's shutter block; the bit-11 note |
| 6 | 419f9941 |
F-3 (pre-handle arm): an attachable object's storage redefined IN PLACE bumps g_attachmentBackendIdGeneration (texture twin's regeneration of mutable storage, renderbuffer twin's re-allocation); the renderbuffer case asserts on the handle arm only. Found by the F-3 scenarios on the 0x1ff / 0 / pull lanes of gate 1 |
| 7 | 6035c9d7 |
commit 6's two bumps compiled under MOBILEGL_PIPE_PUSH; the two texture cases decline by name on the pull build. Found by G1 on gate 2 (SyncMipmapsToBackend +16, BackendRenderbufferObject::SyncToBackend +13 = 0/0/2/0, rc 1) |
Seven single-line [Type] (Scope): description messages; no attribution trailer (git log --format=%B 4678519f..HEAD | grep -ciE 'co-authored|claude|assistant' = 0). Commits 6 and 7 are what the gates found
in the round's own work and are kept as their own commits rather than folded into 1, so the history says
who found what.
2. Seam by seam: root cause, change, scenario, red/green
The RED column is the run on 4678519f + the new tests with every behavioural change stashed
(git stash push -- <8 fix files>, build-push, ctest -R 'P4aSeamAudit|Tracker(Aggregates|Walk)', stash
pop); the GREEN column is the staged commit's own run and the final tree's (every lane, section 6).
2.1 F-3 - an attached object's storage redefinition never reached the framebuffer record (commits 1, 6, 7)
Root cause, handle arm. set_framebuffer_state inlines each attachment's InternalFormat,
TextureTarget, extent, Samples and Complete at emission (D-C1, FramebufferEmit.h
MGPipeBuildSurface). The setters that change those - TextureObjectBase::SetInternalFormat /
SetSamples / SetFixedSampleLocations, TextureObjectWithOneMipmap::AllocateStorage /
TruncateMipmapLevels, the cube, view and buffer-texture variants, and
RenderbufferObject::SetInternalFormat / AllocateStorage / SetSamples - bump the two TEXTURE aggregates
(or, for a renderbuffer, nothing at all) and never the attachment aggregate bit 11 reads. So
glTexImage2D(RGB-class); attach; draw; glTexImage2D(RGBA-class); draw left the framebuffer record on the
first format while the resource record moved, and Espryt's handle arm computed its four cross-object masks
(Managers.cpp IsSnormFallbackSurface / IsUnormFallbackSurface / IsAlphaWidenedColorSurface /
IsIntegerColorSurface, consumed in BackendFramebufferObject::SyncToBackend's Draw-target block) from
the stale surface.
Root cause, pre-handle arm (gate 1 on commit 5: the same three cases red at 0x1ff, 0 and on the pull
build, alpha 1.0 / red 0.25). The FBO twin's memo re-enters SyncToBackend on a frontend framebuffer
version or on g_attachmentBackendIdGeneration, which RecreateBackendTexture bumps when a respecify
RE-MINTS the driver id. A redefinition that keeps the id - BackendTextureObject::SyncMipmapsToBackend's
needsRegeneration path regenerating MUTABLE storage in place, BackendRenderbufferObject::SyncToBackend
re-allocating behind the same name - moved neither, so the masks of the storage the object was attached
with stayed. The audit's reading that the legacy arm "re-reads the frontend" was true of the re-minting
case only.
What changed.
- Commit 1:
MobileGL/MG_State/GLState/TextureState/TextureObject.cppTextureObjectBase::PipePublishDescriptor()(push-only, the one funnel every storage-defining entry point takes - the comment atAllocateStoragealready said so) now also doesMGP_NOTE_AGGREGATE(FramebufferAttachment);MobileGL/MG_State/GLState/RenderbufferState/RenderbufferObject.cppPipePublishDescriptor()- the same bump, the D-D2 paragraph rewritten (its "widening the shutter would fire the framebuffer emission on an unrelated renderbuffer write" was the seam);MobileGL/MG_Pipe/PipeMutation.h- theFramebufferAttachmentenumerator's comment names the new bump sites.Tracker.hbit 11's shutter is unchanged (it already reads the attachment aggregate); its "a renderbuffer respecify is still invisible here, and deliberately so" paragraph is replaced in commit 5. - Commit 6:
MobileGL/MG_Backend/DirectGLES/Managers.cpp- inSyncMipmapsToBackend'sif (needsRegeneration)block,if (m_isInitialized && !m_backendStorageImmutable) ++FramebufferImpl::g_attachmentBackendIdGeneration;(a first definition is not a redefinition; a redefinition that went throughRecreateBackendTexturehas already bumped); inBackendRenderbufferObject::SyncToBackendbeforeBind()/ the allocation,if (m_isInitialized) ++....Managers.h: the generation's comment now says it counts "did I change something under an attachment point that no frontend version can tell the framebuffer about", of which the re-mint is one case. - Commit 7: both bumps inside
#if MOBILEGL_PIPE_PUSH(3.4 says why); the two texture cases gate their alpha / DST_ALPHA assertion onPeekEsprytFramebufferHandleArmIsLiveRETURNING true (it returns true exactly where it could look, which for a case that already skipped off Espryt means "a push build") and decline by name on the pull build, printing the value they read. - Unit:
TrackerAggregates.ATextureStorageDefinitionMovesTheFramebufferAggregateToo(both aggregates move, the other four do not) and...ARenderbufferStorageDefinitionMovesTheFramebufferAggregate(ExpectOnly); pull skip twins added, G2 stays 0.
Why the setter and not the shutter. The task offered "mix the generation that texture/renderbuffer
respecify moves" into bit 11. For textures that would be the content + params aggregates, which fire on
every glTexSubImage2D and every glTexParameter - MC 26.3 does 185 uploads a frame - and each fire costs
the two 304-byte record builds and hashes in EmitFramebufferState; and for renderbuffers there is no
generation at all to mix. Bumping the aggregate the emitter's bit already reads from the one storage funnel
fires bit 11 exactly when an inlined field can have moved, costs nothing per frame, adds no counter to
either object (G1), and is the literal reading of the rule: the setter of a record field moves a shutter the
emitter reads. The dirty-surface derivation is unchanged by it (the aggregate hop is credited to callers
through MGPipeNoteAggregate's switch; no row names these object setters): --check rc 0, --self-test
all tripped at every stage.
Scenario (P4aSeamAuditScenario, DirectGLES only - the masks are Espryt's substitution machinery, the
same reason SnormAttachment / ThreeChannelAttachment skip on Magma): the format pair is GL_SRGB8 <->
GL_SRGB8_ALPHA8, because on llvmpipe GL_SRGB8 is only renderable through Espryt's three-channel
widening (GL_RGB8 is native there and would not carry the mask).
| case | sequence | RED on 4678519f (push 0x1fff) |
RED on commit 5 at 0x1ff / 0 / pull | GREEN on 6035c9d7 |
|---|---|---|---|---|
ATextureRespecifiedWhileAttachedReachesTheFramebufferRecord |
SRGB8 tex, attach, draw a=0.25, respecify SRGB8_ALPHA8 while attached, draw a=0.25, read alpha | :328 alpha 1.0 != 0.25 (the stale widened mask kept masking alpha off) |
alpha 1.0 (in-place regeneration, memo never re-entered) | 0x1fff / 0x1ff / 0 pass asserting; pull passes declining by name, "alpha read 1" |
ARenderbufferRestoragedWhileAttachedReachesTheFramebufferRecord |
the same with glRenderbufferStorage |
:389 alpha 1.0 |
alpha 1.0 | 0x1fff passes asserting; 0x1ff / 0 / pull pass declining by name (D-D2 hole, pre-P4a code) |
ATextureRespecifiedToThreeChannelsWhileAttachedReachesTheFramebufferRecord |
SRGB8_ALPHA8 -> SRGB8 while attached, then GL_DST_ALPHA blend of white must read 1.0 | :462 red 0.25 (the stale not-widened mask let the draw write 0.25 into the carrier) |
red 0.25 | 0x1fff / 0x1ff / 0 pass asserting; pull passes declining, "red read 0.25098" |
A decline is a printed [ P4aSeamAudit ] ... DECLINED ... line plus RecordProperty("p4a_seam_white_box", "declined"), the shape TextureParamsWithoutASamplerViewScenario.cpp argues for; the public half (the draw
lands, green channel 1.0) is asserted on every arm.
2.2 F-1 / F-1b - the view set did not follow the program, and E's epoch could not see it (commit 2)
Root cause. EmitSamplerViews resolves the set for GetProgramForDraw() and memoises the resolution
on (lifetime id, link version, backend state version); bit 12's shutter was Mix(textureContent, bindGeneration) - no program input, no params input. glUseProgram(P2) moved nothing, so
BoundSamplerViews stayed P1's; and E's UnitBindingsEpochFromRecords (the two set serials) then never
rebuilt g_unitTextureSyncList, so a texture bound to an EMPTY slot of an already-touched unit under P1 was
never SyncTextureObjectToBackend'd for P2 - ResolveAndBindUnitTextures found no twin and cleared the
native target (probe, 2.5: resolve unit=1 name=2 twin=0).
What changed (MobileGL/MG_Impl/Pipe/Tracker.h): a new walk local opaqueUnits = the emitter's memo
key - Mix(shader, program->GetBackendStateVersion()) on the plain arm, Mix(stageLinks, stageOpaque)
under a pipeline where stageOpaque mixes each stage program's backend state version (the sampler-unit
assignments reach the composite through the uniform mirror) - and bit 12 = Mix(Mix(Mix(textureContent, textureParams), bindGeneration), opaqueUnits). The params aggregate is the second F-1 input:
SamplesAsIncompleteTexture reads the effective sampler's filters, so a glTexParameteri(MIN_FILTER) that
completes a texture fired bit 13 and left the view entry null. DirtySurface.def: X(UseProgram, NEW_SHADER|NEW_SAMPLER_VIEWS) with the matching undecided mark (the same taint as bit 6's -
DestroyProgramSlot writes a non-m_ member); gen_pipe_dirty_surface.py control 21 now reads the mark
list instead of counting two, so a row that gains a bit is counted (marked_pairs must each appear as an
"UNDECIDED answer for " line and the count must match). Unit:
TrackerWalk.AProgramSwitchAloneFiresTheSamplerViewBit,
TrackerWalk.ATextureParameterAloneFiresTheSamplerViewBit.
Scenario ATextureBoundToAnEmptySlotUnderOneProgramIsSampledByTheNext (both backends): P1 samples
unit 0 (red), unit 1 is touched through its 3D slot; P1 in use BEFORE the first verb; draw; bind green to
unit 1's EMPTY 2D slot; draw with P1 again (the bind's own bit-12 fire resolves under P1 and suppresses);
switch to P2 (samples unit 1); draw; read back. RED on 4678519f: :551 "region should be all green, but
144 of 144 pixels are not; first offender is black" on DirectGLES (Magma green). GREEN after commit 2 on
every lane. Two earlier drafts of this case were green before the fix and are recorded because they say
what the seam needs: with the switch inside the same verb gap as the bind, the bind's fire resolves under
P2; with no program in use at the first verb, the first set is [null, null] and P1's later [red, null]
is a different set that moves the serial (probe: views program=0 ... emit=1 then views program=1 ... emit=1).
2.3 F-2 / SD-4 - the image window did not follow the program (commit 3)
Root cause. Bit 14's plain arm mixed program->GetImageUnitVersion() alone - a counter that only
SetUniformSamplerOrImageUnitIndex moves for an image uniform, so it is 0 for every program that takes its
image units from layout(binding) and 0 against no program. A glUseProgram between two such programs, or
from no program to one, moved nothing, and set_shader_images kept the previous window. That is also E's
SD-4: the probe shows EmitShaderImages running only with program=0 for LoadsTextureBuffer (no OPQ
line at all - the resolver was never asked about the buffer-image program) while LoadsTexture2D got one
program=1 maxImageUnit=0 count=1 emission - rescued by a frontend generation the image-bindable re-mint of
a 2D texture moves between the bind and the dispatch and a buffer texture, which has no storage of its own,
does not. The reflection is fine (isImage=1 unit=0 type=0x9063 for the image kinds that were asked).
What changed (Tracker.h): programImages = Mix(shader, program->GetImageUnitVersion()) on the plain
arm - the identity the pipeline arm already mixed as stageLinks; ImageEmit.h's invariant paragraph is
untouched (the shutter still reads only frontend counters). DirtySurface.def: UseProgram row + mark gain
NEW_SHADER_IMAGES. Unit: TrackerWalk.AProgramSwitchBetweenEqualImageUnitCountersFiresTheShaderImageBit.
Scenario AProgramSwitchWithEqualImageUnitCountersMovesTheImageWindow (both backends; white-box half
declined by name where Espryt's sampler family is not on its handle arm): two BUFFER-image compute programs,
P1 naming unit 0, P2 naming units 0 and 1, both images bound before any dispatch; after P1's dispatch the
applier's window must be Count 1, after P2's Count 2; the public half checks both stores landed (7 and 9).
RED on 4678519f: :607 Count 0 != 1 and :620 Count 0 != 2. GREEN after commit 3. The five itest cases E
named (ImageTargetKind.Loads/StoresTextureBuffer, BufferTexture.AnImageStoreIntoABufferTextureIsVisibleToTheCpu,
the two NonCoreImageFormat buffer cases) no longer carry the I2 line: the default-arm census on the final
tree reads 0 lines / 0 tests for every row (section 6), with all eleven NonCoreImageFormat /
ImageTargetKind names present in the 545-name list it censused.
2.4 F-4 - the record arm's sampler bind could never hit (commit 4)
Root cause. BindCurrentUnitSamplers' record arm did g_backendSamplerObjects.FindByHandle( st.BoundSamplerStates[unit]): a CONTENT-addressed handle (MGPipeSlots().Allocate(SamplerCso) in C's
cache) asked of a table whose every twin was minted off a SamplerObject's lifetime id. One allocator, two
disjoint slot families - a miss on every draw, read by E's S4 comment as a within-draw ordering fact. The
picture stayed right because the pre-handle program pass bound the identity twin (synced from the same
record since S-1); the record arm's only live effect was the unbind of null and out-of-window units. The
audit's white-box reading confirmed it on this tree: unit 0 driver sampler 1, bind_sampler_states handle {6, 0} inside window yes, CSO twin 0, identity twin 1.
What changed.
Managers.h/Managers.cpp(SamplerImpl):BackendSamplerObject* ResolveSamplerCsoTwin(MGPipeHandle cso)- record first (PipeSamplerCsoRecordForHandle; no record is a loud named refusal in the sampler family's census shape), theng_backendSamplerObjects.GetOrCreateByHandle(cso)(the same slot table: one allocator serves both handle families of the kind, so a content slot and an identity slot never coincide; a generation behind the live entry is refused loudly), thenSyncToBackend(nullptr, cso), serial-gated.SyncToBackend's null-object prologue admits a null object beside a live handle (push-only text; the pull build's three lines are unchanged token for token), the two log lines name the handle instead of a GL name, the legacy body returns on a null object it can never receive.DirectGLES.cpp: the record arm bindsResolveSamplerCsoTwin(handle)(S4 retired); the program pass's sampler override binds the SAME CSO twin for a unit inside the received window and takes the pre-handle path (identity twin, handle carried toSyncToBackend) otherwise, so the two arms cannot ping-pong a unit between two driver samplers.- Death: a CSO twin lives until its slot is recycled (the client's LRU eviction frees the slot and the next
handout arrives with a moved generation, which
GetOrCreate(handle)answers by resetting the twin) - bounded by the cache's capacity, never by draw count; there is nodelete_sampler_statehook in the ops table to retire it earlier, and none is added (recorded in the header comment).
Scenario ABoundSamplerObjectIsDrivenThroughItsCsoTwinOnTheHandleArm (both backends): a
CLAMP_TO_BORDER white-border sampler object bound over a REPEAT red texture, sampled at (1.5, 1.5) - the
public half (white wins) passes on every arm; the white-box half (Espryt handle arm only, via
P4aSeamPeek) requires the unit's driver sampler == the twin at the CSO handle, and no identity twin for
the object. RED on 4678519f: :706 CSO twin 0, :710 driver sampler != CSO twin, :713 identity twin 1.
GREEN after commit 4.
2.5 The two probes (temporary MGLOG_E lines, never committed)
- SD-4 (
SamplerEmit.hFor()+ImageEmit.h):LoadsTextureBuffer-> 19 xIMG program=0 link=0 maxImageUnit=-1 ... count=0, noOPQline;LoadsTexture2D->OPQ program=1 loc=0 unit=0 isImage=1 isTexture=0 isOpaque=1 type=0x9063thenIMG program=1 link=1 maxImageUnit=0 programWindow=1 count=1. - F-1b (
DirectGLES.cppsync list / resolve +SamplerEmit.hemitters), on the draft that was green:views program=0 count=2 ... emit=1 u0={0,0} u1={0,0}at the clear, then at the P1 draw after the bindviews program=1 count=2 ... emit=1 u0={15,0} u1={0,0}->syncList ... epoch=3042.../1476...rebuilt ->syncTex name=2. With P1 in use before the clear the first set is alreadyu0={15,0}, the bind's re-resolution is suppressed, and the P2 draw hits the list memo: black.
3. Judgement calls
3.1 F-3 at the setter (2.1, "why the setter and not the shutter").
3.2 E's record epoch is NOT widened with GetTextureBindGeneration()
The audit's F-1b fix offered two shapes: mix the bind generation into E's epoch, or make the serial move on the F-1 fix. The first would re-run the program-independent list rebuild on every redundant re-bind (26.2 re-binds a sampler around every texture-unit switch), which is exactly the walk E's e2 removed. With F-1 fixed the two serials move whenever the PROGRAM-RESOLVED set moves - a program switch, a relink, a sampler-uniform re-assignment, a bind onto a slot the program samples, a completeness flip - and that is the set of events after which a not-yet-synced texture can be SAMPLED. A texture bound to a slot no program samples stays unsynced until something samples it, at which point the set moves; the read-attachment path, the copy-image endpoints and the image sweep have their own sync lists. So the record epoch is narrower than the walk only for textures nothing can read, and the scenario that pins the audit's sequence is green through the F-1 fix alone. Recorded, not changed.
3.3 The scenario file ships its own peek TU (a second CMake line)
Two of the four seams are correct pictures over a permanent silent fallback (F-2/SD-4: the server's
window/high-water union; F-4: the program pass) and have no public-GL observable, so their cases are
white-box. PipeApplyPeek.cpp is F's (gates v3) and this round may not edit it; a scenario TU cannot include
Managers.h beside the GL prototype headers (PipeSlotPeek.h's rule). So Harness/P4aSeamPeek.{h,cpp} is a
new TU of its own - the task said one added CMake line for the scenario; this is two, one per new source
file, in the same list. Its four entry points (PeekEsprytSamplerHandleArmIsLive,
PeekEsprytFramebufferHandleArmIsLive, PeekPipeShaderImageWindow, PeekEsprytUnitSampler) all return
false where they cannot look (pull build, Android, a backend that is not Espryt) and a false teaches the
caller nothing.
3.4 What was NOT changed, and why
- The pull build's in-place-regeneration hole (F-3's pre-handle half on pre-P4a code). Commit 6's two
bumps are Espryt code the pull build shares, and G1 caught them as two resized functions (+32 bytes
.text). The charter's G1 is 0/0/0/0 and its meaning is that P4a lands nothing in the pull library, so commit 7 brackets them with#if MOBILEGL_PIPE_PUSH. The pull build therefore still answers alpha 1.0 / red 0.25 to the two texture cases (they say so by name, every run). The right vehicle is a separate[Fix] (Espryt)commit on dev of exactly those twoifstatements (no P4a dependency; the three F-3 cases are its regression test once the#ifis dropped andp4a-before-libMobileGL.sore-baselined) - an integrator decision, not this round's. - A renderbuffer re-storaged while attached, pre-handle arm. On the 0x1ff / 0 / pull lanes the
renderbuffer twin is only reached from inside the FBO walk the memo skips, the frontend setters bump no
version (D-D2) and no framebuffer version sees them, so the commit-6 bump inside
SyncToBackendis never reached before the memo decides; the resource record is what closes it, and the case asserts on the handle arm only. Pre-P4a code, D-D2's documented hole; fixing it on the legacy arm means teaching the FBO memo a renderbuffer version that does not exist today. Not this round's. ImageEmit.h's invariant-2 paragraph ("all three FRONTEND counters"): still true -programImagesis a frontend identity, not a server epoch - so the sentence stands.DirtySurface.def'sBindProgramPipelineObjectprose predates c0d (recorded by the audit): prose answer, no derivation checks it, not this round's.- No
delete_sampler_stateops hook for the CSO twin (2.4). - The two
.Handles-style census shapes inManagers.cppare not reworded (the tool is fixed instead, section 5).
4. Deviations from the charter
- Seven commits rather than five: 6 and 7 are the gates' own findings on the round's work (2.1).
- Two CMake lines rather than one (3.3).
- F-3 fixed at
PipePublishDescriptorrather than inTracker.h's shutter (3.1);Tracker.hgains the comment and the table only. scripts/gen_pipe_dirty_surface.pycontrol 21 rewritten to read the undecided-mark list (the hard-coded "two marks" made the self-test red the moment UseProgram gained a second bit);DirtySurface.def's prose for the list updated to match ("Control 21 is what proves every marked row still needs its mark").- The two F-3 texture cases assert on push builds only (3.4, first bullet); the renderbuffer case on the handle arm only.
5. The census tool
~/w7/notes/tools/wsl_p4a_refusal_census.sh gains the two built-in-sampler-CSO shapes whose "has no
applier record" straddles a string-literal line break in Managers.cpp (ResolvePushedBuiltinSampler,
"declining the built-in sampler push"; the border-colour block of SyncTextureParamsToBackend, "so its
border colour cannot be pushed") - matched on the half of the LOG line unique to each - plus a stem line
for the pair ("built-in sampler CSO (two shapes)") and a "TEN SHAPES" comment. Managers.cpp's D-N regions
are untouched (the two sentences are not in them either, but the tool fix was preferred as asked). bash -n
clean; the default-arm run on the final tree is in section 6.
6. Gates on 6035c9d7
Run by one detached script after the commit (fablefix-gate.sh; builds -j 8, CCACHE_BASEDIR=/home/swung/w7,
GLIBC_TUNABLES=glibc.malloc.tcache_count=0). The same script ran on 72b4c91a (gate 1: the F-3 reds on
the legacy lanes) and on 419f9941 (gate 2: G1 0/0/2/0, everything else identical to the numbers below).
| gate | result |
|---|---|
| builds | build-linux / build-push / build-verify rc 0, 0 warnings each |
G1 symbol_report --threshold 0 --fail-on-symbol-set-change --fail-on-added-bytes 0 vs p4a-before-libMobileGL.so |
rc 0: .text 10806323 -> 10806323 (+0), 27811 -> 27811 symbols: 0 added, 0 removed, 0 resized, 0 renamed |
scripts/p3a_untouched_regions.sh 37da3c3a HEAD |
rc 0, byte-identical |
scripts/p4a_untouched_regions.sh 37da3c3a HEAD |
rc 0, depth-stencil-sampling / format-caveat regions byte-identical |
scripts/p4a_untouched_regions.sh --self-test |
rc 0, 8 negative controls all tripped and named |
scripts/gen_pipe.py --check |
up to date |
scripts/gen_pipe_dirty_surface.py --check |
rc 0 |
scripts/gen_pipe_dirty_surface.py --self-test |
rc 0, 27 negative controls all tripped, positive controls OK (control 21 now "every P4a undecided mark - 4 of them - is still needed") |
scripts/check_include_closure.py --compiler clang++ |
rc 0, 4 probes, 0 skipped, 0 problems |
check_include_closure.py --mode text --self-test --require-all --expect-probes 4 |
rc 0 |
unit -L unit |
build-linux 1772/1772, build-push 1772/1772, build-verify 1772/1772 |
| G2 pull-vs-push ctest names | 0 diff lines (2863 == 2863) |
G14 names vs p4a-before-ctest-names.txt |
0 removed, 275 added |
integration-gpu, build-push, MOBILEGL_PIPE_PUSH default (0x1fff) |
1091/1091 |
| integration-gpu, build-push, 0x1ff | 1091/1091 |
| integration-gpu, build-push, 0 | 1091/1091 |
DirectVulkan lane (-R ^DirectVulkan, build-push) |
546/546 |
| integration-gpu, build-linux (pull) | 1091/1091 |
integration-verify, build-verify, -j 4 |
896/896, Fatal{ lines 0 |
retrace (retrace_gate.py --lib build-push/libMobileGL.so -j 4, fixtures re-copied, 0 LFS pointers) |
rc 0, 79 / 79, failed [] |
retrace refusal census over 79 mobilegl.logs |
0 for every needle: the ten shapes ("storage and uploads cannot be driven", "built-in sampler cannot be pushed", "parameters cannot be pushed", "read buffer cannot be pushed", "it is not configured here", "no shader-CSO applier record", "sampler ... has no applier record", "storage cannot be allocated", "declining the built-in sampler push", "border colour cannot be pushed"), "does not describe the binding it names", Fatal{, "cannot be adopted on the handle arm"; the deduped MGPipe: ERROR/FATAL list is empty |
itest default-arm census (wsl_p4a_refusal_census.sh default, 545 names, 525 logs) |
0 lines / 0 tests on all ten rows; stems "no applier record on the handle arm" 0, "no shader-CSO applier record" 0, "built-in sampler CSO (two shapes)" 0; FATAL lines 1050 = the 2 ambient EGL loads x 525 logs, none real |
| the new scenarios (12 cases: 6 DirectGLES + 6 DirectVulkan) at 0x1fff | 12/12; the three F-3 cases 3/3 at 0x1fff, 0x1ff, 0 and on pull (declines by name as in 2.1) |
Baseline for the integration lanes was 1079/1079; the twelve added are the six new cases on both backends.
7. The shutter table
Carried in Tracker.h at the shutter block (commit 5, before // ---- the object-class bits 9..17 ----)
and reproduced here for the P4b brief:
THE RECORD-FIELD -> SETTER -> SHUTTER TABLE FOR THE SEVEN P4a BITS.
THE RULE (P4a fable seam audit, section C.1): every field of every emitted
record names the frontend setter that changes it, and that setter moves a
counter the emitting bit's shutter reads - or the emission is unconditional at
the setter (the resource_* family, set_texture_params). A record field whose
setter moves no shutter input is a stale record with nothing to refuse: c0d
(bit 13 without the bind generation), SD-0 (an image re-bind), F-1 (the
program behind the view set), F-2 (the program behind the image window) and
F-3 (an attached object's storage) were all this one class. DirtySurface.def
cannot catch it - it maps MUTATORS to bits and cannot see that a DERIVED field
depends on a mutator whose row is another family's - so the table lives here,
beside the shutters, and a row is added whenever a record gains a field.
bit 6 create/bind_shader_state, set_draw/dispatch_program (ProgramEmit.h)
fields: Cso, StageMask, GlobalUboSize, the artefact blob refs, the two
bound handles
setters: glUseProgram (m_currentProgram), glLinkProgram (link version),
glBindProgramPipeline / glUseProgramStages (pipeline name +
per-stage {lifetime id, link version})
shutter: lifetime id x link version, or stageLinks under a pipeline
bit 7 the program's bindings (image units, block bindings, uniform write set)
setters: glUniform1i on an opaque uniform (backend state version, image
unit version), glUniformBlockBinding / glShaderStorageBlockBinding
(block binding version), any glUniform* (uniform write set)
shutter: the four per-program counters, x stageLinks under a pipeline
bit 8 set_global_constants: ShaderCso, Version, the default-block image
setters: any glUniform* on the default block (UBO content version),
glUseProgram (lifetime id)
shutter: lifetime id x UBO content version, or stageState
bit 11 set_framebuffer_state: Fbo, Color[8]/Depth/Stencil/ReadSurface
(Res, Kind, InternalFormat, TextureTarget, Layered, Level, Layer,
UploadTarget), DrawBuffers[8], Width/Height/Layers/Samples/
FixedSampleLocations, IsDefault, Complete, Target
setters: glFramebufferTexture*/glFramebufferRenderbuffer, glDrawBuffer(s),
glReadBuffer, glFramebufferParameteri (the attachment
aggregate); glBindFramebuffer (the two binding slot versions);
AND a storage redefinition of an ATTACHED texture or
renderbuffer - glTexImage*/glTexStorage*/glTexBuffer/
glTextureView/glRenderbufferStorage* - because InternalFormat,
TextureTarget, the extent, Samples and Complete are INLINED at
emission (D-C1): those bump the attachment aggregate from the
object's PipePublishDescriptor (F-3)
shutter: attachment aggregate x draw bind version x read bind version
bit 12 set_sampler_views: per unit {View, Texture}
setters: glBindTexture / glActiveTexture (bind generation), a texture's
or a sampler object's parameters (SamplesAsIncompleteTexture -
the params aggregate), an upload that defines a level (content
aggregate), the default texture's image appearing (bind
generation, TextureObject.cpp); AND the program in use -
glUseProgram, a relink, glUniform1i on a sampler uniform (which
unit a uniform's TYPE resolves) - F-1
shutter: content x params x bind generation x opaqueUnits
bit 13 bind_sampler_states: per unit the sampler CSO handle
setters: glBindSampler (bind generation, c0d), glSamplerParameter* /
glTexParameter* (params aggregate + sampling resolution),
glDeleteSamplers (bind generation)
shutter: params x sampling resolution x bind generation
bit 14 set_shader_images: per unit {Res, InternalFormat, Layer, Level,
Layered, Access} over the program's image-unit window
setters: glBindImageTexture (bind generation, SD-0), a texture's
content/params, glUniform1i on an image uniform (image unit
version); AND the program in use - glUseProgram, a relink -
F-2
shutter: content x params x bind generation x programImages
(lifetime id x link version x image unit version)
8. Logs
Every ~/w7/fablefix-* log, ~/w7/fablefix-logs/, ~/w7/fablefix-stage/ and ~/w7/fablefix-census/ are
deleted at the end of the round; ~/w7/retrace-out/p4a-fablefix keeps only summary.txt / summary.json.
The worktree ~/w7/p4a-fablefix stays (branch p4a/fablefix, HEAD 6035c9d7, clean; fixtures are the
copies from ~/w7/pipe under assume-unchanged). The shared scratchpad wsl directory is untouched except
for this round's own fablefix-* scripts.