[Feat] (Pipe): land the P2 contract - real storage for the three swallowed capabilities, the render-state chunk table and its subset hash, the in-process applier, the slot allocator, the subsystem bitmask and the residual ratchet down to 8

- FramebufferSrgb, DepthClamp and TextureCubeMapSeamless get real storage. All three fell
  to SetCapability's "not supported currently" arm and IsCapabilityEnabled's default:
  glEnable was swallowed and glIsEnabled lied, so DirectGLES' sRGB block and the
  DirectVulkan read points consumed a constant. The three Bools land in the three
  alignment bytes at [581, 584) between ColorMasks and ClearColor, so
  sizeof(RenderStateParameters) stays 1168 and NO existing offset moves - Espryt's
  kBlendSpanBegin/kBlendSpanEnd (312/536) and the whole chunk table depend on that.
- MGPipeRenderStateSpans.{h,cpp}: the pipeline/dynamic split, written in exactly one place.
  The rule is the only rule - a byte is pipeline state iff a public RenderState setter that
  calls BumpVersions() writes it - which makes G7's "the subset hash moves iff
  m_pipelineStateVersion moves" true by construction. 16 boundaries, all offsetof or
  sizeof, alternating dynamic/pipeline: 8 dynamic chunks / 772 bytes and 7 pipeline chunks
  / 396 bytes, partitioning [0, 1168) exactly, asserted at compile time.
  MGPipeComputePipelineSubsetHash is XXH64 over the seven pipeline chunks, seeded with a
  table version so a chunk-table change invalidates every persisted key.
- The pipeline subset is now a strict SUPERSET of the 24 members ComputePipelineStateHash
  hashed: 44 members, adding sample coverage, the front face, the provoking vertex, the
  scissor-test mask, the back polygon mode, eleven capability bools the hash never read and
  the three above. Demoting those setters to ++m_version instead would have changed
  MG_State semantics in the PULL build for the push path's sake. The hash runs only when
  m_pipelineStateVersion moves, which is exactly when Magma re-hashed before.
- PipeApply.{h,cpp}: the in-process applier, the server half of the P2 calls. The server's
  working RenderStateParameters IS PipeInputs::m_renderState, which is why DirectGLES'
  SyncRenderState is not one line changed and why the verify comparator stops being a
  tautology. Per-context CSO store indexed by slot, gen-validated; the residual block's
  capability bits are compared against the assembled block, so a capability a later call
  takes over and forgets to carry is Fatal{PipeResidualDiverged}.
  MGPipeDeriveRenderStateFields is a declared STUB - its 29 derivations are commit c1.
- SlotAllocator.{h,cpp}: the client's per-kind {slot, gen} allocator, free list plus
  high-water, first allocatable slot 1, gen bumping only on slot REUSE, a debug assert on
  gen wrap, the composite ShaderCso band held back, and a lifetimeId -> slot map per kind so
  a GL name never enters a key. In the contract because both Track H slices need it.
- ResidualValueBlock 1248 -> 8 bytes, one Uint64 of capability bits.
  RenderStateParameters retired to create/bind_render_state and set_dynamic_state, Pack to
  set_pixel_pack_state, the patch quintet to set_patch_state. gen_pipe.py now emits the
  member-by-member offsetof assertions the ratchet comment always promised.
- gen_pipe.py: PIPELINE_STATE_MEMBERS grows to the 44-member set in declaration order and
  PipeSpanTable.inc's "deliberately absent" block records the answers instead of the
  questions; Coverage.def gains MGP_COVERAGE_EMITTED_LIST (34 rows) and PipeFilled.inc
  gains kMGPipeFieldEmittedBy[], which is what lets the residual fill loop skip a field a
  P2 call now supplies. One more --self-test negative control covers the new list.
- MOBILEGL_PIPE_PUSH becomes a per-subsystem bitmask with named bits (0..6 migrated at P2,
  bit 63 the CSO-content-addressing negative control), defaulting to 0x7f in a push build
  and staying 0 in a pull build. New CMake option MOBILEGL_PIPE_LEGACY_MEMOS, ON, forced ON
  when MOBILEGL_PIPE_PUSH=OFF where it is the only arm. New Features.PipeHandleAbaControl
  under MOBILEGL_PIPE_PUSH, negative control C for HandleRecycleScenario.
- PipeStats gains CallClass::{RenderStateCsoMints, RenderStateCsoBinds} (csom / csob on the
  summary line), and they are PUSH-ONLY: growing the enum in the pull build would resize
  the counter arrays, the name table and FormatWindowLine for two counters that could never
  leave zero, and G1 admits no such resize.
- Four MG_Test/Pipe stubs plus their CMake registration, so the packages that own their
  contents never touch MG_Test/Pipe/CMakeLists.txt.

G1, pull build, symbol_report --threshold 0: 0 added, 0 removed, 0 renamed, 4 resized, and
every resize is attributed:
  RenderState::RenderState()                       1700 -> 1848 (+148)  the three {}
  RenderState::SetCapability(CapabilityInput,bool)   850 ->  927  (+77)  three switch arms
  RenderState::IsCapabilityEnabled(CapabilityInput)  239 ->  268  (+29)  three switch arms
  _GLOBAL__sub_I_DirectGLES.cpp                     1340 -> 1331   (-9)  the static
    initialiser of DirectGLES.cpp's `static RenderStateParameters
    g_syncedRenderStateParameters` re-scheduling around the three new default-initialised
    members. A shrink, and the only unforeseen entry; it is a direct consequence of the
    struct gaining members and touches no interface.
This commit is contained in:
2026-09-06 07:45:07 -04:00
parent 4826806881
commit 9c6a8a25d8
30 changed files with 1826 additions and 130 deletions
+24
View File
@@ -27,6 +27,13 @@ option(MOBILEGL_BUILD_SERVER_SPIKE "Build the P0 spike-A MobileGLServer delivery
# MGPipe/PipeInputs source is compiled, every MGP_FILL is ((void)0).
option(MOBILEGL_PIPE_PUSH "Backends read frontend state through the MGPipe PipeInputs block instead of MG_State::pGLContext (ARCHITECTURE.md 9.2 phase A)" OFF)
option(MOBILEGL_PIPE_VERIFY "Compile SnapshotFromGLContext() and the G4 per-verb shadow comparator; implies MOBILEGL_PIPE_PUSH; never shipped" OFF)
# Track H's old-versus-new arm (ARCHITECTURE.md 9.6). With a MOBILEGL_PIPE_PUSH bit clear
# the backend would still run the RE-KEYED memo code, so the bitmask alone stops being a
# valid A/B the moment a handle wave lands: this option compiles the pre-handle arm - the
# registries, OwnerEquals, the TwinLookupMemos, g_fbSlotCache, ComputePipelineStateHash,
# the address-keyed VaoDrawMemo - beside it, behind the same PipeInputs interface. ON for
# the whole migration window; it retires with the pull path itself at P13.
option(MOBILEGL_PIPE_LEGACY_MEMOS "Compile the pre-handle memo arm beside the {slot, gen} arm so Track H has a real A/B (ARCHITECTURE.md 9.6)" ON)
set(MOBILEGL_LOG_ACTIVE_LEVEL "MOBILEGL_LOG_LEVEL_INFO" CACHE STRING "MobileGL active log level macro")
set(MOBILEGL_VULKAN_LIBRARY "" CACHE FILEPATH "Vulkan loader/MoltenVK library to link for iOS builds")
@@ -464,11 +471,25 @@ if (MOBILEGL_PIPE_VERIFY AND NOT MOBILEGL_PIPE_PUSH)
set(MOBILEGL_PIPE_PUSH ON)
endif()
# In a pull build the legacy arm is the ONLY arm, so the option cannot be off there.
# A normal variable, not a forced cache write, for the same reason as the two above.
if (NOT MOBILEGL_PIPE_PUSH AND NOT MOBILEGL_PIPE_LEGACY_MEMOS)
message(STATUS "MobileGL: MOBILEGL_PIPE_PUSH=OFF forces MOBILEGL_PIPE_LEGACY_MEMOS ON for this "
"configure: with nothing pushed it is the only arm there is")
set(MOBILEGL_PIPE_LEGACY_MEMOS ON)
endif()
if (MOBILEGL_PIPE_PUSH)
message(STATUS "MobileGL: PipeInputs push ON, appending the MGPipe fill sources")
list(APPEND SOURCE_FILES
MobileGL/MG_Backend/MGPipe/PipeInputs.cpp
MobileGL/MG_Impl/Pipe/PipeFill.cpp
# P2's contract: the chunk table and its subset hash, the in-process applier, and
# the client's {slot, gen} allocator. All three are push-only, which is how the
# pull build gains no symbol from P2 (G1) - a declaration emits nothing.
MobileGL/MG_Pipe/MGPipeRenderStateSpans.cpp
MobileGL/MG_Pipe/PipeApply.cpp
MobileGL/MG_Impl/Pipe/SlotAllocator.cpp
)
endif()
@@ -551,6 +572,9 @@ endif()
if (MOBILEGL_PIPE_VERIFY)
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_VERIFY=1)
endif()
if (MOBILEGL_PIPE_LEGACY_MEMOS)
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_LEGACY_MEMOS=1)
endif()
message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}")
+19 -4
View File
@@ -319,10 +319,18 @@ namespace MobileGL::MG_Config {
// --- MGPipe (the disaggregation plan's explicit frontend/backend boundary) ---
// MOBILEGL_PIPE_PUSH: per-subsystem bitmask selecting which state the frontend
// PUSHES over MGPipe instead of leaving the backend to pull it out of GLContext.
// 0 - the default and the only shipped value until the migration lands - is "pull
// everything", i.e. exactly today's behaviour. One bit of it also turns OFF
// client-side content addressing of CSOs, which is the negative control the CSO
// design is measured against. Accepts decimal or 0x-prefixed hex.
// 0 - the only shipped value until the migration lands - is "pull everything",
// i.e. exactly today's behaviour, and is the default of a PULL build, where the
// knob is meaningless anyway. A PUSH build defaults to every subsystem migrated so
// far (MG_Pipe::kMGPipeSubsystemsMigratedAtP2), so MOBILEGL_PIPE_PUSH=0 in the
// environment is the all-pull control. Accepts decimal or 0x-prefixed hex, and
// operators pass it as hex, so the bits are listed here (MG_Pipe/MGPipe.h owns them):
// 0x01 render state (create/bind_render_state + set_dynamic_state)
// 0x02 pixel pack 0x04 patch state 0x08 vertex attrib defaults
// 0x10 residual values 0x20 Espryt slots 0x40 Magma vertex input
// 1<<63 NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of
// CSOs, so every pipeline-version change mints a fresh CSO and the map is
// never probed. The negative control the CSO design is measured against.
Uint64 PipePush = 0;
// MOBILEGL_PIPE_VERIFY: per-draw, per-FIELD shadow comparison of the pushed state
// against a snapshot taken from GLContext the old way, printing the first field
@@ -349,6 +357,13 @@ namespace MobileGL::MG_Config {
// Fatal{UnmigratedPipeInput} (negative control B). Unknown name is
// Fatal{PipeVerifyBadKnob}.
String PipePoisonOmit;
// MOBILEGL_PIPE_HANDLE_ABA_CONTROL (negative control C, P2 brief D18): defeat the
// two guards the {slot, gen} re-key replaces - hash the raw BufferObject* instead
// of its lifetime id, and skip the VAO lifetime-id compare - so
// HandleRecycleScenario.AbaControl reproduces the ABA and asserts the WRONG pixels.
// That is what proves the reproducer still reproduces. Under MOBILEGL_PIPE_PUSH
// only, so it cannot exist in a shipping pull build.
Bool PipeHandleAbaControl = false;
#endif
// MOBILEGL_PIPE_STATS: dump the boundary counters (bytes, calls, roundtrips,
// texture pulls, upload shapes, residual-block bytes, index mirror bytes).
+15
View File
@@ -7,6 +7,11 @@
// End of Source File Header
#include "Config.h"
#if MOBILEGL_PIPE_PUSH
// For kMGPipeSubsystemsMigratedAtP2, the push build's PipePush default. Push-only, so
// the pull build's translation unit is unchanged.
#include <MG_Pipe/MGPipe.h>
#endif
#include <cerrno>
#include <cstdlib>
@@ -242,7 +247,16 @@ namespace MobileGL::MG_ConfigLoader {
// MGPipe. Nothing here needs adding to an allow-list: InitializeAcceptedEnvVariables
// accepts every MOBILEGL_ / LIBGL_ prefixed variable in the environment, so a name
// that starts with MOBILEGL_ is visible to these queries by construction.
#if MOBILEGL_PIPE_PUSH
// A push build with the knob unset runs every subsystem migrated so far, so the
// shipped path is the one the gates measure; MOBILEGL_PIPE_PUSH=0 in the
// environment is the all-subsystems-pull control that reproduces P1 exactly.
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP2);
#else
// Meaningless in a pull build: there is nothing to push. Config.h documents 0 as
// "pull everything" and that stays literally true.
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", 0);
#endif
features.PipeVerify = QueryEnvFlag("MOBILEGL_PIPE_VERIFY");
#if MOBILEGL_PIPE_PUSH
// Defaults ON: read as a tri-state so only an explicitly falsy value turns it off.
@@ -250,6 +264,7 @@ namespace MobileGL::MG_ConfigLoader {
QueryEnvQuirkOverride("MOBILEGL_PIPE_VERIFY_FATAL") != MG_Config::QuirkOverride::ForceOff;
QueryEnvVariable("MOBILEGL_PIPE_VERIFY_CORRUPT", features.PipeVerifyCorrupt, "");
QueryEnvVariable("MOBILEGL_PIPE_POISON_OMIT", features.PipePoisonOmit, "");
features.PipeHandleAbaControl = QueryEnvFlag("MOBILEGL_PIPE_HANDLE_ABA_CONTROL");
#endif
features.PipeStats = QueryEnvFlag("MOBILEGL_PIPE_STATS");
// Defaults ON, so the flag has to be read as a tri-state rather than as a plain
+7
View File
@@ -609,6 +609,13 @@ namespace MobileGL::MG_Pipe {
// The one door into the storage from the client side (MG_Impl/Pipe/PipeFill.cpp):
// the filler's per-field copies and stamps, and the verify snapshot.
friend struct MGPipeFillAccess;
// The other door, and the one that exists because of what this block IS after P2:
// the server's working RenderStateParameters. MG_Pipe/PipeApply.cpp scatters
// bind_render_state's and set_dynamic_state's chunks straight into m_renderState,
// which is why DirectGLES' SyncRenderState is not one line changed. It deliberately
// does NOT stamp the poison generations - a stamp says "the filler published this
// for THIS verb", which is the walk's statement, not the applier's.
friend struct MGPipeApplyAccess;
// ---- identity ----
const void* m_contextIdentity = nullptr;
+174
View File
@@ -0,0 +1,174 @@
// MobileGL - MobileGL/MG_Impl/Pipe/SlotAllocator.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// SlotAllocator.h. Compiled only under MOBILEGL_PIPE_PUSH.
#include <MG_Impl/Pipe/SlotAllocator.h>
namespace MobileGL::MG_Pipe {
namespace {
// The ShaderCso band the ordinary allocator must never enter: the top 1/16 of the
// ShaderCso slot space is reserved for PROGRAM PIPELINE COMPOSITES, which are minted
// client-side out of the stage programs bound to a pipeline object. Reserving a band
// rather than a flag keeps the composite resolver's lifetime bookkeeping out of here
// (MGPipeHandles.h, ARCHITECTURE.md 5.6.3).
Bool SlotIsAllocatable(MGPipeKind kind, Uint32 slot) {
if (slot < kMGPipeFirstAllocatableSlot) return false;
if (kind != MGPipeKind::ShaderCso) return true;
return slot < kMGPipeShaderCsoCompositeSlotBase;
}
} // namespace
MGPipeSlotAllocator::KindState& MGPipeSlotAllocator::StateOf(MGPipeKind kind) {
const SizeT index = static_cast<SizeT>(kind);
MOBILEGL_ASSERT(index < kKindCount, "MGPipeKind %zu out of range", index);
return m_kinds[index < kKindCount ? index : 0];
}
const MGPipeSlotAllocator::KindState& MGPipeSlotAllocator::StateOf(MGPipeKind kind) const {
const SizeT index = static_cast<SizeT>(kind);
MOBILEGL_ASSERT(index < kKindCount, "MGPipeKind %zu out of range", index);
return m_kinds[index < kKindCount ? index : 0];
}
MGPipeHandle MGPipeSlotAllocator::Allocate(MGPipeKind kind) {
KindState& state = StateOf(kind);
if (state.Slots.empty()) {
// Slot 0 exists so the vector is slot-indexed, and is never handed out.
state.Slots.resize(kMGPipeFirstAllocatableSlot);
}
Uint32 slot = 0;
Bool reused = false;
while (!state.FreeList.empty()) {
const Uint32 candidate = state.FreeList.back();
state.FreeList.pop_back();
if (!SlotIsAllocatable(kind, candidate)) continue;
slot = candidate;
reused = true;
break;
}
if (!reused) {
slot = static_cast<Uint32>(state.Slots.size());
MOBILEGL_ASSERT(SlotIsAllocatable(kind, slot),
"MGPipe slot space of kind %u is exhausted at slot %u",
static_cast<Uint32>(kind), slot);
if (!SlotIsAllocatable(kind, slot)) return kMGPipeNullHandle;
state.Slots.emplace_back();
}
SlotState& entry = state.Slots[slot];
if (entry.EverHandedOut) {
// The one place Gen may move. 2^32 recycles of ONE slot is ~50 days of continuous
// churn at one recycle per frame at 1000 fps, which is why the bound is asserted
// in a debug allocator rather than defended in release.
MOBILEGL_ASSERT(entry.Gen != ~Uint32{0},
"MGPipe handle generation wrapped on kind %u slot %u; {slot, gen} is "
"no longer unique",
static_cast<Uint32>(kind), slot);
++entry.Gen;
}
entry.EverHandedOut = true;
entry.Live = true;
entry.LifetimeId = 0;
++state.LiveCount;
return MGPipeHandle{slot, entry.Gen};
}
MGPipeHandle MGPipeSlotAllocator::AllocateFor(MGPipeKind kind, Uint64 lifetimeId) {
const MGPipeHandle handle = Allocate(kind);
if (MGPipeHandleIsNull(handle)) return handle;
KindState& state = StateOf(kind);
state.Slots[handle.Slot].LifetimeId = lifetimeId;
if (lifetimeId != 0) {
MOBILEGL_ASSERT(state.ByLifetimeId.find(lifetimeId) == state.ByLifetimeId.end(),
"lifetime id %llu already owns a slot of kind %u",
static_cast<unsigned long long>(lifetimeId), static_cast<Uint32>(kind));
state.ByLifetimeId[lifetimeId] = handle.Slot;
}
return handle;
}
MGPipeHandle MGPipeSlotAllocator::FindByLifetimeId(MGPipeKind kind, Uint64 lifetimeId) const {
if (lifetimeId == 0) return kMGPipeNullHandle;
const KindState& state = StateOf(kind);
const auto it = state.ByLifetimeId.find(lifetimeId);
if (it == state.ByLifetimeId.end()) return kMGPipeNullHandle;
const Uint32 slot = it->second;
if (slot >= state.Slots.size() || !state.Slots[slot].Live) return kMGPipeNullHandle;
return MGPipeHandle{slot, state.Slots[slot].Gen};
}
MGPipeHandle MGPipeSlotAllocator::Acquire(MGPipeKind kind, Uint64 lifetimeId) {
const MGPipeHandle existing = FindByLifetimeId(kind, lifetimeId);
if (!MGPipeHandleIsNull(existing)) return existing;
return AllocateFor(kind, lifetimeId);
}
void MGPipeSlotAllocator::Free(MGPipeKind kind, MGPipeHandle handle) {
KindState& state = StateOf(kind);
if (handle.Slot >= state.Slots.size()) return;
SlotState& entry = state.Slots[handle.Slot];
// A stale handle must not free the slot its successor now owns - that is the whole
// reason the generation is in the key.
if (!entry.Live || entry.Gen != handle.Gen) return;
if (entry.LifetimeId != 0) {
const auto it = state.ByLifetimeId.find(entry.LifetimeId);
if (it != state.ByLifetimeId.end() && it->second == handle.Slot) {
state.ByLifetimeId.erase(it);
}
}
entry.Live = false;
entry.LifetimeId = 0;
--state.LiveCount;
state.FreeList.push_back(handle.Slot);
}
Bool MGPipeSlotAllocator::IsLive(MGPipeKind kind, MGPipeHandle handle) const {
const KindState& state = StateOf(kind);
if (handle.Slot >= state.Slots.size()) return false;
const SlotState& entry = state.Slots[handle.Slot];
return entry.Live && entry.Gen == handle.Gen;
}
Uint32 MGPipeSlotAllocator::GenOfSlot(MGPipeKind kind, Uint32 slot) const {
const KindState& state = StateOf(kind);
if (slot >= state.Slots.size()) return 0;
return state.Slots[slot].Gen;
}
Uint64 MGPipeSlotAllocator::LifetimeIdOfSlot(MGPipeKind kind, Uint32 slot) const {
const KindState& state = StateOf(kind);
if (slot >= state.Slots.size()) return 0;
return state.Slots[slot].LifetimeId;
}
Uint32 MGPipeSlotAllocator::HighWater(MGPipeKind kind) const {
return static_cast<Uint32>(StateOf(kind).Slots.size());
}
Uint32 MGPipeSlotAllocator::LiveCount(MGPipeKind kind) const { return StateOf(kind).LiveCount; }
Uint32 MGPipeSlotAllocator::FreeCount(MGPipeKind kind) const {
return static_cast<Uint32>(StateOf(kind).FreeList.size());
}
void MGPipeSlotAllocator::Reset() {
for (KindState& state : m_kinds) {
state.Slots.clear();
state.FreeList.clear();
state.ByLifetimeId.clear();
state.LiveCount = 0;
}
}
MGPipeSlotAllocator& MGPipeSlots() {
static MGPipeSlotAllocator allocator;
return allocator;
}
} // namespace MobileGL::MG_Pipe
+100
View File
@@ -0,0 +1,100 @@
// MobileGL - MobileGL/MG_Impl/Pipe/SlotAllocator.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include <MG_Pipe/MGPipeHandles.h>
// The CLIENT's slot allocator: the thing that mints every MGPipeHandle in the system
// (ARCHITECTURE.md 4.2 - no create_* call in the catalogue returns a server-cast handle,
// which is what lets the whole catalogue be remoted with zero creation round trips).
//
// Per kind: a free list plus a high-water mark, so slots stay DENSE and the server's object
// table is an array rather than a hash map. It has nothing to do with MG_State's
// IndexGenerator - that container's LIFO GL-name reuse is the very problem {slot, gen}
// exists to close, and the whole point of the identity is that an ABA on the GL name, on
// the heap address or on the lifetime id cannot reproduce a handle.
//
// Gen increments ONLY when a slot is reused, never on a respecify: a glBufferData on a live
// buffer keeps the same {slot, gen}, because the object is the same object. Two generations
// exist in the design and they are strictly separate - this is the client's answer to "is
// this still the same GL object"; MGGen is the server's epoch for "did I recast my driver
// object", and no MGPipe call may require the client to know it.
//
// The lifetimeId -> slot map is what keeps a GL NAME out of every key (ARCHITECTURE.md 4.2):
// the frontend object's lifetime id is the client's own identity for it, so the backend key
// is the handle and the frontend key is the lifetime id, and neither is a recyclable name.
//
// Lives in MG_Impl (the client side, unrestricted) and is compiled only under
// MOBILEGL_PIPE_PUSH. It is in the P2 CONTRACT commit rather than in a Track H package
// because both Track H slices - Espryt 0b and Magma subsystem 4 - key off it.
namespace MobileGL::MG_Pipe {
class MGPipeSlotAllocator {
public:
static constexpr SizeT kKindCount = static_cast<SizeT>(MGPipeKind::KindCount);
// A fresh {slot, gen} of this kind, from the free list if one is waiting and from the
// high-water mark otherwise. Never returns slot 0 (reserved: null, and the default
// framebuffer for kind Framebuffer), and never returns a ShaderCso slot inside the
// composite band, which the program-pipeline resolver mints out of separately.
MGPipeHandle Allocate(MGPipeKind kind);
// Allocate and remember `lifetimeId` as this handle's frontend identity.
MGPipeHandle AllocateFor(MGPipeKind kind, Uint64 lifetimeId);
// The handle a lifetime id was allocated for, or kMGPipeNullHandle. A recycled heap
// address does NOT reproduce a mapping: MG_State hands out a fresh lifetime id per
// object, so the map key is unique for the life of the process.
MGPipeHandle FindByLifetimeId(MGPipeKind kind, Uint64 lifetimeId) const;
// FindByLifetimeId, then AllocateFor when it misses. The ordinary client path.
MGPipeHandle Acquire(MGPipeKind kind, Uint64 lifetimeId);
// Returns the slot to the free list. The Gen bump happens on the NEXT handout of that
// slot, not here, so a handle that is freed twice cannot skip a generation and the
// "gen moves only on reuse" contract holds for an object that is never reused.
void Free(MGPipeKind kind, MGPipeHandle handle);
Bool IsLive(MGPipeKind kind, MGPipeHandle handle) const;
// 0 for a slot that was never handed out; the generation of the LAST handout
// otherwise, live or not.
Uint32 GenOfSlot(MGPipeKind kind, Uint32 slot) const;
Uint64 LifetimeIdOfSlot(MGPipeKind kind, Uint32 slot) const;
// One past the highest slot ever handed out of this kind, i.e. what a server-side
// slot-indexed table must be sized to.
Uint32 HighWater(MGPipeKind kind) const;
Uint32 LiveCount(MGPipeKind kind) const;
Uint32 FreeCount(MGPipeKind kind) const;
// Context teardown / server reset / a unit test's fixture.
void Reset();
private:
struct SlotState {
Uint32 Gen = 0;
Bool Live = false;
Bool EverHandedOut = false;
Uint64 LifetimeId = 0;
};
struct KindState {
// Indexed by slot; [0] is the reserved slot and is never live.
Vector<SlotState> Slots;
Vector<Uint32> FreeList;
UnorderedMap<Uint64, Uint32> ByLifetimeId;
Uint32 LiveCount = 0;
};
KindState& StateOf(MGPipeKind kind);
const KindState& StateOf(MGPipeKind kind) const;
Array<KindState, kKindCount> m_kinds{};
};
// The monolith's one client allocator. Under split there is one per client context.
MGPipeSlotAllocator& MGPipeSlots();
} // namespace MobileGL::MG_Pipe
+52 -3
View File
@@ -71,9 +71,10 @@
X(GetProgramForDispatch, SetDispatchProgram) \
X(GetProgramForDraw, SetDrawProgram) \
X(GetProgramObject, CreateShaderState) \
/* 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 (section 10.3-5). */ \
/* ANSWERED by P2: it is in the pipeline half. SetProvokingVertexMode calls */ \
/* BumpVersions(), and the chunk table's rule is exactly that, so it rides */ \
/* pipeline chunk P4 - a strict superset of what ComputePipelineStateHash used */ \
/* to hash (MGPipeRenderStateSpans.cpp records the provenance). */ \
X(GetProvokingVertexMode, CreateRenderState) \
X(GetRenderStateParameters, CreateRenderState) \
X(GetRenderStateParametersVersion, BindRenderState) \
@@ -128,4 +129,52 @@
X(handle-ify (wire handle), kStructuralHandle) \
X(Buffer ops delta, ResourceRespecify)
// X(Accessor, PipeCall) - the EMITTED list (P2 brief D5): which P2 call now SUPPLIES this
// PipeInputs field, so the per-verb residual fill loop no longer has to pull it out of
// GLContext. gen_pipe.py turns it into kMGPipeFieldEmittedBy[] (generated/PipeFilled.inc);
// a field with no row here keeps going through the fill loop, which is what makes the
// MOBILEGL_PIPE_PUSH bitmask a true per-subsystem A/B rather than an all-or-nothing switch.
//
// Every name must be an accessor in MGP_COVERAGE_ACCESSOR_LIST and every call must be a
// real call in PipeCalls.def; gen_pipe.py refuses anything else.
//
// The one row whose call differs from the accessor list's is GetPrimitiveRestartIndex:
// coverage maps it onto draw_vbo because that is where a backend reads it, but the VALUE
// travels in dynamic chunk D6, so set_dynamic_state is what supplies it.
#define MGP_COVERAGE_EMITTED_LIST(X) \
X(GetBlendColor, SetDynamicState) \
X(GetBlendEquationIndexed, CreateRenderState) \
X(GetBlendFuncIndexed, CreateRenderState) \
X(GetClampReadColor, SetDynamicState) \
X(GetClearColor, SetDynamicState) \
X(GetClearDepth, SetDynamicState) \
X(GetClearStencil, SetDynamicState) \
X(GetColorMaskIndexed, CreateRenderState) \
X(GetCullFaceMode, CreateRenderState) \
X(GetCurrentVertexAttribute, SetVertexAttribDefaults) \
X(GetDepthFunc, CreateRenderState) \
X(GetDepthMask, CreateRenderState) \
X(GetDepthRangeIndexed, SetDynamicState) \
X(GetLineWidth, SetDynamicState) \
X(GetLogicOp, CreateRenderState) \
X(GetMinSampleShadingValue, CreateRenderState) \
X(GetPatchDefaultInnerLevel, SetPatchState) \
X(GetPatchDefaultOuterLevel, SetPatchState) \
X(GetPatchVertices, SetPatchState) \
X(GetPipelineStateVersion, BindRenderState) \
X(GetPixelStoreParameters, SetPixelPackState) \
X(GetPolygonModeFront, CreateRenderState) \
X(GetPolygonOffsetFactor, SetDynamicState) \
X(GetPolygonOffsetUnits, SetDynamicState) \
X(GetPrimitiveRestartIndex, SetDynamicState) \
X(GetProvokingVertexMode, CreateRenderState) \
X(GetRenderStateParameters, CreateRenderState) \
X(GetRenderStateParametersVersion, BindRenderState) \
X(GetScissorBox, SetDynamicState) \
X(GetStencilState, CreateRenderState) \
X(GetViewport, SetDynamicState) \
X(GetViewportIndexed, SetDynamicState) \
X(IsCapabilityEnabled, CreateRenderState) \
X(IsCapabilityEnabledIndexed, CreateRenderState)
// clang-format on
+28 -4
View File
@@ -55,10 +55,34 @@ namespace MobileGL::MG_Pipe {
};
// The pipeline/dynamic split of RenderStateParameters, defined exactly once (section
// 4.5.2). Generated by G7 from the field list ComputePipelineStateHash already hashes;
// MGPipeRenderStateSpans.cpp and the setter-consistency test land with P2, which is
// when the chunk table can be filled with real offsets.
struct MGPipeRenderStateSpans;
// 4.5.2): MG_Pipe/MGPipeRenderStateSpans.{h,cpp}, which landed with P2 and computes
// every chunk boundary with offsetof. Include that header to use it; what stays here
// is the generated member list at the bottom of this file, which is what the chunk
// table was derived from.
// ---- MOBILEGL_PIPE_PUSH's runtime bitmask (Config.h Features.PipePush) ----
//
// One bit per SUBSYSTEM, so an A/B is per subsystem rather than all-or-nothing, and
// bit 63 for the one BEHAVIOUR the design has to be measured against. Bits are
// allocated in ROADMAP order and never reused: an operator's recorded 0x7f has to keep
// meaning what it meant.
//
// A clear subsystem bit means "keep pulling", which after P2 is only a valid control
// while MOBILEGL_PIPE_LEGACY_MEMOS compiles the pre-handle arm beside it.
inline constexpr Uint64 kMGPipeSubsystemRenderState = 1ull << 0;
inline constexpr Uint64 kMGPipeSubsystemPixelPack = 1ull << 1;
inline constexpr Uint64 kMGPipeSubsystemPatchState = 1ull << 2;
inline constexpr Uint64 kMGPipeSubsystemVertexAttribDefaults = 1ull << 3;
inline constexpr Uint64 kMGPipeSubsystemResidualValues = 1ull << 4;
inline constexpr Uint64 kMGPipeSubsystemEsprytSlots = 1ull << 5; // Track H, Espryt 0b
inline constexpr Uint64 kMGPipeSubsystemMagmaVertexInput = 1ull << 6; // Track H, Magma subsystem 4
// bits 7..62 reserved for the later phases, allocated in ROADMAP order.
// NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of CSOs, so
// every pipeline-version change mints a fresh CSO and the map is never probed. This is
// the negative control the whole CSO design is measured against (ROADMAP.md P2).
inline constexpr Uint64 kMGPipeBehaviourNoCsoContentAddressing = 1ull << 63;
// The default of a push build with the knob unset (ConfigLoader.cpp).
inline constexpr Uint64 kMGPipeSubsystemsMigratedAtP2 = 0x7full; // bits 0..6
// The catalogue itself. Only macros, so it is safe to expand inside the namespace, and
// consumers (the unit test, later the transport) get MGP_CALL_LIST from this header.
+196
View File
@@ -0,0 +1,196 @@
// MobileGL - MobileGL/MG_Pipe/MGPipeRenderStateSpans.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The definitions behind MGPipeRenderStateSpans.h and behind the two arrays
// generated/PipeSpanTable.inc has declared since P0. Compiled ONLY under
// MOBILEGL_PIPE_PUSH (CMakeLists.txt appends it to SOURCE_FILES there), which is how the
// pull build gains no symbol from the split - a declaration emits nothing.
//
// PROVENANCE OF THE PIPELINE HALF. It began as the enumeration
// VulkanRenderer::ComputePipelineStateHash carried above itself, which was the contract
// that function had without being able to say so; it moves here because this file is now
// that contract. Verbatim, from VulkanRenderer.cpp at feat/disaggregated@48268068:
//
// Value hash over every fixed-function GL state the pipeline payload reads that
// the memo key's other fields (mode, program hash, vertex-input hash, render-pass
// hash, transform flags) do not already pin down. Enumerated against the payload
// build in GetOrCreatePipeline - any new GL-state read there must be added here:
// - capability bits: CullFace, DepthTest, PolygonOffsetFill (mode gating rides
// the memo's mode key), RasterizerDiscard, ColorLogicOp, StencilTest,
// PrimitiveRestart(+FixedIndex), SampleShading, SampleMask, plus the depth write mask
// - patch vertices, polygon mode, cull face mode, depth func, logic op,
// min sample shading, the glSampleMaski word
// - front/back stencil ops + compare funcs (ref/mask are dynamic state)
// - per draw buffer up to the render pass's colour span: indexed blend enable,
// blend factors/equations, indexed colour write mask (broadcast from index 0
// when the device lacks independentBlend - the same read the payload does)
// FBO-derived payload inputs (attachment presence/formats/draw-buffer gating) are
// pinned by the render-pass hash key, exactly as the version-keyed memo relied on.
//
// P2's pipeline half is a strict SUPERSET of that list. It adds SampleCoverageValue,
// SampleCoverageInvert, FrontFaceModeSetting, ProvokingVertexModeSetting,
// ScissorTestEnabledMask, PolygonModeBack, the eleven capability bools the hash never read
// (DebugOutput, DebugOutputSynchronous, Dither, LineSmooth, PolygonOffsetLine,
// PolygonOffsetPoint, PolygonSmooth, SampleAlphaToCoverage, SampleAlphaToOne, SampleCoverage,
// ProgramPointSize) and the three capabilities P2 gave storage to (FramebufferSrgb,
// DepthClamp, TextureCubeMapSeamless). All of them are written by a setter that calls
// BumpVersions(), so under the header's rule they are pipeline. The alternative - demoting
// those setters to ++m_version - would change MG_State semantics in the PULL build for the
// sake of the push path. Growing the subset costs nothing measurable: the hash runs only
// when m_pipelineStateVersion moves, which is exactly when Magma recomputed
// ComputePipelineStateHash before.
//
// The render-pass facts are deliberately NOT here. ComputePipelineStateHash's signature is
// (colorAttachmentCount, rasterizationSamples) and it folds ResolveEffectiveSampleMask, so
// it was never a pure function of RenderStateParameters; a CSO handle cannot replace it on
// its own and Magma keeps renderPassHash as a separate memo-key component.
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/MGPipeRenderStateSpans.h>
#include <cstring>
namespace MobileGL::MG_Pipe {
namespace {
// Half-local chunk index -> global chunk index. The halves alternate, so this is
// arithmetic rather than a table.
constexpr SizeT GlobalPipelineChunk(SizeT halfIndex) { return halfIndex * 2 + 1; }
constexpr SizeT GlobalDynamicChunk(SizeT halfIndex) { return halfIndex * 2; }
const Uint8* BytesOf(const RenderStateParameters& params) {
return reinterpret_cast<const Uint8*>(&params);
}
Uint8* BytesOf(RenderStateParameters& params) { return reinterpret_cast<Uint8*>(&params); }
SizeT BlobBytes(Uint32 chunkMask, SizeT halfCount, SizeT (*toGlobal)(SizeT)) {
SizeT total = 0;
for (SizeT i = 0; i < halfCount; ++i) {
if ((chunkMask & (1u << i)) == 0) continue;
total += MGPipeRenderStateChunkAt(toGlobal(i)).Length;
}
return total;
}
void Gather(const RenderStateParameters& params, Uint32 chunkMask, void* dst, SizeT halfCount,
SizeT (*toGlobal)(SizeT)) {
Uint8* out = static_cast<Uint8*>(dst);
const Uint8* src = BytesOf(params);
for (SizeT i = 0; i < halfCount; ++i) {
if ((chunkMask & (1u << i)) == 0) continue;
const MGPStateChunk chunk = MGPipeRenderStateChunkAt(toGlobal(i));
std::memcpy(out, src + chunk.Offset, chunk.Length);
out += chunk.Length;
}
}
void Scatter(const void* src, Uint32 chunkMask, RenderStateParameters& dst, SizeT halfCount,
SizeT (*toGlobal)(SizeT)) {
const Uint8* in = static_cast<const Uint8*>(src);
Uint8* out = BytesOf(dst);
for (SizeT i = 0; i < halfCount; ++i) {
if ((chunkMask & (1u << i)) == 0) continue;
const MGPStateChunk chunk = MGPipeRenderStateChunkAt(toGlobal(i));
std::memcpy(out + chunk.Offset, in, chunk.Length);
in += chunk.Length;
}
}
Uint32 ChunksThatMoved(const RenderStateParameters& a, const RenderStateParameters& b,
SizeT halfCount, SizeT (*toGlobal)(SizeT)) {
const Uint8* left = BytesOf(a);
const Uint8* right = BytesOf(b);
Uint32 mask = 0;
for (SizeT i = 0; i < halfCount; ++i) {
const MGPStateChunk chunk = MGPipeRenderStateChunkAt(toGlobal(i));
if (std::memcmp(left + chunk.Offset, right + chunk.Offset, chunk.Length) != 0) {
mask |= 1u << i;
}
}
return mask;
}
constexpr Uint32 AllChunks(SizeT halfCount) {
return halfCount >= 32 ? ~Uint32{0} : static_cast<Uint32>((Uint64{1} << halfCount) - 1);
}
} // namespace
// The two arrays generated/PipeSpanTable.inc declares. Every entry is
// MGPipeRenderStateChunkAt(), so a boundary can only be written once.
const MGPStateChunk kMGPipePipelineChunks[kMGPipePipelineChunkCount] = {
MGPipeRenderStateChunkAt(GlobalPipelineChunk(0)), MGPipeRenderStateChunkAt(GlobalPipelineChunk(1)),
MGPipeRenderStateChunkAt(GlobalPipelineChunk(2)), MGPipeRenderStateChunkAt(GlobalPipelineChunk(3)),
MGPipeRenderStateChunkAt(GlobalPipelineChunk(4)), MGPipeRenderStateChunkAt(GlobalPipelineChunk(5)),
MGPipeRenderStateChunkAt(GlobalPipelineChunk(6)),
};
static_assert(sizeof(kMGPipePipelineChunks) / sizeof(kMGPipePipelineChunks[0]) == kMGPipePipelineChunkCount,
"kMGPipePipelineChunks lost an entry");
const MGPStateChunk kMGPipeDynamicChunks[kMGPipeDynamicChunkCount] = {
MGPipeRenderStateChunkAt(GlobalDynamicChunk(0)), MGPipeRenderStateChunkAt(GlobalDynamicChunk(1)),
MGPipeRenderStateChunkAt(GlobalDynamicChunk(2)), MGPipeRenderStateChunkAt(GlobalDynamicChunk(3)),
MGPipeRenderStateChunkAt(GlobalDynamicChunk(4)), MGPipeRenderStateChunkAt(GlobalDynamicChunk(5)),
MGPipeRenderStateChunkAt(GlobalDynamicChunk(6)), MGPipeRenderStateChunkAt(GlobalDynamicChunk(7)),
};
static_assert(sizeof(kMGPipeDynamicChunks) / sizeof(kMGPipeDynamicChunks[0]) == kMGPipeDynamicChunkCount,
"kMGPipeDynamicChunks lost an entry");
void MGPipeGatherPipelineBytes(const RenderStateParameters& params, void* dst) {
Gather(params, AllChunks(kMGPipePipelineChunkCount), dst, kMGPipePipelineChunkCount,
GlobalPipelineChunk);
}
void MGPipeScatterPipelineBytes(const void* src, RenderStateParameters& dst) {
Scatter(src, AllChunks(kMGPipePipelineChunkCount), dst, kMGPipePipelineChunkCount,
GlobalPipelineChunk);
}
SizeT MGPipePipelineChunkBlobBytes(Uint32 chunkMask) {
return BlobBytes(chunkMask, kMGPipePipelineChunkCount, GlobalPipelineChunk);
}
void MGPipeGatherPipelineChunks(const RenderStateParameters& params, Uint32 chunkMask, void* dst) {
Gather(params, chunkMask, dst, kMGPipePipelineChunkCount, GlobalPipelineChunk);
}
void MGPipeScatterPipelineChunks(const void* src, Uint32 chunkMask, RenderStateParameters& dst) {
Scatter(src, chunkMask, dst, kMGPipePipelineChunkCount, GlobalPipelineChunk);
}
SizeT MGPipeDynamicChunkBlobBytes(Uint32 chunkMask) {
return BlobBytes(chunkMask, kMGPipeDynamicChunkCount, GlobalDynamicChunk);
}
void MGPipeGatherDynamicChunks(const RenderStateParameters& params, Uint32 chunkMask, void* dst) {
Gather(params, chunkMask, dst, kMGPipeDynamicChunkCount, GlobalDynamicChunk);
}
void MGPipeScatterDynamicChunks(const void* src, Uint32 chunkMask, RenderStateParameters& dst) {
Scatter(src, chunkMask, dst, kMGPipeDynamicChunkCount, GlobalDynamicChunk);
}
Uint32 MGPipeDynamicChunksThatMoved(const RenderStateParameters& a, const RenderStateParameters& b) {
return ChunksThatMoved(a, b, kMGPipeDynamicChunkCount, GlobalDynamicChunk);
}
Uint32 MGPipePipelineChunksThatMoved(const RenderStateParameters& a, const RenderStateParameters& b) {
return ChunksThatMoved(a, b, kMGPipePipelineChunkCount, GlobalPipelineChunk);
}
Uint64 MGPipeHashPipelineBytes(const void* bytes) {
return static_cast<Uint64>(
XXH64(bytes, kMGPipePipelineChunkBytes, kMGPipeRenderStateChunkTableVersion));
}
Uint64 MGPipeComputePipelineSubsetHash(const RenderStateParameters& params) {
// 396 bytes on the stack. A streaming XXH64_state_t would allocate; gathering first
// is also what CsoCache wants, because the same bytes are what a hash hit memcmps
// against before the handle is reused.
Uint8 gathered[kMGPipePipelineChunkBytes];
MGPipeGatherPipelineBytes(params, gathered);
return MGPipeHashPipelineBytes(gathered);
}
} // namespace MobileGL::MG_Pipe
+203
View File
@@ -0,0 +1,203 @@
// MobileGL - MobileGL/MG_Pipe/MGPipeRenderStateSpans.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include "MGPipeTypes.h"
#include "MGPipeValueTypes.h"
// G7: the pipeline/dynamic split of RenderStateParameters, written in EXACTLY ONE PLACE
// (ARCHITECTURE.md 5.3, D-B1).
//
// The rule that decides the split, and it is the only rule:
//
// A byte of RenderStateParameters is in the PIPELINE half if and only if some public
// RenderState setter that calls BumpVersions() writes it. Every other byte is in the
// DYNAMIC half. There is no third set.
//
// That makes the G7 invariant - the pipeline-subset hash moves IF AND ONLY IF
// m_pipelineStateVersion moves - true by CONSTRUCTION rather than by inspection, and it is
// what MG_Test/Pipe/RenderStateSpansTest.cpp walks every setter to confirm.
//
// The chunks alternate: chunk 0 is dynamic, chunk 1 is pipeline, and so on, so the whole
// table is 16 BOUNDARIES rather than 15 hand-written ranges. Every boundary is an offsetof
// or a sizeof - never a literal - because a python guess at a layout it cannot see is
// exactly the drift the setter-consistency test exists to catch. 8 dynamic chunks + 7
// pipeline chunks = 15, and both counts fit the Uint32 ChunkMask of MGPRenderStateDesc and
// MGPDynamicState with room to spare.
//
// Note the two splits are ORTHOGONAL and coexist (ARCHITECTURE.md 5.3): DirectGLES'
// head [0, 312) / blend [312, 536) / tail [536, 1168) spans cut ACROSS this table, and
// nothing about them changes. StencilFaceState is deliberately NOT reordered - reordering
// would move Espryt's shadow bytes for no gain.
namespace MobileGL::MG_Pipe {
namespace MGPipeRenderStateChunkDetail {
using RSP = RenderStateParameters;
using SFS = StencilFaceState;
inline constexpr SizeT kStencilFace0 = offsetof(RSP, StencilStates);
inline constexpr SizeT kStencilFace1 = kStencilFace0 + sizeof(SFS);
// The pipeline half of one stencil face is [Func, Ref) + [FailOp, end); the dynamic
// half is [Ref, FailOp) - Ref and ValueMask are VK_DYNAMIC_STATE_STENCIL_REFERENCE /
// _COMPARE_MASK and WriteMask is _WRITE_MASK, which is why glStencilFunc changing only
// the reference must not evict a cached pipeline (RenderState.cpp SetStencilFunc).
inline constexpr SizeT kFaceDynamicBegin = offsetof(SFS, Ref);
inline constexpr SizeT kFaceDynamicEnd = offsetof(SFS, FailOp);
} // namespace MGPipeRenderStateChunkDetail
// 15 chunks, 16 boundaries, strictly ascending, [0, sizeof(RenderStateParameters)).
inline constexpr SizeT kMGPipeRenderStateChunkCount = 15;
inline constexpr Array<SizeT, kMGPipeRenderStateChunkCount + 1> kMGPipeRenderStateChunkBoundaries = {
// D0 dynamic: Viewports[16], LineWidth, PointSize
SizeT{0},
// P0 pipeline: PatchVertices, PatchDefaultOuterLevel, PatchDefaultInnerLevel
offsetof(RenderStateParameters, PatchVertices),
// D1 dynamic: PolygonOffsetFactor/Units/Clamp, ClipOrigin, ClipDepthMode
offsetof(RenderStateParameters, PolygonOffsetFactor),
// P1 pipeline: BlendStates[8], LogicOp, DepthTestEnabled, DepthFunc, DepthMask,
// ColorMasks[8], FramebufferSrgbEnabled, DepthClampEnabled,
// TextureCubeMapSeamlessEnabled
offsetof(RenderStateParameters, BlendStates),
// D2 dynamic: ClearColor, ClearDepth, ClearStencil, BlendColor, DepthRanges[16]
offsetof(RenderStateParameters, ClearColor),
// P2 pipeline: SampleCoverageValue, SampleCoverageInvert, SampleMaskValue,
// MinSampleShadingValue, StencilStates[0].Func
offsetof(RenderStateParameters, SampleCoverageValue),
// D3 dynamic: StencilStates[0].{Ref, ValueMask, WriteMask}
MGPipeRenderStateChunkDetail::kStencilFace0 + MGPipeRenderStateChunkDetail::kFaceDynamicBegin,
// P3 pipeline: StencilStates[0].{FailOp, PassDepthFailOp, PassDepthPassOp},
// StencilStates[1].Func
MGPipeRenderStateChunkDetail::kStencilFace0 + MGPipeRenderStateChunkDetail::kFaceDynamicEnd,
// D4 dynamic: StencilStates[1].{Ref, ValueMask, WriteMask}
MGPipeRenderStateChunkDetail::kStencilFace1 + MGPipeRenderStateChunkDetail::kFaceDynamicBegin,
// P4 pipeline: StencilStates[1].{FailOp, PassDepthFailOp, PassDepthPassOp},
// CullFaceEnabled, CullFaceModeSetting, FrontFaceModeSetting,
// ProvokingVertexModeSetting
MGPipeRenderStateChunkDetail::kStencilFace1 + MGPipeRenderStateChunkDetail::kFaceDynamicEnd,
// D5 dynamic: the four hints, PointFadeThresholdSize, PointSpriteCoordOrigin,
// ClampReadColor
offsetof(RenderStateParameters, LineSmoothHint),
// P5 pipeline: PolygonModeFront, PolygonModeBack
offsetof(RenderStateParameters, PolygonModeFront),
// D6 dynamic: PrimitiveRestartIndex
offsetof(RenderStateParameters, PrimitiveRestartIndex),
// P6 pipeline: the 20 capability bools ColorLogicOpEnabled..ProgramPointSizeEnabled,
// ScissorTestEnabledMask
offsetof(RenderStateParameters, ColorLogicOpEnabled),
// D7 dynamic: ScissorBoxes[16], ScissorBoxWrittenMask, ClipDistanceEnabledMask
offsetof(RenderStateParameters, ScissorBoxes),
sizeof(RenderStateParameters),
};
// Chunk 0 is dynamic and they alternate, which is not a coincidence: every boundary above
// is a transition between a run of BumpVersions()-written members and a run of
// ++m_version-only members, so two adjacent chunks of the same half would mean a boundary
// that separates nothing.
constexpr Bool MGPipeRenderStateChunkIsPipeline(SizeT index) { return (index % 2) == 1; }
constexpr MGPStateChunk MGPipeRenderStateChunkAt(SizeT index) {
return MGPStateChunk{static_cast<Uint16>(kMGPipeRenderStateChunkBoundaries[index]),
static_cast<Uint16>(kMGPipeRenderStateChunkBoundaries[index + 1] -
kMGPipeRenderStateChunkBoundaries[index])};
}
namespace MGPipeRenderStateChunkDetail {
constexpr SizeT CountHalf(Bool pipeline) {
SizeT count = 0;
for (SizeT i = 0; i < kMGPipeRenderStateChunkCount; ++i) {
if (MGPipeRenderStateChunkIsPipeline(i) == pipeline) ++count;
}
return count;
}
constexpr SizeT BytesOfHalf(Bool pipeline) {
SizeT bytes = 0;
for (SizeT i = 0; i < kMGPipeRenderStateChunkCount; ++i) {
if (MGPipeRenderStateChunkIsPipeline(i) == pipeline) {
bytes += MGPipeRenderStateChunkAt(i).Length;
}
}
return bytes;
}
} // namespace MGPipeRenderStateChunkDetail
inline constexpr SizeT kMGPipePipelineChunkCount = MGPipeRenderStateChunkDetail::CountHalf(true);
inline constexpr SizeT kMGPipeDynamicChunkCount = MGPipeRenderStateChunkDetail::CountHalf(false);
// The CSO's content-addressed identity is exactly this many bytes; CsoCache stores them
// per entry and memcmps them on a hash hit.
inline constexpr SizeT kMGPipePipelineChunkBytes = MGPipeRenderStateChunkDetail::BytesOfHalf(true);
inline constexpr SizeT kMGPipeDynamicChunkBytes = MGPipeRenderStateChunkDetail::BytesOfHalf(false);
// Seeds MGPipeComputePipelineSubsetHash, so a chunk-table change invalidates every
// persisted key rather than silently aliasing an old one. BUMP IT whenever a boundary,
// an ordering or the halves' membership moves.
inline constexpr Uint64 kMGPipeRenderStateChunkTableVersion = 1;
// ---- the trip wires. A mistake in the table is a build break, here. ----
static_assert(kMGPipeRenderStateChunkBoundaries[0] == 0,
"the chunk table must start at byte 0 of RenderStateParameters");
static_assert(kMGPipeRenderStateChunkBoundaries[kMGPipeRenderStateChunkCount] ==
sizeof(RenderStateParameters),
"the chunk table must cover RenderStateParameters to its last byte");
static_assert(kMGPipePipelineChunkCount == 7);
static_assert(kMGPipeDynamicChunkCount == 8);
static_assert(kMGPipePipelineChunkCount + kMGPipeDynamicChunkCount == kMGPipeRenderStateChunkCount);
static_assert(kMGPipePipelineChunkBytes + kMGPipeDynamicChunkBytes == sizeof(RenderStateParameters),
"the two halves must partition the block exactly - no gap, no overlap");
static_assert(kMGPipeRenderStateChunkCount <= 32,
"a chunk index has to fit the Uint32 ChunkMask of MGPRenderStateDesc/MGPDynamicState");
// Sorted, non-overlapping and complete: because every chunk is [b[i], b[i+1]) the only
// way to violate that is a non-ascending boundary, so this is the whole check.
constexpr Bool MGPipeRenderStateChunkBoundariesAscend() {
for (SizeT i = 0; i < kMGPipeRenderStateChunkCount; ++i) {
if (!(kMGPipeRenderStateChunkBoundaries[i] < kMGPipeRenderStateChunkBoundaries[i + 1])) {
return false;
}
if (kMGPipeRenderStateChunkBoundaries[i + 1] > 0xffffu) return false;
}
return true;
}
static_assert(MGPipeRenderStateChunkBoundariesAscend(),
"the chunk boundaries must strictly ascend and fit MGPStateChunk's Uint16 fields");
// The measured sizes. They are DERIVED above; these two assertions only pin what the P2
// brief and MEASUREMENTS.md quote, so a table change that moves them is loud.
static_assert(kMGPipePipelineChunkBytes == 396, "the pipeline subset is 396 bytes");
static_assert(kMGPipeDynamicChunkBytes == 772, "the dynamic subset is 772 bytes");
// ---- the operations everything else is written against ----
// The 396 pipeline bytes of `params`, in ascending chunk order, into `dst`.
void MGPipeGatherPipelineBytes(const RenderStateParameters& params, void* dst);
// The inverse: `src` is kMGPipePipelineChunkBytes bytes in the same order.
void MGPipeScatterPipelineBytes(const void* src, RenderStateParameters& dst);
// Incremental create_render_state: only the pipeline chunks named by `chunkMask` (bit i
// is pipeline chunk i, 0-based within the pipeline half), concatenated ascending.
SizeT MGPipePipelineChunkBlobBytes(Uint32 chunkMask);
void MGPipeGatherPipelineChunks(const RenderStateParameters& params, Uint32 chunkMask, void* dst);
void MGPipeScatterPipelineChunks(const void* src, Uint32 chunkMask, RenderStateParameters& dst);
// set_dynamic_state: bit i of `chunkMask` is dynamic chunk i, 0-based within the dynamic
// half; the blob is those chunks concatenated in ascending order.
SizeT MGPipeDynamicChunkBlobBytes(Uint32 chunkMask);
void MGPipeGatherDynamicChunks(const RenderStateParameters& params, Uint32 chunkMask, void* dst);
void MGPipeScatterDynamicChunks(const void* src, Uint32 chunkMask, RenderStateParameters& dst);
// Which dynamic chunks differ between two blocks - the chunk-level suppressor's answer.
Uint32 MGPipeDynamicChunksThatMoved(const RenderStateParameters& a, const RenderStateParameters& b);
// Which pipeline chunks differ - the incremental-create mask against a base CSO.
Uint32 MGPipePipelineChunksThatMoved(const RenderStateParameters& a, const RenderStateParameters& b);
// XXH64 over the seven pipeline chunks in ascending order, seeded with the table version.
// Runs ONLY when m_pipelineStateVersion moved, i.e. never in the steady state.
Uint64 MGPipeComputePipelineSubsetHash(const RenderStateParameters& params);
// The same hash over already-gathered bytes (CsoCache holds them, so it does not re-gather).
Uint64 MGPipeHashPipelineBytes(const void* bytes);
} // namespace MobileGL::MG_Pipe
+23 -13
View File
@@ -231,8 +231,13 @@ namespace MobileGL::MG_Pipe {
// The half of the render state that must NOT mint a CSO: viewport, scissor, depth
// range, blend colour, line width, polygon offset, stencil ref/write mask, clear
// values, sample coverage, hints and the point-size family. This is what keeps
// glViewport from evicting Magma's pipeline memo (D-B1).
// values, hints, the point-size family and the primitive-restart index. This is what
// keeps glViewport from evicting Magma's pipeline memo (D-B1).
//
// SAMPLE COVERAGE IS NOT IN IT, and this comment used to say it was. P2's rule is that
// a byte is pipeline state if and only if a public RenderState setter that calls
// BumpVersions() writes it, and SetSampleCoverage does - so SampleCoverageValue and
// SampleCoverageInvert are in pipeline chunk P2 (MGPipeRenderStateSpans.h).
struct MGPDynamicState {
Uint32 ChunkMask;
Uint16 Version;
@@ -514,14 +519,17 @@ namespace MobileGL::MG_Pipe {
// to it because both sides are the same translation unit. G3 emits the offsetof
// assertions; under split the block is serialized field-wise rather than memcpy'd.
struct ResidualValueBlock {
RenderStateParameters RenderState; // until create/bind_render_state + set_dynamic_state land
PixelStoreParameters Pack; // until set_pixel_pack_state lands
// The 35 CapabilityInput bits, packed in enum order. P2 retired everything else:
// RenderStateParameters to create/bind_render_state + set_dynamic_state, Pack to
// set_pixel_pack_state, and the patch quintet to set_patch_state.
//
// What is left is deliberately REDUNDANT. Every one of the 35 capabilities is
// answerable from the assembled working block now that P2 gave FramebufferSrgb,
// DepthClamp and TextureCubeMapSeamless real storage - which is the point: the
// applier compares the two answers bit by bit, so the day a later call takes a
// capability over and forgets to carry it, the block says so on the next draw
// (Fatal{PipeResidualDiverged, "<Cap>"}, MG_Pipe/PipeApply.cpp).
Uint64 CapabilityBits;
Uint32 PatchVertices;
Uint32 Pad0;
Float PatchOuter[4];
Float PatchInner[2];
Uint32 Pad1[2];
};
static_assert(std::is_trivially_copyable_v<ResidualValueBlock>);
// The retirement ratchet. This number only ever goes DOWN: every stage that lands a real
@@ -530,10 +538,12 @@ namespace MobileGL::MG_Pipe {
// gone. Shrinking the block without lowering the number, or growing it at all, is a build
// break - which is the point.
//
// Stable across the ABIs MobileGL ships on: every member of RenderStateParameters and
// PixelStoreParameters is a fixed-width scalar or an array of one, with no pointer and no
// SizeT.
#define MGL_RESIDUAL_BLOCK_SIZE 1248
// Stable across the ABIs MobileGL ships on: the one member is a fixed-width scalar.
//
// P2: 1248 -> 8. RenderStateParameters (1168) retired to create/bind_render_state and
// set_dynamic_state, PixelStoreParameters (28) to set_pixel_pack_state, and the patch
// quintet (52 with its padding) to set_patch_state.
#define MGL_RESIDUAL_BLOCK_SIZE 8
static_assert(sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE,
"the residual value block changed size; lower MGL_RESIDUAL_BLOCK_SIZE if a field "
"retired, and do not raise it");
+15
View File
@@ -289,6 +289,21 @@ namespace MobileGL {
// Every entry is initialized to all-true in RenderState's constructor.
Array<BoolVec4, kMGMaxDrawBuffers> ColorMasks;
// GL_FRAMEBUFFER_SRGB / GL_DEPTH_CLAMP / GL_TEXTURE_CUBE_MAP_SEAMLESS. Until P2 these
// three fell to SetCapability's "not supported currently" arm - glEnable was swallowed
// and IsCapabilityEnabled answered a compile-time false, so DirectGLES' sRGB block and
// the DirectVulkan read points consumed a constant while glIsEnabled lied about it.
// Placed HERE, in the three alignment bytes between ColorMasks (32 bytes, align 1) and
// ClearColor (align 4), so sizeof(RenderStateParameters) stays 1168 and no existing
// offset moves: the Espryt span constants and the P2 chunk table both depend on that.
// All three are PIPELINE state (their setters call BumpVersions): FramebufferSrgb is
// what ARCHITECTURE.md 5.3 asks for, DepthClamp is
// VkPipelineRasterizationStateCreateInfo::depthClampEnable, and TextureCubeMapSeamless
// changes sampler interpretation.
Bool FramebufferSrgbEnabled = false;
Bool DepthClampEnabled = false;
Bool TextureCubeMapSeamlessEnabled = false;
// Clear State
FloatVec4 ClearColor = FloatVec4(0.0f, 0.0f, 0.0f, 1.0f);
Float ClearDepth = 1.0f;
+270
View File
@@ -0,0 +1,270 @@
// MobileGL - MobileGL/MG_Pipe/PipeApply.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The in-process applier (PipeApply.h). Compiled only under MOBILEGL_PIPE_PUSH.
//
// This is the one .cpp under MG_Pipe/ that reaches UP to MG_Backend/MGPipe/PipeInputs.h,
// and that is the point: under split it becomes the server, and the server is where the
// working state lives. Nothing in MG_Pipe's HEADERS reaches it, so purity gate A
// (MGPipeValueTypes.h's include closure) is untouched.
#include <MG_Pipe/PipeApply.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <cstdlib>
#include <cstring>
namespace MobileGL::MG_Pipe {
// The applier's door into PipeInputs' storage, the write-side twin of PipeFill.cpp's
// MGPipeFillAccess. It does NOT stamp the poison generations: a stamp says "the filler
// published this field for THIS verb", and that statement belongs to the walk that
// called the applier, not to the applier - MG_Impl/Pipe/PipeFill.cpp stamps what it
// emitted, exactly as it stamps what it copied.
struct MGPipeApplyAccess {
static RenderStateParameters& RenderState(PipeInputs& inputs) { return inputs.m_renderState; }
static PixelStoreParameters& PackState(PipeInputs& inputs) { return inputs.m_pixelStore[0]; }
static Bool* Capabilities(PipeInputs& inputs) { return inputs.m_capability; }
static PipeInputs::CurrentVertexAttributeValue* VertexAttribDefaults(PipeInputs& inputs) {
return inputs.m_currentVertexAttribute;
}
static void SetRenderStateVersions(PipeInputs& inputs, Uint parameters, Uint pipeline) {
inputs.m_renderStateParametersVersion = parameters;
inputs.m_pipelineStateVersion = pipeline;
}
static void SetRenderStateParametersVersion(PipeInputs& inputs, Uint parameters) {
inputs.m_renderStateParametersVersion = parameters;
}
static void SetPatchState(PipeInputs& inputs, Uint vertices, const FloatVec4& outer,
const FloatVec2& inner) {
inputs.m_patchVertices = vertices;
inputs.m_patchDefaultOuterLevel = outer;
inputs.m_patchDefaultInnerLevel = inner;
}
};
namespace {
// CapabilityInput in enum order, so the residual block's bit i and this name agree by
// construction. The static_assert below is what makes a capability added to the enum
// without a name here a build break rather than an "<unknown>" in a Fatal line.
constexpr const char* kCapabilityNames[] = {
"Blend",
"ClipDistance0",
"ClipDistance1",
"ClipDistance2",
"ClipDistance3",
"ClipDistance4",
"ClipDistance5",
"ClipDistance6",
"ClipDistance7",
"ColorLogicOp",
"CullFace",
"DebugOutput",
"DebugOutputSynchronous",
"DepthClamp",
"DepthTest",
"Dither",
"FramebufferSrgb",
"LineSmooth",
"Multisample",
"PolygonOffsetFill",
"PolygonOffsetLine",
"PolygonOffsetPoint",
"PolygonSmooth",
"PrimitiveRestart",
"PrimitiveRestartFixedIndex",
"RasterizerDiscard",
"SampleAlphaToCoverage",
"SampleAlphaToOne",
"SampleCoverage",
"SampleShading",
"SampleMask",
"ScissorTest",
"StencilTest",
"TextureCubeMapSeamless",
"ProgramPointSize",
};
constexpr SizeT kCapabilityCount = static_cast<SizeT>(CapabilityInput::CapabilityInputCount);
static_assert(sizeof(kCapabilityNames) / sizeof(kCapabilityNames[0]) == kCapabilityCount,
"CapabilityInput gained a value; name it here or the residual trip wire "
"cannot say which capability diverged");
static_assert(kCapabilityCount <= 64,
"ResidualValueBlock::CapabilityBits is a Uint64; 35 bits fit, 65 would not");
MGPipeApplierState g_applier{};
MGPipeRenderStateCsoRecord* FindCso(MGPipeHandle handle) {
if (handle.Slot >= g_applier.RenderStateCsos.size()) return nullptr;
MGPipeRenderStateCsoRecord& record = g_applier.RenderStateCsos[handle.Slot];
if (!record.Live || record.Gen != handle.Gen) return nullptr;
return &record;
}
constexpr Uint32 kAllPipelineChunks =
static_cast<Uint32>((Uint64{1} << kMGPipePipelineChunkCount) - 1);
} // namespace
MGPipeApplierState& MGPipeApplier() { return g_applier; }
void MGPipeApplierReset() {
g_applier.RenderStateCsos.clear();
g_applier.BoundRenderStateCso = kMGPipeNullHandle;
g_applier.Residual = ResidualValueBlock{};
g_applier.HasResidual = false;
}
void MGPipeApplyCreateRenderState(const MGPRenderStateDesc& desc, const void* chunkBytes) {
MOBILEGL_ASSERT(desc.Cso.Slot >= kMGPipeFirstAllocatableSlot,
"create_render_state named the reserved slot 0");
if (desc.Cso.Slot >= g_applier.RenderStateCsos.size()) {
g_applier.RenderStateCsos.resize(desc.Cso.Slot + 1);
}
MGPipeRenderStateCsoRecord& record = g_applier.RenderStateCsos[desc.Cso.Slot];
if (MGPipeHandleIsNull(desc.BaseCso)) {
// A brand-new CSO carries its whole content; there is no earlier record to
// inherit the unnamed chunks from.
MOBILEGL_ASSERT((desc.ChunkMask & kAllPipelineChunks) == kAllPipelineChunks,
"create_render_state with no BaseCso must name every pipeline chunk "
"(mask=0x%x, expected 0x%x)",
desc.ChunkMask, kAllPipelineChunks);
record.PipelineBytes = {};
} else {
const MGPipeRenderStateCsoRecord* base = FindCso(desc.BaseCso);
MOBILEGL_ASSERT(base != nullptr,
"create_render_state named a dead BaseCso {slot=%u, gen=%u}",
desc.BaseCso.Slot, desc.BaseCso.Gen);
if (base != nullptr) record.PipelineBytes = base->PipelineBytes;
}
// The chunk bytes land in the record's own gathered order, so the record is always a
// complete pipeline half whatever mask minted it.
RenderStateParameters staging{};
MGPipeScatterPipelineBytes(record.PipelineBytes.data(), staging);
MGPipeScatterPipelineChunks(chunkBytes, desc.ChunkMask, staging);
MGPipeGatherPipelineBytes(staging, record.PipelineBytes.data());
record.Gen = desc.Cso.Gen;
record.Live = true;
}
void MGPipeApplyBindRenderState(const MGPBindRenderState& bind) {
const MGPipeRenderStateCsoRecord* record = FindCso(bind.Cso);
MOBILEGL_ASSERT(record != nullptr, "bind_render_state named a dead CSO {slot=%u, gen=%u}",
bind.Cso.Slot, bind.Cso.Gen);
if (record == nullptr) return;
PipeInputs& inputs = gPipeInputs;
MGPipeScatterPipelineBytes(record->PipelineBytes.data(),
MGPipeApplyAccess::RenderState(inputs));
MGPipeApplyAccess::SetRenderStateVersions(inputs, bind.Version, bind.PipelineVersion);
g_applier.BoundRenderStateCso = bind.Cso;
MGPipeDeriveRenderStateFields(inputs);
}
void MGPipeApplyDeleteRenderState(const MGPHandleOnly& handle) {
MOBILEGL_ASSERT(handle.Kind == static_cast<Uint32>(MGPipeKind::RenderStateCso),
"delete_render_state on kind %u", handle.Kind);
MGPipeRenderStateCsoRecord* record = FindCso(handle.Handle);
if (record == nullptr) return;
record->Live = false;
// The Gen stays: it is the CLIENT allocator that bumps it when the slot is handed
// out again (MGPipeHandles.h: "Gen increments only when a SLOT IS REUSED"), and a
// server-side bump here would put the two identities out of step.
if (g_applier.BoundRenderStateCso == handle.Handle) {
g_applier.BoundRenderStateCso = kMGPipeNullHandle;
}
}
void MGPipeApplySetDynamicState(const MGPDynamicState& dyn, const void* chunkBytes) {
PipeInputs& inputs = gPipeInputs;
MGPipeScatterDynamicChunks(chunkBytes, dyn.ChunkMask, MGPipeApplyAccess::RenderState(inputs));
MGPipeApplyAccess::SetRenderStateParametersVersion(inputs, dyn.Version);
MGPipeDeriveRenderStateFields(inputs);
}
void MGPipeApplySetPixelPackState(const MGPPixelPackState& pack) {
MGPipeApplyAccess::PackState(gPipeInputs) = pack.Pack;
}
void MGPipeApplySetPatchState(const MGPPatchState& patch) {
PipeInputs& inputs = gPipeInputs;
RenderStateParameters& working = MGPipeApplyAccess::RenderState(inputs);
working.PatchVertices = patch.Vertices;
working.PatchDefaultOuterLevel =
FloatVec4(patch.Outer[0], patch.Outer[1], patch.Outer[2], patch.Outer[3]);
working.PatchDefaultInnerLevel = FloatVec2(patch.Inner[0], patch.Inner[1]);
MGPipeApplyAccess::SetPatchState(inputs, working.PatchVertices, working.PatchDefaultOuterLevel,
working.PatchDefaultInnerLevel);
}
void MGPipeApplySetVertexAttribDefaults(const MGPVertexAttribDefaults& hdr,
const MGPAttribValue* tail) {
PipeInputs& inputs = gPipeInputs;
PipeInputs::CurrentVertexAttributeValue* slots = MGPipeApplyAccess::VertexAttribDefaults(inputs);
Uint32 consumed = 0;
for (Uint32 location = 0; location < PipeInputs::kMaxVertexAttribs; ++location) {
if ((hdr.Mask & (1u << location)) == 0) continue;
MOBILEGL_ASSERT(consumed < hdr.Count,
"set_vertex_attrib_defaults: Mask names more attributes than Count");
if (consumed >= hdr.Count) break;
const MGPAttribValue& value = tail[consumed++];
MOBILEGL_ASSERT(value.Location == location,
"set_vertex_attrib_defaults: tail out of ascending location order "
"(%u where %u was expected)",
value.Location, location);
PipeInputs::CurrentVertexAttributeValue& slot = slots[location];
// The three views are always populated; which one a shader input consumes is
// ClassifyVertexAttribType's answer, not the carrier's, so all three cross.
std::memcpy(slot.floatValue.data(), value.Data, sizeof(slot.floatValue));
std::memcpy(slot.intValue.data(), value.Data, sizeof(slot.intValue));
std::memcpy(slot.uintValue.data(), value.Data, sizeof(slot.uintValue));
}
MOBILEGL_ASSERT(consumed == hdr.Count,
"set_vertex_attrib_defaults: Count %u does not match the %u attributes Mask "
"names",
hdr.Count, consumed);
}
void MGPipeApplySetResidualValueState(const ResidualValueBlock& block) {
g_applier.Residual = block;
g_applier.HasResidual = true;
// THE TRIP WIRE (ARCHITECTURE.md 9.4, P2 brief D9). CapabilityBits is redundant with
// the assembled working block by design: every one of the 35 capabilities is
// answerable from RenderStateParameters now that P2 closed the three storage holes.
// So the day a later call takes a capability over and forgets to carry it, the two
// answers part and this says so on the next draw - which is what a migration carrier
// is for.
const Bool* assembled = MGPipeApplyAccess::Capabilities(gPipeInputs);
for (SizeT i = 0; i < kCapabilityCount; ++i) {
const Bool carried = ((block.CapabilityBits >> i) & 1ull) != 0;
if (carried == assembled[i]) continue;
#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY
MGLOG_F("MGPipe: Fatal{PipeResidualDiverged, \"%s\"} carried=%d assembled=%d",
kCapabilityNames[i], static_cast<int>(carried), static_cast<int>(assembled[i]));
std::abort();
#else
MGLOG_E("MGPipe: residual value block diverged on %s (carried=%d assembled=%d)",
kCapabilityNames[i], static_cast<int>(carried), static_cast<int>(assembled[i]));
#endif
}
}
void MGPipeDeriveRenderStateFields(PipeInputs& inputs) {
// STUB (P2 package A commit c1, on p2/spans). The 29 derivations of brief D5 land
// here, each transcribed from its RenderState getter; until then the residual fill
// loop still copies those fields out of GLContext, which is exactly P1's behaviour,
// so an empty body is correct rather than merely harmless. The two transcriptions
// that are not one-liners and must be copied exactly are GetViewport() (viewport 0
// ROUNDED TO INTEGERS) and IsCapabilityEnabled / IsCapabilityEnabledIndexed (the
// 35-way and 2-way switches, including Blend -> BlendStates[0].Enabled and
// ScissorTest -> ScissorTestEnabledMask & 1).
(void)inputs;
}
} // namespace MobileGL::MG_Pipe
+107
View File
@@ -0,0 +1,107 @@
// MobileGL - MobileGL/MG_Pipe/PipeApply.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include "MGPipeRenderStateSpans.h"
#include "MGPipeTypes.h"
// The in-process applier: the SERVER half of the calls P2 emits. Under split this file is
// MG_Remote/Server/PipeApplier (ARCHITECTURE.md 8.3); in the monolith it writes
// MG_Backend/MGPipe/PipeInputs' gPipeInputs directly, so a call and its effect are one
// function call apart and nothing is serialised.
//
// THE SERVER'S PER-CONTEXT WORKING BLOCK *IS* PipeInputs::m_renderState. bind_render_state
// and set_dynamic_state scatter their chunks straight into it, which is why DirectGLES'
// SyncRenderState is not one line changed (ROADMAP.md P2, G5): the block Espryt binds by
// const reference is the assembled block. It is also what makes the MOBILEGL_PIPE_VERIFY
// comparator a real oracle instead of a tautology - the compare-at-read now proves
// "assembled == live", field by field, at every backend read.
//
// This header FORWARD-DECLARES PipeInputs rather than including it: the applier's callers
// (MG_Impl/Pipe) already have it, and MG_Pipe sits below MG_Backend.
//
// Compiled only under MOBILEGL_PIPE_PUSH (CMakeLists.txt), so the pull build gains no symbol.
namespace MobileGL::MG_Pipe {
struct PipeInputs;
// ---------------------------------------------------------------------------------
// The CSO store
// ---------------------------------------------------------------------------------
// One record per live render-state CSO, indexed by MGPipeHandle::Slot. It keeps the 396
// pipeline bytes because an incremental create_render_state names only the chunks that
// moved against a BaseCso - the rest has to come from somewhere, and that somewhere is
// the record the client is naming.
struct MGPipeRenderStateCsoRecord {
Uint32 Gen = 0;
Bool Live = false;
Array<Uint8, kMGPipePipelineChunkBytes> PipelineBytes{};
};
struct MGPipeApplierState {
// Indexed by slot; slot 0 is the reserved null handle and is never live
// (MGPipeHandles.h kMGPipeFirstAllocatableSlot).
Vector<MGPipeRenderStateCsoRecord> RenderStateCsos;
// The last bind, so a rebind of the same handle can be answered without a scatter.
MGPipeHandle BoundRenderStateCso = kMGPipeNullHandle;
// The residual block as last received. Compared against the assembled state on every
// set_residual_value_state; a disagreement is the D9 trip wire.
ResidualValueBlock Residual{};
Bool HasResidual = false;
};
// The monolith's single applier. Under split there is one per served context.
MGPipeApplierState& MGPipeApplier();
// Drops every CSO and the residual mirror. Context teardown, server reset, and the unit
// tests' per-case fixture.
void MGPipeApplierReset();
// ---------------------------------------------------------------------------------
// The seven apply entry points (ARCHITECTURE.md 5.3, ROADMAP.md P2)
// ---------------------------------------------------------------------------------
// create_render_state. `chunkBytes` is the pipeline chunks named by desc.ChunkMask,
// concatenated in ascending chunk order (MGPipeGatherPipelineChunks' output). A
// brand-new CSO must name every chunk; an incremental one starts from desc.BaseCso.
void MGPipeApplyCreateRenderState(const MGPRenderStateDesc& desc, const void* chunkBytes);
// bind_render_state: 12 bytes, no blob, no hashing. Scatters the record's seven pipeline
// chunks into the working block and publishes both versions.
void MGPipeApplyBindRenderState(const MGPBindRenderState& bind);
// delete_render_state: frees the slot. The client's allocator owns the Gen bump on
// REUSE; the record only stops being live here. CsoCache's LRU eviction emits this.
void MGPipeApplyDeleteRenderState(const MGPHandleOnly& handle);
// set_dynamic_state: the dynamic chunks named by dyn.ChunkMask, concatenated ascending.
void MGPipeApplySetDynamicState(const MGPDynamicState& dyn, const void* chunkBytes);
// set_pixel_pack_state. PACK only, deliberately (MGPipeTypes.h, ARCHITECTURE.md 4.6 D5).
void MGPipeApplySetPixelPackState(const MGPPixelPackState& pack);
// set_patch_state. The trio also travels in pipeline chunk P0, and the applier asserts
// under verify that the two carriers agree - the redundancy is a trip wire, not waste.
void MGPipeApplySetPatchState(const MGPPatchState& patch);
// set_vertex_attrib_defaults: `tail` is hdr.Count MGPAttribValues for the attributes
// named by hdr.Mask, in ascending location order.
void MGPipeApplySetVertexAttribDefaults(const MGPVertexAttribDefaults& hdr, const MGPAttribValue* tail);
// set_residual_value_state: what has no call of its own. Since P2 that is one Uint64 of
// capability bits, and every one of them is ALSO answerable from the assembled working
// block - which is the point. A disagreement is Fatal{PipeResidualDiverged, "<Cap>"}.
void MGPipeApplySetResidualValueState(const ResidualValueBlock& block);
// ---------------------------------------------------------------------------------
// The derivation step (ARCHITECTURE.md 5.3, P2 brief D5)
// ---------------------------------------------------------------------------------
// Recomputes every PipeInputs field that is a pure function of the working
// RenderStateParameters, instead of pulling it out of GLContext a second time. Called by
// the applier after ANY scatter.
//
// The guard is the oracle P1 built: MOBILEGL_PIPE_VERIFY's compare-at-read re-reads each
// of these from the live context at every backend read, so a transcription error is
// caught on the first draw that reads it.
void MGPipeDeriveRenderStateFields(PipeInputs& inputs);
} // namespace MobileGL::MG_Pipe
+6 -2
View File
@@ -144,8 +144,11 @@
#define MGP_FIELDS_MGPPatchState(F) \
F(Vertices) F(Outer) F(Inner)
// P2 ratcheted this block from six rows to one: RenderStateParameters retired to
// create/bind_render_state + set_dynamic_state, Pack to set_pixel_pack_state and the
// patch trio to set_patch_state. What is left is the redundant capability trip wire.
#define MGP_FIELDS_ResidualValueBlock(F) \
F(RenderState) F(Pack) F(CapabilityBits) F(PatchVertices) F(PatchOuter) F(PatchInner)
F(CapabilityBits)
#define MGP_FIELDS_MGPResidualValueState(F) \
F(Version) F(Blob)
@@ -231,7 +234,8 @@
F(Viewports) F(LineWidth) F(PointSize) F(PatchVertices) F(PatchDefaultOuterLevel) \
F(PatchDefaultInnerLevel) F(PolygonOffsetFactor) F(PolygonOffsetUnits) F(PolygonOffsetClamp) \
F(ClipOrigin) F(ClipDepthMode) F(BlendStates) F(LogicOp) F(DepthTestEnabled) F(DepthFunc) \
F(DepthMask) F(ColorMasks) F(ClearColor) F(ClearDepth) F(ClearStencil) F(BlendColor) \
F(DepthMask) F(ColorMasks) F(FramebufferSrgbEnabled) F(DepthClampEnabled) \
F(TextureCubeMapSeamlessEnabled) F(ClearColor) F(ClearDepth) F(ClearStencil) F(BlendColor) \
F(DepthRanges) F(SampleCoverageValue) F(SampleCoverageInvert) F(SampleMaskValue) \
F(MinSampleShadingValue) F(StencilStates) F(CullFaceEnabled) F(CullFaceModeSetting) \
F(FrontFaceModeSetting) F(ProvokingVertexModeSetting) F(LineSmoothHint) F(PolygonSmoothHint) \
+91
View File
@@ -299,6 +299,97 @@ inline constexpr const char* kMGPipeInputFieldFilledBy[kMGPipeInputFieldCount] =
"SetStreamOutputTargets",
};
// P2 brief D5: the call that now SUPPLIES a field, so the residual fill loop no
// longer pulls it out of GLContext. kNone means the field is still pulled - which
// is what makes MOBILEGL_PIPE_PUSH a true per-subsystem A/B instead of a single
// switch. Rows come from Coverage.def's MGP_COVERAGE_EMITTED_LIST.
enum class MGPipeFieldEmitter : Uint8 {
kNone = 0,
BindRenderState,
CreateRenderState,
SetDynamicState,
SetPatchState,
SetPixelPackState,
SetVertexAttribDefaults,
};
inline constexpr const char* kMGPipeFieldEmitterNames[] = {
"kNone",
"BindRenderState",
"CreateRenderState",
"SetDynamicState",
"SetPatchState",
"SetPixelPackState",
"SetVertexAttribDefaults",
};
inline constexpr MGPipeFieldEmitter kMGPipeFieldEmittedBy[kMGPipeInputFieldCount] = {
MGPipeFieldEmitter::kNone, // GetActiveTextureUnit
MGPipeFieldEmitter::SetDynamicState, // GetBlendColor
MGPipeFieldEmitter::CreateRenderState, // GetBlendEquationIndexed
MGPipeFieldEmitter::CreateRenderState, // GetBlendFuncIndexed
MGPipeFieldEmitter::kNone, // GetBoundTransformFeedbackName
MGPipeFieldEmitter::kNone, // GetBoundVertexArray
MGPipeFieldEmitter::kNone, // GetBufferBindingSlot
MGPipeFieldEmitter::kNone, // GetBufferBindingPoint
MGPipeFieldEmitter::kNone, // GetBufferBindingPointCount
MGPipeFieldEmitter::kNone, // GetTouchedBufferBindingPointCount
MGPipeFieldEmitter::SetDynamicState, // GetClampReadColor
MGPipeFieldEmitter::SetDynamicState, // GetClearColor
MGPipeFieldEmitter::SetDynamicState, // GetClearDepth
MGPipeFieldEmitter::SetDynamicState, // GetClearStencil
MGPipeFieldEmitter::CreateRenderState, // GetColorMaskIndexed
MGPipeFieldEmitter::CreateRenderState, // GetCullFaceMode
MGPipeFieldEmitter::SetVertexAttribDefaults, // GetCurrentVertexAttribute
MGPipeFieldEmitter::CreateRenderState, // GetDepthFunc
MGPipeFieldEmitter::CreateRenderState, // GetDepthMask
MGPipeFieldEmitter::SetDynamicState, // GetDepthRangeIndexed
MGPipeFieldEmitter::kNone, // GetFramebufferBindingSlot
MGPipeFieldEmitter::kNone, // GetImageTextureBinding
MGPipeFieldEmitter::SetDynamicState, // GetLineWidth
MGPipeFieldEmitter::CreateRenderState, // GetLogicOp
MGPipeFieldEmitter::kNone, // GetMaxTouchedTextureUnit
MGPipeFieldEmitter::CreateRenderState, // GetMinSampleShadingValue
MGPipeFieldEmitter::SetPatchState, // GetPatchDefaultInnerLevel
MGPipeFieldEmitter::SetPatchState, // GetPatchDefaultOuterLevel
MGPipeFieldEmitter::SetPatchState, // GetPatchVertices
MGPipeFieldEmitter::BindRenderState, // GetPipelineStateVersion
MGPipeFieldEmitter::SetPixelPackState, // GetPixelStoreParameters
MGPipeFieldEmitter::CreateRenderState, // GetPolygonModeFront
MGPipeFieldEmitter::SetDynamicState, // GetPolygonOffsetFactor
MGPipeFieldEmitter::SetDynamicState, // GetPolygonOffsetUnits
MGPipeFieldEmitter::SetDynamicState, // GetPrimitiveRestartIndex
MGPipeFieldEmitter::kNone, // GetProgramForDispatch
MGPipeFieldEmitter::kNone, // GetProgramForDraw
MGPipeFieldEmitter::kNone, // GetProgramObject
MGPipeFieldEmitter::CreateRenderState, // GetProvokingVertexMode
MGPipeFieldEmitter::CreateRenderState, // GetRenderStateParameters
MGPipeFieldEmitter::BindRenderState, // GetRenderStateParametersVersion
MGPipeFieldEmitter::kNone, // GetSamplingResolutionGeneration
MGPipeFieldEmitter::SetDynamicState, // GetScissorBox
MGPipeFieldEmitter::CreateRenderState, // GetStencilState
MGPipeFieldEmitter::kNone, // GetTextureBindGeneration
MGPipeFieldEmitter::kNone, // GetTextureContextId
MGPipeFieldEmitter::kNone, // GetTextureObject
MGPipeFieldEmitter::kNone, // GetTextureUnitObject
MGPipeFieldEmitter::kNone, // GetTransformFeedbackCapturedVertices
MGPipeFieldEmitter::kNone, // GetTransformFeedbackGeneration
MGPipeFieldEmitter::kNone, // GetTransformFeedbackPausedPrimitiveCounter
MGPipeFieldEmitter::kNone, // GetTransformFeedbackProgram
MGPipeFieldEmitter::SetDynamicState, // GetViewport
MGPipeFieldEmitter::SetDynamicState, // GetViewportIndexed
MGPipeFieldEmitter::CreateRenderState, // IsCapabilityEnabled
MGPipeFieldEmitter::CreateRenderState, // IsCapabilityEnabledIndexed
MGPipeFieldEmitter::kNone, // IsTransformFeedbackActive
MGPipeFieldEmitter::kNone, // IsTransformFeedbackPaused
MGPipeFieldEmitter::kNone, // InvalidateCompileEnv
MGPipeFieldEmitter::kNone, // ValidateProgramName
MGPipeFieldEmitter::kNone, // RecordError
MGPipeFieldEmitter::kNone, // GetBoundTransformFeedbackLifetimeId
MGPipeFieldEmitter::kNone, // HasOpenTransformFeedbackSpan
};
inline constexpr SizeT kMGPipeEmittedFieldCount = 34;
struct MGPipeFilledState {
Uint64 CurrentVerbSerial;
Uint64 FilledGen[kMGPipeInputFieldCount];
+60 -34
View File
@@ -14,54 +14,80 @@
// D-B1 rejected three CSOs and demanded this table instead, so the table needs its own
// completeness trip wire: MG_Test walks every public RenderState setter and asserts that
// the pipeline-subset hash moves IF AND ONLY IF m_pipelineStateVersion moves. That test
// and MGPipeRenderStateSpans.cpp land with P2; what P0 pins is the MEMBER LIST, taken from
// what VulkanRenderer::ComputePipelineStateHash hashes today, so the later offsets are
// derived from a list that was reviewed rather than invented.
// the pipeline-subset hash moves IF AND ONLY IF m_pipelineStateVersion moves
// (MG_Test/Pipe/RenderStateSpansTest.cpp).
//
// Deliberately absent, and each absence is a question P2 has to answer before the chunk
// table freezes:
// - FramebufferSrgb and DepthClamp have NO STORAGE at all (RenderState.cpp's SetCapability
// falls to "not supported currently" and IsCapabilityEnabled returns false), so six
// backend read points are constant false today. Pipeline state or dead capability?
// - ProvokingVertexModeSetting is Vulkan pipeline state but is not hashed today.
// - FrontFaceModeSetting, ClipOrigin and ClipDepthMode are pipeline state on Vulkan and
// are handled elsewhere in the payload path rather than in the memo word.
// P2 replaced P0's provenance with a RULE, and the rule is the only thing that decides
// membership: a member is pipeline state IF AND ONLY IF some public RenderState setter that
// calls BumpVersions() writes it. That is what makes the G7 invariant true by construction
// rather than by inspection, and it turns the subset into a strict SUPERSET of the 24
// members VulkanRenderer::ComputePipelineStateHash used to hash.
//
// The three questions P0 left open are ANSWERED here, and the answers are in this list:
// - FramebufferSrgb, DepthClamp and TextureCubeMapSeamless had NO STORAGE at all -
// SetCapability fell to "not supported currently" and IsCapabilityEnabled answered a
// compile-time false. P2 gave all three real storage in the three padding bytes between
// ColorMasks and ClearColor, and their setters call BumpVersions(), so: pipeline state.
// - ProvokingVertexModeSetting: SetProvokingVertexMode calls BumpVersions(), so pipeline.
// - FrontFaceModeSetting likewise. ClipOrigin and ClipDepthMode do NOT (SetClipControl is
// ++m_version only), so they are dynamic, in chunk D1.
//
// The complement of this list is the DYNAMIC subset - the half whose whole purpose is that
// glViewport must not mint a new CSO.
inline constexpr const char* const kMGPipePipelineStateMembers[] = {
"CullFaceEnabled",
"DepthTestEnabled",
"PolygonOffsetFillEnabled",
"RasterizerDiscardEnabled",
"ColorLogicOpEnabled",
"StencilTestEnabled",
"PrimitiveRestartEnabled",
"PrimitiveRestartFixedIndexEnabled",
"DepthMask",
"SampleShadingEnabled",
"MultisampleEnabled",
"SampleMaskEnabled",
"SampleMaskValue",
"MinSampleShadingValue",
"PatchVertices",
"PatchDefaultOuterLevel",
"PatchDefaultInnerLevel",
"PolygonModeFront",
"CullFaceModeSetting",
"DepthFunc",
"LogicOp",
"StencilStates",
"BlendStates",
"LogicOp",
"DepthTestEnabled",
"DepthFunc",
"DepthMask",
"ColorMasks",
"FramebufferSrgbEnabled",
"DepthClampEnabled",
"TextureCubeMapSeamlessEnabled",
"SampleCoverageValue",
"SampleCoverageInvert",
"SampleMaskValue",
"MinSampleShadingValue",
"StencilStates",
"CullFaceEnabled",
"CullFaceModeSetting",
"FrontFaceModeSetting",
"ProvokingVertexModeSetting",
"PolygonModeFront",
"PolygonModeBack",
"ColorLogicOpEnabled",
"DebugOutputEnabled",
"DebugOutputSynchronousEnabled",
"DitherEnabled",
"LineSmoothEnabled",
"MultisampleEnabled",
"PolygonOffsetFillEnabled",
"PolygonOffsetLineEnabled",
"PolygonOffsetPointEnabled",
"PolygonSmoothEnabled",
"PrimitiveRestartEnabled",
"PrimitiveRestartFixedIndexEnabled",
"RasterizerDiscardEnabled",
"SampleAlphaToCoverageEnabled",
"SampleAlphaToOneEnabled",
"SampleCoverageEnabled",
"SampleMaskEnabled",
"SampleShadingEnabled",
"StencilTestEnabled",
"ProgramPointSizeEnabled",
"ScissorTestEnabledMask",
};
inline constexpr SizeT kMGPipePipelineStateMemberCount = 24;
inline constexpr SizeT kMGPipePipelineStateMemberCount = 44;
static_assert(kMGPipePipelineStateMemberCount ==
sizeof(kMGPipePipelineStateMembers) / sizeof(kMGPipePipelineStateMembers[0]));
// Filled in by MG_Pipe/MGPipeRenderStateSpans.cpp (P2), which computes the offsets
// in C++ with offsetof rather than guessing them in python.
// Defined by MG_Pipe/MGPipeRenderStateSpans.cpp (P2), which computes every
// boundary in C++ with offsetof rather than guessing it in python. 7 pipeline
// chunks / 396 bytes and 8 dynamic chunks / 772 bytes, and the two halves
// partition [0, sizeof(RenderStateParameters)) exactly - asserted there.
extern const MGPStateChunk kMGPipePipelineChunks[];
extern const MGPStateChunk kMGPipeDynamicChunks[];
+8
View File
@@ -929,3 +929,11 @@ inline Bool MGPipeApplyWireRecord(MGPWireOp op, const void* record, Uint64 size,
}
#undef MGP_WIRE_CHECK_BOUNDS
// The ResidualValueBlock layout, from PipeFields.def's
// MGP_FIELDS_ResidualValueBlock. Retiring a field without lowering
// MGL_RESIDUAL_BLOCK_SIZE is a build break, which is the point.
static_assert(offsetof(ResidualValueBlock, CapabilityBits) == 0,
"the residual block's first member must sit at offset 0");
static_assert(sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE,
"the residual ratchet only ever goes down");
@@ -317,9 +317,11 @@ namespace MobileGL {
SET_CAPABILITY(ColorLogicOp, enabled);
SET_CAPABILITY(DebugOutput, enabled);
SET_CAPABILITY(DebugOutputSynchronous, enabled);
SET_CAPABILITY(DepthClamp, enabled);
SET_CAPABILITY(DepthTest, enabled);
SET_CAPABILITY(CullFace, enabled);
SET_CAPABILITY(Dither, enabled);
SET_CAPABILITY(FramebufferSrgb, enabled);
SET_CAPABILITY(LineSmooth, enabled);
SET_CAPABILITY(Multisample, enabled);
SET_CAPABILITY(PolygonOffsetFill, enabled);
@@ -335,6 +337,7 @@ namespace MobileGL {
SET_CAPABILITY(SampleMask, enabled);
SET_CAPABILITY(SampleShading, enabled);
SET_CAPABILITY(StencilTest, enabled);
SET_CAPABILITY(TextureCubeMapSeamless, enabled);
SET_CAPABILITY(ProgramPointSize, enabled);
case CapabilityInput::Blend: {
Bool stateChanged = false;
@@ -378,7 +381,9 @@ namespace MobileGL {
++m_version;
break;
}
default: // not supported currently
// Every CapabilityInput now has storage; the arm is a backstop for a value
// outside the enum, not a silent swallow of a real glEnable.
default:
break;
}
#undef SET_CAPABILITY
@@ -392,9 +397,11 @@ namespace MobileGL {
RETURN_CAPABILITY(ColorLogicOp);
RETURN_CAPABILITY(DebugOutput);
RETURN_CAPABILITY(DebugOutputSynchronous);
RETURN_CAPABILITY(DepthClamp);
RETURN_CAPABILITY(DepthTest);
RETURN_CAPABILITY(CullFace);
RETURN_CAPABILITY(Dither);
RETURN_CAPABILITY(FramebufferSrgb);
RETURN_CAPABILITY(LineSmooth);
RETURN_CAPABILITY(Multisample);
RETURN_CAPABILITY(PolygonOffsetFill);
@@ -410,6 +417,7 @@ namespace MobileGL {
RETURN_CAPABILITY(SampleMask);
RETURN_CAPABILITY(SampleShading);
RETURN_CAPABILITY(StencilTest);
RETURN_CAPABILITY(TextureCubeMapSeamless);
RETURN_CAPABILITY(ProgramPointSize);
case CapabilityInput::Blend:
return m_parameters.BlendStates[0].Enabled;
@@ -169,11 +169,16 @@ namespace MobileGL {
// not evict a cached pipeline. Keeping one counter for both made a glViewport call
// knock the next draw off the pipeline memo AND the draw fast path.
Uint16 m_pipelineStateVersion = 0;
RenderStateParameters m_parameters;
// Value-initialised, PADDING INCLUDED. The MGPipe CSO key is a byte-range
// hash over RenderStateParameters and the residual block's trip wire is a
// byte-level compare, so indeterminate padding would make a CSO handle
// reproducible only within one context and would make the trip wire
// meaningless. Costs one .text resize of this constructor.
RenderStateParameters m_parameters{};
// Pixel Store
PixelStoreParameters m_pixelStorePackParameters;
PixelStoreParameters m_pixelStoreUnpackParameters;
PixelStoreParameters m_pixelStorePackParameters{};
PixelStoreParameters m_pixelStoreUnpackParameters{};
};
} // namespace GLState
} // namespace MG_State
+30
View File
@@ -53,6 +53,36 @@ if (MSVC)
target_compile_options(PipeInputsTest PRIVATE /Zc:preprocessor)
endif()
# The four P2 suites. Their targets and this registration are the CONTRACT commit's; their
# CONTENTS belong to the packages named in each file's header, so no package after A has to
# come back to this file. Each links gtest_main - none of them needs a main() of its own,
# unlike PipeInputsTest, whose abort cases read a log file back.
foreach(pipeTest RenderStateSpansTest TrackerTest SlotAllocatorTest CsoCacheTest)
add_executable(${pipeTest} ${pipeTest}.cpp)
target_include_directories(${pipeTest} PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/MobileGL/MG_Pipe
${MGL_ROOT}/3rdparty/xxHash
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
${MGL_ROOT}/3rdparty/SPIRV-Reflect
)
target_link_libraries(${pipeTest} PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
if (MSVC)
target_compile_options(${pipeTest} PRIVATE /Zc:preprocessor)
endif()
endforeach()
include(GoogleTest)
gtest_discover_tests(PipeCatalogueTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(PipeInputsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
foreach(pipeTest RenderStateSpansTest TrackerTest SlotAllocatorTest CsoCacheTest)
gtest_discover_tests(${pipeTest} DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
endforeach()
+32
View File
@@ -0,0 +1,32 @@
// MobileGL - MobileGL/MG_Test/Pipe/CsoCacheTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The 64-entry render-state CSO cache: hash, probe, memcmp, LRU evict, and the content-addressing-off control (P2 brief D7).
//
// STUB, and deliberately one. It is created by the P2 CONTRACT commit together with its
// CMakeLists.txt registration, so that the package which owns its CONTENTS
// (P2 package B, p2/tracker) never has to touch MG_Test/Pipe/CMakeLists.txt - no two P2
// packages edit the same file, which is what keeps the integrator's rebases clean.
//
// The placeholder case is not decoration: without it the binary has no test, and
// gtest_discover_tests on a binary with no test is a silently green lane.
#include <gtest/gtest.h>
#include "Includes.h"
#include <MG_Pipe/MGPipe.h>
using namespace MobileGL;
using namespace MobileGL::MG_Pipe;
namespace {
// The cache does not exist yet; the behaviour bit it is measured against does, and it
// is deliberately the TOP bit so no subsystem allocation can ever collide with it.
TEST(CsoCache, PlaceholderUntilTheOwningPackageFillsThisIn) {
EXPECT_EQ(kMGPipeBehaviourNoCsoContentAddressing, 1ull << 63);
}
} // namespace
+50 -19
View File
@@ -110,8 +110,13 @@ TEST(PipeCatalogue, UninstalledTablesAreAllNull) {
TEST(PipeCatalogue, ResidualBlockSizeIsPinned) {
static_assert(sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE);
EXPECT_EQ(sizeof(ResidualValueBlock), static_cast<SizeT>(MGL_RESIDUAL_BLOCK_SIZE));
// It carries the whole of both value structs today; that is what the later stages eat.
EXPECT_GE(sizeof(ResidualValueBlock), sizeof(RenderStateParameters) + sizeof(PixelStoreParameters));
// P2 ate 1240 of the 1248: RenderStateParameters retired to create/bind_render_state and
// set_dynamic_state, PixelStoreParameters to set_pixel_pack_state, the patch quintet to
// set_patch_state. What is left is one Uint64 of capability bits, and it is redundant on
// purpose - the applier's trip wire compares it against the assembled block.
EXPECT_EQ(sizeof(ResidualValueBlock), 8u);
EXPECT_LT(sizeof(ResidualValueBlock), sizeof(RenderStateParameters));
EXPECT_EQ(offsetof(ResidualValueBlock, CapabilityBits), 0u);
}
// P0.5 moved the value structs into MG_Pipe/MGPipeValueTypes.h. These are the runtime twins
@@ -138,13 +143,18 @@ TEST(PipeCatalogue, ValueTypeLayoutsArePinned) {
EXPECT_EQ(kMGMaxDrawBuffers, 8u);
}
// The move did not alter the carrier: the residual block is still the render-state struct,
// then the pack struct, then the 8-aligned capability word, at the offsets it had before.
// P2 ATE THE TWO VALUE STRUCTS AND THE PATCH TAIL the name still remembers, and the name
// stays because a removed test name is a gate failure of its own (G14, additions only).
// What it now pins is the other half of the same statement: the carrier is one capability
// word, at offset 0, and the members it used to carry are gone rather than merely moved -
// which is exactly what "MGL_RESIDUAL_BLOCK_SIZE only ever goes down" has to mean.
TEST(PipeCatalogue, ResidualBlockIsExactlyItsTwoValueStructsPlusPatchTail) {
EXPECT_EQ(offsetof(ResidualValueBlock, RenderState), 0u);
EXPECT_EQ(offsetof(ResidualValueBlock, Pack), sizeof(RenderStateParameters));
EXPECT_EQ(offsetof(ResidualValueBlock, CapabilityBits), 1200u);
EXPECT_EQ(offsetof(ResidualValueBlock, PatchVertices), 1208u);
EXPECT_EQ(offsetof(ResidualValueBlock, CapabilityBits), 0u);
EXPECT_EQ(sizeof(ResidualValueBlock), sizeof(Uint64));
// The three carriers that took the retired members over.
EXPECT_EQ(sizeof(MGPPixelPackState), sizeof(PixelStoreParameters));
EXPECT_EQ(sizeof(MGPPatchState), 40u);
EXPECT_EQ(sizeof(MGPBindRenderState), 12u);
}
// G3's opcode numbering is the wire protocol. Position in PipeCalls.def, 1-based, no holes.
@@ -307,21 +317,35 @@ TEST(PipeCatalogue, FloatVectorsCompareBitwise) {
EXPECT_TRUE(MGPipeFieldEqual(1.5f, 1.5f));
EXPECT_FALSE(MGPipeFieldEqual(-0.f, 0.f));
// The residual carrier is one field since P2, so the nested-struct case it used to
// demonstrate is demonstrated on RenderStateParameters directly - which is where it
// actually matters now that the block travels as create/bind_render_state chunks.
ResidualValueBlock left{};
ResidualValueBlock right{};
const char* field = nullptr;
EXPECT_TRUE(MGPipeVerify(left, right, &field));
right.RenderState.BlendStates[3].SrcFactorRGB = BlendFactor::DstColor;
right.CapabilityBits = 1ull << static_cast<Uint64>(CapabilityInput::FramebufferSrgb);
EXPECT_FALSE(MGPipeVerify(left, right, &field));
EXPECT_STREQ(field, "RenderState");
EXPECT_STREQ(field, "CapabilityBits");
RenderStateParameters leftState{};
RenderStateParameters rightState{};
const char* inner = nullptr;
EXPECT_FALSE(MGPipeVerify(left.RenderState, right.RenderState, &inner));
EXPECT_TRUE(MGPipeVerify(leftState, rightState, &inner));
rightState.BlendStates[3].SrcFactorRGB = BlendFactor::DstColor;
EXPECT_FALSE(MGPipeVerify(leftState, rightState, &inner));
EXPECT_STREQ(inner, "BlendStates");
// A NaN patch level in the render state equals itself too.
right = left;
left.RenderState.PatchDefaultOuterLevel = FloatVec4{nan, 1.f, 1.f, 1.f};
right.RenderState.PatchDefaultOuterLevel = FloatVec4{nan, 1.f, 1.f, 1.f};
EXPECT_TRUE(MGPipeVerify(left, right, &field));
// P2's three new capability bools are members like any other, so the comparator names
// them rather than folding them into a neighbour's padding.
rightState = leftState;
rightState.FramebufferSrgbEnabled = true;
EXPECT_FALSE(MGPipeVerify(leftState, rightState, &inner));
EXPECT_STREQ(inner, "FramebufferSrgbEnabled");
// A NaN patch level equals itself too.
rightState = leftState;
leftState.PatchDefaultOuterLevel = FloatVec4{nan, 1.f, 1.f, 1.f};
rightState.PatchDefaultOuterLevel = FloatVec4{nan, 1.f, 1.f, 1.f};
EXPECT_TRUE(MGPipeVerify(leftState, rightState, &inner));
}
// The six value structs have field lists of their own (P1 brief D8): 63 + 6 payloads, and
@@ -352,9 +376,16 @@ TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
// G7 pins the member list the pipeline/dynamic split is derived from.
TEST(PipeCatalogue, PipelineSubsetMembersArePinned) {
EXPECT_EQ(kMGPipePipelineStateMemberCount, 24u);
EXPECT_STREQ(kMGPipePipelineStateMembers[0], "CullFaceEnabled");
EXPECT_STREQ(kMGPipePipelineStateMembers[kMGPipePipelineStateMemberCount - 1], "ColorMasks");
// 44 as of P2, in DECLARATION order. It grew from the 24 members
// ComputePipelineStateHash used to hash because the chunk table's rule is "a byte is
// pipeline state iff a setter that calls BumpVersions() writes it", and that is a strict
// superset: sample coverage, front face, provoking vertex, the scissor-test mask, the
// back polygon mode, eleven capability bools the hash never read, and the three
// capabilities P2 gave storage to.
EXPECT_EQ(kMGPipePipelineStateMemberCount, 44u);
EXPECT_STREQ(kMGPipePipelineStateMembers[0], "PatchVertices");
EXPECT_STREQ(kMGPipePipelineStateMembers[kMGPipePipelineStateMemberCount - 1],
"ScissorTestEnabledMask");
}
// The reverse channel is exactly ten callbacks (section 7.1).
@@ -0,0 +1,33 @@
// MobileGL - MobileGL/MG_Test/Pipe/RenderStateSpansTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// G7: the render-state chunk table, its subset hash and the setter-consistency walk (P2 brief D19).
//
// STUB, and deliberately one. It is created by the P2 CONTRACT commit together with its
// CMakeLists.txt registration, so that the package which owns its CONTENTS
// (P2 package A, commit c2 on p2/spans) never has to touch MG_Test/Pipe/CMakeLists.txt - no two P2
// packages edit the same file, which is what keeps the integrator's rebases clean.
//
// The placeholder case is not decoration: without it the binary has no test, and
// gtest_discover_tests on a binary with no test is a silently green lane.
#include <gtest/gtest.h>
#include "Includes.h"
#include <MG_Pipe/MGPipe.h>
using namespace MobileGL;
using namespace MobileGL::MG_Pipe;
namespace {
// The one fact this file can already state in EVERY build: the member list the chunk
// table was derived from is non-empty and is what generated/PipeSpanTable.inc pins.
TEST(RenderStateSpans, PlaceholderUntilTheOwningPackageFillsThisIn) {
EXPECT_GT(kMGPipePipelineStateMemberCount, 0u);
EXPECT_STREQ(kMGPipePipelineStateMembers[0], "PatchVertices");
}
} // namespace
@@ -0,0 +1,34 @@
// MobileGL - MobileGL/MG_Test/Pipe/SlotAllocatorTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The client slot allocator's identity contract: gen moves only on reuse, and a recycled address never reproduces a handle (P2 brief C.0 c3).
//
// STUB, and deliberately one. It is created by the P2 CONTRACT commit together with its
// CMakeLists.txt registration, so that the package which owns its CONTENTS
// (P2 package A, commit c3 on p2/spans) never has to touch MG_Test/Pipe/CMakeLists.txt - no two P2
// packages edit the same file, which is what keeps the integrator's rebases clean.
//
// The placeholder case is not decoration: without it the binary has no test, and
// gtest_discover_tests on a binary with no test is a silently green lane.
#include <gtest/gtest.h>
#include "Includes.h"
#include <MG_Pipe/MGPipe.h>
using namespace MobileGL;
using namespace MobileGL::MG_Pipe;
namespace {
// Slot 0 is reserved for every kind - null, and the default framebuffer for kind
// Framebuffer - so the first allocatable slot is 1 in every build.
TEST(SlotAllocator, PlaceholderUntilTheOwningPackageFillsThisIn) {
EXPECT_EQ(kMGPipeFirstAllocatableSlot, 1u);
EXPECT_TRUE(MGPipeHandleIsNull(kMGPipeNullHandle));
EXPECT_FALSE(MGPipeHandleIsNull(kMGPipeDefaultFramebuffer));
}
} // namespace
+32
View File
@@ -0,0 +1,32 @@
// MobileGL - MobileGL/MG_Test/Pipe/TrackerTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The frontend state tracker: the dirty walk, the five aggregate generations, the per-bit fire counters (P2 brief D4).
//
// STUB, and deliberately one. It is created by the P2 CONTRACT commit together with its
// CMakeLists.txt registration, so that the package which owns its CONTENTS
// (P2 package B, p2/tracker) never has to touch MG_Test/Pipe/CMakeLists.txt - no two P2
// packages edit the same file, which is what keeps the integrator's rebases clean.
//
// The placeholder case is not decoration: without it the binary has no test, and
// gtest_discover_tests on a binary with no test is a silently green lane.
#include <gtest/gtest.h>
#include "Includes.h"
#include <MG_Pipe/MGPipe.h>
using namespace MobileGL;
using namespace MobileGL::MG_Pipe;
namespace {
// The tracker does not exist yet; what is true in every build is that the subsystem
// bitmask it dispatches on is allocated and does not overlap the behaviour bit.
TEST(Tracker, PlaceholderUntilTheOwningPackageFillsThisIn) {
EXPECT_EQ(kMGPipeSubsystemsMigratedAtP2 & kMGPipeBehaviourNoCsoContentAddressing, 0ull);
}
} // namespace
+10
View File
@@ -151,6 +151,12 @@ namespace {
}
EXPECT_NE(line.find("gates["), String::npos) << line;
EXPECT_NE(line.find("tex[emit="), String::npos) << line;
#if MOBILEGL_PIPE_PUSH
// P2's two render-state CSO counters ride the same line, short-named. Push-only:
// the pull build has no CSO to mint and must stay symbol-identical.
EXPECT_NE(line.find("cso[csom="), String::npos) << line;
EXPECT_NE(line.find("csob="), String::npos) << line;
#endif
}
// Per-frame fields carry two decimals for the same reason acc/draw does: they are small
@@ -267,6 +273,10 @@ namespace {
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageIndirectCmd), "stage-indirect-cmd");
EXPECT_STREQ(PS::NameOf(PS::ByteClass::PersistentMapPush), "persistent-map-push");
EXPECT_STREQ(PS::NameOf(PS::ByteClass::ResidualValueBlock), "residual-value-block");
#if MOBILEGL_PIPE_PUSH
EXPECT_STREQ(PS::NameOf(PS::CallClass::RenderStateCsoMints), "render-state-cso-mints");
EXPECT_STREQ(PS::NameOf(PS::CallClass::RenderStateCsoBinds), "render-state-cso-binds");
#endif
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytRenderState), "espryt-render-state");
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytTextureSyncList), "espryt-texture-sync-list");
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytUnitBindingsEpoch), "espryt-unit-bindings-epoch");
+10
View File
@@ -173,6 +173,9 @@ namespace MobileGL::MG_Util::PipeStats {
const char* const kCallClassNames[kCallClassCount] = {
"draws", "accessor-calls", "tex-upload-emissions", "tex-upload-box", "tex-upload-rect",
"tex-upload-jobs",
#if MOBILEGL_PIPE_PUSH
"render-state-cso-mints", "render-state-cso-binds",
#endif
};
const char* const kGateNames[kGateCount] = {
"espryt-render-state", "espryt-texture-sync-list", "espryt-unit-bindings-epoch",
@@ -416,6 +419,13 @@ namespace MobileGL::MG_Util::PipeStats {
line += " box=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureUploadBoxEmissions)]);
line += " rect=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureUploadRectEmissions)]);
line += " jobs=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureUploadJobs)]);
#if MOBILEGL_PIPE_PUSH
// Push-only, like the two counters themselves: in a pull build there is no CSO to
// mint, and a "csom=0 csob=0" that can never be anything else is noise on the one
// line an operator greps.
line += "] cso[csom=" + std::to_string(calls[static_cast<Uint32>(CallClass::RenderStateCsoMints)]);
line += " csob=" + std::to_string(calls[static_cast<Uint32>(CallClass::RenderStateCsoBinds)]);
#endif
line += "] gates[";
for (Uint32 i = 0; i < kGateCount; ++i) {
if (i != 0) {
+16
View File
@@ -98,6 +98,22 @@ namespace MobileGL::MG_Util::PipeStats {
// Driver upload jobs issued by those emissions: 1 per box emission, N per rect-list
// emission.
TextureUploadJobs,
#if MOBILEGL_PIPE_PUSH
// P2's two, and they are PUSH-ONLY on purpose: a render-state CSO exists only in a
// push build, and the pull build has to stay symbol-identical (G1) - growing this
// enum there would resize the counter arrays, the name table and FormatWindowLine
// for a pair of counters that could never leave zero.
//
// Render-state CSOs MINTED: a pipeline-subset hash that missed the CsoCache and had
// to be created. The Blaze3D blend toggle is the shape this exists to answer for -
// enable/draw/disable/draw forever must mint 2 and then never mint again - and it is
// half of what a P13 retune of the 64-entry capacity reads.
RenderStateCsoMints,
// Render-state CSOs BOUND: one per bind_render_state, mint or reuse. mints/binds is
// the cache's hit rate, and it is the number the CSO content-addressing negative
// control moves.
RenderStateCsoBinds,
#endif
Count
};
+164 -47
View File
@@ -62,41 +62,71 @@ GENERATED_BANNER = """// MobileGL - MobileGL/MG_Pipe/generated/{name}
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
"""
# G7. The pipeline subset of RenderStateParameters, BY MEMBER NAME, taken from the fields
# VulkanRenderer::ComputePipelineStateHash hashes today
# (MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:4805-4906 at dev@81b17c0b,
# including ResolveEffectiveSampleMask, which the hash folds in twice - once as the
# effective enable bit and once as the mask word).
# G7. The pipeline subset of RenderStateParameters, BY MEMBER NAME, in DECLARATION order.
#
# P0 took this list from what VulkanRenderer::ComputePipelineStateHash hashed. P2 replaced
# that provenance with a RULE, and the rule is the only thing that decides membership:
#
# A member is in the pipeline subset if and only if some public RenderState setter that
# calls BumpVersions() writes it. Everything else is dynamic. There is no third set.
#
# That makes G7's invariant - the pipeline-subset hash moves IFF m_pipelineStateVersion
# moves - true by construction, and it makes the subset a strict SUPERSET of the 24 members
# the Vulkan hash read: it adds sample coverage, the front face, the provoking vertex, the
# scissor-test mask, the back polygon mode, the eleven capability bools the hash never read,
# and the three capabilities P2 gave storage to. The alternative - demoting those setters to
# ++m_version - would change MG_State semantics in the PULL build for the push path's sake.
#
# Names only: offsets are NOT computed here. The chunk table with real offsets is
# MGPipeRenderStateSpans.cpp, built in C++ with offsetof, because a python guess at the
# layout of a struct it cannot see is exactly the kind of drift the G7 setter-consistency
# test exists to catch (plan B section 4.5.2).
# test exists to catch (plan B section 4.5.2). StencilStates is named once and straddles:
# per face, Func and the three ops are pipeline, Ref/ValueMask/WriteMask are dynamic.
PIPELINE_STATE_MEMBERS = [
"CullFaceEnabled",
"DepthTestEnabled",
"PolygonOffsetFillEnabled",
"RasterizerDiscardEnabled",
"ColorLogicOpEnabled",
"StencilTestEnabled",
"PrimitiveRestartEnabled",
"PrimitiveRestartFixedIndexEnabled",
"DepthMask",
"SampleShadingEnabled",
"MultisampleEnabled",
"SampleMaskEnabled",
"SampleMaskValue",
"MinSampleShadingValue",
"PatchVertices",
"PatchDefaultOuterLevel",
"PatchDefaultInnerLevel",
"PolygonModeFront",
"CullFaceModeSetting",
"DepthFunc",
"LogicOp",
"StencilStates",
"BlendStates",
"LogicOp",
"DepthTestEnabled",
"DepthFunc",
"DepthMask",
"ColorMasks",
"FramebufferSrgbEnabled",
"DepthClampEnabled",
"TextureCubeMapSeamlessEnabled",
"SampleCoverageValue",
"SampleCoverageInvert",
"SampleMaskValue",
"MinSampleShadingValue",
"StencilStates",
"CullFaceEnabled",
"CullFaceModeSetting",
"FrontFaceModeSetting",
"ProvokingVertexModeSetting",
"PolygonModeFront",
"PolygonModeBack",
"ColorLogicOpEnabled",
"DebugOutputEnabled",
"DebugOutputSynchronousEnabled",
"DitherEnabled",
"LineSmoothEnabled",
"MultisampleEnabled",
"PolygonOffsetFillEnabled",
"PolygonOffsetLineEnabled",
"PolygonOffsetPointEnabled",
"PolygonSmoothEnabled",
"PrimitiveRestartEnabled",
"PrimitiveRestartFixedIndexEnabled",
"RasterizerDiscardEnabled",
"SampleAlphaToCoverageEnabled",
"SampleAlphaToOneEnabled",
"SampleCoverageEnabled",
"SampleMaskEnabled",
"SampleShadingEnabled",
"StencilTestEnabled",
"ProgramPointSizeEnabled",
"ScissorTestEnabledMask",
]
@@ -482,7 +512,20 @@ def parse_coverage():
if name in dict(sticky):
sys.exit("Coverage.def: sticky field %s is listed twice" % name)
sticky.append((name, reason))
return accessors, deltas, sticky
emitted = []
block = re.search(r"#define MGP_COVERAGE_EMITTED_LIST\(X\)(.*?)\n\n", text, re.S)
if not block:
sys.exit("Coverage.def: MGP_COVERAGE_EMITTED_LIST is missing")
seen = set()
for name, call in re.findall(r"X\((\w+)\s*,\s*(\w+)\)", block.group(1)):
if name not in accessor_names:
sys.exit("Coverage.def: emitted field %s is not an accessor in "
"MGP_COVERAGE_ACCESSOR_LIST" % name)
if name in seen:
sys.exit("Coverage.def: emitted field %s is listed twice" % name)
seen.add(name)
emitted.append((name, call))
return accessors, deltas, sticky, emitted
INVENTORY_ROW_RE = re.compile(r"^\|\s*(\d+)\s*\|([^|]*)\|([^|]*)\|([^|]*)\|")
@@ -559,7 +602,7 @@ def gen_thunks(calls):
return "\n".join(out)
def gen_wire(calls):
def gen_wire(calls, residual_fields=None):
out = [banner("PipeWire.inc", "G3: wire records, size assertions and the applier's bounds gate.",
"PipeCalls.def")]
out.append("""// Every record is a fixed header plus its payload, padded to the stream's 8-byte
@@ -641,6 +684,25 @@ inline Bool MGPipeApplyWireRecord(MGPWireOp op, const void* record, Uint64 size,
}
#undef MGP_WIRE_CHECK_BOUNDS""")
# The migration carrier's layout, asserted MEMBER BY MEMBER and not only by sizeof
# (plan 6.3): a heterogeneous POD is where padding differs across ABIs, and the monolith
# verify harness is blind to it because both sides are the same translation unit. The
# first member is pinned at 0 and the rest are pinned to ascend, which is the strongest
# statement a generator that cannot see the layout can make; sizeof plus
# MGL_RESIDUAL_BLOCK_SIZE pins the rest, and the ratchet only ever goes DOWN.
if residual_fields:
out.append("")
out.append("// The ResidualValueBlock layout, from PipeFields.def's")
out.append("// MGP_FIELDS_ResidualValueBlock. Retiring a field without lowering")
out.append("// MGL_RESIDUAL_BLOCK_SIZE is a build break, which is the point.")
out.append("static_assert(offsetof(ResidualValueBlock, %s) == 0," % residual_fields[0])
out.append(" \"the residual block's first member must sit at offset 0\");")
for previous, member in zip(residual_fields, residual_fields[1:]):
out.append("static_assert(offsetof(ResidualValueBlock, %s) >" % member)
out.append(" offsetof(ResidualValueBlock, %s)," % previous)
out.append(" \"the residual block's members must stay in declaration order\");")
out.append("static_assert(sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE,")
out.append(" \"the residual ratchet only ever goes down\");")
return "\n".join(out) + "\n"
@@ -744,7 +806,48 @@ inline Bool MGPipeFieldEqual(const T (&a)[N], const T (&b)[N]) {
return "\n".join(out) + "\n"
def gen_filled(accessors, calls, sticky):
def gen_emitted_by(accessors, calls, emitted):
"""P2 brief D5: which P2 call SUPPLIES each PipeInputs field, so the per-verb residual
fill loop can skip it. Emitters are the distinct calls named in MGP_COVERAGE_EMITTED_LIST,
sorted so the enum is stable against the order rows are written in."""
call_names = set(c.Name for c in calls)
emitted_map = dict(emitted)
for name, call in emitted:
if call not in call_names:
sys.exit("Coverage.def: MGP_COVERAGE_EMITTED_LIST names %s for %s, which is not a "
"call in PipeCalls.def" % (call, name))
emitters = sorted(set(call for _, call in emitted))
out = []
out.append("// P2 brief D5: the call that now SUPPLIES a field, so the residual fill loop no")
out.append("// longer pulls it out of GLContext. kNone means the field is still pulled - which")
out.append("// is what makes MOBILEGL_PIPE_PUSH a true per-subsystem A/B instead of a single")
out.append("// switch. Rows come from Coverage.def's MGP_COVERAGE_EMITTED_LIST.")
out.append("enum class MGPipeFieldEmitter : Uint8 {")
out.append(" kNone = 0,")
for emitter in emitters:
out.append(" %s," % emitter)
out.append("};")
out.append("")
out.append("inline constexpr const char* kMGPipeFieldEmitterNames[] = {")
out.append(" \"kNone\",")
for emitter in emitters:
out.append(" \"%s\"," % emitter)
out.append("};")
out.append("")
out.append("inline constexpr MGPipeFieldEmitter kMGPipeFieldEmittedBy[kMGPipeInputFieldCount] = {")
for name, _ in accessors:
emitter = emitted_map.get(name)
if emitter is None:
out.append(" MGPipeFieldEmitter::kNone, // %s" % name)
else:
out.append(" MGPipeFieldEmitter::%s, // %s" % (emitter, name))
out.append("};")
out.append("inline constexpr SizeT kMGPipeEmittedFieldCount = %d;" % len(emitted))
out.append("")
return "\n".join(out)
def gen_filled(accessors, calls, sticky, emitted):
call_names = set(c.Name for c in calls)
sticky_map = dict(sticky)
out = [banner("PipeFilled.inc", "G5: PipeInputs field ids and the per-verb poison generations.",
@@ -796,6 +899,7 @@ def gen_filled(accessors, calls, sticky):
out.append(" \"%s\",%s" % (call, marker))
out.append("};")
out.append("")
out.append(gen_emitted_by(accessors, calls, emitted))
out.append("""struct MGPipeFilledState {
Uint64 CurrentVerbSerial;
Uint64 FilledGen[kMGPipeInputFieldCount];
@@ -1041,19 +1145,23 @@ def gen_span_table():
"the field list in scripts/gen_pipe.py")]
out.append("""// D-B1 rejected three CSOs and demanded this table instead, so the table needs its own
// completeness trip wire: MG_Test walks every public RenderState setter and asserts that
// the pipeline-subset hash moves IF AND ONLY IF m_pipelineStateVersion moves. That test
// and MGPipeRenderStateSpans.cpp land with P2; what P0 pins is the MEMBER LIST, taken from
// what VulkanRenderer::ComputePipelineStateHash hashes today, so the later offsets are
// derived from a list that was reviewed rather than invented.
// the pipeline-subset hash moves IF AND ONLY IF m_pipelineStateVersion moves
// (MG_Test/Pipe/RenderStateSpansTest.cpp).
//
// Deliberately absent, and each absence is a question P2 has to answer before the chunk
// table freezes:
// - FramebufferSrgb and DepthClamp have NO STORAGE at all (RenderState.cpp's SetCapability
// falls to "not supported currently" and IsCapabilityEnabled returns false), so six
// backend read points are constant false today. Pipeline state or dead capability?
// - ProvokingVertexModeSetting is Vulkan pipeline state but is not hashed today.
// - FrontFaceModeSetting, ClipOrigin and ClipDepthMode are pipeline state on Vulkan and
// are handled elsewhere in the payload path rather than in the memo word.
// P2 replaced P0's provenance with a RULE, and the rule is the only thing that decides
// membership: a member is pipeline state IF AND ONLY IF some public RenderState setter that
// calls BumpVersions() writes it. That is what makes the G7 invariant true by construction
// rather than by inspection, and it turns the subset into a strict SUPERSET of the 24
// members VulkanRenderer::ComputePipelineStateHash used to hash.
//
// The three questions P0 left open are ANSWERED here, and the answers are in this list:
// - FramebufferSrgb, DepthClamp and TextureCubeMapSeamless had NO STORAGE at all -
// SetCapability fell to "not supported currently" and IsCapabilityEnabled answered a
// compile-time false. P2 gave all three real storage in the three padding bytes between
// ColorMasks and ClearColor, and their setters call BumpVersions(), so: pipeline state.
// - ProvokingVertexModeSetting: SetProvokingVertexMode calls BumpVersions(), so pipeline.
// - FrontFaceModeSetting likewise. ClipOrigin and ClipDepthMode do NOT (SetClipControl is
// ++m_version only), so they are dynamic, in chunk D1.
//
// The complement of this list is the DYNAMIC subset - the half whose whole purpose is that
// glViewport must not mint a new CSO.
@@ -1066,8 +1174,10 @@ inline constexpr const char* const kMGPipePipelineStateMembers[] = {""")
out.append("static_assert(kMGPipePipelineStateMemberCount ==")
out.append(" sizeof(kMGPipePipelineStateMembers) / sizeof(kMGPipePipelineStateMembers[0]));")
out.append("")
out.append("// Filled in by MG_Pipe/MGPipeRenderStateSpans.cpp (P2), which computes the offsets")
out.append("// in C++ with offsetof rather than guessing them in python.")
out.append("// Defined by MG_Pipe/MGPipeRenderStateSpans.cpp (P2), which computes every")
out.append("// boundary in C++ with offsetof rather than guessing it in python. 7 pipeline")
out.append("// chunks / 396 bytes and 8 dynamic chunks / 772 bytes, and the two halves")
out.append("// partition [0, sizeof(RenderStateParameters)) exactly - asserted there.")
out.append("extern const MGPStateChunk kMGPipePipelineChunks[];")
out.append("extern const MGPStateChunk kMGPipeDynamicChunks[];")
return "\n".join(out) + "\n"
@@ -1117,6 +1227,11 @@ def self_test(accessors):
accessors, text=verb_row.sub("X(DrawArrays, kDraw) X(NotAVerb, kDraw)", fill_text, count=1))))
controls.append(("field row naming a non-accessor", lambda: parse_fill_points(
accessors, text=field_row.sub("X(kDraw, NotAnAccessor)", fill_text, count=1))))
# The EMITTED list's own gate: a row naming a call that is not in PipeCalls.def would
# generate an enumerator nothing can dispatch on.
calls_for_control = parse_calls()
controls.append(("emitted row naming a call that does not exist", lambda: gen_emitted_by(
[("GetViewport", "SetDynamicState")], calls_for_control, [("GetViewport", "NotACall")])))
trips = 0
for name, fn in controls:
trips += expect_trip(name, fn)
@@ -1143,7 +1258,7 @@ def main():
payloads = parse_verify_payloads()
check_call_payloads_have_field_lists(calls, payloads)
check_field_lists_cover_struct_members(parse_field_lists(), payloads)
accessors, deltas, sticky = parse_coverage()
accessors, deltas, sticky, emitted = parse_coverage()
if args.self_test:
return self_test(accessors)
scan_live_accessors(accessors)
@@ -1157,9 +1272,11 @@ def main():
changed = []
write(os.path.join(GENERATED_DIR, "PipeTables.inc"), gen_tables(calls), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeThunks.inc"), gen_thunks(calls), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeWire.inc"), gen_wire(calls), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeWire.inc"),
gen_wire(calls, parse_field_lists().get("ResidualValueBlock")), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeVerify.inc"), gen_verify(payloads), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeFilled.inc"), gen_filled(accessors, calls, sticky), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeFilled.inc"), gen_filled(accessors, calls, sticky, emitted),
args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeFillPoints.inc"),
gen_fill_points(accessors, sticky, verbs, classes, fields), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeCoverage.inc"), coverage_text, args.check, changed)
@@ -1167,9 +1284,9 @@ def main():
screen = sum(1 for c in calls if c.IsScreen)
print("gen_pipe: %d calls (%d screen, %d context), %d verify payloads, %d PipeInputs fields "
"(%d sticky), %d verbs, %d classes"
"(%d sticky, %d emitted by a P2 call), %d verbs, %d classes"
% (len(calls), screen, len(calls) - screen, len(payloads), len(accessors), len(sticky),
len(verbs), len(classes)))
len(emitted), len(verbs), len(classes)))
print("gen_pipe: inventory %d rows: %d -> call, %d client-resolved, %d reverse-channel, "
"%d structural handle, %d UNMAPPED"
% (len(rows), mapped, pseudo["kClientResolved"], pseudo["kReverseChannel"],