[Feat] (Pipe): compute the per-verb dirty mask at the validate point and count how often each bit fires

- MGPipeFillForVerb becomes MGPipeValidateForVerb and MGP_FILL expands to the new name. The
  macro spelling, the 83 call sites and the verb enum do not change: the dispatch is
  kMGPipeVerbClass's nine classes, which is the same code as nine named ValidateFor* entry
  points with one call site per verb instead of nine (P2 brief D1, against ARCHITECTURE.md's
  eight - FillPoints.def argues in its own comment for splitting kProgramOp out, and the landed
  table is what runs).
- MG_Impl/Pipe/Tracker.h: MGPipeDirty's 18 bits, the widened Uint16 shutters, the per-verb walk
  and the per-bit-per-verb-class fire tallies. The widening happens in the TRACKER and MG_State
  is not changed for it; a wrap costs one extra re-push and never a missed one.
- NOTHING IS EMITTED YET. The mask is computed, latched and counted, and the full P1 residual
  fill runs after it unchanged. That is the point of this step: it says the walk is
  semantically free before any field stops being pulled, so a regression in the next commit
  cannot be blamed on the walk.
- Every shutter over-fires on purpose. Bits 2 and 3 are BYTE compares, not value compares,
  because a NaN patch level is a legal glPatchParameterfv value and has to equal itself; bits
  5..17 are composed with a mixing hash, which can in principle collide, and that is stated in
  the file and is acceptable only because nothing consumes those bits in P2.
- The shutter for bits 6..8 reads the current program's version counters WITHOUT
  GetProgramForDraw, so the walk never joins a pending link to answer "did the shader move".
- Header-only rather than Tracker.{h,cpp}: the root CMakeLists.txt that would have to name a
  new .cpp belongs to package A and is frozen behind the p2/contract tag. One translation unit
  in the library includes it, so inline costs nothing, and splitting it out is one list(APPEND)
  line whenever the ownership allows.
- MG_Test/ScopedPipeVerb.h and MG_Test/Pipe/PipeInputsTest.cpp follow the rename. Five comment
  references to the old name live in package A's files (MGPipe.h, the two generated .inc, and
  gen_pipe.py) and are deliberately left for their owner.
This commit is contained in:
2026-09-07 23:18:09 -04:00
parent de532f55a9
commit fcd4ad3799
5 changed files with 432 additions and 36 deletions
+14 -3
View File
@@ -16,6 +16,7 @@
#include <MG_State/GLState/BufferState/BufferState.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Impl/Pipe/PipeFill.h>
#include <MG_Impl/Pipe/Tracker.h>
#include <MG_Pipe/PipeMutation.h>
#include <Config.h>
@@ -595,8 +596,8 @@ namespace MobileGL::MG_Pipe {
MGPipeFillAccess::SetVerb(inputs, MGPipeVerb::kVerbCount);
}
// ---- the filler ----
void MGPipeFillForVerb(MGPipeVerb verb) {
// ---- the validate point (P2 brief D1) ----
void MGPipeValidateForVerb(MGPipeVerb verb) {
PipeInputs& inputs = gPipeInputs;
ParsePoisonOmissionKnob();
#if MOBILEGL_PIPE_VERIFY
@@ -620,7 +621,17 @@ namespace MobileGL::MG_Pipe {
auto* ctx = LiveContext();
MGPipeFillAccess::SetIdentity(inputs, ctx);
if (ctx == nullptr) return;
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(kMGPipeVerbClass[static_cast<SizeT>(verb)])];
const MGPipeVerbClass verbClass = kMGPipeVerbClass[static_cast<SizeT>(verb)];
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(verbClass)];
// ---- step 2: the dirty walk (P2 brief D1, D4) ----
// The mask is computed, latched and counted here and nothing is emitted from it
// yet: this commit is the safety net that says the walk is semantically free
// before any field stops being pulled. The emission steps land on top of it.
const Uint32 dirty = MGPipeTrackerInstance().Update(*ctx, verbClass);
(void)dirty;
// ---- step 4: the residual fill ----
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
const auto field = static_cast<MGPipeInputField>(i);
if (!MGPipeFieldMaskHas(mask, field)) continue;
+16 -6
View File
@@ -18,11 +18,21 @@
namespace MobileGL::MG_Pipe {
struct PipeInputs;
// PipeFill.cpp. Bumps the per-verb serial, records the verb and the context identity,
// and copies every field in the verb class's may-read mask (kMGPipeClassFieldMask) out
// of the live GLContext, stamping each with the new serial. In a verify build it then
// runs the entry compare against a second snapshot (P1 brief D8).
void MGPipeFillForVerb(MGPipeVerb verb);
// PipeFill.cpp. THE VALIDATE POINT (ARCHITECTURE.md 5.1, P2 brief D1). In order:
// 1. bump the per-verb serial, record the verb and the context identity;
// 2. run the tracker's DIRTY WALK for this verb's class (MG_Impl/Pipe/Tracker.h);
// 3. EMIT, for each set dirty bit whose subsystem bit is on in the runtime
// MOBILEGL_PIPE_PUSH bitmask, the P2 call that carries it;
// 4. run the P1 residual fill for every field an emitted call did NOT supply,
// stamping each with the new serial exactly as before;
// 5. in a verify build, the entry compare against a second snapshot (P1 brief D8) -
// which stops being a tautology the moment step 3 supplies a field step 4 skips.
//
// It was MGPipeFillForVerb through P1, when steps 2 and 3 did not exist. The macro
// spelling, the 83 call sites and the verb enum are unchanged: the dispatch is
// kMGPipeVerbClass's nine classes, which is the same code as nine named ValidateFor*
// entry points with one call site per verb instead of nine.
void MGPipeValidateForVerb(MGPipeVerb verb);
// Ends the verb in flight without starting another: bumps the serial, so every field the
// verb stamped goes stale, and puts the current verb back to "none", so a read made after
@@ -49,7 +59,7 @@ namespace MobileGL::MG_Pipe {
void SnapshotFromGLContext(PipeInputs& snapshot, const MGPipeFieldMask& mask);
#endif
} // namespace MobileGL::MG_Pipe
#define MGP_FILL(Verb) ::MobileGL::MG_Pipe::MGPipeFillForVerb(::MobileGL::MG_Pipe::MGPipeVerb::Verb)
#define MGP_FILL(Verb) ::MobileGL::MG_Pipe::MGPipeValidateForVerb(::MobileGL::MG_Pipe::MGPipeVerb::Verb)
#else
#define MGP_FILL(Verb) ((void)0)
#endif
+375
View File
@@ -0,0 +1,375 @@
// MobileGL - MobileGL/MG_Impl/Pipe/Tracker.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>
// The frontend state tracker (ARCHITECTURE.md 5.2, P2 brief D4).
//
// WHERE IT RUNS. Not above MGP_FILL and not in the GL setter: MGPipeValidateForVerb, the
// one statement MGP_FILL already expands to before every gBackendFunctionsTable.GL call
// (PipeFill.h). Blaze3D brackets every batch with glEnable/glDisable(GL_BLEND), so a
// setter that pushed would push twice per batch for a state the batch may not even read;
// the validate point coalesces the whole bracket into the two draws that observe it
// (ARCHITECTURE.md 5.1).
//
// WHAT IT DOES. One Uint32 dirty mask per verb, one bit per row of ARCHITECTURE.md 5.2,
// computed by comparing a shutter against what the tracker last pushed. P2 EMITS for bits
// 0..4 only (the value-class ones); bits 5..17 are computed, latched and counted so the
// per-bit fire rate is a measurement rather than a plan, and their fields keep going
// through the residual fill until P3a/P3b/P4a/P4b.
//
// WHY EVERY SHUTTER OVER-FIRES. A bit that fires too often costs one extra push. A bit
// that fires too rarely renders stale, and ARCHITECTURE.md 13.2 names that as the
// dangerous direction precisely because the P1 verify comparator cannot see it for
// object-class state (it compares those by identity only). So each shutter below is
// deliberately coarser than the state it guards - five bits share one buffer aggregate,
// the framebuffer bit fires on any attachment write anywhere - and the narrowing is P3's
// work, paid for with the fire rates this file publishes.
//
// NO TIMER LIVES HERE. ROADMAP.md forbids committing hot-path instrumentation; the
// absolute ns/draw comes from DriverBench, which times whole frames from outside the
// library (P2 brief D17). The only counting is the per-bit fire tally, behind
// PipeStats::Enabled() like every other counting site in the tree.
//
// HEADER-ONLY, and that is an ownership decision rather than a design one: the P2 brief
// asks for Tracker.{h,cpp}, but the root CMakeLists.txt that would have to name a new .cpp
// belongs to package A and is frozen behind the p2/contract tag. Everything here is
// included by exactly one translation unit in the library (MG_Impl/Pipe/PipeFill.cpp) plus
// the unit tests, so inline costs nothing. Splitting it back out is one list(APPEND) line.
#if MOBILEGL_PIPE_PUSH
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/MGPipeValueTypes.h>
#include <MG_State/GLState/Core.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <cstring>
namespace MobileGL::MG_Pipe {
// One bit per row of the ARCHITECTURE.md 5.2 table, hand-written rather than generated:
// the list is design, not derived data, and the generator has nothing to derive it from.
enum class MGPipeDirty : Uint32 {
// ---- value class: P2 emits for these five ----
NewRenderState = 0, // RenderState::m_version -> set_dynamic_state
NewPipelineState, // RenderState::m_pipelineStateVersion -> create/bind_render_state
NewPixelPack, // PixelStoreParameters (pack) -> set_pixel_pack_state
NewPatchState, // the patch trio, NaN legal -> set_patch_state
NewVertexAttribDefaults, // glVertexAttrib* defaults -> set_vertex_attrib_defaults
// ---- value class: computed and counted, emitted from P3a on ----
NewVertexElements, // the bound VAO's attribute configuration
NewShader, // the current program's link version
NewShaderBindings, // image units, block bindings, uniform write set
NewGlobalConstants, // the default-uniform-block image
// ---- object class: computed and counted, emitted from P3b/P4b on ----
NewVertexBuffers,
NewIndexBuffer,
NewFramebuffer,
NewSamplerViews,
NewSamplers,
NewShaderImages,
NewConstBuffers,
NewShaderBuffers,
NewSoTargets,
Count,
};
inline constexpr SizeT kMGPipeDirtyCount = static_cast<SizeT>(MGPipeDirty::Count);
static_assert(kMGPipeDirtyCount <= 32, "the dirty mask is a Uint32");
inline constexpr Uint32 MGPipeDirtyBit(MGPipeDirty bit) {
return Uint32{1} << static_cast<Uint32>(bit);
}
// The five P2 emits for.
inline constexpr Uint32 kMGPipeDirtyEmittedAtP2 =
MGPipeDirtyBit(MGPipeDirty::NewRenderState) | MGPipeDirtyBit(MGPipeDirty::NewPipelineState) |
MGPipeDirtyBit(MGPipeDirty::NewPixelPack) | MGPipeDirtyBit(MGPipeDirty::NewPatchState) |
MGPipeDirtyBit(MGPipeDirty::NewVertexAttribDefaults);
inline constexpr const char* kMGPipeDirtyNames[kMGPipeDirtyCount] = {
"NEW_RENDER_STATE",
"NEW_PIPELINE_STATE",
"NEW_PIXEL_PACK",
"NEW_PATCH_STATE",
"NEW_VERTEX_ATTRIB_DEFAULTS",
"NEW_VERTEX_ELEMENTS",
"NEW_SHADER",
"NEW_SHADER_BINDINGS",
"NEW_GLOBAL_CONSTANTS",
"NEW_VERTEX_BUFFERS",
"NEW_INDEX_BUFFER",
"NEW_FRAMEBUFFER",
"NEW_SAMPLER_VIEWS",
"NEW_SAMPLERS",
"NEW_SHADER_IMAGES",
"NEW_CONST_BUFFERS",
"NEW_SHADER_BUFFERS",
"NEW_SO_TARGETS",
};
// Which runtime MOBILEGL_PIPE_PUSH subsystem bit gates a dirty bit's emission. Zero for
// a bit P2 does not emit, which is what makes "the bitmask is a true per-subsystem A/B"
// literally true rather than approximately.
inline constexpr Uint64 MGPipeSubsystemForDirty(MGPipeDirty bit) {
switch (bit) {
case MGPipeDirty::NewRenderState:
case MGPipeDirty::NewPipelineState:
return kMGPipeSubsystemRenderState;
case MGPipeDirty::NewPixelPack:
return kMGPipeSubsystemPixelPack;
case MGPipeDirty::NewPatchState:
return kMGPipeSubsystemPatchState;
case MGPipeDirty::NewVertexAttribDefaults:
return kMGPipeSubsystemVertexAttribDefaults;
default:
// Bits 5..17 have no call of their own until P3a/P3b/P4a/P4b, so there is no
// subsystem to switch and the residual fill keeps supplying their fields.
return 0;
}
}
// A COMPOSITE shutter, for the bits whose "did anything move" is more than one counter.
// It is a hash, so two different states can in principle collide and cost a MISSED fire.
// That is acceptable for bits 5..17 and only for them: nothing consumes those bits in
// P2, and P3 replaces each with its own exact shutter as it takes the subsystem over.
// The five bits P2 EMITS for are never composed - they are widened counters and byte
// compares, neither of which can collide.
inline constexpr Uint64 MGPipeMixShutter(Uint64 accumulator, Uint64 value) {
accumulator ^= value + 0x9e3779b97f4a7c15ull + (accumulator << 6) + (accumulator >> 2);
return accumulator;
}
// A Uint16 counter widened at the TRACKER boundary, never in MG_State
// (ARCHITECTURE.md 5.2: MG_State is not changed for this). A decrease is a wrap and adds
// 65536. A wrap is harmless locally - one extra re-push, never a missed one - which is
// exactly what TrackerTest.WrapAroundRePushesButNeverMisses pins.
class MGPipeWidenedCounter {
public:
Uint64 Observe(Uint16 now) {
if (m_started && now < m_last) m_high += 0x10000ull;
m_started = true;
m_last = now;
return m_high + now;
}
void Reset() {
m_high = 0;
m_last = 0;
m_started = false;
}
private:
Uint64 m_high = 0;
Uint16 m_last = 0;
Bool m_started = false;
};
class MGPipeTracker {
public:
using GLContext = MG_State::GLState::GLContext;
// The dirty walk. Compares every shutter against what was last pushed, LATCHES the
// new values, counts the fires per verb class, and returns the mask. Latching here
// rather than after emission is deliberate: a bit whose subsystem is switched off is
// not emitted, but its fields are then still pulled by the residual fill, so the
// pushed block is correct either way and a bit can never fire twice for one change.
Uint32 Update(GLContext& ctx, MGPipeVerbClass verbClass) {
// A different context is a different server: nothing the tracker latched about
// the old one says anything about this one, and the first walk on a fresh
// context must publish a COMPLETE state rather than an increment.
if (m_context != &ctx) {
Reset();
m_context = &ctx;
}
Uint64 now[kMGPipeDirtyCount];
const RenderStateParameters& render = ctx.GetRenderStateParameters();
// ---- bits 0..1: the two Uint16 render-state counters, widened HERE ----
now[Index(MGPipeDirty::NewRenderState)] =
m_renderStateVersion.Observe(static_cast<Uint16>(ctx.GetRenderStateParametersVersion()));
now[Index(MGPipeDirty::NewPipelineState)] =
m_pipelineStateVersion.Observe(static_cast<Uint16>(ctx.GetPipelineStateVersion()));
// ---- bit 4 and the value-class bits 5..8 ----
now[Index(MGPipeDirty::NewVertexAttribDefaults)] = ctx.GetAnyVertexAttribDefaultGeneration();
const auto& vao = ctx.GetBoundVertexArray();
const Uint64 vaoIdentity =
vao ? MGPipeMixShutter(vao->GetLifetimeId(), vao->GetConfigVersion()) : 0;
now[Index(MGPipeDirty::NewVertexElements)] = vaoIdentity;
// Deliberately NOT GetProgramForDraw: that joins a pending link, and the tracker
// must not force a compile just to answer "did the shader move". These version
// counters are plain members and are exactly what the backends already read
// without joining (Core.cpp, the glUseProgram half of join site J1).
const auto& program = ctx.GetCurrentProgram();
Uint64 shader = 0;
Uint64 bindings = 0;
Uint64 constants = 0;
Uint64 programImages = 0;
if (program) {
shader = MGPipeMixShutter(program->GetLifetimeId(), program->GetLinkVersion());
bindings = MGPipeMixShutter(
MGPipeMixShutter(MGPipeMixShutter(program->GetImageUnitVersion(),
program->GetBackendStateVersion()),
program->GetBlockBindingVersion()),
program->GetUniformWriteSetVersion());
constants = MGPipeMixShutter(program->GetLifetimeId(), program->GetUBOContentVersion());
programImages = program->GetImageUnitVersion();
}
now[Index(MGPipeDirty::NewShader)] = shader;
now[Index(MGPipeDirty::NewShaderBindings)] = bindings;
now[Index(MGPipeDirty::NewGlobalConstants)] = constants;
// ---- the object-class bits 9..17 ----
const Uint64 textureContent = ctx.GetAnyTextureContentGeneration();
const Uint64 textureParams = ctx.GetAnyTextureParamsGeneration();
const Uint64 buffers = ctx.GetAnyBufferChangeGeneration();
now[Index(MGPipeDirty::NewVertexBuffers)] =
MGPipeMixShutter(ctx.GetAnyVaoAttributeGeneration(), vaoIdentity);
// The index buffer lives in the bound VAO's element slot and P2 has no cheap
// shutter for that slot alone, so it shares the buffer aggregate and over-fires
// on any buffer write anywhere. P3b narrows it when it takes the subsystem over.
now[Index(MGPipeDirty::NewIndexBuffer)] = MGPipeMixShutter(buffers, vaoIdentity);
now[Index(MGPipeDirty::NewFramebuffer)] = MGPipeMixShutter(
ctx.GetAnyFramebufferAttachmentGeneration(),
m_framebufferBind.Observe(
ctx.GetFramebufferBindingSlot(FramebufferTarget::Draw).GetVersion()));
now[Index(MGPipeDirty::NewSamplerViews)] =
MGPipeMixShutter(textureContent, ctx.GetTextureBindGeneration());
now[Index(MGPipeDirty::NewSamplers)] =
MGPipeMixShutter(textureParams, ctx.GetSamplingResolutionGeneration());
now[Index(MGPipeDirty::NewShaderImages)] =
MGPipeMixShutter(MGPipeMixShutter(textureContent, textureParams), programImages);
now[Index(MGPipeDirty::NewConstBuffers)] = buffers;
now[Index(MGPipeDirty::NewShaderBuffers)] = buffers;
now[Index(MGPipeDirty::NewSoTargets)] =
MGPipeMixShutter(buffers, ctx.GetTransformFeedbackGeneration());
Uint32 dirty = 0;
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
// Bits 2 and 3 are handled below: they are BitwiseEqual shutters, not
// counters, so they have no entry in `now`.
if (i == Index(MGPipeDirty::NewPixelPack) || i == Index(MGPipeDirty::NewPatchState)) {
continue;
}
if (!m_primed || now[i] != m_lastPushed[i]) dirty |= Uint32{1} << static_cast<Uint32>(i);
m_lastPushed[i] = now[i];
}
// ---- bit 2: the PACK half of the pixel store, BitwiseEqual ----
const PixelStoreParameters pack = ctx.GetPixelStoreParameters(false);
if (!m_primed || std::memcmp(&pack, &m_pack, sizeof(pack)) != 0) {
dirty |= MGPipeDirtyBit(MGPipeDirty::NewPixelPack);
m_pack = pack;
}
// ---- bit 3: the patch trio, BitwiseEqual, and NaN IS LEGAL ----
// A NaN outer level is a legal glPatchParameterfv value and must compare equal to
// itself (ARCHITECTURE.md 5.2). Float equality says it is not; memcmp says it is,
// which is the whole reason this is a byte compare.
PatchTrio patch{};
patch.PatchVertices = render.PatchVertices;
for (SizeT i = 0; i < 4; ++i) patch.Outer[i] = render.PatchDefaultOuterLevel[i];
for (SizeT i = 0; i < 2; ++i) patch.Inner[i] = render.PatchDefaultInnerLevel[i];
if (!m_primed || std::memcmp(&patch, &m_patch, sizeof(patch)) != 0) {
dirty |= MGPipeDirtyBit(MGPipeDirty::NewPatchState);
m_patch = patch;
}
m_primed = true;
m_lastDirty = dirty;
if (MG_Util::PipeStats::Enabled()) {
const SizeT cls = static_cast<SizeT>(verbClass);
++m_walks[cls];
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
if (dirty & (Uint32{1} << static_cast<Uint32>(i))) ++m_fires[i][cls];
}
}
return dirty;
}
// Context teardown, server reset, a unit test's fixture. The next Update returns
// every bit set, which is what makes the first verb on a fresh context publish a
// complete state rather than an increment. Deliberately does NOT clear the fire
// tallies: they are a per-run measurement, not per-context state.
void Reset() {
std::memset(m_lastPushed, 0, sizeof(m_lastPushed));
m_renderStateVersion.Reset();
m_pipelineStateVersion.Reset();
m_framebufferBind.Reset();
m_pack = PixelStoreParameters{};
m_patch = PatchTrio{};
m_context = nullptr;
m_lastDirty = 0;
m_primed = false;
}
void ResetCounters() {
std::memset(m_fires, 0, sizeof(m_fires));
std::memset(m_walks, 0, sizeof(m_walks));
}
Uint64 FireCount(MGPipeDirty bit, MGPipeVerbClass verbClass) const {
return m_fires[Index(bit)][static_cast<SizeT>(verbClass)];
}
Uint64 FireCount(MGPipeDirty bit) const {
Uint64 total = 0;
for (SizeT i = 0; i < kMGPipeVerbClassCount; ++i) total += m_fires[Index(bit)][i];
return total;
}
Uint64 WalkCount(MGPipeVerbClass verbClass) const {
return m_walks[static_cast<SizeT>(verbClass)];
}
Uint64 WalkCount() const {
Uint64 total = 0;
for (SizeT i = 0; i < kMGPipeVerbClassCount; ++i) total += m_walks[i];
return total;
}
Uint32 LastDirty() const { return m_lastDirty; }
Bool Primed() const { return m_primed; }
private:
static constexpr SizeT Index(MGPipeDirty bit) { return static_cast<SizeT>(bit); }
struct PatchTrio {
Uint PatchVertices;
Float Outer[4];
Float Inner[2];
};
Uint64 m_lastPushed[kMGPipeDirtyCount]{};
MGPipeWidenedCounter m_renderStateVersion;
MGPipeWidenedCounter m_pipelineStateVersion;
// The draw framebuffer BINDING slot version, widened for the same reason: a Uint16
// that wrapped would let a composite shutter repeat and cost a missed fire.
MGPipeWidenedCounter m_framebufferBind;
// Bits 2 and 3 are BitwiseEqual shutters, not counters.
PixelStoreParameters m_pack{};
PatchTrio m_patch{};
const void* m_context = nullptr;
Uint32 m_lastDirty = 0;
Bool m_primed = false;
Uint64 m_fires[kMGPipeDirtyCount][kMGPipeVerbClassCount]{};
Uint64 m_walks[kMGPipeVerbClassCount]{};
};
// The monolith's one tracker. Under split there is one per client context; the context
// identity check inside Update is what makes the single instance safe today.
inline MGPipeTracker& MGPipeTrackerInstance() {
static MGPipeTracker tracker;
return tracker;
}
} // namespace MobileGL::MG_Pipe
#endif // MOBILEGL_PIPE_PUSH
+25 -25
View File
@@ -190,12 +190,12 @@ TEST_F(PipeInputsTest, OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) {
#if !MOBILEGL_PIPE_POISON
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
#else
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
EXPECT_TRUE(Fresh(MGPipeInputField::GetActiveTextureUnit));
EXPECT_TRUE(Fresh(MGPipeInputField::GetTextureUnitObject));
MGPipeSetPoisonOmission("GenerateMipmap", "GetActiveTextureUnit");
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
EXPECT_TRUE(Fresh(MGPipeInputField::GetTextureUnitObject));
EXPECT_FALSE(Fresh(MGPipeInputField::GetActiveTextureUnit));
// The value was still copied: only the stamp is withheld.
@@ -212,7 +212,7 @@ TEST_F(PipeInputsTest, OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) {
}
}
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
EXPECT_FALSE(Fresh(MGPipeInputField::GetActiveTextureUnit));
EXPECT_TRUE(Fresh(MGPipeInputField::GetBoundVertexArray));
EXPECT_TRUE(Fresh(MGPipeInputField::GetRenderStateParameters));
@@ -220,7 +220,7 @@ TEST_F(PipeInputsTest, OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) {
EXPECT_TRUE(Fresh(MGPipeInputField::RecordError));
// And the omission is scoped to its verb: a different verb of the same class keeps it.
MGPipeFillForVerb(MGPipeVerb::BindImageTexture);
MGPipeValidateForVerb(MGPipeVerb::BindImageTexture);
EXPECT_TRUE(Fresh(MGPipeInputField::GetActiveTextureUnit));
#endif
}
@@ -237,9 +237,9 @@ TEST_F(PipeInputsTest, ReadingAnOmittedFieldAbortsNamingTheVerb) {
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
const ChildResult r = RunInChild([] {
MGPipeSetPoisonOmission("GenerateMipmap", "GetActiveTextureUnit");
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
(void)gPipeInputs.GetRenderStateParameters(); // a filled field of the preceding draw: must not abort
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
(void)gPipeInputs.GetTextureUnitObject(0); // the sibling field: filled, must not abort
(void)gPipeInputs.GetActiveTextureUnit(); // the omitted field: Fatal
::_exit(3); // reached only if the poison failed
@@ -258,9 +258,9 @@ TEST_F(PipeInputsTest, ReadingAFilledFieldCompletes) {
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
const ChildResult r = RunInChild([] {
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
(void)gPipeInputs.GetRenderStateParameters();
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
(void)gPipeInputs.GetTextureUnitObject(0);
(void)gPipeInputs.GetActiveTextureUnit();
});
@@ -309,10 +309,10 @@ TEST_F(PipeInputsTest, PoisonOmitKnobArmsTheOmission) {
GTEST_SKIP() << "no fork() on this platform";
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
MGPipeFillForVerb(MGPipeVerb::DrawArrays); // the parent's parse saw an empty knob
MGPipeValidateForVerb(MGPipeVerb::DrawArrays); // the parent's parse saw an empty knob
const ChildResult r = RunInChild([] {
MG_Config::Features.PipePoisonOmit = kOmissionKnob;
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
(void)gPipeInputs.GetTextureUnitObject(0); // the sibling field: filled, must not abort
(void)gPipeInputs.GetActiveTextureUnit(); // the omitted field: Fatal
::_exit(3);
@@ -334,7 +334,7 @@ TEST_F(PipeInputsTest, BadPoisonOmitKnobIsFatalNamingTheKnob) {
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
const ChildResult r = RunInChild([] {
MG_Config::Features.PipePoisonOmit = "NoSuchVerb:GetActiveTextureUnit";
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
::_exit(3);
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
@@ -354,7 +354,7 @@ TEST_F(PipeInputsTest, CorruptedSnapshotFieldIsNamedWithItsSerial) {
GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)";
#else
const Uint64 serialBefore = gPipeInputs.FilledState().CurrentVerbSerial;
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial;
EXPECT_EQ(serial, serialBefore + 1);
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(MGPipeVerbClass::kDraw)];
@@ -392,11 +392,11 @@ TEST_F(PipeInputsTest, MutatedFieldIsNamedAtRead) {
GTEST_SKIP() << "no fork() on this platform";
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
MGPipeFillForVerb(MGPipeVerb::Clear); // the parent armed nothing: Features.PipeVerify is false here
MGPipeValidateForVerb(MGPipeVerb::Clear); // the parent armed nothing: Features.PipeVerify is false here
const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial + 1; // the child's DrawArrays fill
const ChildResult r = RunInChild([] {
MG_Config::Features.PipeVerify = true;
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
const Float boundary = gPipeInputs.GetLineWidth(); // boundary == live: completes
(void)gPipeInputs.GetRenderStateParameters();
MG_State::pGLContext->SetLineWidth(boundary + 1.0f);
@@ -426,12 +426,12 @@ TEST_F(PipeInputsTest, VerifyCorruptKnobNamesTheFieldAtEntry) {
GTEST_SKIP() << "no fork() on this platform";
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
MGPipeFillForVerb(MGPipeVerb::Clear);
MGPipeValidateForVerb(MGPipeVerb::Clear);
const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial + 1;
const ChildResult r = RunInChild([] {
MG_Config::Features.PipeVerify = true;
MG_Config::Features.PipeVerifyCorrupt = "GetRenderStateParameters";
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
::_exit(3);
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
@@ -455,7 +455,7 @@ TEST_F(PipeInputsTest, BadVerifyCorruptKnobIsFatalNamingTheKnob) {
const ChildResult r = RunInChild([] {
MG_Config::Features.PipeVerify = true;
MG_Config::Features.PipeVerifyCorrupt = "NoSuchField";
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
::_exit(3);
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
@@ -479,14 +479,14 @@ TEST_F(PipeInputsTest, VerifyFatalOffLogsTheDivergenceAndContinues) {
GTEST_SKIP() << "no fork() on this platform";
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
MGPipeFillForVerb(MGPipeVerb::Clear);
MGPipeValidateForVerb(MGPipeVerb::Clear);
const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial + 1;
const ChildResult r = RunInChild([] {
MG_Config::Features.PipeVerify = true;
MG_Config::Features.PipeVerifyFatal = false;
MG_Config::Features.PipeVerifyCorrupt = "GetRenderStateParameters";
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
MGPipeFillForVerb(MGPipeVerb::DrawElements);
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
MGPipeValidateForVerb(MGPipeVerb::DrawElements);
std::exit(0);
});
ASSERT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log;
@@ -511,7 +511,7 @@ TEST_F(PipeInputsTest, EveryVerbFillsItsClassAndNothingElse) {
#else
for (SizeT v = 0; v < kMGPipeVerbCount; ++v) {
const auto verb = static_cast<MGPipeVerb>(v);
MGPipeFillForVerb(verb);
MGPipeValidateForVerb(verb);
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(kMGPipeVerbClass[v])];
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) {
const auto field = static_cast<MGPipeInputField>(f);
@@ -532,7 +532,7 @@ TEST_F(PipeInputsTest, EveryVerbFillsItsClassAndNothingElse) {
// Without the notice the two reads below differ and the pushed value is the stale one.
TEST_F(PipeInputsTest, AFrontendMutationInsideAVerbRefreshesThePushedField) {
auto& ctx = *MG_State::pGLContext;
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
ASSERT_EQ(gPipeInputs.GetSamplingResolutionGeneration(), ctx.GetSamplingResolutionGeneration());
ASSERT_EQ(gPipeInputs.GetTextureBindGeneration(), ctx.GetTextureBindGeneration());
ASSERT_EQ(gPipeInputs.GetMaxTouchedTextureUnit(), ctx.GetMaxTouchedTextureUnit());
@@ -566,7 +566,7 @@ TEST_F(PipeInputsTest, TheMutationNoticeRefreshesTheValueButNotTheStamp) {
#else
auto& ctx = *MG_State::pGLContext;
MGPipeSetPoisonOmission("DrawArrays", "GetSamplingResolutionGeneration");
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
ASSERT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration));
ctx.BumpSamplingResolutionGeneration();
EXPECT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration))
@@ -575,7 +575,7 @@ TEST_F(PipeInputsTest, TheMutationNoticeRefreshesTheValueButNotTheStamp) {
// FenceSync is a kQuery verb: its mask holds no texture field at all, so the notice must
// leave the generation unfilled and a read of it Fatal{UnmigratedPipeInput}.
MGPipeSetPoisonOmission(nullptr, nullptr);
MGPipeFillForVerb(MGPipeVerb::FenceSync);
MGPipeValidateForVerb(MGPipeVerb::FenceSync);
ASSERT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration));
ctx.BumpSamplingResolutionGeneration();
EXPECT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration))
@@ -598,7 +598,7 @@ TEST_F(PipeInputsTest, AFrontendMutationInsideAVerbDoesNotDivergeAtRead) {
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
const ChildResult r = RunInChild([] {
MG_Config::Features.PipeVerify = true;
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
(void)gPipeInputs.GetSamplingResolutionGeneration(); // boundary == live: completes
auto& ctx = *MG_State::pGLContext;
const Uint64 before = ctx.GetSamplingResolutionGeneration();
+2 -2
View File
@@ -45,7 +45,7 @@ namespace MobileGL::MG_Test {
explicit ScopedPipeVerb([[maybe_unused]] MG_Pipe::MGPipeVerb verb)
#if MOBILEGL_PIPE_PUSH
: m_verb(verb) {
MG_Pipe::MGPipeFillForVerb(m_verb);
MG_Pipe::MGPipeValidateForVerb(m_verb);
}
#else
{
@@ -60,7 +60,7 @@ namespace MobileGL::MG_Test {
// entry point's MGP_FILL would.
void Renew() {
#if MOBILEGL_PIPE_PUSH
MG_Pipe::MGPipeFillForVerb(m_verb);
MG_Pipe::MGPipeValidateForVerb(m_verb);
#endif
}