Files
MobileGL/docs/Disaggregated/notes/p2/scout-render-state.md
T
swung0x48 aba329cc53 [Docs] (Disaggregated): P6's plan and contract draft; the working notes move from a local directory into the repository
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.
2026-09-18 23:31:39 -04:00

52 KiB
Raw Blame History

Scout: render state → CSO + dynamic state (P2 groundwork)

Worktree C:/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL-disagg, branch feat/disaggregated @ e7a6a72f. Every line number below was opened in this worktree at that commit. Paths are repo-relative to that root unless absolute.


1. RenderStateParameters as it is today

File: MobileGL/MG_Pipe/MGPipeValueTypes.h, struct RenderStateParameters at line 239, closing brace line 387. It moved here from MG_State/GLState in P0.5; the MG_State headers now include this file, so every old spelling still compiles (header comment lines 17-25).

1.1 Trip wires around it (MGPipeValueTypes.h:539-544)

539  static_assert(std::is_trivially_copyable_v<RenderStateParameters>);
540  static_assert(std::is_standard_layout_v<RenderStateParameters>);   // offsetof legality
541  static_assert(sizeof(RenderStateParameters) == 1168, "...MGL_RESIDUAL_BLOCK_SIZE and the Espryt spans depend on it");
543  static_assert(offsetof(RenderStateParameters, BlendStates) < offsetof(RenderStateParameters, LogicOp));
544  static_assert(std::tuple_size_v<decltype(RenderStateParameters::BlendStates)> == kMGMaxDrawBuffers);

Supporting element sizes, also asserted in the same file: PerBufferBlendState = 28 (:537), StencilFaceState = 28 (:538), PixelStoreParameters = 28 (:536). kMGMaxDrawBuffers = 8 at :30; MAX_VIEWPORTS = 16 is a static constexpr member at :244 (no storage).

Vector sizes (from MobileGL/MG_Util/Math/VectorTypes.h:16-18 — VecBase is a bare Array<T,N>, :215-224 for the aliases): FloatVec4/IntVec4 = 16, FloatVec2 = 8, BoolVec4 = 4 (Bool is 1 byte), and enum classes default to a 4-byte underlying type.

1.2 Every member, in declaration order, with computed offset/size

Offsets below are derived by hand from the above sizes and align-4 layout; they sum to exactly 1168, which is the static_assert at :541, so the derivation is self-checking. P2 must still compute them with offsetof in MGPipeRenderStateSpans.cpp — scripts/gen_pipe.py:72-74 explicitly refuses to guess them in python.

# Member Decl line Off Size Class today Notes
1 Array<FloatVec4,16> Viewports 255 0 256 dynamic float state since GL 4.1; index 0 is what glViewport/GetIntegerv address
2 Float LineWidth 256 256 4 dynamic VK_DYNAMIC_STATE_LINE_WIDTH
3 Float PointSize 257 260 4 dynamic Espryt head span, ES 1.x only
4 Uint PatchVertices 259 264 4 pipeline in G7 list
5 FloatVec4 PatchDefaultOuterLevel 265 268 16 pipeline in G7 list; baked into synthesized TCS
6 FloatVec2 PatchDefaultInnerLevel 266 284 8 pipeline in G7 list
7 Float PolygonOffsetFactor 267 292 4 dynamic VK_DYNAMIC_STATE_DEPTH_BIAS
8 Float PolygonOffsetUnits 268 296 4 dynamic idem
9 Float PolygonOffsetClamp 272 300 4 neither no backend reads it (grep: 0 hits under MG_Backend/)
10 GLenum ClipOrigin 276 304 4 neither getter-only; only reader is MG_Impl/GLImpl/Getter/GL_Getter.cpp:1541
11 GLenum ClipDepthMode 277 308 4 neither idem, GL_Getter.cpp:1544
12 Array<PerBufferBlendState,8> BlendStates 280 312 224 pipeline span boundary: offsetof(BlendStates) = Espryt kBlendSpanBegin
13 LogicOperation LogicOp 281 536 4 pipeline span boundary: offsetof(LogicOp) = Espryt kBlendSpanEnd
14 Bool DepthTestEnabled 284 540 1 pipeline
15 DepthTestFunc DepthFunc 285 544 4 pipeline
16 Bool DepthMask 286 548 1 pipeline
17 Array<BoolVec4,8> ColorMasks 290 549 32 pipeline align 1; note the odd offset
18 FloatVec4 ClearColor 293 584 16 dynamic
19 Float ClearDepth 294 600 4 dynamic
20 Uint32 ClearStencil 295 604 4 dynamic
21 FloatVec4 BlendColor 296 608 16 dynamic VK_DYNAMIC_STATE_BLEND_CONSTANTS
22 Array<FloatVec2,16> DepthRanges 303 624 128 dynamic rides VkViewport::min/maxDepth
23 Float SampleCoverageValue 304 752 4 see §2 conflict Espryt-only; SetSampleCoverage bumps the pipeline version but the field is NOT in the G7 list
24 Bool SampleCoverageInvert 305 756 1 idem idem
25 Uint32 SampleMaskValue 306 760 4 pipeline in G7 list
26 Float MinSampleShadingValue 310 764 4 pipeline in G7 list
27 Array<StencilFaceState,2> StencilStates 311 768 56 split .Func/.FailOp/.PassDepthFailOp/.PassDepthPassOp = pipeline; .Ref/.ValueMask/.WriteMask = dynamic — the one member that straddles the split
28 Bool CullFaceEnabled 314 824 1 pipeline
29 CullFaceMode CullFaceModeSetting 315 828 4 pipeline
30 FrontFaceMode FrontFaceModeSetting 316 832 4 Espryt-only Magma never reads it (see §4.4)
31 ProvokingVertexMode ProvokingVertexModeSetting 317 836 4 unhashed pipeline Magma reads it at VulkanRenderer.cpp:12768 but it is not in the hash
32 GLenum LineSmoothHint 320 840 4 neither no backend reader
33 GLenum PolygonSmoothHint 321 844 4 neither
34 GLenum TextureCompressionHint 322 848 4 neither
35 GLenum FragmentShaderDerivativeHint 323 852 4 neither
36 Float PointFadeThresholdSize 326 856 4 neither no backend reader
37 GLenum PointSpriteCoordOrigin 327 860 4 neither
38 GLenum ClampReadColor 330 864 4 dynamic read at VulkanRenderer.cpp:2772; Coverage.def maps it to SetDynamicState
39 GLenum PolygonModeFront 334 868 4 pipeline in G7 list
40 GLenum PolygonModeBack 335 872 4 neither never read by a backend (Espryt pushes GL_FRONT_AND_BACK with the front value at DirectGLES.cpp:2513)
41 Uint32 PrimitiveRestartIndex 339 876 4 per-verb Coverage.def maps it to DrawVbo, not to a CSO
42 Bool ColorLogicOpEnabled 342 880 1 pipeline
43 Bool DebugOutputEnabled 343 881 1 neither
44 Bool DebugOutputSynchronousEnabled 344 882 1 neither
45 Bool DitherEnabled 345 883 1 Espryt-only SYNC_CAPABILITY(Dither, …) DirectGLES.cpp:2112
46 Bool LineSmoothEnabled 346 884 1 neither
47 Bool MultisampleEnabled 347 885 1 pipeline in G7 list (feeds ResolveEffectiveSampleMask) + Espryt :2113
48 Bool PolygonOffsetFillEnabled 348 886 1 pipeline
49 Bool PolygonOffsetLineEnabled 349 887 1 neither
50 Bool PolygonOffsetPointEnabled 350 888 1 neither
51 Bool PolygonSmoothEnabled 351 889 1 neither
52 Bool PrimitiveRestartEnabled 352 890 1 pipeline
53 Bool PrimitiveRestartFixedIndexEnabled 353 891 1 pipeline
54 Bool RasterizerDiscardEnabled 354 892 1 pipeline
55 Bool SampleAlphaToCoverageEnabled 355 893 1 Espryt-only SYNC_CAPABILITY :2114; Magma has no alphaToCoverageEnable at all
56 Bool SampleAlphaToOneEnabled 356 894 1 neither not even in Espryt's macro list
57 Bool SampleCoverageEnabled 357 895 1 Espryt-only SYNC_CAPABILITY :2115
58 Bool SampleMaskEnabled 358 896 1 pipeline in G7 list
59 Bool SampleShadingEnabled 359 897 1 pipeline in G7 list
60 Bool StencilTestEnabled 360 898 1 pipeline
61 Bool ProgramPointSizeEnabled 361 899 1 neither
62 Uint32 ScissorTestEnabledMask 367 900 4 conflict comment :362-367 says it does bump the pipeline version (Espryt turns it into a real glEnable), yet it is a dynamic input on Magma (DynamicTailKey::scissorEnabled) and is absent from the G7 list
63 Array<IntVec4,16> ScissorBoxes 368 904 256 dynamic
64 Uint32 ScissorBoxWrittenMask 380 1160 4 dynamic deliberately beside ScissorBoxes so it lands in Espryt's tail span (:376-379)
65 Uint32 ClipDistanceEnabledMask 386 1164 4 dynamic RenderState.cpp:374-378 documents "deliberately NOT BumpVersions()"

Total = 1168 ✓.

The three Espryt spans therefore are: head [0, 312), blend [312, 536), tail [536, 1168).

1.3 The FramebufferSrgb / DepthClamp storage hole

CapabilityInput::DepthClamp (MGPipeValueTypes.h:182) and CapabilityInput::FramebufferSrgb (:185) exist in the enum and in all three converters (MG_Util/Converters/GLToMG/RenderStateEnumConverter.cpp:261,267; MGToGL/…:261,267; MGToStr/…:243,249) — but RenderStateParameters has no FramebufferSrgbEnabled / DepthClampEnabled field at all.

Consequence chain:

  • RenderState::SetCapability (MobileGL/MG_State/GLState/RenderState/RenderState.cpp:308-385) falls through to default: break; at :381-382 for both — the glEnable is silently swallowed, no version bumps.
  • RenderState::IsCapabilityEnabled (:387-432) falls to default: return false; at :429-430.
  • Under push, MG_Impl/Pipe/PipeFill.cpp:229-235 copies the whole capability array including FramebufferSrgb — the comment at :230 says it "copies today's constant false".
  • Six backend read points are hard-constant false today, all for FramebufferSrgb (DepthClamp has zero backend readers):
    1. MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:2168 (the sRGB-write block in SyncRenderState)
    2. MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp:58
    3. MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp:614
    4. …/VkRenderPassManager.cpp:965-968
    5. …/VkRenderPassManager.cpp:1112
    6. MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp:953
  • This is called out as an open question in MobileGL/MG_Pipe/generated/PipeSpanTable.inc:24-26 and in docs/Disaggregated/ARCHITECTURE.md:189 (the doc's proposal: give both real storage and put FramebufferSrgb on the pipeline side, because it changes the interpretation of the attachment/blend). docs/Disaggregated/ROADMAP.md:18 lists "补 FramebufferSrgb/DepthClamp 存储" as P2 scope.

Note the size trap: adding two Bools changes sizeof(RenderStateParameters) away from 1168 and breaks MGPipeValueTypes.h:541, MGL_RESIDUAL_BLOCK_SIZE (MG_Pipe/MGPipeTypes.h:536, which "only ever goes DOWN" — a growth is a deliberate build break, :529-535), and MG_Test/Pipe/PipeCatalogueTest.cpp:111-112. There are 7 free padding bytes after ProgramPointSizeEnabled at offsets 900… no — offsets 880-899 are 20 packed bools with no padding, and 549+32=581 → 584 leaves 3 free bytes after ColorMasks. Placing the two new bools in that 3-byte hole (i.e. declaring them right after ColorMasks and before ClearColor) keeps sizeof at 1168 but would move them into the tail span and change no offsets — worth checking against the field-order load-bearing note in ARCHITECTURE.md:183.

1.4 The mirror field list

MobileGL/MG_Pipe/PipeFields.def:230-247 — MGP_FIELDS_RenderStateParameters(F) names all 65 members in the same order. gen_pipe.py asserts every direct data member appears here (PipeFields.def:224-228), so a new member without a row here fails the pipe gates. That is the enforcement point for the FramebufferSrgb/DepthClamp addition.


2. Frontend RenderState — every public setter and which version it bumps

Files: MobileGL/MG_State/GLState/RenderState/RenderState.h (180 lines), MobileGL/MG_State/GLState/RenderState/RenderState.cpp (973 lines).

State: Uint16 m_version (RenderState.h:164), Uint16 m_pipelineStateVersion (:171, with the rationale comment at :165-170), RenderStateParameters m_parameters (:172), plus two PixelStoreParameters (:175-176) that live outside the parameter block. BumpVersions() is the private inline at RenderState.h:159-162 — ++m_version; ++m_pipelineStateVersion;.

Readers: GetVersion() :20/.cpp:53, GetPipelineStateVersion() :22/.cpp:57, GetAllParameters() :23/.cpp:61.

Constructor RenderState::RenderState() .cpp:37-51 — all-true ColorMasks, (0,1) DepthRanges; the viewport/scissor all-zero default deliberately means "never written".

2.1 Group A — setters that bump both versions (BumpVersions() / SET_CAPABILITY)

This is the list the G7 consistency test must enumerate on the "pipeline subset hash MUST move" side.

Setter .cpp lines
SetPolygonMode(front, back) 174-179
SetPatchVertices 210-215
SetPatchDefaultOuterLevel 229-234 (bitwise-equal guard, :225-228)
SetPatchDefaultInnerLevel 240-245
SetCapability — the 22 SET_CAPABILITY arms macro 309-314, arms 317-338
SetCapability(Blend) 339-348
SetCapability(ScissorTest) 353-359
SetCapabilityIndexed(ScissorTest, i) 442-453
SetCapabilityIndexed(Blend, i) 455-469
SetBlendFunc 497-513
SetBlendFuncIndexed 523-539
SetBlendEquation 553-565
SetBlendEquationIndexed 572-584
SetLogicOp 595-600
SetDepthFunc 607-612
SetDepthMask 618-623
SetStencilOp 652-664
SetColorMask 671-681
SetColorMaskIndexed 688-692
SetSampleCoverage 778-784
SetSampleMaskValue 794-799
SetMinSampleShadingValue 805-813 (explicit comment: Vulkan bakes minSampleShading)
SetCullFaceMode 882-887
SetFrontFaceMode 893-898
SetProvokingVertexMode 904-909

The 22 SET_CAPABILITY names, in source order (.cpp:317-338): ColorLogicOp, DebugOutput, DebugOutputSynchronous, DepthTest, CullFace, Dither, LineSmooth, Multisample, PolygonOffsetFill, PolygonOffsetLine, PolygonOffsetPoint, PolygonSmooth, PrimitiveRestart, PrimitiveRestartFixedIndex, RasterizerDiscard, SampleAlphaToCoverage, SampleAlphaToOne, SampleCoverage, SampleMask, SampleShading, StencilTest, ProgramPointSize.

2.2 Group B — setters that bump m_version only

Setter .cpp lines
SetViewport (broadcasts all 16) 69-79
SetViewportIndexed 90-99
SetLineWidth 109-114
SetHint 120-132
SetPointFadeThresholdSize 144-148
SetPointSpriteCoordOrigin 154-158
SetClampReadColor 164-168
SetPrimitiveRestartIndex 189-193
SetPointSize 199-204
SetPolygonOffset (delegates to Clamped) 251-258
SetPolygonOffsetClamped 268-277
SetClipControl 283-289
SetCapability(ClipDistance0..7) 360-380 (explicit "deliberately NOT BumpVersions", :374-377)
SetStencilMask 644-650
SetClearColor 699-704
SetClearDepth 710-715
SetClearStencil 721-726
SetBlendColor 732-737
SetDepthRange (broadcasts all 16) 745-753
SetDepthRangeIndexed 759-768
SetScissorBox 918-941 (also flips ScissorBoxWrittenMask; the transition itself counts as a change, :925-939)
SetScissorBoxIndexed 947-962

2.3 Group C — the one conditional setter

SetStencilFunc(face, func, ref, mask) .cpp:629-642: always ++m_version (:640), and ++m_pipelineStateVersion only if Func moved (:636, :641). Comment :633-635: Ref and ValueMask are VK_DYNAMIC_STATE_STENCIL_REFERENCE/_COMPARE_MASK. This is the setter that proves the split is per-field, not per-setter — a G7 test that walks setters must special-case it (drive it twice: once changing only Ref, once changing Func).

2.4 Group D — setters that bump nothing

SetPixelStoreParam .cpp:820-848 (macro :821-825). Pixel-store state is not in the parameter block and moves neither counter; it becomes set_pixel_pack_state (Coverage.def).

2.5 Conflicts the G7 test will surface immediately

The G7 invariant per docs/Disaggregated/ARCHITECTURE.md:186 and generated/PipeSpanTable.inc:15-20 is: pipeline-subset hash moves ⟺ m_pipelineStateVersion moves. Against today's tree that invariant is already false in at least six ways, all in the "version moved but hash did not" direction:

  1. SetSampleCoverage (.cpp:778-784) bumps the pipeline version, but neither SampleCoverageValue nor SampleCoverageInvert is in kMGPipePipelineStateMembers.
  2. SetFrontFaceMode (.cpp:893-898) bumps it; FrontFaceModeSetting is not in the list, and (see §4.4) Magma does not read it at all.
  3. SetProvokingVertexMode (.cpp:904-909) bumps it; ProvokingVertexModeSetting is not in the list. Coverage.def:74-77 already flags this ("Not in ComputePipelineStateHash today even though Vulkan makes it pipeline state; recorded here so the G7 chunk table has to answer for it before it freezes").
  4. SetCapability(ScissorTest) / SetCapabilityIndexed(ScissorTest, …) bump it; ScissorTestEnabledMask is not in the list and is a dynamic input on Magma (DynamicTailKey::scissorEnabled).
  5. Eleven of the 22 SET_CAPABILITY names are absent from the list: DebugOutput, DebugOutputSynchronous, Dither, LineSmooth, PolygonOffsetLine, PolygonOffsetPoint, PolygonSmooth, SampleAlphaToCoverage, SampleAlphaToOne, SampleCoverage, ProgramPointSize.
  6. SetPolygonMode(front, back) bumps it even when only PolygonModeBack moved; PolygonModeBack is not in the list (and no backend reads it).

Directionally the other way there is nothing: every one of the 24 listed members is written only by a Group-A setter (StencilStates is the exception, and its dynamic third is written by SetStencilMask/SetStencilFunc's Ref, which correctly do not bump the pipeline version).

Implication for P2: either the chunk table grows past the 24 hashed members (making the pipeline subset a superset of what Magma hashes), or several Group-A setters must be demoted to ++m_version. Demoting is behaviour-changing for Espryt, which pushes Dither, SampleAlphaToCoverage, SampleCoverage and the scissor-test enable through SYNC_CAPABILITY — but Espryt gates on m_version, not the pipeline version, so demotion is safe there. Decide this before the chunk table freezes.


3. Espryt's consumer — SyncRenderState

File: MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp (10639 lines total). namespace RenderStateImpl spans 1983-2688; void SyncRenderState(Bool forColorClear) is 2024-2687 (664 lines of body). docs/Disaggregated/ROADMAP.md:47 and ARCHITECTURE.md:188 state the contract as "SyncRenderState(693 行)一行未动" — the exact count differs by how the file-scope shadows are counted, but the contract is unambiguous: nothing in 1983-2688 may move. Declaration: DirectGLES.h:225.

3.1 The file-scope shadow state (1984-2017)

1984  static Uint16 g_syncedRenderStateVersion = 0;
1985  static Bool   g_hasSyncedRenderState = false;
1986  static RenderStateParameters g_syncedRenderStateParameters;    // the byte shadow
1987  static IntVec4 g_syncedBackendViewport   = (-1,-1,-1,-1);      // RESOLVED viewport
1990  static IntVec4 g_syncedBackendScissorBox = (-1,-1,-1,-1);      // RESOLVED scissor
1993  static Bool g_syncedSrgbFramebufferWrites = true;
2003  static Bool g_forceFullRenderStateResync = true;               // see 1994-2002
2011  static Uint32 g_syncedColorMaskAlphaWidenMask = 0;             // NOT derivable from the block, 2006-2010
2016  static Array<PerBufferBlendState, MAX_DRAW_BUFFERS> g_dualSourceDeclinedBlendStates;
2018  void InvalidateSyncedRenderState()                             // 2018-2023

InvalidateSyncedRenderState callers: DirectGLES.cpp:3902, DirectGLES.cpp:10079 (MakeCurrent), MG_Test/Framebuffer/FramebufferTest.cpp:1171,1179.

3.2 The version early-out (2028-2046)

Uint16 currentRenderStateVersion = MGB_CTX->GetRenderStateParametersVersion(); at 2028 — this is the only MGPipe accessor call on the hit path. forceFullPush is read and cleared at 2029-2030. appliedWidenMask / colorMaskWidenDirty 2035-2036 (these are not frontend state; they come from FramebufferImpl::g_alphaWidenedDrawBufferMask and from forColorClear). The early-out itself is 2037-2046, and it is "Gate 1 of section 2.3.1" (PipeStats::Gate::EsprytRenderState, 2041-2044).

Under a CSO scheme the server must keep this shape: the working RenderStateParameters block and a Uint16 that increases whenever any chunk is scattered in. MGPBindRenderState carries both counters (MGPipeTypes.h:225-230: Version, PipelineVersion), which is exactly what feeds line 2028.

3.3 The three-segment memcmp (2070-2083)

2070  static_assert(std::is_trivially_copyable_v<RenderStateParameters>, "span memcmp/memcpy below treats the parameter block as raw bytes");
2072  constexpr SizeT kBlendSpanBegin = offsetof(RenderStateParameters, BlendStates);   // 312
2073  constexpr SizeT kBlendSpanEnd   = offsetof(RenderStateParameters, LogicOp);       // 536
2074  currentBytes = (const unsigned char*)&parameters;
2075  syncedBytes  = (const unsigned char*)&g_syncedRenderStateParameters;
2076  headSpanDirty  = !g_hasSyncedRenderState || memcmp(cur, syn, kBlendSpanBegin) != 0;
2078  blendSpanDirty = !g_hasSyncedRenderState || memcmp(cur+Begin, syn+Begin, End-Begin) != 0;
2081  tailSpanDirty  = !g_hasSyncedRenderState || memcmp(cur+End,   syn+End,   sizeof(RSP)-End) != 0;

Rationale comment 2058-2069 (the Blaze3D per-batch GL_BLEND toggle). Note 2064-2066: memcmp can false-DIFFER on padding but never false-match, and the write-back below makes the shadow byte-identical including padding.

The offsetof(BlendStates) < offsetof(LogicOp) assertion in MGPipeValueTypes.h:543 exists solely to keep these two constants ordered.

3.4 The gated blocks, in file order

Lines Gate What it pushes
2085-2097 ungated resolved viewport (MGB_CTX->GetViewport() at 2085, surface-size fallback 2086-2092, shadow compare 2093-2097)
2101-2139 tailSpanDirty SYNC_CAPABILITY macro (2102-2109) × 11 caps (2110-2120) + scissor-test bit 0 (2131-2138)
2141-2161 tailSpanDirty && SupportsClipDistance ClipDistanceEnabledMask → 8 glEnable/glDisable
2163-2175 ungated GL_FRAMEBUFFER_SRGB — reads MGB_CTX->IsCapabilityEnabled(FramebufferSrgb) at 2168 (constant false today)
2177-2187 tailSpanDirty primitive restart → GL_PRIMITIVE_RESTART_FIXED_INDEX
2196-2410 blendSpanDirty the whole blend block, incl. the dual-source-decline path 2200-2264
2412-2422 tailSpanDirty glDepthFunc, glDepthMask, glDepthRangef(DepthRanges[0])
2424-2448 tailSpanDirty stencil: glStencilFuncSeparate / glStencilMaskSeparate / glStencilOpSeparate × 2 faces
2450-2505 tailSpanDirty || colorMaskWidenDirty colour mask (broadcast vs. glColorMaski), widen-mask doctoring 2462-2468
2507-2515 tailSpanDirty polygon mode (NV/ANGLE ext; front value only)
2517-2532 tailSpanDirty clear colour/depth/stencil + glBlendColor
2534-2543 tailSpanDirty glCullFace, glFrontFace
2545-2585 tailSpanDirty resolved scissor (ScissorBoxWrittenMask bit 0 → surface fallback 2571-2577; shadow compare 2580-2584). The long comment 2545-2569 is the scissor_zero_dimension history
2587-2596 tailSpanDirty glLogicOp (null-checked)
2598-2603 headSpanDirty glPolygonOffset
2605-2609 headSpanDirty glLineWidth
2611-2617 headSpanDirty glPointSize (null-checked)
2619-2625 tailSpanDirty glSampleCoverage
2627-2632 tailSpanDirty glSampleMaski
2634-2655 tailSpanDirty sample shading enable + glMinSampleShading

SYNC_CAPABILITY's 11 GL caps (2110-2120): DepthTest, ColorLogicOp, Dither, Multisample, SampleAlphaToCoverage, SampleCoverage, SampleMask, PolygonOffsetFill, RasterizerDiscard, StencilTest, CullFace. Note SampleAlphaToOne is absent.

3.5 The write-back (2657-2686)

2657  g_syncedRenderStateVersion = currentRenderStateVersion;
2662  syncedBytesMut = (unsigned char*)&g_syncedRenderStateParameters;
2663  if (headSpanDirty)  memcpy(syncedBytesMut,          currentBytes,          kBlendSpanBegin);
2666  if (blendSpanDirty) memcpy(syncedBytesMut+Begin,    currentBytes+Begin,    End-Begin);
2677     …then re-stamp the dual-source-DECLINED draw buffers from g_dualSourceDeclinedBlendStates (2677-2680)
2682  if (tailSpanDirty)  memcpy(syncedBytesMut+End,      currentBytes+End,      sizeof(RSP)-End);
2686  g_hasSyncedRenderState = true;

Comment 2658-2661 explains the byte copy (padding included) and 2669-2676 explains why the declined blend states are re-stamped.

3.6 What must NOT move, precisely

  • The const RenderStateParameters& binding at 2056 — the server must hand SyncRenderState a reference to a whole 1168-byte block, not chunks.
  • The Uint16 version read at 2028 and the early-out at 2037-2046.
  • The two offsetof constants at 2072-2073 and the three memcmps at 2076-2083.
  • The memcpy write-back at 2662-2685.
  • The two resolved shadows (g_syncedBackendViewport 1987, g_syncedBackendScissorBox 1990) — those are backend-derived values, unrelated to the CSO split.

The Espryt head/blend/tail split and the pipeline/dynamic split are orthogonal and coexist — ARCHITECTURE.md:188 states this explicitly. In particular a pipeline chunk (BlendStates, ColorMasks, StencilStates) and a dynamic chunk (ScissorBoxes, ClearColor, StencilStates.Ref) both land inside Espryt's tail span, and StencilStates lands in the tail for both halves.

3.7 SyncRenderState call sites (Espryt)

DirectGLES.cpp 2996, 4123 (forColorClear = mask & GL_COLOR_BUFFER_BIT), 6031, 6066, 6693, 6787, 7380, 7426, 7439, 7455, 7471, 7491, 7508, 7528; plus MG_Test/Framebuffer/FramebufferTest.cpp 1238, 1271, 1283, 1323, 1356.


4. Magma's consumer

File: MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp unless noted.

4.1 ComputePipelineStateHash — 4821-4910

Signature (Uint32 colorAttachmentCount, VkSampleCountFlagBits rasterizationSamples) const; declaration VulkanRenderer.h:849-850. Mixer CombinePipelineStateWord at 4776-4778 (boost-style, seed 0x243F6A8885A308D3 at 4848). Enumeration comment 4780-4794 — this is the list scripts/gen_pipe.py:75-99 was copied from, and it names itself as the contract ("any new GL-state read there must be added here").

One bulk fetch at 4829 (const RenderStateParameters& p = MGB_CTX->GetRenderStateParameters();, rationale 4823-4828 — replaced ~17 accessor calls).

Fields hashed, in order:

Lines Field(s)
4831-4840 capabilityBits bits 0-9: CullFaceEnabled, DepthTestEnabled, PolygonOffsetFillEnabled, RasterizerDiscardEnabled, ColorLogicOpEnabled, StencilTestEnabled, PrimitiveRestartEnabled, PrimitiveRestartFixedIndexEnabled, DepthMask, SampleShadingEnabled
4846-4847 bit 10 = effective sample-mask enable, via ResolveEffectiveSampleMask
4852-4856 MinSampleShadingValue, hashed by bits (memcpy), not value
4863 ResolveEffectiveSampleMask(...) word (i.e. SampleMaskValue gated)
4864 PatchVertices
4871-4878 PatchDefaultOuterLevel[0..3], PatchDefaultInnerLevel[0..1], all bit_cast
4879 PolygonModeFront
4880 CullFaceModeSetting
4881 DepthFunc
4882 LogicOp
4885-4891 StencilStates[0] then [1]: FailOp | PassDepthPassOp<<16 | PassDepthFailOp<<32 | Func<<48 — Ref/ValueMask/WriteMask deliberately excluded
4895-4908 per draw buffer i < colorAttachmentCount: BlendStates[i].Enabled, ColorMasks[independentBlend ? i : 0] rgba, then the four factors and two equations

ResolveEffectiveSampleMask is 4813-4819 and reads IsCapabilityEnabled(Multisample) (4816), IsCapabilityEnabled(SampleMask) (4817) and GetRenderStateParameters().SampleMaskValue (4818). Comment 4795-4812 explains the GL-vs-Vulkan rule; both the hash and the payload go through it so they cannot disagree (4811-4812).

Not hashed but pipeline-relevant (already flagged in PipeSpanTable.inc:27-29): ProvokingVertexModeSetting, FrontFaceModeSetting, ClipOrigin, ClipDepthMode.

4.2 GetOrCreatePipeline — 4951 onward; the memo

  • Signature 4951-4958: (mode, program, programObj, transformFlags, vao, renderPassEntry, primitiveRestartEnable).
  • vertexLayoutHash 4976-4977, renderPassHash 4978.
  • Version → hash gate 4985-4995: MGB_CTX->GetPipelineStateVersion() at 4985, then the cached-hash key (version, colorAttachmentCount, sampleCount) (4986-4994). Comment 4979-4984 is the "glViewport must not evict a good VkPipeline" argument.
  • Memo probe 4997-5013: linear scan comparing mode, programHash, vertexInputHash, renderPassHash, pipelineStateHash, primitiveRestartEnable, transformFlags. Gate 5 stats at 5005-5009.
  • Memo storage: VulkanRenderer.h:818-841 — struct PipelineMemoEntry (818-837), kPipelineMemoSize = 8 (838), m_pipelineMemo[8] (839), m_pipelineMemoCount (840), m_pipelineMemoNext (841, round-robin). Cached hash: m_pipelineStateHashVersion (855), m_pipelineStateHashColorCount (856), m_pipelineStateHashSampleCount (860), m_pipelineStateHash (861), m_pipelineStateHashValid (862). InvalidatePipelineMemo() at 881-885 clears all three. Insertion at VulkanRenderer.cpp:5665-5675.
  • Payload build reads (all MGB_CTX->): capabilities 5173-5182 (CullFace, DepthTest, PolygonOffsetFill+DrawModeUsesPolygonFill, RasterizerDiscard, ColorLogicOp+ m_logicOpFeatureEnabled, StencilTest); FBO depth/stencil gating 5186-5199; stencil faces 5200-5201; GetPolygonModeFront() 5203; the payload initializer 5276-5332 with IsCapabilityEnabled(SampleShading) 5288, GetMinSampleShadingValue() 5289, ResolveEffectiveSampleMask 5291, GetPatchVertices() 5295, GetCullFaceMode() 5299, .frontFace = VK_FRONT_FACE_CLOCKWISE hardcoded at 5301, GetDepthMask() 5313, GetDepthFunc() 5318, GetLogicOp() 5319; the tessellation defaults 5355-5356; the per-colour- attachment blend/mask loop 5429-5448 (GetBlendFuncIndexed 5436, GetBlendEquationIndexed 5437, IsCapabilityEnabledIndexed(Blend, i) 5438, GetColorMaskIndexed 5443). PipeStats constant of 15 reads at 5258-5275 (with its own inventory).
  • ResolvePrimitiveRestartEnable 4936-4949 — bulk fetch at 4941, reads PrimitiveRestartFixedIndexEnabled, PrimitiveRestartEnabled, PrimitiveRestartIndex. It is a draw input, not a CSO input (the answer depends on whether the draw is indexed and on the index type; see VulkanRenderer.h:830-835).
  • PipelineFactory::GetOrCreatePipeline (the second, hash-keyed level) is MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp:252-…, declared PipelineFactory.h:132; frame-boundary bookkeeping at PipelineFactory.h:184.

4.3 ApplyDynamicDrawStateTail — 5902-6004, and its shadow

struct DynamicStateShadow is the anonymous-namespace type at VulkanRenderer.cpp:300-397, instance static DynamicStateShadow g_dynamicStateShadow; at 398.

  • Per-state shadows 300-317 (viewport, scissor, blend constants, depth bias, line width, the six stencil words).
  • Whole-tail gate fields 328-332: dynamicTailValid, dynamicTailParamsVersion, dynamicTailExtentX/Y, dynamicTailIsDefaultFbo. Rationale 318-327.
  • struct DynamicTailKey 355-395 with operator== 372-394. This is the ready-made shape of the dynamic chunk set: viewport[4], depthRange[2], blendColor[4], polygonOffsetFactor, polygonOffsetUnits, lineWidth, stencilValueMask[2], stencilWriteMask[2], stencilRef[2], scissorBox[4], extentX, extentY, preTransform, scissorEnabled, isDefaultFbo.
  • The complete input inventory is spelled out at 341-354, one line per reader: ApplyGLViewportState → Viewports[0], DepthRanges[0]; ApplyBlendConstants → BlendColor; ApplyPolygonOffsetState → PolygonOffsetUnits, PolygonOffsetFactor; ApplyLineWidthState → LineWidth; ApplyStencilState → StencilStates[0..1].{ValueMask, WriteMask, Ref}; scissor rect → ScissorTestEnabledMask bit 0, ScissorBoxes[0]. The caveat at 351-354: ApplyLineWidthState also clamps against device limits, which are not in the key.

Function body:

  • multi-viewport arm 5905-5916 (delegates to ApplyMultiViewportDynamicState 5878-5900, which invalidates the single-element shadow at 5896-5899).
  • version gate 5920-5930: MGB_CTX->GetRenderStateParametersVersion() at 5920, compared with extent + isDefaultFbo; Gate 6 stats 5925-5928.
  • value gate: one bulk fetch at 5944, key built 5945-5976, compared 5977-5982 (a hit re-arms the cheap version gate at 5980).
  • the six applies 5983-5997, shadow update 5998-6003.

The five Apply* helpers (each with its own per-state shadow early-out): ComputeGLViewport 451-505 (GetViewportIndexed(index) 464, GetDepthRangeIndexed(index) 469), ApplyGLViewportState 507-521, ApplyBlendConstants 523-543 (GetBlendColor() 524), ApplyPolygonOffsetState 556-568 (GetPolygonOffsetUnits() 557, GetPolygonOffsetFactor() 558), ApplyLineWidthState 570-589 (GetLineWidth() 571, device clamp 572-581), ApplyStencilState 649-677 (GetStencilState(Front/Back) 650-651 → six vkCmdSetStencil* 671-676). Scissor helpers: MakeClampedScissorRect 591-604, MapScissorRectToDefaultFramebuffer 609-617, MakeDefaultFramebufferScissorRect 619-647, ComputeGLScissorRect (…-5869, reads ScissorTestEnabledMask 5860 and ScissorBoxes[index] 5866).

The declared Vulkan dynamic states are exactly eight: MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp:390-397 — VIEWPORT, SCISSOR, BLEND_CONSTANTS, DEPTH_BIAS, LINE_WIDTH, STENCIL_COMPARE_MASK, STENCIL_WRITE_MASK, STENCIL_REFERENCE. Note PipelineFactory.cpp:485 warns against adding VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE.

4.4 Magma facts worth flagging

  • FrontFaceModeSetting is never read by Magma. .frontFace is hardcoded VK_FRONT_FACE_CLOCKWISE at 5301 (and 4427, 4538); a repo-wide grep for GetFrontFaceMode / FrontFaceModeSetting finds only DirectGLES.cpp:2539-2542, GL_Getter.cpp:1632, Core.cpp:1143-1144, Core.h:296 and RenderState.{h,cpp}. The winding is folded into ConvertCullFaceModeToVkEnum(mode, invertClockwise) at 5299 instead. The claim in PipeSpanTable.inc:28-29 that FrontFaceModeSetting is "handled elsewhere in the payload path" is not backed by a read site — verify before relying on it.
  • Magma implements no alphaToCoverageEnable / alphaToOneEnable / depthClampEnable (grep across PipelineFactory.{h,cpp} and VulkanRenderer.cpp is empty). So SampleAlphaToCoverageEnabled is Espryt-only and SampleAlphaToOneEnabled is dead everywhere.

4.5 Other Magma readers of the version / block

  • TrySetupDrawFastPath (SetupDrawSnapshot gate): GetPipelineStateVersion() at 6122, then a bulk fetch at 6131 to check only DepthTestEnabled || StencilTestEnabled against snap.drawUsesDepthStencil (6131-6135); the cached-hash refresh repeats at 6335-6343 and the memo scan at 6347-6353. Snapshot write at 6973 (snap.renderStateVersion = MGB_CTX->GetPipelineStateVersion()).
  • VulkanRenderer.cpp:4061 — index-rewrite path, bulk fetch for PrimitiveRestartEnabled/FixedIndex/PrimitiveRestartIndex.
  • VulkanRenderer.cpp:12019 — multi-draw merge granularity, same two restart bits.
  • Clear path 7269-7271 (GetClearColor/Depth/Stencil), 7207/7210/8621 (GetScissorBox), 7301/7390/7411/7527/7584 (GetColorMaskIndexed), 7328/7370/7513/7602 (GetDepthMask), 7341/7374/7515/7610 (GetStencilState), 7258/7440 (RasterizerDiscard), 7278/7482/8620 (ScissorTest), 6831-6832 (DepthTest/StencilTest), 3449-3450 (restart caps), 2772 (GetClampReadColor), 12768 (GetProvokingVertexMode).

5. How both backends read render state through the P1 accessors

The switch: MobileGL/MG_Pipe/PipeInputsSwitch.h:19-26. Push arm MGB_CTX = &MobileGL::MG_Pipe::gPipeInputs (:19); pull arm = MG_State::pGLContext (:24). The push-arm struct is MobileGL/MG_Backend/MGPipe/PipeInputs.h, the global at :682.

Accessor → storage-field table in PipeInputs.h:84-126 (X(Accessor, member) rows). The render-state ones: GetClearColor→m_clearColor (84), GetColorMaskIndexed→m_colorMask (87), GetDepthRangeIndexed→m_depthRange (92), GetPipelineStateVersion→m_pipelineStateVersion (102), GetPixelStoreParameters→m_pixelStore (103), GetRenderStateParameters→m_renderState (111), GetRenderStateParametersVersion→m_renderStateParametersVersion (112), GetScissorBox→m_scissorBox (114), GetStencilState→m_stencil (115), GetViewport→m_viewport (123), GetViewportIndexed→m_viewportIndexed (124), IsCapabilityEnabled→m_capability (125), IsCapabilityEnabledIndexed→m_capabilityIndexed (126).

Accessor bodies (each MGP_INPUT_CHECK + MGP_INPUT_VERIFY_READ then a plain field read): GetClearColor 249-253, GetClearDepth 254-258, GetClearStencil 259-…, GetColorMaskIndexed 264-…, GetDepthRangeIndexed 294-…, GetPipelineStateVersion 338-342, GetRenderStateParametersVersion 343-347, GetPixelStoreParameters 348-…, GetRenderStateParameters 378-…, GetScissorBox 388-…, GetStencilState 393-…, GetViewport 428-432, GetViewportIndexed 433-441, IsCapabilityEnabled 442-447, IsCapabilityEnabledIndexed 452-…, GetBlendColor 204-208, GetBlendEquationIndexed 209-218, GetBlendFuncIndexed 219-231, GetClampReadColor 244-248. Constants: kCapabilityCount 173, kMaxViewports 174, kStencilFaceCount 176, struct IndexedCapabilities { Bool Blend[8]; Bool ScissorTest[16]; } 180-183.

Fill side: MobileGL/MG_Impl/Pipe/PipeFill.cpp — GetClearColor 90-91, GetClearDepth 93-94, GetClearStencil 96-97, GetColorMaskIndexed 99-101 (loop), GetDepthRangeIndexed 118-120 (loop over 16), GetRenderStateParameters 183-184 (whole-block copy), GetRenderStateParametersVersion 186-187, GetScissorBox 192-193, GetStencilState 195-197 (both faces), GetViewport 221-222, GetViewportIndexed 224-227, IsCapabilityEnabled 229-234 (all caps incl. the constant-false FramebufferSrgb, note at 230), IsCapabilityEnabledIndexed 236-243 (Blend ×8, ScissorTest ×16). Cost note at 448.

Verb fill points: MobileGL/MG_Pipe/FillPoints.def — GetRenderStateParameters + GetRenderStateParametersVersion are filled for kDraw (128-129), kClear (184-185), kBlitOrCopy (208-209), kReadback (257-258), kProgramOp (280-281).

5.1 Complete backend call-site inventory (grep MGB_CTX-><accessor> under MG_Backend/)

Accessor n Sites
GetRenderStateParameters 11 GLES 2056, 3828, 4158; VK 4061, 4818, 4829, 4941, 5859, 5944, 6131, 12019
GetRenderStateParametersVersion 2 GLES 2028; VK 5920
GetPipelineStateVersion 3 VK 4985, 6122, 6973
IsCapabilityEnabled 29 GLES 2168, 3824, 4398, 4399; DirectGLES/MultiDraw.cpp 52, 53; VkClearManager.cpp 58; VkRenderPassManager.cpp 614, 966, 1112; VkTextureManager.cpp 953; VK 3449, 3450, 4816, 4817, 5173, 5174, 5176, 5179, 5181, 5182, 5288, 6831, 6832, 7258, 7278, 7440, 7482, 8620
IsCapabilityEnabledIndexed 1 VK 5438
GetViewport 1 GLES 2085
GetViewportIndexed 1 VK 464
GetScissorBox 3 VK 7207, 7210, 8621
GetDepthRangeIndexed 1 VK 469
GetStencilState 8 VK 650, 651, 5200, 5201, 7341, 7374, 7515, 7610
GetColorMaskIndexed 6 VK 5443, 7301, 7390, 7411, 7527, 7584
GetClearColor / GetClearDepth / GetClearStencil 1 each VK 7269 / 7270 / 7271
GetBlendColor 1 VK 524
GetBlendFuncIndexed / GetBlendEquationIndexed 1 each VK 5436 / 5437
GetLogicOp / GetDepthFunc 1 each VK 5319 / 5318
GetDepthMask 5 VK 5313, 7328, 7370, 7513, 7602
GetCullFaceMode 1 VK 5299
GetProvokingVertexMode 1 VK 12768
GetLineWidth 1 VK 571
GetPatchVertices 3 GLES 2844; DirectGLES/Managers.cpp 7194; VK 5295
GetPatchDefaultOuterLevel 3 GLES 2846; Managers.cpp 7202; VK 5355
GetPatchDefaultInnerLevel 3 GLES 2848; Managers.cpp 7205; VK 5356
GetPolygonOffsetFactor / GetPolygonOffsetUnits 1 each VK 558 / 557
GetPolygonModeFront 1 VK 5203
GetMinSampleShadingValue 1 VK 5289
GetPrimitiveRestartIndex 3 GLES 4404, 4444; MultiDraw.cpp 46
GetClampReadColor 1 VK 2772
GetPixelStoreParameters 6 GLES 6187, 7556, 9043, 9422; DirectGLES/Utils.cpp 2302; VK 10690

Zero backend call sites: GetClipOrigin, GetClipDepthMode, GetHint, GetPointSize, GetPointFadeThresholdSize, GetPointSpriteCoordOrigin, GetPolygonOffsetClamp, GetPolygonModeBack, GetFrontFaceMode, GetSampleCoverageValue, GetSampleCoverageInvert, GetSampleMaskValue — all of these are reached (when at all) only through the bulk GetRenderStateParameters() field reads, or not at all.


6. What already exists for the CSO / spans

6.1 The member list (G7) — the only thing P0 pinned

MobileGL/MG_Pipe/generated/PipeSpanTable.inc (68 lines, generated).

  • Header comment 9-32, including the D-B1 history at 15-20 and the "deliberately absent" list at 22-29 (FramebufferSrgb/DepthClamp storage hole; ProvokingVertexModeSetting; FrontFaceModeSetting/ClipOrigin/ClipDepthMode).
  • kMGPipePipelineStateMembers[] 34-59, 24 names: CullFaceEnabled, DepthTestEnabled, PolygonOffsetFillEnabled, RasterizerDiscardEnabled, ColorLogicOpEnabled, StencilTestEnabled, PrimitiveRestartEnabled, PrimitiveRestartFixedIndexEnabled, DepthMask, SampleShadingEnabled, MultisampleEnabled, SampleMaskEnabled, SampleMaskValue, MinSampleShadingValue, PatchVertices, PatchDefaultOuterLevel, PatchDefaultInnerLevel, PolygonModeFront, CullFaceModeSetting, DepthFunc, LogicOp, StencilStates, BlendStates, ColorMasks.
  • kMGPipePipelineStateMemberCount = 24 at 60, self-check 61-62.
  • extern const MGPStateChunk kMGPipePipelineChunks[]; 66 and extern const MGPStateChunk kMGPipeDynamicChunks[]; 67 — declared, NEVER DEFINED. No MGPipeRenderStateSpans.cpp exists in the tree (grep confirms: only the forward declaration struct MGPipeRenderStateSpans; at MG_Pipe/MGPipe.h:61, and the comments at MGPipe.h:57-60, MGPipeTypes.h:83-85).
  • Generator: scripts/gen_pipe.py — PIPELINE_STATE_MEMBERS list at 75-99, provenance comment 65-74 (copied from VulkanRenderer.cpp:4805-4906 at dev@81b17c0b, explicitly refusing to compute offsets in python), emitter gen_span_table() 1040-1072, written at 1166.
  • Only current consumer: MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp:354-358 (TEST(PipeCatalogue, PipelineSubsetMembersArePinned) — asserts count 24, first CullFaceEnabled, last ColorMasks). The G7 setter-consistency test does not exist yet.

6.2 Wire types (all defined, none used yet)

MobileGL/MG_Pipe/MGPipeTypes.h:

  • struct MGPStateChunk { Uint16 Offset; Uint16 Length; } 86-89, MGP_ASSERT_POD(…, 4) 90; comment 83-85 states the split "is defined exactly once, in MGPipeRenderStateSpans".
  • struct MGPRenderStateDesc { MGPipeHandle Cso; MGPipeHandle BaseCso; Uint32 ChunkMask; Uint32 Pad0; MGPBlobRef Blob; } 215-221, size 48 (222). Comment 213-214: carries ONLY the pipeline subset's chunk bytes; ChunkMask allows an incremental create against BaseCso.
  • struct MGPBindRenderState { MGPipeHandle Cso; Uint16 Version; Uint16 PipelineVersion; } 225-229, size 12 (230). Comment 224: "Steady state: 12 bytes on the wire, no hashing, no blob."
  • struct MGPDynamicState { Uint32 ChunkMask; Uint16 Version; Uint16 Pad0; MGPBlobRef Blob; } 236-241, size 32 (242). Comment 232-235 enumerates the dynamic half verbatim: "viewport, scissor, depth range, blend colour, line width, polygon offset, stencil ref/write mask, clear values, sample coverage, hints and the point-size family".

Note the discrepancy: that comment puts sample coverage and hints in the dynamic half, while SetSampleCoverage bumps the pipeline version (§2.5 item 1). One of the two has to change.

6.3 Calls and handles

MobileGL/MG_Pipe/PipeCalls.def:

 92  X(CreateRenderState,  MGPRenderStateDesc,  kCtxCso,   kHasBlob)
 93  X(BindRenderState,    MGPBindRenderState,  kCtxCso,   kNone)
 94  X(DeleteRenderState,  MGPHandleOnly,       kCtxCso,   kNone)
106  X(SetDynamicState,    MGPDynamicState,     kCtxState, kHasBlob)

Class taxonomy comment 15-50 (kCtxCso = 13 create/bind/delete entries). MGPipeHandleKind::RenderStateCso at MobileGL/MG_Pipe/MGPipeHandles.h:31 (alongside VertexElementsCso 32, SamplerCso 33, SamplerViewCso 34, ShaderCso 35). Call-class enum kCtxCso at MGPipe.h:31.

Field lists for the comparators: PipeFields.def:62-63 (MGP_FIELDS_MGPRenderStateDesc), :65-66 (MGP_FIELDS_MGPBindRenderState); both are in MGP_VERIFY_PAYLOAD_LIST at PipeFields.def:298-299.

6.4 The residual carrier that the CSO retires

MGPipeTypes.h:516-525 — struct ResidualValueBlock whose first member is RenderStateParameters RenderState; (517) with the comment "until create/bind_render_state + set_dynamic_state land", plus PixelStoreParameters Pack (518), CapabilityBits (519), PatchVertices (520), PatchOuter[4] (522), PatchInner[2] (523). #define MGL_RESIDUAL_BLOCK_SIZE 1248 at 536 with the ratchet comment 527-535 (only ever goes DOWN; P13 asserts 0). Carrier call X(SetResidualValueState, MGPResidualValueState, kCtxState, kHasBlob) at PipeCalls.def:126; field list PipeFields.def:147-148. Landing the render-state CSO must delete RenderState from this struct and lower 1248 by 1168 → 80 (and the PixelStoreParameters Pack removal is set_pixel_pack_state, separate). Test that will break: MG_Test/Pipe/PipeCatalogueTest.cpp:111-112.

6.5 Coverage map (which call answers which read)

MobileGL/MG_Pipe/Coverage.def already routes every render-state accessor:

  • → CreateRenderState: GetBlendEquationIndexed (32), GetBlendFuncIndexed (33), GetColorMaskIndexed (50), GetCullFaceMode (51), GetDepthFunc (53), GetDepthMask (54), GetLogicOp (59), GetMinSampleShadingValue (61), GetPolygonModeFront (67), GetProvokingVertexMode (77, with the "must answer for it before it freezes" note at 74-76), GetRenderStateParameters (78), GetStencilState (82), IsCapabilityEnabled (93), IsCapabilityEnabledIndexed (94).
  • → BindRenderState: GetPipelineStateVersion (65), GetRenderStateParametersVersion (79).
  • → SetDynamicState: GetBlendColor (31), GetClampReadColor (46), GetClearColor (47), GetClearDepth (48), GetClearStencil (49), GetDepthRangeIndexed (55), GetLineWidth (58), GetPolygonOffsetFactor (68), GetPolygonOffsetUnits (69), GetScissorBox (80), GetViewport (91), GetViewportIndexed (92).
  • → SetPatchState: GetPatchDefaultInnerLevel (62), GetPatchDefaultOuterLevel (63), GetPatchVertices (64) — note: the patch trio is a separate call, not part of the render-state CSO, even though all three ARE in kMGPipePipelineStateMembers. That is a live inconsistency the chunk table must resolve.
  • → SetPixelPackState: GetPixelStoreParameters (66). → DrawVbo: GetPrimitiveRestartIndex (70).

6.6 Missing entirely

Grep across MobileGL/, scripts/, docs/ finds no CsoCache, no ComputePipelineSubsetHash / MGPipeComputePipelineSubsetHash, no MGPipeRenderStateSpans.{h,cpp}, and no implementation of kMGPipePipelineChunks / kMGPipeDynamicChunks. CsoCache (64 entries, keyed on the pipeline subset) and MGPipeComputePipelineSubsetHash() are named only in the plan: docs/Disaggregated/ROADMAP.md:18 (P2 row) and docs/Disaggregated/ARCHITECTURE.md:186. MobileGL/Config.h:324 mentions "client-side content addressing of CSOs, which is the negative control the CSO …" — the A/B switch the ROADMAP asks for.

6.7 The plan's own design constraints (for reference, docs/Disaggregated/ARCHITECTURE.md:183-190)

  • Whole-block, not three CSOs: RenderStateParameters is a trivially-copyable POD whose field order is load-bearing (ScissorBoxWrittenMask, ClipDistanceEnabledMask are deliberately in the tail); splitting into blend/depth-stencil/rasterizer CSOs would need a hand-maintained ~150-field table with no trip wire (:183).
  • Subset identity, not whole-block content addressing: hashing the whole block would make glViewport/glScissor/glBlendColor/glClearColor mint a new CSO every call and flush the server's pipeline memo — the regression RenderState.h:165-170 records (:184).
  • Dynamic subset enumerated at :185.
  • The split lives in exactly one place: MG_Pipe/MGPipeRenderStateSpans.{h,cpp} chunk table + MGPipeComputePipelineSubsetHash(), shared by client and both backends; G7 walks every public RenderState setter (:186).
  • Server side: one working RenderStateParameters (1168 B) per context; bind and set_dynamic_state each scatter their chunks into it. Espryt gets an unchanged const RenderStateParameters&; Magma's pipeline memo is rekeyed on cso.slot and the dynamic tail still goes through ApplyDynamicDrawStateTail (:188).
  • Client fetch order (:187): pipeline version unchanged → reuse the last CSO handle, zero hashing; changed → xxHash the pipeline subset (~25-30 words, what Magma already hashes) → CSO map probe → hit sends a 12-byte bind, miss sends a create of the changed chunks then a bind; m_version changed but subset unchanged → set_dynamic_state only (~200 B).

7. Open questions the implementers must answer before the chunk table freezes

  1. The six-way G7 invariant violation in §2.5. Either grow the pipeline subset beyond Magma's 24 hashed members, or demote the offending setters to ++m_version. SetSampleCoverage, SetProvokingVertexMode, SetFrontFaceMode, SetCapability(ScissorTest), the 11 unlisted capabilities, and SetPolygonMode's back-face half.
  2. StencilStates straddles the split (pipeline: Func/FailOp/PassDepthFailOp/ PassDepthPassOp; dynamic: Ref/ValueMask/WriteMask). A byte-range chunk table cannot put one array in both halves without sub-member chunks. StencilFaceState is 28 bytes laid out Func(0) Ref(4) ValueMask(8) WriteMask(12) FailOp(16) PassDepthFailOp(20) PassDepthPassOp(24) — so per face the pipeline half is [0,4) ∪ [16,28) and the dynamic half is [4,16). Four chunks for the two faces, or reorder StencilFaceState (which changes nothing's size but does move Espryt's shadow bytes, which is fine — Espryt compares against its own shadow).
  3. The patch trio is SetPatchState in Coverage.def but is in the pipeline member list. Pick one.
  4. FramebufferSrgb/DepthClamp storage, and the sizeof == 1168 / MGL_RESIDUAL_BLOCK_SIZE / PipeFields.def triple that must be updated with it.
  5. ScissorTestEnabledMask is both — it bumps the pipeline version (for Espryt's glEnable) and is a dynamic-tail input on Magma. Under the plan's model it is dynamic, and the Espryt glEnable is served by the working block Espryt reads anyway, so demoting its version bump is likely correct — but check MGPipeValueTypes.h:362-367, which asserts the opposite.
  6. ~14 members no backend reads at all (§1.2 "neither"). They still have to live somewhere in the working block for glGet, but they need not be in either chunk set if the client answers their getters locally (kClientResolved). That would shrink the wire payload meaningfully.