mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 20:58:31 +09:00
[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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user