mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
[Fix] (Pipe): scope the derivation to the chunks a scatter moved, and give the patch trio and the residual block trip wires that do not depend on call order
- MGPipeDeriveRenderStateFieldsForChunks: the applier no longer recomputes all 29 fields on every scatter. The four wide walks - the 8-wide blend/colour-mask loop, the 16-wide viewport loop, the 16-wide depth-range loop and the 35-arm capability switch - are guarded by the chunks whose bytes they read, so a per-frame glViewport (the D8 case that sends dynamic chunk D0 alone) pays for one 16-entry copy instead of ~170 stores and 35 switch dispatches. That cost sat on the per-draw path and the gate it threatened is G11's pinned ns/draw. - Every guard is MGPipeRenderStateChunkBitsCovering(offsetof(member), sizeof(member)) over the members the guarded block reads, computed from the boundary table: there is no second, hand-maintained member-to-chunk mapping to go stale when a boundary moves. The scalar copies stay unguarded on purpose - they cannot go stale, which keeps the risk of the scoping confined to four guards. - MGP_PLAIN_CAPABILITY_LIST is written once and used twice, for DeriveCapability's switch arms and for the capability guard's chunk set, so the two cannot drift. - set_patch_state now asserts under poison/verify that the trio agrees with what pipeline chunk P0 delivered, which D6 and D10 both ask for and which was missing: a stale set_patch_state silently clobbered the CSO-delivered levels. Compared bitwise, because a NaN outer level is legal and must equal itself; armed only once a CSO has been bound, which states the ordering contract rather than assuming it. - set_residual_value_state's trip wire compares the carried bits against the WORKING BLOCK instead of PipeInputs::m_capability. Only the derivation writes m_capability, so a residual block emitted before the first bind of a context - what "once per context" means - compared against all-false storage while Dither and Multisample default to true, and aborted under poison. The check is unchanged in strength and now has no ordering contract at all. - PipeApply.h no longer implies the verify comparator is this package's oracle: it arms off MG_Config::Features.PipeVerify, which a unit-test process never sets, so the unit oracle is named for what it is and the comparator is credited to the retrace and integration-verify lanes.
This commit is contained in:
Executable → Regular
+57
@@ -173,6 +173,63 @@ namespace MobileGL::MG_Pipe {
|
|||||||
static_assert(kMGPipePipelineChunkBytes == 396, "the pipeline subset is 396 bytes");
|
static_assert(kMGPipePipelineChunkBytes == 396, "the pipeline subset is 396 bytes");
|
||||||
static_assert(kMGPipeDynamicChunkBytes == 772, "the dynamic subset is 772 bytes");
|
static_assert(kMGPipeDynamicChunkBytes == 772, "the dynamic subset is 772 bytes");
|
||||||
|
|
||||||
|
// ---- global chunk bits, so nothing downstream hand-maintains a second table ----
|
||||||
|
|
||||||
|
// The GLOBAL chunk indices (bit i is chunk i of the 15) whose byte range overlaps
|
||||||
|
// [offset, offset + size). It falls straight out of the boundary table, which is the
|
||||||
|
// whole point: the applier scopes its derivation by the chunks a scatter actually moved
|
||||||
|
// (D5/D8), and a hand-written member -> chunk mapping is exactly the second table that
|
||||||
|
// would go stale the first time a boundary moves.
|
||||||
|
constexpr Uint32 MGPipeRenderStateChunkBitsCovering(SizeT offset, SizeT size) {
|
||||||
|
Uint32 bits = 0;
|
||||||
|
for (SizeT i = 0; i < kMGPipeRenderStateChunkCount; ++i) {
|
||||||
|
const SizeT begin = kMGPipeRenderStateChunkBoundaries[i];
|
||||||
|
const SizeT end = kMGPipeRenderStateChunkBoundaries[i + 1];
|
||||||
|
if (offset < end && begin < offset + size) bits |= Uint32{1} << i;
|
||||||
|
}
|
||||||
|
return bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The wire masks are HALF-LOCAL (bit i of MGPRenderStateDesc::ChunkMask is pipeline chunk
|
||||||
|
// i); these widen them to the global indices the boundary table is written in. The
|
||||||
|
// halves alternate with chunk 0 dynamic, so the two conversions are arithmetic.
|
||||||
|
constexpr Uint32 MGPipeGlobalChunkBitsOfPipelineMask(Uint32 pipelineMask) {
|
||||||
|
Uint32 bits = 0;
|
||||||
|
for (SizeT i = 0; i < kMGPipePipelineChunkCount; ++i) {
|
||||||
|
if (((pipelineMask >> i) & 1u) != 0) bits |= Uint32{1} << (i * 2 + 1);
|
||||||
|
}
|
||||||
|
return bits;
|
||||||
|
}
|
||||||
|
constexpr Uint32 MGPipeGlobalChunkBitsOfDynamicMask(Uint32 dynamicMask) {
|
||||||
|
Uint32 bits = 0;
|
||||||
|
for (SizeT i = 0; i < kMGPipeDynamicChunkCount; ++i) {
|
||||||
|
if (((dynamicMask >> i) & 1u) != 0) bits |= Uint32{1} << (i * 2);
|
||||||
|
}
|
||||||
|
return bits;
|
||||||
|
}
|
||||||
|
inline constexpr Uint32 kMGPipeAllGlobalChunks =
|
||||||
|
static_cast<Uint32>((Uint64{1} << kMGPipeRenderStateChunkCount) - 1);
|
||||||
|
|
||||||
|
// The two conversions must agree with MGPipeRenderStateChunkIsPipeline, and together they
|
||||||
|
// must cover the table exactly - a widening that dropped or doubled a chunk would make
|
||||||
|
// the applier's scoping silently wrong rather than loud.
|
||||||
|
namespace MGPipeRenderStateChunkDetail {
|
||||||
|
inline constexpr Uint32 kAllPipelineHalfBits =
|
||||||
|
static_cast<Uint32>((Uint64{1} << kMGPipePipelineChunkCount) - 1);
|
||||||
|
inline constexpr Uint32 kAllDynamicHalfBits =
|
||||||
|
static_cast<Uint32>((Uint64{1} << kMGPipeDynamicChunkCount) - 1);
|
||||||
|
inline constexpr Uint32 kWidenedPipeline = MGPipeGlobalChunkBitsOfPipelineMask(kAllPipelineHalfBits);
|
||||||
|
inline constexpr Uint32 kWidenedDynamic = MGPipeGlobalChunkBitsOfDynamicMask(kAllDynamicHalfBits);
|
||||||
|
} // namespace MGPipeRenderStateChunkDetail
|
||||||
|
static_assert((MGPipeRenderStateChunkDetail::kWidenedPipeline &
|
||||||
|
MGPipeRenderStateChunkDetail::kWidenedDynamic) == 0,
|
||||||
|
"the two half-local -> global widenings must not overlap");
|
||||||
|
static_assert((MGPipeRenderStateChunkDetail::kWidenedPipeline |
|
||||||
|
MGPipeRenderStateChunkDetail::kWidenedDynamic) == kMGPipeAllGlobalChunks,
|
||||||
|
"the two half-local -> global widenings must cover the whole chunk table");
|
||||||
|
static_assert(MGPipeRenderStateChunkBitsCovering(0, sizeof(RenderStateParameters)) == kMGPipeAllGlobalChunks,
|
||||||
|
"every chunk must be covered by the whole block");
|
||||||
|
|
||||||
// ---- the operations everything else is written against ----
|
// ---- the operations everything else is written against ----
|
||||||
|
|
||||||
// The 396 pipeline bytes of `params`, in ascending chunk order, into `dst`.
|
// The 396 pipeline bytes of `params`, in ascending chunk order, into `dst`.
|
||||||
|
|||||||
Executable → Regular
+194
-65
@@ -20,7 +20,80 @@
|
|||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
|
// The 25 capabilities whose storage is a plain `<Name>Enabled` bool. Written ONCE and used
|
||||||
|
// twice - once for the switch arms of DeriveCapability and once for the chunk set that guards
|
||||||
|
// the capability walk - so the two cannot drift apart. The three P2 gave storage to
|
||||||
|
// (DepthClamp, FramebufferSrgb, TextureCubeMapSeamless) are in the list like any other; the
|
||||||
|
// three that are NOT are Blend (BlendStates[i].Enabled), ScissorTest (a 16-bit mask) and the
|
||||||
|
// eight ClipDistances (an 8-bit mask), each handled by name below.
|
||||||
|
#define MGP_PLAIN_CAPABILITY_LIST(X) \
|
||||||
|
X(ColorLogicOp) \
|
||||||
|
X(DebugOutput) \
|
||||||
|
X(DebugOutputSynchronous) \
|
||||||
|
X(DepthClamp) \
|
||||||
|
X(DepthTest) \
|
||||||
|
X(CullFace) \
|
||||||
|
X(Dither) \
|
||||||
|
X(FramebufferSrgb) \
|
||||||
|
X(LineSmooth) \
|
||||||
|
X(Multisample) \
|
||||||
|
X(PolygonOffsetFill) \
|
||||||
|
X(PolygonOffsetLine) \
|
||||||
|
X(PolygonOffsetPoint) \
|
||||||
|
X(PolygonSmooth) \
|
||||||
|
X(PrimitiveRestart) \
|
||||||
|
X(PrimitiveRestartFixedIndex) \
|
||||||
|
X(RasterizerDiscard) \
|
||||||
|
X(SampleAlphaToCoverage) \
|
||||||
|
X(SampleAlphaToOne) \
|
||||||
|
X(SampleCoverage) \
|
||||||
|
X(SampleMask) \
|
||||||
|
X(SampleShading) \
|
||||||
|
X(StencilTest) \
|
||||||
|
X(TextureCubeMapSeamless) \
|
||||||
|
X(ProgramPointSize)
|
||||||
|
|
||||||
namespace MobileGL::MG_Pipe {
|
namespace MobileGL::MG_Pipe {
|
||||||
|
namespace {
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// WHICH CHUNKS EACH DERIVATION READS.
|
||||||
|
//
|
||||||
|
// The derivation is called after every scatter, and a scatter usually moves ONE
|
||||||
|
// chunk: a per-frame glViewport sends dynamic chunk D0 and nothing else (D8). So the
|
||||||
|
// three wide loops and the 35-arm capability switch are guarded by the chunks whose
|
||||||
|
// bytes they read, and a scatter that did not touch those bytes does not pay for them.
|
||||||
|
//
|
||||||
|
// Nothing here is hand-mapped. Every constant is
|
||||||
|
// MGPipeRenderStateChunkBitsCovering(offsetof(member), sizeof(member)) over the
|
||||||
|
// members the guarded block actually reads, so the ONLY claim a reader has to check
|
||||||
|
// is "does this block read anything else?" - and a boundary move re-computes the
|
||||||
|
// guards rather than invalidating them.
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
using RSP = RenderStateParameters;
|
||||||
|
#define MGP_CHUNKS_OF(member) MGPipeRenderStateChunkBitsCovering(offsetof(RSP, member), sizeof(RSP::member))
|
||||||
|
|
||||||
|
// The per-draw-buffer loop reads BlendStates (equations, factors, Enabled) and
|
||||||
|
// ColorMasks, and nothing else.
|
||||||
|
constexpr Uint32 kChunksBlendLoop = MGP_CHUNKS_OF(BlendStates) | MGP_CHUNKS_OF(ColorMasks);
|
||||||
|
// m_viewportIndexed[16] and the rounded m_viewport both read Viewports, and nothing else.
|
||||||
|
constexpr Uint32 kChunksViewportLoop = MGP_CHUNKS_OF(Viewports);
|
||||||
|
constexpr Uint32 kChunksDepthRangeLoop = MGP_CHUNKS_OF(DepthRanges);
|
||||||
|
constexpr Uint32 kChunksScissorEnableLoop = MGP_CHUNKS_OF(ScissorTestEnabledMask);
|
||||||
|
// DeriveCapability's sources: the 25 plain bools, plus the three masks/arrays the
|
||||||
|
// three special arms read.
|
||||||
|
#define MGP_CAPABILITY_CHUNKS(capability) | MGP_CHUNKS_OF(capability##Enabled)
|
||||||
|
constexpr Uint32 kChunksCapabilityWalk = MGP_CHUNKS_OF(BlendStates) |
|
||||||
|
MGP_CHUNKS_OF(ScissorTestEnabledMask) |
|
||||||
|
MGP_CHUNKS_OF(ClipDistanceEnabledMask)
|
||||||
|
MGP_PLAIN_CAPABILITY_LIST(MGP_CAPABILITY_CHUNKS);
|
||||||
|
#undef MGP_CAPABILITY_CHUNKS
|
||||||
|
|
||||||
|
// The scalar copies are left unguarded on purpose: they are ~20 stores and two
|
||||||
|
// 28-byte struct copies, so guarding each would cost more branches than it saves
|
||||||
|
// stores - and an unguarded copy cannot go stale, which keeps the risk of the scoping
|
||||||
|
// confined to the four guards above.
|
||||||
|
#undef MGP_CHUNKS_OF
|
||||||
|
} // namespace
|
||||||
|
|
||||||
// The applier's door into PipeInputs' storage, the write-side twin of PipeFill.cpp's
|
// 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
|
// MGPipeFillAccess. It does NOT stamp the poison generations: a stamp says "the filler
|
||||||
@@ -76,31 +149,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
case CapabilityInput::capability: \
|
case CapabilityInput::capability: \
|
||||||
return p.capability##Enabled;
|
return p.capability##Enabled;
|
||||||
switch (cap) {
|
switch (cap) {
|
||||||
MGP_DERIVE_CAPABILITY(ColorLogicOp)
|
MGP_PLAIN_CAPABILITY_LIST(MGP_DERIVE_CAPABILITY)
|
||||||
MGP_DERIVE_CAPABILITY(DebugOutput)
|
|
||||||
MGP_DERIVE_CAPABILITY(DebugOutputSynchronous)
|
|
||||||
MGP_DERIVE_CAPABILITY(DepthClamp)
|
|
||||||
MGP_DERIVE_CAPABILITY(DepthTest)
|
|
||||||
MGP_DERIVE_CAPABILITY(CullFace)
|
|
||||||
MGP_DERIVE_CAPABILITY(Dither)
|
|
||||||
MGP_DERIVE_CAPABILITY(FramebufferSrgb)
|
|
||||||
MGP_DERIVE_CAPABILITY(LineSmooth)
|
|
||||||
MGP_DERIVE_CAPABILITY(Multisample)
|
|
||||||
MGP_DERIVE_CAPABILITY(PolygonOffsetFill)
|
|
||||||
MGP_DERIVE_CAPABILITY(PolygonOffsetLine)
|
|
||||||
MGP_DERIVE_CAPABILITY(PolygonOffsetPoint)
|
|
||||||
MGP_DERIVE_CAPABILITY(PolygonSmooth)
|
|
||||||
MGP_DERIVE_CAPABILITY(PrimitiveRestart)
|
|
||||||
MGP_DERIVE_CAPABILITY(PrimitiveRestartFixedIndex)
|
|
||||||
MGP_DERIVE_CAPABILITY(RasterizerDiscard)
|
|
||||||
MGP_DERIVE_CAPABILITY(SampleAlphaToCoverage)
|
|
||||||
MGP_DERIVE_CAPABILITY(SampleAlphaToOne)
|
|
||||||
MGP_DERIVE_CAPABILITY(SampleCoverage)
|
|
||||||
MGP_DERIVE_CAPABILITY(SampleMask)
|
|
||||||
MGP_DERIVE_CAPABILITY(SampleShading)
|
|
||||||
MGP_DERIVE_CAPABILITY(StencilTest)
|
|
||||||
MGP_DERIVE_CAPABILITY(TextureCubeMapSeamless)
|
|
||||||
MGP_DERIVE_CAPABILITY(ProgramPointSize)
|
|
||||||
// The non-indexed query of an INDEXED capability answers for index 0
|
// The non-indexed query of an INDEXED capability answers for index 0
|
||||||
// (GL 4.6 core 22.1) - RenderState.cpp says it in the same words.
|
// (GL 4.6 core 22.1) - RenderState.cpp says it in the same words.
|
||||||
case CapabilityInput::Blend:
|
case CapabilityInput::Blend:
|
||||||
@@ -126,40 +175,57 @@ namespace MobileGL::MG_Pipe {
|
|||||||
#undef MGP_DERIVE_CAPABILITY
|
#undef MGP_DERIVE_CAPABILITY
|
||||||
}
|
}
|
||||||
|
|
||||||
static void DeriveRenderStateFields(PipeInputs& inputs) {
|
// `chunkBits` names the GLOBAL chunks the scatter that called this actually moved;
|
||||||
|
// kMGPipeAllGlobalChunks is the whole-block form. See the guard constants at the top
|
||||||
|
// of this file for why the four wide walks are scoped and the scalars are not.
|
||||||
|
static void DeriveRenderStateFields(PipeInputs& inputs, Uint32 chunkBits) {
|
||||||
const RenderStateParameters& p = inputs.m_renderState;
|
const RenderStateParameters& p = inputs.m_renderState;
|
||||||
|
|
||||||
// Per draw buffer: GetBlendEquationIndexed, GetBlendFuncIndexed,
|
// Per draw buffer: GetBlendEquationIndexed, GetBlendFuncIndexed,
|
||||||
// GetColorMaskIndexed and IsCapabilityEnabledIndexed(Blend).
|
// GetColorMaskIndexed and IsCapabilityEnabledIndexed(Blend).
|
||||||
for (Uint i = 0; i < kMGMaxDrawBuffers; ++i) {
|
if ((chunkBits & kChunksBlendLoop) != 0) {
|
||||||
const PerBufferBlendState& blend = p.BlendStates[i];
|
for (Uint i = 0; i < kMGMaxDrawBuffers; ++i) {
|
||||||
inputs.m_blendEquation[i][0] = blend.ColorEquation;
|
const PerBufferBlendState& blend = p.BlendStates[i];
|
||||||
inputs.m_blendEquation[i][1] = blend.AlphaEquation;
|
inputs.m_blendEquation[i][0] = blend.ColorEquation;
|
||||||
inputs.m_blendFunc[i][0] = blend.SrcFactorRGB;
|
inputs.m_blendEquation[i][1] = blend.AlphaEquation;
|
||||||
inputs.m_blendFunc[i][1] = blend.DstFactorRGB;
|
inputs.m_blendFunc[i][0] = blend.SrcFactorRGB;
|
||||||
inputs.m_blendFunc[i][2] = blend.SrcFactorAlpha;
|
inputs.m_blendFunc[i][1] = blend.DstFactorRGB;
|
||||||
inputs.m_blendFunc[i][3] = blend.DstFactorAlpha;
|
inputs.m_blendFunc[i][2] = blend.SrcFactorAlpha;
|
||||||
inputs.m_colorMask[i] = p.ColorMasks[i];
|
inputs.m_blendFunc[i][3] = blend.DstFactorAlpha;
|
||||||
inputs.m_capabilityIndexed.Blend[i] = blend.Enabled;
|
inputs.m_colorMask[i] = p.ColorMasks[i];
|
||||||
|
inputs.m_capabilityIndexed.Blend[i] = blend.Enabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per viewport: GetViewportIndexed, GetDepthRangeIndexed and
|
// GetViewportIndexed, and GetViewport: viewport 0 ROUNDED - the other derivation
|
||||||
// IsCapabilityEnabledIndexed(ScissorTest).
|
// that is not a field copy. glGetIntegerv on floating-point state rounds to
|
||||||
for (Uint i = 0; i < PipeInputs::kMaxViewports; ++i) {
|
// nearest (GL 4.6 core 22.2), and truncating a 63.5-wide viewport would also hand
|
||||||
inputs.m_viewportIndexed[i] = p.Viewports[i];
|
// the backends a rectangle one pixel short of what was asked for. std::lround,
|
||||||
inputs.m_depthRange[i] = p.DepthRanges[i];
|
// exactly as RenderState::GetViewport does it.
|
||||||
inputs.m_capabilityIndexed.ScissorTest[i] = (p.ScissorTestEnabledMask & (1u << i)) != 0;
|
if ((chunkBits & kChunksViewportLoop) != 0) {
|
||||||
|
for (Uint i = 0; i < PipeInputs::kMaxViewports; ++i) {
|
||||||
|
inputs.m_viewportIndexed[i] = p.Viewports[i];
|
||||||
|
}
|
||||||
|
const FloatVec4& viewport = p.Viewports[0];
|
||||||
|
inputs.m_viewport =
|
||||||
|
IntVec4(static_cast<Int>(std::lround(viewport.x())), static_cast<Int>(std::lround(viewport.y())),
|
||||||
|
static_cast<Int>(std::lround(viewport.z())), static_cast<Int>(std::lround(viewport.w())));
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetViewport: viewport 0 ROUNDED - the other derivation that is not a field
|
// GetDepthRangeIndexed. Its own chunk (D2) - a glClearColor moves that chunk and
|
||||||
// copy. glGetIntegerv on floating-point state rounds to nearest (GL 4.6 core
|
// a glViewport does not, so it cannot ride with the viewports.
|
||||||
// 22.2), and truncating a 63.5-wide viewport would also hand the backends a
|
if ((chunkBits & kChunksDepthRangeLoop) != 0) {
|
||||||
// rectangle one pixel short of what was asked for. std::lround, exactly as
|
for (Uint i = 0; i < PipeInputs::kMaxViewports; ++i) {
|
||||||
// RenderState::GetViewport does it.
|
inputs.m_depthRange[i] = p.DepthRanges[i];
|
||||||
const FloatVec4& viewport = p.Viewports[0];
|
}
|
||||||
inputs.m_viewport =
|
}
|
||||||
IntVec4(static_cast<Int>(std::lround(viewport.x())), static_cast<Int>(std::lround(viewport.y())),
|
|
||||||
static_cast<Int>(std::lround(viewport.z())), static_cast<Int>(std::lround(viewport.w())));
|
// IsCapabilityEnabledIndexed(ScissorTest): 16 bits of one pipeline word.
|
||||||
|
if ((chunkBits & kChunksScissorEnableLoop) != 0) {
|
||||||
|
for (Uint i = 0; i < PipeInputs::kMaxViewports; ++i) {
|
||||||
|
inputs.m_capabilityIndexed.ScissorTest[i] = (p.ScissorTestEnabledMask & (1u << i)) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The scalar copies, in the order MGP_COVERAGE_EMITTED_LIST names them.
|
// The scalar copies, in the order MGP_COVERAGE_EMITTED_LIST names them.
|
||||||
inputs.m_blendColor = p.BlendColor;
|
inputs.m_blendColor = p.BlendColor;
|
||||||
@@ -192,8 +258,12 @@ namespace MobileGL::MG_Pipe {
|
|||||||
inputs.m_stencil[face] = p.StencilStates[face];
|
inputs.m_stencil[face] = p.StencilStates[face];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (SizeT i = 0; i < PipeInputs::kCapabilityCount; ++i) {
|
// The 35-arm switch, dispatched 35 times. The widest single thing the derivation
|
||||||
inputs.m_capability[i] = DeriveCapability(p, static_cast<CapabilityInput>(i));
|
// does, and the one a per-frame glViewport most obviously must not pay for.
|
||||||
|
if ((chunkBits & kChunksCapabilityWalk) != 0) {
|
||||||
|
for (SizeT i = 0; i < PipeInputs::kCapabilityCount; ++i) {
|
||||||
|
inputs.m_capability[i] = DeriveCapability(p, static_cast<CapabilityInput>(i));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -314,7 +384,10 @@ namespace MobileGL::MG_Pipe {
|
|||||||
MGPipeApplyAccess::RenderState(inputs));
|
MGPipeApplyAccess::RenderState(inputs));
|
||||||
MGPipeApplyAccess::SetRenderStateVersions(inputs, bind.Version, bind.PipelineVersion);
|
MGPipeApplyAccess::SetRenderStateVersions(inputs, bind.Version, bind.PipelineVersion);
|
||||||
g_applier.BoundRenderStateCso = bind.Cso;
|
g_applier.BoundRenderStateCso = bind.Cso;
|
||||||
MGPipeDeriveRenderStateFields(inputs);
|
// A bind scatters the WHOLE pipeline half - the record is always a complete one,
|
||||||
|
// whatever mask minted it - so the pipeline chunks are all "moved" here.
|
||||||
|
MGPipeDeriveRenderStateFieldsForChunks(
|
||||||
|
inputs, MGPipeGlobalChunkBitsOfPipelineMask(kAllPipelineChunks));
|
||||||
}
|
}
|
||||||
|
|
||||||
void MGPipeApplyDeleteRenderState(const MGPHandleOnly& handle) {
|
void MGPipeApplyDeleteRenderState(const MGPHandleOnly& handle) {
|
||||||
@@ -335,7 +408,7 @@ namespace MobileGL::MG_Pipe {
|
|||||||
PipeInputs& inputs = gPipeInputs;
|
PipeInputs& inputs = gPipeInputs;
|
||||||
MGPipeScatterDynamicChunks(chunkBytes, dyn.ChunkMask, MGPipeApplyAccess::RenderState(inputs));
|
MGPipeScatterDynamicChunks(chunkBytes, dyn.ChunkMask, MGPipeApplyAccess::RenderState(inputs));
|
||||||
MGPipeApplyAccess::SetRenderStateParametersVersion(inputs, dyn.Version);
|
MGPipeApplyAccess::SetRenderStateParametersVersion(inputs, dyn.Version);
|
||||||
MGPipeDeriveRenderStateFields(inputs);
|
MGPipeDeriveRenderStateFieldsForChunks(inputs, MGPipeGlobalChunkBitsOfDynamicMask(dyn.ChunkMask));
|
||||||
}
|
}
|
||||||
|
|
||||||
void MGPipeApplySetPixelPackState(const MGPPixelPackState& pack) {
|
void MGPipeApplySetPixelPackState(const MGPPixelPackState& pack) {
|
||||||
@@ -345,10 +418,50 @@ namespace MobileGL::MG_Pipe {
|
|||||||
void MGPipeApplySetPatchState(const MGPPatchState& patch) {
|
void MGPipeApplySetPatchState(const MGPPatchState& patch) {
|
||||||
PipeInputs& inputs = gPipeInputs;
|
PipeInputs& inputs = gPipeInputs;
|
||||||
RenderStateParameters& working = MGPipeApplyAccess::RenderState(inputs);
|
RenderStateParameters& working = MGPipeApplyAccess::RenderState(inputs);
|
||||||
|
const FloatVec4 outer(patch.Outer[0], patch.Outer[1], patch.Outer[2], patch.Outer[3]);
|
||||||
|
const FloatVec2 inner(patch.Inner[0], patch.Inner[1]);
|
||||||
|
|
||||||
|
#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY
|
||||||
|
// THE SECOND TRIP WIRE (D6, D10). The patch trio travels TWICE - once in pipeline
|
||||||
|
// chunk P0, because it is pipeline state, and once as set_patch_state, because both
|
||||||
|
// backends bake it into the synthesized control stage from a shader-build path. The
|
||||||
|
// redundancy is the point: if the two carriers ever part, a stale set_patch_state
|
||||||
|
// silently clobbers what bind_render_state scattered and the tessellation levels a
|
||||||
|
// draw uses stop being the ones its CSO was minted for.
|
||||||
|
//
|
||||||
|
// Compared BITWISE, because a NaN outer level is a legal glPatchParameterfv value
|
||||||
|
// (ARCHITECTURE.md 5.2) and must compare equal to itself.
|
||||||
|
//
|
||||||
|
// Only once a CSO has delivered chunk P0: before the first bind_render_state of a
|
||||||
|
// context the working block still holds its defaults, and a set_patch_state that
|
||||||
|
// legitimately precedes the first bind has nothing to agree with yet. That is the
|
||||||
|
// ordering contract this trip wire places on the tracker - within a validate, the
|
||||||
|
// bind comes first.
|
||||||
|
if (!MGPipeHandleIsNull(g_applier.BoundRenderStateCso)) {
|
||||||
|
const Bool agrees = working.PatchVertices == patch.Vertices &&
|
||||||
|
std::memcmp(&working.PatchDefaultOuterLevel, &outer, sizeof(outer)) == 0 &&
|
||||||
|
std::memcmp(&working.PatchDefaultInnerLevel, &inner, sizeof(inner)) == 0;
|
||||||
|
if (!agrees) {
|
||||||
|
MGLOG_F("MGPipe: Fatal{PipePatchCarriersDiffer} set_patch_state says vertices=%u "
|
||||||
|
"outer=(%g,%g,%g,%g) inner=(%g,%g); chunk P0 delivered vertices=%u "
|
||||||
|
"outer=(%g,%g,%g,%g) inner=(%g,%g)",
|
||||||
|
patch.Vertices, static_cast<double>(outer.x()), static_cast<double>(outer.y()),
|
||||||
|
static_cast<double>(outer.z()), static_cast<double>(outer.w()),
|
||||||
|
static_cast<double>(inner.x()), static_cast<double>(inner.y()), working.PatchVertices,
|
||||||
|
static_cast<double>(working.PatchDefaultOuterLevel.x()),
|
||||||
|
static_cast<double>(working.PatchDefaultOuterLevel.y()),
|
||||||
|
static_cast<double>(working.PatchDefaultOuterLevel.z()),
|
||||||
|
static_cast<double>(working.PatchDefaultOuterLevel.w()),
|
||||||
|
static_cast<double>(working.PatchDefaultInnerLevel.x()),
|
||||||
|
static_cast<double>(working.PatchDefaultInnerLevel.y()));
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
working.PatchVertices = patch.Vertices;
|
working.PatchVertices = patch.Vertices;
|
||||||
working.PatchDefaultOuterLevel =
|
working.PatchDefaultOuterLevel = outer;
|
||||||
FloatVec4(patch.Outer[0], patch.Outer[1], patch.Outer[2], patch.Outer[3]);
|
working.PatchDefaultInnerLevel = inner;
|
||||||
working.PatchDefaultInnerLevel = FloatVec2(patch.Inner[0], patch.Inner[1]);
|
|
||||||
MGPipeApplyAccess::SetPatchState(inputs, working.PatchVertices, working.PatchDefaultOuterLevel,
|
MGPipeApplyAccess::SetPatchState(inputs, working.PatchVertices, working.PatchDefaultOuterLevel,
|
||||||
working.PatchDefaultInnerLevel);
|
working.PatchDefaultInnerLevel);
|
||||||
}
|
}
|
||||||
@@ -391,25 +504,41 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// So the day a later call takes a capability over and forgets to carry it, the two
|
// 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
|
// answers part and this says so on the next draw - which is what a migration carrier
|
||||||
// is for.
|
// is for.
|
||||||
const Bool* assembled = MGPipeApplyAccess::Capabilities(gPipeInputs);
|
//
|
||||||
|
// The comparison reads the WORKING BLOCK, not PipeInputs::m_capability. Those two
|
||||||
|
// agree after any scatter - the derivation is what puts the block's answer there -
|
||||||
|
// but m_capability is written ONLY by the derivation, so comparing against it would
|
||||||
|
// make this trip wire depend on a bind_render_state or a set_dynamic_state having
|
||||||
|
// already been applied to this context. The residual block is emitted ONCE PER
|
||||||
|
// CONTEXT (D9) and may legitimately be the first call of all, at which point
|
||||||
|
// m_capability is still all-false while the block's own defaults have Dither and
|
||||||
|
// Multisample true - the trip wire would fire on a context that is perfectly correct.
|
||||||
|
// Asking DeriveCapability the same question the derivation asks removes that ordering
|
||||||
|
// contract without weakening the check by one bit.
|
||||||
|
const RenderStateParameters& working = MGPipeApplyAccess::RenderState(gPipeInputs);
|
||||||
for (SizeT i = 0; i < kCapabilityCount; ++i) {
|
for (SizeT i = 0; i < kCapabilityCount; ++i) {
|
||||||
const Bool carried = ((block.CapabilityBits >> i) & 1ull) != 0;
|
const Bool carried = ((block.CapabilityBits >> i) & 1ull) != 0;
|
||||||
if (carried == assembled[i]) continue;
|
const Bool assembledBit = MGPipeApplyAccess::DeriveCapability(working, static_cast<CapabilityInput>(i));
|
||||||
|
if (carried == assembledBit) continue;
|
||||||
#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY
|
#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY
|
||||||
MGLOG_F("MGPipe: Fatal{PipeResidualDiverged, \"%s\"} carried=%d assembled=%d",
|
MGLOG_F("MGPipe: Fatal{PipeResidualDiverged, \"%s\"} carried=%d assembled=%d",
|
||||||
kCapabilityNames[i], static_cast<int>(carried), static_cast<int>(assembled[i]));
|
kCapabilityNames[i], static_cast<int>(carried), static_cast<int>(assembledBit));
|
||||||
std::abort();
|
std::abort();
|
||||||
#else
|
#else
|
||||||
MGLOG_E("MGPipe: residual value block diverged on %s (carried=%d assembled=%d)",
|
MGLOG_E("MGPipe: residual value block diverged on %s (carried=%d assembled=%d)",
|
||||||
kCapabilityNames[i], static_cast<int>(carried), static_cast<int>(assembled[i]));
|
kCapabilityNames[i], static_cast<int>(carried), static_cast<int>(assembledBit));
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MGPipeDeriveRenderStateFields(PipeInputs& inputs) {
|
void MGPipeDeriveRenderStateFields(PipeInputs& inputs) {
|
||||||
// The derivation itself lives in MGPipeApplyAccess above, because that is the one
|
// The derivation itself lives in MGPipeApplyAccess above, because that is the one
|
||||||
// struct PipeInputs names as a friend - see D5 there for what it recomputes, which
|
// struct PipeInputs names as a friend - see D5 there for what it recomputes and which
|
||||||
// getter each line was transcribed from, and why the verify comparator is its guard.
|
// getter each line was transcribed from.
|
||||||
MGPipeApplyAccess::DeriveRenderStateFields(inputs);
|
MGPipeApplyAccess::DeriveRenderStateFields(inputs, kMGPipeAllGlobalChunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MGPipeDeriveRenderStateFieldsForChunks(PipeInputs& inputs, Uint32 globalChunkBits) {
|
||||||
|
MGPipeApplyAccess::DeriveRenderStateFields(inputs, globalChunkBits);
|
||||||
}
|
}
|
||||||
} // namespace MobileGL::MG_Pipe
|
} // namespace MobileGL::MG_Pipe
|
||||||
|
|||||||
Executable → Regular
+19
-5
@@ -97,11 +97,25 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// ---------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
// Recomputes every PipeInputs field that is a pure function of the working
|
// 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
|
// RenderStateParameters, instead of pulling it out of GLContext a second time.
|
||||||
// the applier after ANY scatter.
|
|
||||||
//
|
//
|
||||||
// The guard is the oracle P1 built: MOBILEGL_PIPE_VERIFY's compare-at-read re-reads each
|
// The oracle is the one P1 built: MOBILEGL_PIPE_VERIFY's compare-at-read re-reads each of
|
||||||
// of these from the live context at every backend read, so a transcription error is
|
// these from the live context at every backend read, so a transcription error is caught
|
||||||
// caught on the first draw that reads it.
|
// on the first draw that reads it - on the retrace and integration-verify LANES, which is
|
||||||
|
// where the comparator arms (MG_Config::Features.PipeVerify). A unit-test process never
|
||||||
|
// runs the config loader, so the unit oracle is a different one:
|
||||||
|
// RenderStateSpansTest.DerivationMatchesTheFrontendGetters walks every setter and
|
||||||
|
// compares all 29 derived values against the frontend getters they were transcribed from.
|
||||||
void MGPipeDeriveRenderStateFields(PipeInputs& inputs);
|
void MGPipeDeriveRenderStateFields(PipeInputs& inputs);
|
||||||
|
|
||||||
|
// The same derivation, SCOPED to the chunks a scatter actually moved (bit i is global
|
||||||
|
// chunk i - MGPipeGlobalChunkBitsOf{Pipeline,Dynamic}Mask widens a wire mask to it). This
|
||||||
|
// is what the applier calls, and it is why a per-frame glViewport - the D8 case whose
|
||||||
|
// whole point is that it sends dynamic chunk D0 alone - does not pay for the 8-wide blend
|
||||||
|
// loop, the 16-wide depth-range loop or the 35-arm capability switch. Every guard's chunk
|
||||||
|
// set is computed from the boundary table with MGPipeRenderStateChunkBitsCovering, so a
|
||||||
|
// boundary move cannot leave one stale, and
|
||||||
|
// RenderStateSpansTest.IncrementalChunksKeepEveryDerivedFieldInStep drives the scoped
|
||||||
|
// path against the frontend getters family by family.
|
||||||
|
void MGPipeDeriveRenderStateFieldsForChunks(PipeInputs& inputs, Uint32 globalChunkBits);
|
||||||
} // namespace MobileGL::MG_Pipe
|
} // namespace MobileGL::MG_Pipe
|
||||||
|
|||||||
Reference in New Issue
Block a user