mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-10 21:28:32 +09:00
[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:
Executable
+174
@@ -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
|
||||
Executable
+100
@@ -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
|
||||
Reference in New Issue
Block a user