mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Executable
+196
@@ -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*>(¶ms);
|
||||
}
|
||||
Uint8* BytesOf(RenderStateParameters& params) { return reinterpret_cast<Uint8*>(¶ms); }
|
||||
|
||||
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
|
||||
Executable
+203
@@ -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
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
Executable
+270
@@ -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
|
||||
Executable
+107
@@ -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
|
||||
@@ -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) \
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user