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