[Test] (Pipe): require a named pipeline member to be WHOLLY pipeline, drive every indexed setter at index 0, and walk the incremental chunk path

- ChunkTablePartitionsTheBlock asserted only that a named pipeline member is TOUCHED
  by the pipeline half. A boundary that demotes part of one - four bytes at the head
  of BlendStates, i.e. the glEnablei(GL_BLEND, 0) bit - left the case green except
  for the two byte-count constants, which P3 will legitimately move; after that a
  partial demotion would have been invisible, and its consequence is one CSO handle
  serving two different pipeline states. Now IsWhollyPipeline for every named member,
  with StencilStates the one documented straddler.
- Every indexed setter is driven at index 0 as well as at a middle index. Index 0 is
  the element both backends consume and the one a head-of-array boundary demotes
  first; with it, the demotion above also fails SetterConsistency, naming the setter.
- The round trip D2 rests on is asserted: the assembled block is memcmp-equal to the
  live one. That is the only cover for the ~25 members with no derived field at all -
  SampleCoverage*, SampleMaskValue, PolygonModeBack, the hints, ScissorBoxes[1..15],
  ClipDistanceEnabledMask and the raw capability bools - which is exactly the set
  Espryt's SyncRenderState reads through its span memcmp.
- IncrementalChunksKeepEveryDerivedFieldInStep: the shape the tracker actually emits.
  A create_render_state naming only the pipeline chunks that moved against a BaseCso
  (D7 step 2's miss path) and a set_dynamic_state naming only the dynamic chunks that
  moved (D8's suppressor), ten steps plus all 35 capabilities one at a time, each
  followed by the full derived-field comparison. It is the first caller of the
  base-inherit branch, of MGPipeScatterPipelineChunks, MGPipePipelineChunkBlobBytes,
  MGPipe{Dynamic,Pipeline}ChunksThatMoved and MGPipeHashPipelineBytes, and it is the
  oracle for the applier's chunk-scoped derivation - a whole-block apply asks for
  every chunk and so cannot tell a correct guard from one that is too narrow.
- The 25 kDraw comparisons move into ExpectDerivedDrawFieldsMatch, shared by the
  whole-block walk and the incremental one.
This commit is contained in:
2026-09-06 09:00:04 -04:00
parent a9bb99a46a
commit ad1238bd6f
+293 -57
View File
@@ -90,6 +90,91 @@ namespace {
}
Bool IsWhollyPipeline(SizeT offset, SizeT size) { return PipelineBytesOf(offset, size) == size; }
// ---------------------------------------------------------------------------------
// D5's 25 kDraw derivations against the frontend getters they were transcribed from.
// Factored out because two cases need exactly this comparison: the whole-block walk
// below, and the INCREMENTAL walk that drives the applier's chunk-scoped derivation one
// family at a time. Everything here is a kDraw fill point (MG_Pipe/FillPoints.def), so
// the caller must be inside a kDraw verb; the three clear values and GetClampReadColor
// belong to kClear and kReadback and are checked in their own phases.
// ---------------------------------------------------------------------------------
void ExpectDerivedDrawFieldsMatch(GLContext& ctx, const char* tag) {
SCOPED_TRACE(tag);
EXPECT_EQ(gPipeInputs.GetBlendColor(), ctx.GetBlendColor());
for (Uint i = 0; i < kMGMaxDrawBuffers; ++i) {
BlendEquation gotColor{}, gotAlpha{}, wantColor{}, wantAlpha{};
gPipeInputs.GetBlendEquationIndexed(i, gotColor, gotAlpha);
ctx.GetBlendEquationIndexed(i, wantColor, wantAlpha);
EXPECT_EQ(gotColor, wantColor) << "blend equation " << i;
EXPECT_EQ(gotAlpha, wantAlpha) << "blend equation " << i;
BlendFactor gotSrcRGB{}, gotDstRGB{}, gotSrcA{}, gotDstA{};
BlendFactor wantSrcRGB{}, wantDstRGB{}, wantSrcA{}, wantDstA{};
gPipeInputs.GetBlendFuncIndexed(i, gotSrcRGB, gotDstRGB, gotSrcA, gotDstA);
ctx.GetBlendFuncIndexed(i, wantSrcRGB, wantDstRGB, wantSrcA, wantDstA);
EXPECT_EQ(gotSrcRGB, wantSrcRGB) << "blend func " << i;
EXPECT_EQ(gotDstRGB, wantDstRGB) << "blend func " << i;
EXPECT_EQ(gotSrcA, wantSrcA) << "blend func " << i;
EXPECT_EQ(gotDstA, wantDstA) << "blend func " << i;
EXPECT_EQ(gPipeInputs.GetColorMaskIndexed(i), ctx.GetColorMaskIndexed(i)) << "colour mask " << i;
EXPECT_EQ(gPipeInputs.IsCapabilityEnabledIndexed(CapabilityInput::Blend, i),
ctx.IsCapabilityEnabledIndexed(CapabilityInput::Blend, i))
<< "indexed blend enable " << i;
}
EXPECT_EQ(gPipeInputs.GetCullFaceMode(), ctx.GetCullFaceMode());
EXPECT_EQ(gPipeInputs.GetDepthFunc(), ctx.GetDepthFunc());
EXPECT_EQ(gPipeInputs.GetDepthMask(), ctx.GetDepthMask());
for (Uint i = 0; i < RenderStateParameters::MAX_VIEWPORTS; ++i) {
EXPECT_EQ(gPipeInputs.GetDepthRangeIndexed(i), ctx.GetDepthRangeIndexed(i)) << "depth range " << i;
EXPECT_EQ(gPipeInputs.GetViewportIndexed(i), ctx.GetViewportIndexed(i)) << "viewport " << i;
EXPECT_EQ(gPipeInputs.IsCapabilityEnabledIndexed(CapabilityInput::ScissorTest, i),
ctx.IsCapabilityEnabledIndexed(CapabilityInput::ScissorTest, i))
<< "indexed scissor enable " << i;
}
EXPECT_EQ(gPipeInputs.GetLineWidth(), ctx.GetLineWidth());
EXPECT_EQ(gPipeInputs.GetLogicOp(), ctx.GetLogicOp());
EXPECT_EQ(gPipeInputs.GetMinSampleShadingValue(), ctx.GetMinSampleShadingValue());
EXPECT_EQ(gPipeInputs.GetPatchDefaultInnerLevel(), ctx.GetPatchDefaultInnerLevel());
EXPECT_EQ(gPipeInputs.GetPatchDefaultOuterLevel(), ctx.GetPatchDefaultOuterLevel());
EXPECT_EQ(gPipeInputs.GetPatchVertices(), ctx.GetPatchVertices());
EXPECT_EQ(gPipeInputs.GetPolygonModeFront(), ctx.GetPolygonModeFront());
EXPECT_EQ(gPipeInputs.GetPolygonOffsetFactor(), ctx.GetPolygonOffsetFactor());
EXPECT_EQ(gPipeInputs.GetPolygonOffsetUnits(), ctx.GetPolygonOffsetUnits());
EXPECT_EQ(gPipeInputs.GetPrimitiveRestartIndex(), ctx.GetPrimitiveRestartIndex());
EXPECT_EQ(gPipeInputs.GetProvokingVertexMode(), ctx.GetProvokingVertexMode());
EXPECT_EQ(gPipeInputs.GetScissorBox(), ctx.GetScissorBox());
EXPECT_EQ(gPipeInputs.GetViewport(), ctx.GetViewport());
for (const StencilFace face : {StencilFace::Front, StencilFace::Back}) {
const StencilFaceState& got = gPipeInputs.GetStencilState(face);
const StencilFaceState& want = ctx.GetStencilState(face);
EXPECT_EQ(std::memcmp(&got, &want, sizeof(StencilFaceState)), 0)
<< "stencil face " << static_cast<int>(face);
}
for (SizeT i = 0; i < static_cast<SizeT>(CapabilityInput::CapabilityInputCount); ++i) {
const CapabilityInput cap = static_cast<CapabilityInput>(i);
EXPECT_EQ(gPipeInputs.IsCapabilityEnabled(cap), ctx.IsCapabilityEnabled(cap)) << "capability " << i;
}
}
// THE ROUND TRIP D2's whole argument rests on: "the block they read IS the assembled
// block" (ARCHITECTURE.md 5.3), which is what lets Espryt's SyncRenderState stay
// untouched. The 29 derived fields cover barely half of RenderStateParameters; the other
// ~25 members - SampleCoverageValue/Invert, SampleMaskValue, PolygonModeBack, PointSize,
// PointFadeThresholdSize, PointSpriteCoordOrigin, the four hints, ClipOrigin,
// ClipDepthMode, PolygonOffsetClamp, FrontFaceModeSetting, ScissorBoxes[1..15],
// ScissorBoxWrittenMask, ClipDistanceEnabledMask and the raw capability bools - have no
// derived field at all and are read RAW, through Espryt's span memcmp. This one line is
// the only thing in the suite that covers them.
void ExpectAssembledBlockIsTheLiveBlock(GLContext& ctx, const char* tag) {
SCOPED_TRACE(tag);
EXPECT_EQ(std::memcmp(&gPipeInputs.GetRenderStateParameters(), &ctx.GetRenderStateParameters(),
sizeof(RenderStateParameters)),
0)
<< "the assembled working block is not byte-identical to the live one - a chunk of "
"RenderStateParameters is not being carried, and Espryt reads those bytes raw";
}
#endif // MOBILEGL_PIPE_PUSH
// -------------------------------------------------------------------------------------
@@ -136,8 +221,26 @@ namespace {
const SizeT pipelineBytes = PipelineBytesOf(member.Offset, member.Size);
if (pipelineNames.count(member.Name) != 0) {
++namedFound;
EXPECT_GT(pipelineBytes, SizeT{0})
<< member.Name << " is named as pipeline state but no pipeline chunk covers it";
// WHOLLY pipeline, not merely touched by a pipeline chunk. A named member
// with only SOME of its bytes in the pipeline half is the silent form of the
// bug this suite exists to prevent: the demoted bytes drop out of the CSO's
// content-addressed identity, so one handle serves two different pipeline
// states and the same cached VkPipeline draws with, say, draw buffer 0's
// blend enable set both ways. The subset hash cannot see it - a hash over
// the wrong bytes is still a hash - and the setter walk cannot see it either
// as long as SOME byte of the member stayed pipeline, because the version
// and the hash then still move together.
//
// StencilStates is the ONE member allowed to straddle, by design and at
// sub-member granularity; the loop below pins its split face by face.
if (std::strcmp(member.Name, "StencilStates") == 0) {
EXPECT_GT(pipelineBytes, SizeT{0}) << "StencilStates has no pipeline bytes at all";
continue;
}
EXPECT_TRUE(IsWhollyPipeline(member.Offset, member.Size))
<< member.Name << " is named as pipeline state but only " << pipelineBytes << " of its "
<< member.Size << " bytes are in the pipeline half - the rest have silently left the "
"CSO's identity";
} else {
EXPECT_EQ(pipelineBytes, SizeT{0})
<< member.Name << " is not named as pipeline state but " << pipelineBytes
@@ -198,6 +301,14 @@ namespace {
// ---- Rasterization ----
check("SetViewport", [](RenderState& s) { s.SetViewport(IntVec4(1, 2, 30, 40)); });
check("SetViewportIndexed", [](RenderState& s) { s.SetViewportIndexed(3, FloatVec4(4.f, 5.f, 60.f, 70.f)); });
// EVERY indexed setter is driven at INDEX 0 as well as at a middle index, and index 0
// is the one that matters most: it is the element both backends actually consume
// (IsCapabilityEnabled(Blend) is BlendStates[0].Enabled, GetScissorBox/GetViewport
// answer for rectangle 0) and it is the element a chunk boundary landing at the HEAD
// of an array demotes first. A walk that only ever touches index 3 cannot tell a
// boundary that swallowed index 0 from a correct table.
check("SetViewportIndexed(0)",
[](RenderState& s) { s.SetViewportIndexed(0, FloatVec4(0.5f, 1.5f, 31.5f, 41.5f)); });
check("SetLineWidth", [](RenderState& s) { s.SetLineWidth(3.5f); });
check("SetPointSize", [](RenderState& s) { s.SetPointSize(7.25f); });
check("SetPatchVertices", [](RenderState& s) { s.SetPatchVertices(4); });
@@ -279,6 +390,13 @@ namespace {
[](RenderState& s) { s.SetCapabilityIndexed(CapabilityInput::Blend, 3, false); });
check("SetCapabilityIndexed(ScissorTest, 5)",
[](RenderState& s) { s.SetCapabilityIndexed(CapabilityInput::ScissorTest, 5, false); });
// Index 0 of both: BlendStates[0].Enabled is the single bit glEnable(GL_BLEND)
// answers for and the first four bytes of chunk P1, and ScissorTestEnabledMask bit 0
// is what DynamicTailKey::scissorEnabled reads.
check("SetCapabilityIndexed(Blend, 0)",
[](RenderState& s) { s.SetCapabilityIndexed(CapabilityInput::Blend, 0, false); });
check("SetCapabilityIndexed(ScissorTest, 0)",
[](RenderState& s) { s.SetCapabilityIndexed(CapabilityInput::ScissorTest, 0, false); });
// ---- Blending ----
check("SetBlendFunc", [](RenderState& s) {
@@ -288,10 +406,16 @@ namespace {
s.SetBlendFuncIndexed(2, BlendFactor::DstColor, BlendFactor::SrcColor, BlendFactor::DstAlpha,
BlendFactor::SrcAlpha);
});
check("SetBlendFuncIndexed(0)", [](RenderState& s) {
s.SetBlendFuncIndexed(0, BlendFactor::ConstantColor, BlendFactor::ConstantAlpha, BlendFactor::OneMinusDstColor,
BlendFactor::OneMinusDstAlpha);
});
check("SetBlendEquation",
[](RenderState& s) { s.SetBlendEquation(BlendEquation::Subtract, BlendEquation::Min); });
check("SetBlendEquationIndexed",
[](RenderState& s) { s.SetBlendEquationIndexed(4, BlendEquation::ReverseSubtract, BlendEquation::Max); });
check("SetBlendEquationIndexed(0)",
[](RenderState& s) { s.SetBlendEquationIndexed(0, BlendEquation::Max, BlendEquation::Add); });
check("SetLogicOp", [](RenderState& s) { s.SetLogicOp(LogicOperation::Xor); });
// ---- Depth and stencil ----
@@ -325,12 +449,15 @@ namespace {
// ---- Colour mask, clear state, sampling ----
check("SetColorMask", [](RenderState& s) { s.SetColorMask(BoolVec4(true, false, true, false)); });
check("SetColorMaskIndexed", [](RenderState& s) { s.SetColorMaskIndexed(6, BoolVec4(false, false, true, true)); });
check("SetColorMaskIndexed(0)",
[](RenderState& s) { s.SetColorMaskIndexed(0, BoolVec4(false, true, false, true)); });
check("SetClearColor", [](RenderState& s) { s.SetClearColor(FloatVec4(0.1f, 0.2f, 0.3f, 0.4f)); });
check("SetClearDepth", [](RenderState& s) { s.SetClearDepth(0.75f); });
check("SetClearStencil", [](RenderState& s) { s.SetClearStencil(9); });
check("SetBlendColor", [](RenderState& s) { s.SetBlendColor(FloatVec4(0.5f, 0.6f, 0.7f, 0.8f)); });
check("SetDepthRange", [](RenderState& s) { s.SetDepthRange(FloatVec2(0.25f, 0.75f)); });
check("SetDepthRangeIndexed", [](RenderState& s) { s.SetDepthRangeIndexed(9, FloatVec2(0.1f, 0.9f)); });
check("SetDepthRangeIndexed(0)", [](RenderState& s) { s.SetDepthRangeIndexed(0, FloatVec2(0.3f, 0.6f)); });
// SetSampleCoverage calls BumpVersions(), so under the rule it is PIPELINE state -
// which is why MGPipeTypes.h's MGPDynamicState comment no longer claims otherwise.
check("SetSampleCoverage", [](RenderState& s) { s.SetSampleCoverage(0.375f, true); });
@@ -347,6 +474,7 @@ namespace {
check("SetScissorBox(first write)", [](RenderState& s) { s.SetScissorBox(IntVec4(1, 2, 3, 4)); });
check("SetScissorBox(again)", [](RenderState& s) { s.SetScissorBox(IntVec4(5, 6, 7, 8)); });
check("SetScissorBoxIndexed", [](RenderState& s) { s.SetScissorBoxIndexed(11, IntVec4(9, 10, 11, 12)); });
check("SetScissorBoxIndexed(0)", [](RenderState& s) { s.SetScissorBoxIndexed(0, IntVec4(13, 14, 15, 16)); });
// SetPixelStoreParam moves NEITHER counter and touches no byte of
// RenderStateParameters: the pixel store lives in its own two structs and travels as
@@ -479,67 +607,16 @@ namespace {
applyWholeBlock();
// ---- the 25 kDraw fields ----
EXPECT_EQ(gPipeInputs.GetBlendColor(), ctx.GetBlendColor());
for (Uint i = 0; i < kMGMaxDrawBuffers; ++i) {
BlendEquation gotColor{}, gotAlpha{}, wantColor{}, wantAlpha{};
gPipeInputs.GetBlendEquationIndexed(i, gotColor, gotAlpha);
ctx.GetBlendEquationIndexed(i, wantColor, wantAlpha);
EXPECT_EQ(gotColor, wantColor) << "blend equation " << i;
EXPECT_EQ(gotAlpha, wantAlpha) << "blend equation " << i;
BlendFactor gotSrcRGB{}, gotDstRGB{}, gotSrcA{}, gotDstA{};
BlendFactor wantSrcRGB{}, wantDstRGB{}, wantSrcA{}, wantDstA{};
gPipeInputs.GetBlendFuncIndexed(i, gotSrcRGB, gotDstRGB, gotSrcA, gotDstA);
ctx.GetBlendFuncIndexed(i, wantSrcRGB, wantDstRGB, wantSrcA, wantDstA);
EXPECT_EQ(gotSrcRGB, wantSrcRGB) << "blend func " << i;
EXPECT_EQ(gotDstRGB, wantDstRGB) << "blend func " << i;
EXPECT_EQ(gotSrcA, wantSrcA) << "blend func " << i;
EXPECT_EQ(gotDstA, wantDstA) << "blend func " << i;
EXPECT_EQ(gPipeInputs.GetColorMaskIndexed(i), ctx.GetColorMaskIndexed(i)) << "colour mask " << i;
EXPECT_EQ(gPipeInputs.IsCapabilityEnabledIndexed(CapabilityInput::Blend, i),
ctx.IsCapabilityEnabledIndexed(CapabilityInput::Blend, i))
<< "indexed blend enable " << i;
}
EXPECT_EQ(gPipeInputs.GetCullFaceMode(), ctx.GetCullFaceMode());
EXPECT_EQ(gPipeInputs.GetDepthFunc(), ctx.GetDepthFunc());
EXPECT_EQ(gPipeInputs.GetDepthMask(), ctx.GetDepthMask());
for (Uint i = 0; i < RenderStateParameters::MAX_VIEWPORTS; ++i) {
EXPECT_EQ(gPipeInputs.GetDepthRangeIndexed(i), ctx.GetDepthRangeIndexed(i)) << "depth range " << i;
EXPECT_EQ(gPipeInputs.GetViewportIndexed(i), ctx.GetViewportIndexed(i)) << "viewport " << i;
EXPECT_EQ(gPipeInputs.IsCapabilityEnabledIndexed(CapabilityInput::ScissorTest, i),
ctx.IsCapabilityEnabledIndexed(CapabilityInput::ScissorTest, i))
<< "indexed scissor enable " << i;
}
EXPECT_EQ(gPipeInputs.GetLineWidth(), ctx.GetLineWidth());
EXPECT_EQ(gPipeInputs.GetLogicOp(), ctx.GetLogicOp());
EXPECT_EQ(gPipeInputs.GetMinSampleShadingValue(), ctx.GetMinSampleShadingValue());
EXPECT_EQ(gPipeInputs.GetPatchDefaultInnerLevel(), ctx.GetPatchDefaultInnerLevel());
EXPECT_EQ(gPipeInputs.GetPatchDefaultOuterLevel(), ctx.GetPatchDefaultOuterLevel());
EXPECT_EQ(gPipeInputs.GetPatchVertices(), ctx.GetPatchVertices());
EXPECT_EQ(gPipeInputs.GetPolygonModeFront(), ctx.GetPolygonModeFront());
EXPECT_EQ(gPipeInputs.GetPolygonOffsetFactor(), ctx.GetPolygonOffsetFactor());
EXPECT_EQ(gPipeInputs.GetPolygonOffsetUnits(), ctx.GetPolygonOffsetUnits());
EXPECT_EQ(gPipeInputs.GetPrimitiveRestartIndex(), ctx.GetPrimitiveRestartIndex());
EXPECT_EQ(gPipeInputs.GetProvokingVertexMode(), ctx.GetProvokingVertexMode());
EXPECT_EQ(gPipeInputs.GetScissorBox(), ctx.GetScissorBox());
for (const StencilFace face : {StencilFace::Front, StencilFace::Back}) {
const StencilFaceState& got = gPipeInputs.GetStencilState(face);
const StencilFaceState& want = ctx.GetStencilState(face);
EXPECT_EQ(std::memcmp(&got, &want, sizeof(StencilFaceState)), 0)
<< "stencil face " << static_cast<int>(face);
}
ExpectDerivedDrawFieldsMatch(ctx, "whole block, kDraw");
// ...and the ~25 members that have NO derived field, which only a byte compare of the
// whole block reaches.
ExpectAssembledBlockIsTheLiveBlock(ctx, "whole block, kDraw");
// The rounding half of GetViewport, exercised on purpose: viewport 0 is
// (1.5, 2.5, 63.5, 32.25), so a transcription that truncated instead of rounding
// would hand the backends a 63-wide rectangle where 64 was asked for. The literal
// is std::lround's answer - round half AWAY FROM ZERO, so 1.5 -> 2 and 2.5 -> 3,
// not the banker's rounding a nearbyint() transcription would give.
EXPECT_EQ(gPipeInputs.GetViewport(), ctx.GetViewport());
EXPECT_EQ(gPipeInputs.GetViewport(), IntVec4(2, 3, 64, 32));
for (SizeT i = 0; i < static_cast<SizeT>(CapabilityInput::CapabilityInputCount); ++i) {
const CapabilityInput cap = static_cast<CapabilityInput>(i);
EXPECT_EQ(gPipeInputs.IsCapabilityEnabled(cap), ctx.IsCapabilityEnabled(cap)) << "capability " << i;
}
} // phase 1, kDraw
// Phase 2, kClear: the three clear values. The verb's own fill runs FIRST and copies
@@ -626,6 +703,165 @@ namespace {
"scissorEnabled input and this expectation both need re-reading";
EXPECT_TRUE(IsWhollyPipeline(offsetof(RenderStateParameters, ScissorTestEnabledMask),
sizeof(RenderStateParameters::ScissorTestEnabledMask)));
#endif
}
// -------------------------------------------------------------------------------------
// 5. The INCREMENTAL path, which is the shape the tracker actually emits: a
// create_render_state naming only the pipeline chunks that moved against a BaseCso
// (D7 step 2's miss path), and a set_dynamic_state naming only the dynamic chunks that
// moved (D8's chunk-level suppressor). Nothing but this case enters
// MGPipeApplyCreateRenderState's base-inherit branch, and nothing but this case drives
// the applier's CHUNK-SCOPED derivation - a whole-block apply asks for every chunk and
// so cannot tell a correctly scoped guard from one that is too narrow.
// -------------------------------------------------------------------------------------
TEST(RenderStateSpans, IncrementalChunksKeepEveryDerivedFieldInStep) {
#if !MOBILEGL_PIPE_PUSH
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
#else
struct ContextGuard {
UniquePtr<GLContext> Previous;
ContextGuard() : Previous(Move(MG_State::pGLContext)) {
MG_State::pGLContext = MakeUnique<GLContext>();
MGPipeApplierReset();
}
~ContextGuard() {
MGPipeApplierReset();
MG_State::pGLContext = Move(Previous);
}
} guard;
GLContext& ctx = *MG_State::pGLContext;
// Every read below is a kDraw fill point, so one verb covers the whole case. The fill
// runs at construction against the DEFAULT context, which is what makes the first
// step's comparison meaningful rather than a comparison of the filler with itself.
MG_Test::ScopedPipeVerb verb(MGPipeVerb::DrawArrays);
// `staged` is the tracker's "what the server has" mirror (D8). The masks are computed
// from it exactly as the tracker will compute them.
RenderStateParameters staged{};
MGPipeHandle previousCso = kMGPipeNullHandle;
Uint32 nextSlot = kMGPipeFirstAllocatableSlot;
Bool firstPush = true;
const auto push = [&](const char* tag) {
const RenderStateParameters& live = ctx.GetRenderStateParameters();
const Uint32 movedPipeline =
firstPush ? static_cast<Uint32>((Uint64{1} << kMGPipePipelineChunkCount) - 1)
: MGPipePipelineChunksThatMoved(staged, live);
const Uint32 movedDynamic =
firstPush ? static_cast<Uint32>((Uint64{1} << kMGPipeDynamicChunkCount) - 1)
: MGPipeDynamicChunksThatMoved(staged, live);
if (movedPipeline != 0) {
Vector<Uint8> blob(MGPipePipelineChunkBlobBytes(movedPipeline));
MGPipeGatherPipelineChunks(live, movedPipeline, blob.data());
MGPRenderStateDesc desc{};
desc.Cso = MGPipeHandle{nextSlot++, 0};
// The base-inherit branch: everything this desc does NOT name has to come
// from the record the client is pointing at.
desc.BaseCso = previousCso;
desc.ChunkMask = movedPipeline;
MGPipeApplyCreateRenderState(desc, blob.data());
MGPBindRenderState bind{};
bind.Cso = desc.Cso;
bind.Version = static_cast<Uint16>(ctx.GetRenderStateParametersVersion());
bind.PipelineVersion = static_cast<Uint16>(ctx.GetPipelineStateVersion());
MGPipeApplyBindRenderState(bind);
previousCso = desc.Cso;
// The reconstructed record must be the WHOLE pipeline half of the live block,
// byte for byte: an incremental create that inherited the wrong chunk would
// otherwise only show up as a wrong pixel much later, on the first bind that
// scatters it. This is also MGPipeHashPipelineBytes' only caller - and its
// agreement with the from-scratch hash is what lets CsoCache hash the bytes
// it already holds instead of re-gathering them.
Array<Uint8, kMGPipePipelineChunkBytes> gathered{};
MGPipeGatherPipelineBytes(live, gathered.data());
const MGPipeRenderStateCsoRecord& record = MGPipeApplier().RenderStateCsos[desc.Cso.Slot];
EXPECT_EQ(std::memcmp(record.PipelineBytes.data(), gathered.data(), kMGPipePipelineChunkBytes), 0)
<< tag << ": the incrementally created CSO is not the live pipeline half";
EXPECT_EQ(MGPipeHashPipelineBytes(gathered.data()), MGPipeComputePipelineSubsetHash(live))
<< tag << ": hashing the gathered bytes disagrees with hashing the block";
}
if (movedDynamic != 0) {
Vector<Uint8> blob(MGPipeDynamicChunkBlobBytes(movedDynamic));
MGPipeGatherDynamicChunks(live, movedDynamic, blob.data());
MGPDynamicState dyn{};
dyn.ChunkMask = movedDynamic;
dyn.Version = static_cast<Uint16>(ctx.GetRenderStateParametersVersion());
MGPipeApplySetDynamicState(dyn, blob.data());
}
staged = live;
firstPush = false;
// Every derived field, after a scatter that named only the chunks that moved. A
// derivation guard that is too narrow leaves the previous step's value standing
// and this is where it shows.
ExpectDerivedDrawFieldsMatch(ctx, tag);
ExpectAssembledBlockIsTheLiveBlock(ctx, tag);
};
// Step 0: the whole block, so every later step is a genuine delta.
ctx.SetViewportIndexed(0, FloatVec4(1.5f, 2.5f, 63.5f, 32.25f));
push("step 0: the whole block");
// One step per derivation guard, each moving as few chunks as the setter allows.
ctx.SetViewportIndexed(0, FloatVec4(4.f, 5.f, 60.f, 70.f));
ctx.SetViewportIndexed(7, FloatVec4(8.f, 9.f, 10.f, 11.f));
push("step 1: viewports only (dynamic chunk D0)");
ctx.SetDepthRangeIndexed(3, FloatVec2(0.2f, 0.8f));
push("step 2: depth ranges only (dynamic chunk D2)");
ctx.SetBlendFuncIndexed(0, BlendFactor::DstColor, BlendFactor::SrcColor, BlendFactor::DstAlpha,
BlendFactor::SrcAlpha);
ctx.SetBlendEquationIndexed(5, BlendEquation::ReverseSubtract, BlendEquation::Max);
ctx.SetColorMaskIndexed(0, BoolVec4(false, true, false, true));
push("step 3: blend and colour mask (pipeline chunk P1)");
ctx.SetCapabilityIndexed(CapabilityInput::Blend, 0, true);
push("step 4: indexed blend enable at index 0");
ctx.SetCapabilityIndexed(CapabilityInput::ScissorTest, 2, true);
push("step 5: indexed scissor enable (pipeline chunk P6)");
ctx.SetScissorBox(IntVec4(3, 4, 5, 6));
push("step 6: scissor rectangles (dynamic chunk D7)");
ctx.SetStencilFunc(StencilFace::Front, DepthTestFunc::Equal, 7, 0xf0u);
ctx.SetStencilOp(StencilFace::Back, StencilOperation::Replace, StencilOperation::IncrementClamp,
StencilOperation::DecrementWrap);
ctx.SetStencilMask(StencilFace::Back, 0x0fu);
push("step 7: the stencil faces, which straddle four chunks");
ctx.SetLineWidth(3.5f);
ctx.SetPolygonOffsetClamped(1.5f, 2.5f, 0.25f);
ctx.SetLogicOp(LogicOperation::Xor);
ctx.SetDepthFunc(DepthTestFunc::GreaterEqual);
ctx.SetDepthMask(false);
ctx.SetCullFaceMode(CullFaceMode::Front);
ctx.SetProvokingVertexMode(ProvokingVertexMode::FirstVertex);
ctx.SetPrimitiveRestartIndex(0xabcdu);
ctx.SetMinSampleShadingValue(0.625f);
ctx.SetPatchVertices(4);
ctx.SetPatchDefaultOuterLevel(FloatVec4(2.f, 3.f, 4.f, 5.f));
ctx.SetPatchDefaultInnerLevel(FloatVec2(6.f, 7.f));
ctx.SetPolygonMode(GL_LINE, GL_POINT);
push("step 8: the unguarded scalars");
// EVERY capability, one at a time. This is what pins the capability walk's guard
// exhaustively: the 25 plain bools sit in three different pipeline chunks, Blend is
// BlendStates[0].Enabled, ScissorTest is a pipeline mask and the eight ClipDistances
// are a DYNAMIC mask - so a guard that named only "the capability chunk" would leave
// one of those families stale, and the flip that reaches it fails here by name.
for (SizeT i = 0; i < static_cast<SizeT>(CapabilityInput::CapabilityInputCount); ++i) {
const CapabilityInput cap = static_cast<CapabilityInput>(i);
ctx.SetCapability(cap, !ctx.IsCapabilityEnabled(cap));
push(("step 9: capability " + std::to_string(i)).c_str());
}
#endif
}
} // namespace