[Test] (Pipe): pin the tracker's shutters and the CSO cache's content addressing, including the collision the memcmp exists to stop

- TrackerWalk drives the tracker and the cache DIRECTLY rather than through
  MGPipeValidateForVerb: the validate point reaches the library's one process-wide tracker, and
  a unit test that asserts on a shared singleton fails the moment ctest runs the suite in
  parallel. The three lines of emission logic it reproduces are the same three lines.
- BlendToggleReusesTwoCsos is the Blaze3D shape the whole "push at validate, not in the
  setter" decision was made for: 32 enable/draw/disable/draw pairs mint exactly TWO CSOs, bind
  64 times and hit 62. A per-setter design would show up here as 64 mints.
- ViewportDoesNotMintACso is the regression RenderState.h records: 16 glViewports mint nothing,
  never move the pipeline version, and each sends exactly chunk D0 - not the other seven.
- WrapAroundRePushesButNeverMisses drives m_version past 65535 and asserts every one of 70000
  changes fired. The alternating value deliberately never touches the default: a setter that
  early-outs would otherwise make the first iteration a false miss and hide a real one.
- AggregateGenerationCatchesABoundTextureMoving is the first test of the direction the P1
  verify comparator cannot see - it compares object-class fields by identity only, so a bound
  texture whose content moved looks unchanged to it. The bit fires and then settles, so it is a
  shutter and not a stuck flag.
- ANaNPatchLevelEqualsItselfAndDoesNotFireForever: a NaN outer level is a legal
  glPatchParameterfv value, float equality says it differs from itself and a byte compare says
  it does not. That is why the shutter is a memcmp.
- ThePixelPackShutterIsAByteCompareOfThePackHalfOnly asserts an UNPACK write does not move the
  pack shutter, which is the half that deliberately has no carrier.
- HashCollisionDoesNotAliasTwoStates needed a seam and got one: MGPipeCsoCache::s_hashForTest,
  null in every real build, one never-taken branch on a path that runs only when the pipeline
  version moved. Without it the memcmp confirm is unreachable code that nothing can prove is
  doing anything, and what it stops - two different render states on one CSO - is silent wrong
  pixels with no gate that can see it.
- ContentAddressingOffMintsEveryTime pins that bit 63 really changes mint/reuse behaviour, so
  the negative control cannot rot into a dead switch.
- The set-hash suppressor is exercised on all seven slots even though P2 wires one, including
  the reserved-zero contract: a computed hash of 0 is remapped to 1 so it is never confused
  with "never emitted".
This commit is contained in:
2026-09-07 23:18:09 -04:00
parent 3302ee82b5
commit 43bf97cc87
3 changed files with 399 additions and 13 deletions
+11 -1
View File
@@ -72,7 +72,8 @@ namespace MobileGL::MG_Pipe {
const Bool contentAddressed =
(MG_Config::Features.PipePush & kMGPipeBehaviourNoCsoContentAddressing) == 0;
if (contentAddressed) {
const Uint64 hash = MGPipeHashPipelineBytes(bytes.data());
const Uint64 hash = s_hashForTest != nullptr ? s_hashForTest(bytes.data())
: MGPipeHashPipelineBytes(bytes.data());
for (SizeT i = 0; i < m_entries.size(); ++i) {
if (m_entries[i].Hash != hash) continue;
if (std::memcmp(m_entries[i].Bytes.data(), bytes.data(), bytes.size()) != 0) {
@@ -110,6 +111,15 @@ namespace MobileGL::MG_Pipe {
SizeT Size() const { return m_entries.size(); }
const Counters& GetCounters() const { return m_counters; }
// TEST SEAM, and it is here because the thing it tests cannot be reached any other
// way. A 64-bit collision between two DIFFERENT render states is silent wrong pixels
// and it is exactly what the memcmp confirm above exists to stop, so
// CsoCacheTest.HashCollisionDoesNotAliasTwoStates has to be able to make one happen.
// Null in every real build - one never-taken, perfectly-predicted branch on a path
// that runs only when the pipeline version moved, i.e. never in the steady state.
using HashForTestFn = Uint64 (*)(const void* pipelineBytes);
inline static HashForTestFn s_hashForTest = nullptr;
private:
struct Entry {
Uint64 Hash = 0;
+162 -12
View File
@@ -6,27 +6,177 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The 64-entry render-state CSO cache: hash, probe, memcmp, LRU evict, and the content-addressing-off control (P2 brief D7).
// The render-state CSO cache (P2 brief D7). Owned by P2 package B (p2/tracker); the file and
// its CMake registration are the contract commit's.
//
// STUB, and deliberately one. It is created by the P2 CONTRACT commit together with its
// CMakeLists.txt registration, so that the package which owns its CONTENTS
// (P2 package B, p2/tracker) never has to touch MG_Test/Pipe/CMakeLists.txt - no two P2
// packages edit the same file, which is what keeps the integrator's rebases clean.
//
// The placeholder case is not decoration: without it the binary has no test, and
// gtest_discover_tests on a binary with no test is a silently green lane.
// Needs the push sources, so every case is a visible SKIP in a pull build rather than a
// vanishing test.
#include <gtest/gtest.h>
#include "Includes.h"
#include <MG_Pipe/MGPipe.h>
#if MOBILEGL_PIPE_PUSH
#include <Config.h>
#include <MG_Impl/Pipe/CsoCache.h>
#include <MG_Impl/Pipe/SetHashSuppressor.h>
#include <MG_Pipe/MGPipeRenderStateSpans.h>
#include <MG_Pipe/PipeApply.h>
#endif
using namespace MobileGL;
using namespace MobileGL::MG_Pipe;
namespace {
// The cache does not exist yet; the behaviour bit it is measured against does, and it
// is deliberately the TOP bit so no subsystem allocation can ever collide with it.
TEST(CsoCache, PlaceholderUntilTheOwningPackageFillsThisIn) {
EXPECT_EQ(kMGPipeBehaviourNoCsoContentAddressing, 1ull << 63);
#if !MOBILEGL_PIPE_PUSH
TEST(CsoCache, SkippedInAPullBuild) {
GTEST_SKIP() << "the CSO cache is compiled only under MOBILEGL_PIPE_PUSH";
}
#else
class CsoCacheTest : public ::testing::Test {
protected:
void SetUp() override {
m_savedPush = MG_Config::Features.PipePush;
MGPipeCsoCache::s_hashForTest = nullptr;
MGPipeApplierReset();
}
void TearDown() override {
MG_Config::Features.PipePush = m_savedPush;
MGPipeCsoCache::s_hashForTest = nullptr;
MGPipeApplierReset();
}
// A render state that differs from every other `seed` in a PIPELINE byte, so each one
// is a genuinely different CSO. SampleMaskValue is in pipeline chunk P2.
static RenderStateParameters PipelineState(Uint32 seed) {
RenderStateParameters params{};
params.SampleMaskValue = seed;
return params;
}
Uint64 m_savedPush = 0;
};
TEST_F(CsoCacheTest, TheSameStateIsMintedOnceAndReusedForever) {
MGPipeCsoCache cache;
Uint64 bytes = 0;
const RenderStateParameters params = PipelineState(7);
const MGPipeHandle first = cache.Acquire(params, bytes);
for (int i = 0; i < 16; ++i) EXPECT_TRUE(cache.Acquire(params, bytes) == first);
EXPECT_EQ(cache.GetCounters().Mints, 1u);
EXPECT_EQ(cache.GetCounters().Hits, 16u);
EXPECT_EQ(cache.Size(), 1u);
cache.Reset();
}
TEST_F(CsoCacheTest, LruEvictsTheOldestAndEmitsDelete) {
MGPipeCsoCache cache;
Uint64 bytes = 0;
Vector<MGPipeHandle> handles;
for (Uint32 i = 0; i < kMGPipeCsoCacheCapacity; ++i) {
handles.push_back(cache.Acquire(PipelineState(i), bytes));
}
EXPECT_EQ(cache.Size(), kMGPipeCsoCacheCapacity);
EXPECT_EQ(cache.GetCounters().Evictions, 0u);
// Touch entry 0 so it is no longer the oldest; entry 1 becomes the victim.
EXPECT_TRUE(cache.Acquire(PipelineState(0), bytes) == handles[0]);
const MGPipeHandle overflow = cache.Acquire(PipelineState(kMGPipeCsoCacheCapacity), bytes);
EXPECT_EQ(cache.Size(), kMGPipeCsoCacheCapacity);
EXPECT_EQ(cache.GetCounters().Evictions, 1u);
EXPECT_EQ(cache.GetCounters().Mints, kMGPipeCsoCacheCapacity + 1);
EXPECT_FALSE(overflow == handles[1]);
// The one that was touched survived; the evicted one has to be minted again.
EXPECT_TRUE(cache.Acquire(PipelineState(0), bytes) == handles[0]);
const MGPipeHandle reborn = cache.Acquire(PipelineState(1), bytes);
EXPECT_FALSE(reborn == handles[1]);
EXPECT_EQ(cache.GetCounters().Evictions, 2u);
cache.Reset();
}
// A 64-bit hash collision between two different render states would alias them onto one
// CSO, which is silent wrong pixels with no gate that can see it. The memcmp confirm is
// what stops it, and this is what proves the memcmp is doing something.
TEST_F(CsoCacheTest, HashCollisionDoesNotAliasTwoStates) {
MGPipeCsoCache::s_hashForTest = [](const void*) -> Uint64 { return 0x1234'5678'9abc'def0ull; };
MGPipeCsoCache cache;
Uint64 bytes = 0;
const MGPipeHandle a = cache.Acquire(PipelineState(1), bytes);
const MGPipeHandle b = cache.Acquire(PipelineState(2), bytes);
EXPECT_FALSE(a == b) << "two different render states were aliased onto one CSO";
EXPECT_EQ(cache.GetCounters().Collisions, 1u);
EXPECT_EQ(cache.GetCounters().Mints, 2u);
EXPECT_EQ(cache.GetCounters().Hits, 0u);
cache.Reset();
}
// The negative control the whole CSO design is measured against (ROADMAP.md P2). It turns
// off the PROBE and the handle reuse, not the records - otherwise it would measure a
// different design rather than this one without content addressing.
TEST_F(CsoCacheTest, ContentAddressingOffMintsEveryTime) {
MG_Config::Features.PipePush |= kMGPipeBehaviourNoCsoContentAddressing;
MGPipeCsoCache cache;
Uint64 bytes = 0;
const RenderStateParameters params = PipelineState(3);
const MGPipeHandle first = cache.Acquire(params, bytes);
const MGPipeHandle second = cache.Acquire(params, bytes);
const MGPipeHandle third = cache.Acquire(params, bytes);
EXPECT_FALSE(first == second);
EXPECT_FALSE(second == third);
EXPECT_EQ(cache.GetCounters().Mints, 3u);
EXPECT_EQ(cache.GetCounters().Hits, 0u);
cache.Reset();
}
TEST_F(CsoCacheTest, EveryAcquireCountsItsPayloadBytes) {
MGPipeCsoCache cache;
Uint64 bytes = 0;
cache.Acquire(PipelineState(11), bytes);
// A mint puts the descriptor and the whole pipeline half on the wire.
EXPECT_EQ(bytes, sizeof(MGPRenderStateDesc) + kMGPipePipelineChunkBytes);
const Uint64 afterMint = bytes;
cache.Acquire(PipelineState(11), bytes);
// A hit puts NOTHING on the wire: the 12-byte bind is the caller's, not the cache's.
EXPECT_EQ(bytes, afterMint);
cache.Reset();
}
// ---- the set-hash suppressor (D11) ----
TEST(SetHashSuppressorTest, TheFirstEmissionAlwaysGoesOutOnEverySlot) {
MGPipeSetHashSuppressor suppressor;
for (SizeT i = 0; i < kMGPipeSuppressorSlotCount; ++i) {
const auto slot = static_cast<MGPipeSuppressorSlot>(i);
EXPECT_EQ(suppressor.LastEmitted(slot), 0u) << "slot " << i << " did not start at 0";
EXPECT_TRUE(suppressor.ShouldEmit(slot, 0)) << "slot " << i << " suppressed its first set";
EXPECT_FALSE(suppressor.ShouldEmit(slot, 0)) << "slot " << i << " re-emitted an unmoved set";
}
}
TEST(SetHashSuppressorTest, AComputedZeroIsRemappedSoItIsNeverConfusedWithNeverEmitted) {
MGPipeSetHashSuppressor suppressor;
const auto slot = MGPipeSuppressorSlot::SetVertexAttribDefaults;
EXPECT_TRUE(suppressor.ShouldEmit(slot, 0));
EXPECT_EQ(suppressor.LastEmitted(slot), 1u) << "a computed 0 must not read as never emitted";
EXPECT_FALSE(suppressor.ShouldEmit(slot, 0));
}
TEST(SetHashSuppressorTest, SlotsAreIndependent) {
MGPipeSetHashSuppressor suppressor;
EXPECT_TRUE(suppressor.ShouldEmit(MGPipeSuppressorSlot::SetVertexBuffers, 42));
EXPECT_TRUE(suppressor.ShouldEmit(MGPipeSuppressorSlot::SetSamplerViews, 42));
EXPECT_FALSE(suppressor.ShouldEmit(MGPipeSuppressorSlot::SetVertexBuffers, 42));
}
TEST(SetHashSuppressorTest, InvalidateMakesTheNextSetGoOutWhateverItHashesTo) {
MGPipeSetHashSuppressor suppressor;
const auto slot = MGPipeSuppressorSlot::SetShaderImages;
EXPECT_TRUE(suppressor.ShouldEmit(slot, 99));
EXPECT_FALSE(suppressor.ShouldEmit(slot, 99));
suppressor.Invalidate(slot);
EXPECT_TRUE(suppressor.ShouldEmit(slot, 99));
suppressor.InvalidateAll();
EXPECT_TRUE(suppressor.ShouldEmit(slot, 99));
}
#endif // MOBILEGL_PIPE_PUSH
} // namespace
+226
View File
@@ -18,8 +18,17 @@
#include <MG_Pipe/MGPipe.h>
#if MOBILEGL_PIPE_PUSH
#include <Config.h>
#include <MG_Impl/Pipe/CsoCache.h>
#include <MG_Impl/Pipe/Tracker.h>
#include <MG_Pipe/MGPipeRenderStateSpans.h>
#include <MG_Pipe/PipeApply.h>
#include <MG_Pipe/PipeMutation.h>
#include <MG_State/GLState/Core.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <limits>
#include <string>
#endif
using namespace MobileGL;
@@ -169,5 +178,222 @@ namespace {
MG_State::pGLContext = Move(held);
SUCCEED();
}
// ===================================================================================
// The dirty walk itself, and the render-state emission it drives (P2 brief D4, D6, D7)
// ===================================================================================
//
// These drive the tracker and the cache DIRECTLY rather than through
// MGPipeValidateForVerb. That is deliberate: MGPipeValidateForVerb reaches the library's
// one process-wide tracker, and a unit test that asserts on a shared singleton is a test
// that fails when ctest runs the suite in parallel. The emission logic these reproduce is
// three lines long and is the same three lines the validate point runs.
class TrackerWalk : public ::testing::Test {
protected:
void SetUp() override {
m_previous = Move(MG_State::pGLContext);
MG_State::pGLContext = MakeUnique<GLContext>();
m_savedPush = MG_Config::Features.PipePush;
MGPipeApplierReset();
}
void TearDown() override {
MG_Config::Features.PipePush = m_savedPush;
MGPipeApplierReset();
MG_State::pGLContext = Move(m_previous);
}
static GLContext& Ctx() { return *MG_State::pGLContext; }
// What MGPipeValidateForVerb's step 3 does, minus the PipeStats plumbing: acquire a
// CSO when the pipeline version moved, and compute the dynamic chunk mask when
// m_version moved.
Uint32 Walk(MGPipeVerbClass verbClass = MGPipeVerbClass::kDraw) {
const Uint32 dirty = m_tracker.Update(Ctx(), verbClass);
const RenderStateParameters& live = Ctx().GetRenderStateParameters();
m_lastDynamicMask = 0;
if (dirty & MGPipeDirtyBit(MGPipeDirty::NewPipelineState)) {
m_lastCso = m_cache.Acquire(live, m_payloadBytes);
++m_binds;
}
if (dirty & MGPipeDirtyBit(MGPipeDirty::NewRenderState)) {
m_lastDynamicMask = m_tracker.FreshlyPrimed()
? ~0u
: MGPipeDynamicChunksThatMoved(live, m_tracker.Staged());
}
if (dirty & (MGPipeDirtyBit(MGPipeDirty::NewPipelineState) |
MGPipeDirtyBit(MGPipeDirty::NewRenderState))) {
m_tracker.Staged() = live;
}
return dirty;
}
MGPipeTracker m_tracker;
MGPipeCsoCache m_cache;
MGPipeHandle m_lastCso = kMGPipeNullHandle;
Uint32 m_lastDynamicMask = 0;
Uint64 m_payloadBytes = 0;
Uint64 m_binds = 0;
Uint64 m_savedPush = 0;
UniquePtr<GLContext> m_previous;
};
TEST_F(TrackerWalk, EveryBitHasAName) {
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
ASSERT_NE(kMGPipeDirtyNames[i], nullptr);
EXPECT_EQ(std::string(kMGPipeDirtyNames[i]).rfind("NEW_", 0), 0u);
}
}
// The five P2 emits for each name their own subsystem; the rest name none, which is what
// makes MOBILEGL_PIPE_PUSH a per-subsystem A/B instead of one switch.
TEST_F(TrackerWalk, OnlyTheFiveEmittedBitsNameASubsystem) {
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
const auto bit = static_cast<MGPipeDirty>(i);
const Bool emitted = (kMGPipeDirtyEmittedAtP2 & MGPipeDirtyBit(bit)) != 0;
EXPECT_EQ(MGPipeSubsystemForDirty(bit) != 0, emitted) << kMGPipeDirtyNames[i];
}
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewRenderState), kMGPipeSubsystemRenderState);
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewPixelPack), kMGPipeSubsystemPixelPack);
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewPatchState), kMGPipeSubsystemPatchState);
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewVertexAttribDefaults),
kMGPipeSubsystemVertexAttribDefaults);
}
TEST_F(TrackerWalk, TheFirstWalkOnAFreshContextPublishesEverything) {
const Uint32 dirty = Walk();
EXPECT_TRUE(m_tracker.FreshlyPrimed());
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
EXPECT_NE(dirty & (Uint32{1} << static_cast<Uint32>(i)), 0u)
<< kMGPipeDirtyNames[i] << " did not fire on a fresh context";
}
}
// The whole point of a validate-point tracker: two identical draws in a row cost two
// Uint16 compares and emit nothing at all.
TEST_F(TrackerWalk, SteadyStateEmitsNothing) {
Walk();
const Uint64 mintsAfterFirst = m_cache.GetCounters().Mints;
const Uint64 bindsAfterFirst = m_binds;
for (int i = 0; i < 8; ++i) EXPECT_EQ(Walk(), 0u) << "walk " << i << " fired with nothing moved";
EXPECT_EQ(m_cache.GetCounters().Mints, mintsAfterFirst);
EXPECT_EQ(m_binds, bindsAfterFirst);
}
// The Blaze3D shape ARCHITECTURE.md 5.1 names as the reason push happens at validate and
// not in the setter: enable / draw / disable / draw forever mints exactly TWO CSOs and
// reuses them for every toggle after that.
TEST_F(TrackerWalk, BlendToggleReusesTwoCsos) {
constexpr int kToggles = 32;
Walk(); // prime
// The priming walk already minted and cached the blend-DISABLED state, so the cache
// starts empty here or the count below would be one short of the shape it describes.
m_cache.Reset();
m_cache.ResetCounters();
m_binds = 0;
for (int i = 0; i < kToggles; ++i) {
Ctx().SetCapability(CapabilityInput::Blend, true);
Walk();
Ctx().SetCapability(CapabilityInput::Blend, false);
Walk();
}
EXPECT_EQ(m_cache.GetCounters().Mints, 2u)
<< "a two-state ping-pong must mint two CSOs and then never mint again";
EXPECT_EQ(m_binds, static_cast<Uint64>(2 * kToggles));
EXPECT_EQ(m_cache.GetCounters().Hits, static_cast<Uint64>(2 * kToggles - 2));
EXPECT_EQ(m_cache.Size(), 2u);
m_cache.Reset();
}
// The regression RenderState.h records: a glViewport must not evict a cached pipeline.
// It mints nothing and its payload is one dynamic chunk - D0, the viewports - and not the
// other seven.
TEST_F(TrackerWalk, ViewportDoesNotMintACso) {
Walk(); // prime
m_cache.ResetCounters();
for (Int i = 1; i <= 16; ++i) {
Ctx().SetViewport(IntVec4(0, 0, 64 + i, 48 + i));
const Uint32 dirty = Walk();
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewRenderState), 0u);
EXPECT_EQ(dirty & MGPipeDirtyBit(MGPipeDirty::NewPipelineState), 0u)
<< "glViewport moved the PIPELINE version";
EXPECT_EQ(m_lastDynamicMask, 1u) << "glViewport sent something other than chunk D0";
}
EXPECT_EQ(m_cache.GetCounters().Mints, 0u);
EXPECT_EQ(m_binds, 1u) << "only the priming walk may bind";
m_cache.Reset();
}
// RenderState's two shutters are Uint16 and the tracker widens them in its OWN state,
// never in MG_State. A wrap must cost an extra re-push at worst and never a missed one.
TEST_F(TrackerWalk, WrapAroundRePushesButNeverMisses) {
Walk(); // prime
constexpr int kMoves = 70000; // past 65535 with room to spare
Uint64 fired = 0;
for (int i = 0; i < kMoves; ++i) {
// Never the default 1.0f: a setter that early-outs on an unchanged value would
// not move m_version, and the first iteration would then be a false miss.
Ctx().SetLineWidth((i & 1) ? 2.0f : 3.0f);
if (Walk() & MGPipeDirtyBit(MGPipeDirty::NewRenderState)) ++fired;
}
EXPECT_EQ(fired, static_cast<Uint64>(kMoves))
<< "a Uint16 wrap swallowed a render-state change";
m_cache.Reset();
}
// The one direction the P1 verify comparator cannot see: it compares object-class fields
// by IDENTITY only, so a bound texture whose CONTENT moved looks unchanged to it.
// ARCHITECTURE.md 13.2 names under-firing as the dangerous direction, and this is the
// first test of it.
TEST_F(TrackerWalk, AggregateGenerationCatchesABoundTextureMoving) {
const auto& tex = Ctx().CreateTextureObject(1, TextureTarget::Texture2D);
ASSERT_TRUE(tex != nullptr);
Walk(); // prime
EXPECT_EQ(Walk() & MGPipeDirtyBit(MGPipeDirty::NewSamplerViews), 0u);
static_cast<TextureObjectBase*>(tex.get())->BumpContentVersion();
EXPECT_NE(Walk() & MGPipeDirtyBit(MGPipeDirty::NewSamplerViews), 0u)
<< "a bound texture's content moved and NEW_SAMPLER_VIEWS did not fire";
// and it settles again, so the bit is a shutter and not a stuck flag
EXPECT_EQ(Walk() & MGPipeDirtyBit(MGPipeDirty::NewSamplerViews), 0u);
m_cache.Reset();
}
// A NaN outer level is a legal glPatchParameterfv value and has to compare equal to
// itself, which float equality does not do and a byte compare does.
TEST_F(TrackerWalk, ANaNPatchLevelEqualsItselfAndDoesNotFireForever) {
Walk(); // prime
Ctx().SetPatchDefaultOuterLevel(
FloatVec4(std::numeric_limits<Float>::quiet_NaN(), 1.0f, 1.0f, 1.0f));
EXPECT_NE(Walk() & MGPipeDirtyBit(MGPipeDirty::NewPatchState), 0u);
EXPECT_EQ(Walk() & MGPipeDirtyBit(MGPipeDirty::NewPatchState), 0u)
<< "a NaN patch level re-fired against itself";
m_cache.Reset();
}
TEST_F(TrackerWalk, ThePixelPackShutterIsAByteCompareOfThePackHalfOnly) {
Walk(); // prime
EXPECT_EQ(Walk() & MGPipeDirtyBit(MGPipeDirty::NewPixelPack), 0u);
Ctx().SetPixelStoreParam(PixelStoreParam::PackAlignment, 8);
EXPECT_NE(Walk() & MGPipeDirtyBit(MGPipeDirty::NewPixelPack), 0u);
EXPECT_EQ(Walk() & MGPipeDirtyBit(MGPipeDirty::NewPixelPack), 0u);
// The UNPACK half has no carrier at all, so it must not move the pack shutter.
Ctx().SetPixelStoreParam(PixelStoreParam::UnpackAlignment, 8);
EXPECT_EQ(Walk() & MGPipeDirtyBit(MGPipeDirty::NewPixelPack), 0u)
<< "an unpack write moved the PACK shutter";
m_cache.Reset();
}
TEST_F(TrackerWalk, TheFireTalliesOnlyRunWhilePipeStatsIsOn) {
// PipeStats is off in a unit-test process, which is the state the ROADMAP rule about
// hot-path instrumentation cares about: the walk must cost nothing extra there.
ASSERT_FALSE(MG_Util::PipeStats::Enabled());
Walk();
Ctx().SetLineWidth(3.0f);
Walk();
EXPECT_EQ(m_tracker.WalkCount(), 0u);
EXPECT_EQ(m_tracker.FireCount(MGPipeDirty::NewRenderState), 0u);
m_cache.Reset();
}
#endif // MOBILEGL_PIPE_PUSH
} // namespace