mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 04:38:30 +09:00
1085 lines
60 KiB
C++
1085 lines
60 KiB
C++
// MobileGL - MobileGL/MG_Test/Pipe/TrackerTest.cpp
|
|
// 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
|
|
|
|
// The frontend state tracker: the dirty walk, the aggregate generations, the per-bit fire
|
|
// counters (P2 brief D4). Owned by P2 package B (p2/tracker); the file and its CMake
|
|
// registration are the contract commit's.
|
|
//
|
|
// Needs the push sources, so every case is a visible SKIP in a pull build rather than a
|
|
// vanishing test - the shape PipeInputsTest.cpp established.
|
|
#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/PipeFill.h>
|
|
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
|
#include <MG_Impl/Pipe/Tracker.h>
|
|
#include <MG_Impl/Pipe/VertexInputEmit.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 <cstring>
|
|
#include <limits>
|
|
#include <string>
|
|
#endif
|
|
|
|
using namespace MobileGL;
|
|
using namespace MobileGL::MG_Pipe;
|
|
|
|
namespace {
|
|
// The subsystem bitmask the tracker dispatches on is allocated in every build.
|
|
TEST(Tracker, SubsystemBitsDoNotOverlapTheBehaviourBit) {
|
|
EXPECT_EQ(kMGPipeSubsystemsMigratedAtP2 & kMGPipeBehaviourNoCsoContentAddressing, 0ull);
|
|
}
|
|
|
|
#if !MOBILEGL_PIPE_PUSH
|
|
// G2 REQUIRES THE PULL AND PUSH CTEST NAME SETS TO BE IDENTICAL, name for name. A
|
|
// push-only case therefore cannot be ABSENT from a pull build; it has to be there and
|
|
// SKIP, which is the shape PipeInputsTest.cpp established for the same reason. This list
|
|
// declares exactly the suite.name pairs the push build gets from the real cases below, so
|
|
// a case added on one side and forgotten on the other shows up as a ctest-name diff
|
|
// rather than as a test that silently is not there.
|
|
#define MGL_TRACKER_TEST_LIST(X) \
|
|
X(TrackerAggregates, EveryAggregateStartsAtZero) \
|
|
X(TrackerAggregates, AVertexArrayAttributeMovesOnlyTheVaoAggregate) \
|
|
X(TrackerAggregates, AFramebufferObjectWriteMovesOnlyTheFramebufferAggregate) \
|
|
X(TrackerAggregates, AFramebufferDefaultSetterMovesOnlyTheFramebufferAggregate) \
|
|
X(TrackerAggregates, ATextureContentWriteMovesOnlyTheContentAggregate) \
|
|
X(TrackerAggregates, ATextureParameterMovesOnlyTheParamsAggregate) \
|
|
X(TrackerAggregates, ASamplerParameterMovesOnlyTheParamsAggregate) \
|
|
X(TrackerAggregates, ABufferRespecifyMovesOnlyTheBufferAggregate) \
|
|
X(TrackerAggregates, AVertexAttribDefaultMovesOnlyItsOwnAggregate) \
|
|
X(TrackerAggregates, ANoteWithoutALiveContextIsANoOp) \
|
|
X(TrackerWalk, EveryBitHasAName) \
|
|
X(TrackerWalk, OnlyTheFiveEmittedBitsNameASubsystem) \
|
|
X(TrackerWalk, TheFirstWalkOnAFreshContextPublishesEverything) \
|
|
X(TrackerWalk, SteadyStateEmitsNothing) \
|
|
X(TrackerWalk, BlendToggleReusesTwoCsos) \
|
|
X(TrackerWalk, ViewportDoesNotMintACso) \
|
|
X(TrackerWalk, WrapAroundRePushesButNeverMisses) \
|
|
X(TrackerWalk, AggregateGenerationCatchesABoundTextureMoving) \
|
|
X(TrackerWalk, ANaNPatchLevelEqualsItselfAndDoesNotFireForever) \
|
|
X(TrackerWalk, ThePixelPackShutterIsAByteCompareOfThePackHalfOnly) \
|
|
X(TrackerWalk, TheFireTalliesOnlyRunWhilePipeStatsIsOn) \
|
|
X(TrackerWalk, TheIndexBufferBitDoesNotFireOnAnUnrelatedBufferWrite) \
|
|
X(TrackerWalk, TheIndexBufferBitFiresWhenTheSlotVersionWrapsOntoADifferentBuffer) \
|
|
X(TrackerWalk, ABaseInstanceSurvivesTheFirstWalkOnAFreshContext) \
|
|
X(TrackerWalk, ASamplerBindAloneFiresTheSamplerStateBit) \
|
|
X(TrackerWalk, ARestagedProgramPipelineFiresTheProgramBits) \
|
|
X(TrackerWalk, ARelinkOfAStageProgramFiresTheProgramBits) \
|
|
X(TrackerWalk, UseProgramZeroLeavesTheBoundPipelineDrivingTheProgramBits) \
|
|
X(TrackerAttribPayload, AFloatWriteCarriesTheFloatBitsAndNamesItsClass) \
|
|
X(TrackerAttribPayload, AnIntWriteCarriesTheIntWordsAndNamesItsClass) \
|
|
X(TrackerAttribPayload, AUintWriteCarriesTheUintWordsAndNamesItsClass) \
|
|
X(TrackerAttribPayload, TheSameNumbersWrittenThroughADifferentClassAreADifferentValue) \
|
|
X(TrackerShippedEmitter, ABlendToggleThroughTheValidatePointMintsTwoCsos) \
|
|
X(TrackerShippedEmitter, TheSteadyStateThroughTheValidatePointEmitsNothing) \
|
|
X(TrackerShippedEmitter, APushedAttributeDefaultTheApplierCannotReproduceIsRepaired) \
|
|
X(TrackerShippedEmitter, AViewportThroughTheValidatePointMintsNoCso) \
|
|
X(TrackerShippedEmitter, AClipDistanceEnableReArmsTheResidualBlock) \
|
|
X(TrackerShippedEmitter, AFreshContextRepublishesEveryVertexAttributeDefault) \
|
|
X(TrackerShippedEmitter, AFreshContextResetsTheApplierWithTheRenderStateSubsystemOff) \
|
|
X(TrackerShippedEmitter, ABaseInstancedDrawAfterAMakeCurrentPublishesItsOwnBaseInstance)
|
|
|
|
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
|
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
|
MGL_TRACKER_TEST_LIST(MGL_DECLARE_PULL_SKIP)
|
|
#undef MGL_DECLARE_PULL_SKIP
|
|
#else
|
|
using GLContext = MG_State::GLState::GLContext;
|
|
|
|
using MG_State::GLState::TextureObjectBase;
|
|
using MobileGL::TextureTarget;
|
|
|
|
constexpr SizeT kAggregateCount = static_cast<SizeT>(MGPipeAggregate::Count);
|
|
|
|
// A live frontend context for the bump points to find, restored on the way out so the
|
|
// cases stay independent (PipeInputsTest's idiom).
|
|
class TrackerAggregates : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
m_previous = Move(MG_State::pGLContext);
|
|
MG_State::pGLContext = MakeUnique<GLContext>();
|
|
}
|
|
void TearDown() override { MG_State::pGLContext = Move(m_previous); }
|
|
|
|
static GLContext& Ctx() { return *MG_State::pGLContext; }
|
|
|
|
struct Snapshot {
|
|
Uint64 Values[kAggregateCount];
|
|
Uint64 operator[](MGPipeAggregate a) const { return Values[static_cast<SizeT>(a)]; }
|
|
};
|
|
|
|
static Snapshot Snap() {
|
|
GLContext& c = Ctx();
|
|
Snapshot s{};
|
|
s.Values[static_cast<SizeT>(MGPipeAggregate::VaoAttribute)] = c.GetAnyVaoAttributeGeneration();
|
|
s.Values[static_cast<SizeT>(MGPipeAggregate::FramebufferAttachment)] =
|
|
c.GetAnyFramebufferAttachmentGeneration();
|
|
s.Values[static_cast<SizeT>(MGPipeAggregate::TextureContent)] = c.GetAnyTextureContentGeneration();
|
|
s.Values[static_cast<SizeT>(MGPipeAggregate::TextureParams)] = c.GetAnyTextureParamsGeneration();
|
|
s.Values[static_cast<SizeT>(MGPipeAggregate::BufferChange)] = c.GetAnyBufferChangeGeneration();
|
|
s.Values[static_cast<SizeT>(MGPipeAggregate::VertexAttribDefault)] =
|
|
c.GetAnyVertexAttribDefaultGeneration();
|
|
return s;
|
|
}
|
|
|
|
// The whole contract of an aggregate generation in one assertion: the bump point
|
|
// moved ITS counter and moved NO other. The second half is what stops a bump point
|
|
// being wired to the wrong aggregate, which would over-fire one dirty bit and
|
|
// under-fire another - and under-firing is the direction that renders stale.
|
|
static void ExpectOnly(MGPipeAggregate moved, const Snapshot& before, const Snapshot& after) {
|
|
for (SizeT i = 0; i < kAggregateCount; ++i) {
|
|
const auto which = static_cast<MGPipeAggregate>(i);
|
|
if (which == moved) {
|
|
EXPECT_GT(after.Values[i], before.Values[i]) << "aggregate " << i << " did not move";
|
|
} else {
|
|
EXPECT_EQ(after.Values[i], before.Values[i]) << "aggregate " << i << " moved and must not";
|
|
}
|
|
}
|
|
}
|
|
|
|
UniquePtr<GLContext> m_previous;
|
|
};
|
|
|
|
TEST_F(TrackerAggregates, EveryAggregateStartsAtZero) {
|
|
const Snapshot s = Snap();
|
|
for (SizeT i = 0; i < kAggregateCount; ++i) EXPECT_EQ(s.Values[i], 0ull);
|
|
}
|
|
|
|
TEST_F(TrackerAggregates, AVertexArrayAttributeMovesOnlyTheVaoAggregate) {
|
|
const auto& vao = Ctx().CreateVertexArrayObject(1);
|
|
ASSERT_TRUE(vao != nullptr);
|
|
const Snapshot before = Snap();
|
|
vao->EnableAttribute(3);
|
|
ExpectOnly(MGPipeAggregate::VaoAttribute, before, Snap());
|
|
}
|
|
|
|
TEST_F(TrackerAggregates, AFramebufferObjectWriteMovesOnlyTheFramebufferAggregate) {
|
|
const auto& fbo = Ctx().CreateFramebufferObject(1);
|
|
ASSERT_TRUE(fbo != nullptr);
|
|
fbo->SetReadBuffer(FramebufferAttachmentType::Color0);
|
|
const Snapshot before = Snap();
|
|
fbo->SetReadBuffer(FramebufferAttachmentType::Color1);
|
|
ExpectOnly(MGPipeAggregate::FramebufferAttachment, before, Snap());
|
|
}
|
|
|
|
TEST_F(TrackerAggregates, AFramebufferDefaultSetterMovesOnlyTheFramebufferAggregate) {
|
|
const auto& fbo = Ctx().CreateFramebufferObject(2);
|
|
ASSERT_TRUE(fbo != nullptr);
|
|
const Snapshot before = Snap();
|
|
// The five MOBILEGL_DEFINE_FRAMEBUFFER_DEFAULT_SETTER bodies are one macro, so the
|
|
// bump statement inside it has to carry its own line continuation or the macro
|
|
// silently swallows the next line. This case is what says it did not.
|
|
fbo->SetDefaultWidth(64);
|
|
ExpectOnly(MGPipeAggregate::FramebufferAttachment, before, Snap());
|
|
}
|
|
|
|
TEST_F(TrackerAggregates, ATextureContentWriteMovesOnlyTheContentAggregate) {
|
|
const auto& tex = Ctx().CreateTextureObject(1, TextureTarget::Texture2D);
|
|
ASSERT_TRUE(tex != nullptr);
|
|
const Snapshot before = Snap();
|
|
static_cast<TextureObjectBase*>(tex.get())->BumpContentVersion();
|
|
ExpectOnly(MGPipeAggregate::TextureContent, before, Snap());
|
|
}
|
|
|
|
TEST_F(TrackerAggregates, ATextureParameterMovesOnlyTheParamsAggregate) {
|
|
const auto& tex = Ctx().CreateTextureObject(2, TextureTarget::Texture2D);
|
|
ASSERT_TRUE(tex != nullptr);
|
|
const Snapshot before = Snap();
|
|
tex->SetMaxLevel(4);
|
|
ExpectOnly(MGPipeAggregate::TextureParams, before, Snap());
|
|
}
|
|
|
|
TEST_F(TrackerAggregates, ASamplerParameterMovesOnlyTheParamsAggregate) {
|
|
const auto& sampler = Ctx().CreateSamplerObject(1);
|
|
ASSERT_TRUE(sampler != nullptr);
|
|
const Snapshot before = Snap();
|
|
sampler->SetWrapS(MobileGL::SamplerWrapMode::ClampToEdge);
|
|
ExpectOnly(MGPipeAggregate::TextureParams, before, Snap());
|
|
}
|
|
|
|
TEST_F(TrackerAggregates, ABufferRespecifyMovesOnlyTheBufferAggregate) {
|
|
const auto& buffer = Ctx().CreateBufferObject(1);
|
|
ASSERT_TRUE(buffer != nullptr);
|
|
const Snapshot before = Snap();
|
|
buffer->Respecify(64, nullptr);
|
|
ExpectOnly(MGPipeAggregate::BufferChange, before, Snap());
|
|
}
|
|
|
|
TEST_F(TrackerAggregates, AVertexAttribDefaultMovesOnlyItsOwnAggregate) {
|
|
const Snapshot before = Snap();
|
|
Ctx().SetCurrentVertexAttributeFloat(2, Array<Float, 4>{1.0f, 2.0f, 3.0f, 4.0f});
|
|
ExpectOnly(MGPipeAggregate::VertexAttribDefault, before, Snap());
|
|
}
|
|
|
|
TEST_F(TrackerAggregates, ANoteWithoutALiveContextIsANoOp) {
|
|
UniquePtr<GLContext> held = Move(MG_State::pGLContext);
|
|
MGP_NOTE_AGGREGATE(BufferChange); // must not dereference a null context
|
|
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.
|
|
// Resetting the applier without resetting the process-wide cache and tracker would leave
|
|
// the next bind_render_state naming a CSO the applier no longer has - it asserts and
|
|
// returns, leaving m_renderState unwritten. The three are one state, so they are reset
|
|
// together, here and in TrackerShippedEmitter.
|
|
// P3a adds two more pieces to that one state. MGPipeApplierReset is a MAKE-CURRENT and
|
|
// deliberately keeps the object records now, so a fixture that means "this applier is
|
|
// going away" has to say the other verb as well (PipeApply.h); and the vertex-input
|
|
// emitter's latches say "this handle has already published this configuration" about an
|
|
// applier that is about to be empty, so a bind would be suppressed against a record that
|
|
// is no longer there.
|
|
void ResetTheServerSideSingletons() {
|
|
MGPipeApplierReset();
|
|
MGPipeApplierReleaseObjectRecords();
|
|
MGPipeCsoCacheInstance().Reset();
|
|
MGPipeCsoCacheInstance().ResetCounters();
|
|
MGPipeTrackerInstance().Reset();
|
|
MGPipeSetHashSuppressorInstance().InvalidateAll();
|
|
MGPipeVertexInputEmitterInstance().Reset();
|
|
MGPipeVertexInputEmitterInstance().ResetCounters();
|
|
}
|
|
|
|
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;
|
|
ResetTheServerSideSingletons();
|
|
}
|
|
void TearDown() override {
|
|
MG_Config::Features.PipePush = m_savedPush;
|
|
ResetTheServerSideSingletons();
|
|
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.
|
|
// THE NAME IS P2's AND IT STAYS. A test name is never removed (only added), so this case
|
|
// keeps the name it was born with and follows the phase constant instead of a literal
|
|
// five: what it has always asserted is "a bit names a subsystem if and only if this build
|
|
// emits a call for it", which is the property the emission gate and the residual-fill
|
|
// skip both rest on. P3a took the vertex-input family over and P4a takes seven more bits
|
|
// across four subsystems, so the set it compares against is now kMGPipeDirtyEmittedAtP4a -
|
|
// and a bit that gained an arm without gaining an emitter, or the reverse, still fails
|
|
// here.
|
|
TEST_F(TrackerWalk, OnlyTheFiveEmittedBitsNameASubsystem) {
|
|
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
|
|
const auto bit = static_cast<MGPipeDirty>(i);
|
|
const Bool emitted = (kMGPipeDirtyEmittedAtP4a & MGPipeDirtyBit(bit)) != 0;
|
|
EXPECT_EQ(MGPipeSubsystemForDirty(bit) != 0, emitted) << kMGPipeDirtyNames[i];
|
|
}
|
|
// Each phase's constant SURVIVES as the next phase's A/B control, so the three are
|
|
// pinned as a chain rather than one being edited into the next: 0x1ff is P4a's "T2"
|
|
// arm and 0x7f is P3a's, and an operator's recorded mask has to keep meaning what it
|
|
// meant.
|
|
EXPECT_EQ(kMGPipeDirtyEmittedAtP4a & kMGPipeDirtyEmittedAtP3a, kMGPipeDirtyEmittedAtP3a);
|
|
EXPECT_EQ(kMGPipeDirtyEmittedAtP3a & kMGPipeDirtyEmittedAtP2, kMGPipeDirtyEmittedAtP2);
|
|
// The three bits P4a still does not emit for - the const-buffer, shader-buffer and
|
|
// stream-output sets - name no subsystem, so their fields keep going through the
|
|
// residual fill. Stated positively as well as through the loop above, because "only
|
|
// these three are left" is the phase's own scope statement.
|
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewConstBuffers), 0u);
|
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewShaderBuffers), 0u);
|
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewSoTargets), 0u);
|
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewRenderState), kMGPipeSubsystemRenderState);
|
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewPixelPack), kMGPipeSubsystemPixelPack);
|
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewPatchState), kMGPipeSubsystemPatchState);
|
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewVertexAttribDefaults),
|
|
kMGPipeSubsystemVertexAttribDefaults);
|
|
// P3a's three, one subsystem: an operator who clears bit 8 gets the whole legacy
|
|
// vertex-input arm rather than two thirds of it.
|
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewVertexElements), kMGPipeSubsystemVertexInput);
|
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewVertexBuffers), kMGPipeSubsystemVertexInput);
|
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewIndexBuffer), kMGPipeSubsystemVertexInput);
|
|
// P4a's seven, across FOUR subsystems, and the grouping is the whole point: the three
|
|
// program bits are one family because an operator switching programs off has to get
|
|
// the whole legacy arm, and so are the three unit-set bits.
|
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewShader), kMGPipeSubsystemPrograms);
|
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewShaderBindings), kMGPipeSubsystemPrograms);
|
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewGlobalConstants), kMGPipeSubsystemPrograms);
|
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewFramebuffer), kMGPipeSubsystemFramebuffer);
|
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewSamplerViews), kMGPipeSubsystemSamplers);
|
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewSamplers), kMGPipeSubsystemSamplers);
|
|
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewShaderImages), kMGPipeSubsystemSamplers);
|
|
// AND NO BIT NAMES THE TEXTURE-RESOURCE SUBSYSTEM. Its calls are dispatched from the
|
|
// GL entry points that cause them - a constructor, a storage definition, a
|
|
// glTexParameter - not from a dirty walk, exactly as P3a's buffer family is, so a bit
|
|
// that started naming it would gate the emission twice and the two gates would
|
|
// disagree the first time one of them was edited.
|
|
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
|
|
EXPECT_NE(MGPipeSubsystemForDirty(static_cast<MGPipeDirty>(i)),
|
|
kMGPipeSubsystemTextureResources)
|
|
<< kMGPipeDirtyNames[i];
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
// ===================================================================================
|
|
// P3a D-I: bit 10's narrowed shutter
|
|
// ===================================================================================
|
|
//
|
|
// NEW_INDEX_BUFFER used to be MixShutter(the whole buffer-CONTENT aggregate, the VAO
|
|
// identity), so it fired on any buffer write anywhere - a glBufferSubData into a texture
|
|
// upload staging buffer re-published the index binding. It now reads the bound VAO's own
|
|
// element-slot version and the identity of whatever is bound to it.
|
|
//
|
|
// THIS IS THE ONE CASE THE OLD SHUTTER COULD NOT PASS, which is why it is here rather
|
|
// than in the narrowing commit's prose.
|
|
TEST_F(TrackerWalk, TheIndexBufferBitDoesNotFireOnAnUnrelatedBufferWrite) {
|
|
const SharedPtr<MG_State::GLState::BufferObject> indices = Ctx().CreateBufferObject(1);
|
|
indices->Respecify(64, nullptr);
|
|
Ctx().GetBoundVertexArray()->GetIndexBufferBindingSlot().Bind(indices);
|
|
Walk();
|
|
Walk();
|
|
ASSERT_EQ(m_tracker.LastDirty() & MGPipeDirtyBit(MGPipeDirty::NewIndexBuffer), 0u)
|
|
<< "the steady state must be quiet before the interesting half of this case";
|
|
|
|
// An entirely unrelated buffer's contents move. Nothing about the element binding
|
|
// changed, so the bit must stay down.
|
|
const SharedPtr<MG_State::GLState::BufferObject> unrelated = Ctx().CreateBufferObject(2);
|
|
unrelated->Respecify(4096, nullptr);
|
|
Array<Uint8, 16> bytes{};
|
|
unrelated->UploadSubData(DataPtr{bytes.data(), bytes.size()}, 0);
|
|
ASSERT_NE(Ctx().GetAnyBufferChangeGeneration(), 0u) << "the buffer aggregate did move";
|
|
Walk();
|
|
EXPECT_EQ(m_tracker.LastDirty() & MGPipeDirtyBit(MGPipeDirty::NewIndexBuffer), 0u)
|
|
<< "NEW_INDEX_BUFFER fired on a write to a buffer that is not the element binding";
|
|
|
|
// And the control, on the same tracker: the binding itself moving DOES fire it, so
|
|
// the quiet above is a narrowing and not a dead bit.
|
|
const SharedPtr<MG_State::GLState::BufferObject> other = Ctx().CreateBufferObject(3);
|
|
other->Respecify(64, nullptr);
|
|
Ctx().GetBoundVertexArray()->GetIndexBufferBindingSlot().Bind(other);
|
|
Walk();
|
|
EXPECT_NE(m_tracker.LastDirty() & MGPipeDirtyBit(MGPipeDirty::NewIndexBuffer), 0u)
|
|
<< "NEW_INDEX_BUFFER did not fire when the element binding changed";
|
|
m_cache.Reset();
|
|
}
|
|
|
|
// The slot version is a WRAPPING Uint16 that BindingSlot bumps only on a real change, so
|
|
// it is widened at this boundary - and the bound object's lifetime id joins it because
|
|
// identity is what closes the wrap hole. 65536 binds later the version reads the same
|
|
// number it did at the start; if that number were the whole shutter, a binding that had
|
|
// moved onto a DIFFERENT buffer would read as unchanged and the draw would fetch indices
|
|
// from the previous one.
|
|
TEST_F(TrackerWalk, TheIndexBufferBitFiresWhenTheSlotVersionWrapsOntoADifferentBuffer) {
|
|
const SharedPtr<MG_State::GLState::BufferObject> objects[3] = {
|
|
Ctx().CreateBufferObject(1), Ctx().CreateBufferObject(2), Ctx().CreateBufferObject(3)};
|
|
for (const auto& object : objects) object->Respecify(64, nullptr);
|
|
auto& slot = Ctx().GetBoundVertexArray()->GetIndexBufferBindingSlot();
|
|
|
|
slot.Bind(objects[0]);
|
|
Walk();
|
|
Walk();
|
|
ASSERT_EQ(m_tracker.LastDirty() & MGPipeDirtyBit(MGPipeDirty::NewIndexBuffer), 0u);
|
|
const Uint16 versionAtStart = slot.GetVersion();
|
|
|
|
// Drive the Uint16 all the way round WITHOUT the tracker looking, which is exactly
|
|
// the window a wrap needs: every bind between two walks is invisible to it.
|
|
//
|
|
// THREE buffers, not two, and that is the whole construction: BindingSlot bumps its
|
|
// version only on a real change, so strictly alternating between two objects makes
|
|
// the version and the bound object share a parity - 65536 changes always land back on
|
|
// the object they started from, and the wrap is unobservable. Cycling three lands on
|
|
// objects[65536 % 3] == objects[1] at exactly the same raw version.
|
|
for (Uint32 i = 1; i <= 65536u; ++i) slot.Bind(objects[i % 3]);
|
|
ASSERT_EQ(slot.GetVersion(), versionAtStart) << "the version did not come back round";
|
|
ASSERT_EQ(slot.GetBoundObject(), objects[1]) << "the binding did not land on a different buffer";
|
|
|
|
Walk();
|
|
EXPECT_NE(m_tracker.LastDirty() & MGPipeDirtyBit(MGPipeDirty::NewIndexBuffer), 0u)
|
|
<< "the slot version wrapped onto a DIFFERENT buffer and the bit stayed down - the "
|
|
"identity half of the shutter is what has to close that hole";
|
|
m_cache.Reset();
|
|
}
|
|
|
|
// THE PENDING BASE INSTANCE IS THIS CALL'S ARGUMENT, NOT A LATCH, and Update() is where
|
|
// the difference bites: it calls Reset() from inside itself whenever the current
|
|
// GLContext pointer moves, and the draw entry point wrote the value one statement EARLIER
|
|
// (D-H2.1 puts MGP_SET_BASE_INSTANCE immediately above MGP_FILL, and Update is inside
|
|
// MGP_FILL). So `eglMakeCurrent(ctxB); glDrawArraysInstancedBaseInstance(..., 7)` used to
|
|
// put a BaseInstance of 0 on the wire - one silently mis-shifted instanced draw per
|
|
// context switch, on the emulation path, invisible to a single-context retrace corpus and
|
|
// to every case that drives the emitter directly.
|
|
//
|
|
// A FRESH TRACKER IS EXACTLY THAT SWITCH: m_context starts null, so the first Walk() takes
|
|
// the same `m_context != &ctx` branch a make-current does.
|
|
TEST_F(TrackerWalk, ABaseInstanceSurvivesTheFirstWalkOnAFreshContext) {
|
|
m_tracker.SetPendingBaseInstance(7);
|
|
const Uint32 dirty = Walk();
|
|
EXPECT_EQ(m_tracker.PendingBaseInstance(), 7u)
|
|
<< "the walk that follows a make-current cleared the base instance the same call set";
|
|
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewVertexBuffers), 0u)
|
|
<< "NEW_VERTEX_BUFFERS did not fire on the first walk of a fresh context";
|
|
|
|
// And the control, so the assertion above is about Reset() and not about a value that
|
|
// is never cleared at all: the verb that consumes it clears it, and the next walk on
|
|
// the SAME context then sees 0.
|
|
m_tracker.ClearPendingBaseInstance();
|
|
Walk();
|
|
EXPECT_EQ(m_tracker.PendingBaseInstance(), 0u);
|
|
m_cache.Reset();
|
|
}
|
|
|
|
// ===================================================================================
|
|
// P4a c0d: the two under-fires that only a bit WITH an emitter can be hurt by
|
|
// ===================================================================================
|
|
|
|
// BIT 13 IS WHAT bind_sampler_states IS EMITTED OFF (PipeFill.cpp gates EmitSamplerStates
|
|
// on NEW_SAMPLERS), and glBindSampler moved neither half of what its shutter used to read:
|
|
// GL_Sampler.cpp's BindSampler_State calls NoteTextureUnitTouched and then
|
|
// TextureUnit::SetSamplerObject, and BOTH of those bump the TEXTURE BIND generation, which
|
|
// is bit 12's. The only writers of the sampling-resolution generation are parameter
|
|
// changes. This case makes those two calls, in that order, with nothing else moving.
|
|
TEST_F(TrackerWalk, ASamplerBindAloneFiresTheSamplerStateBit) {
|
|
ASSERT_TRUE(Ctx().CreateSamplerObject(1) != nullptr);
|
|
ASSERT_TRUE(Ctx().CreateSamplerObject(2) != nullptr);
|
|
|
|
Ctx().NoteTextureUnitTouched(3);
|
|
Ctx().GetTextureUnitObject(3).SetSamplerObject(Ctx().GetSamplerObject(1));
|
|
Walk();
|
|
ASSERT_EQ(Walk(), 0u) << "the fixture did not reach a steady state";
|
|
|
|
// The one and only change: unit 3 now carries a DIFFERENT sampler object, whose
|
|
// parameters happen to differ from the first one's. No glSamplerParameter*, no
|
|
// glTexParameter*, so the sampling-resolution generation cannot have moved.
|
|
Ctx().NoteTextureUnitTouched(3);
|
|
Ctx().GetTextureUnitObject(3).SetSamplerObject(Ctx().GetSamplerObject(2));
|
|
const Uint32 dirty = Walk();
|
|
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewSamplers), 0u)
|
|
<< "bind_sampler_states is emitted off NEW_SAMPLERS and never saw the sampler bind";
|
|
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewSamplerViews), 0u)
|
|
<< "the view set is re-resolved on a sampler bind too - completeness depends on the "
|
|
"effective sampler - and that half was already right";
|
|
EXPECT_EQ(Walk(), 0u) << "the widened shutter fires forever";
|
|
}
|
|
|
|
// BITS 6/7/8 UNDER A SEPARABLE PROGRAM PIPELINE. GetCurrentProgram() is null for the whole
|
|
// life of a bound pipeline, so all three shutters used to latch 0 and never move again
|
|
// after the first walk: glUseProgramStages would rebuild the composite and EmitShaderState
|
|
// would never be called, leaving the new composite with no ShaderCso handle at all.
|
|
TEST_F(TrackerWalk, ARestagedProgramPipelineFiresTheProgramBits) {
|
|
Vector<Uint> names;
|
|
Ctx().GenProgramPipelineNames(1, names);
|
|
ASSERT_EQ(names.size(), 1u);
|
|
Ctx().CreateProgramPipelineObject(names[0]);
|
|
Ctx().BindProgramPipelineObject(names[0]);
|
|
const auto& pipeline = Ctx().GetBoundProgramPipeline();
|
|
ASSERT_TRUE(pipeline != nullptr);
|
|
ASSERT_TRUE(Ctx().GetCurrentProgram() == nullptr)
|
|
<< "the premise of this case is that the program family has no current program";
|
|
|
|
Walk();
|
|
ASSERT_EQ(Walk(), 0u) << "the fixture did not reach a steady state";
|
|
|
|
// What glUseProgramStages does at the end of its validation: one stage changes.
|
|
const Uint vertex = Ctx().CreateProgram();
|
|
pipeline->SetStageProgram(MobileGL::ShaderStage::Vertex, Ctx().GetProgramObject(vertex));
|
|
const Uint32 dirty = Walk();
|
|
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewShader), 0u)
|
|
<< "the re-composited pipeline would never get a ShaderCso handle";
|
|
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewShaderBindings), 0u);
|
|
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewGlobalConstants), 0u)
|
|
<< "set_global_constants would never be sent for a pipeline draw";
|
|
EXPECT_EQ(Walk(), 0u) << "the pipeline arm fires forever";
|
|
}
|
|
|
|
// The same three bits, moved by the OTHER event that changes what a pipeline draws: a
|
|
// stage program's relink. The stage set does not move at all here - only the link version
|
|
// the composite cache is keyed on, which is what makes GetProgramForDraw build a new one.
|
|
TEST_F(TrackerWalk, ARelinkOfAStageProgramFiresTheProgramBits) {
|
|
Vector<Uint> names;
|
|
Ctx().GenProgramPipelineNames(1, names);
|
|
ASSERT_EQ(names.size(), 1u);
|
|
Ctx().CreateProgramPipelineObject(names[0]);
|
|
Ctx().BindProgramPipelineObject(names[0]);
|
|
const auto& pipeline = Ctx().GetBoundProgramPipeline();
|
|
ASSERT_TRUE(pipeline != nullptr);
|
|
|
|
const Uint vertex = Ctx().CreateProgram();
|
|
const SharedPtr<MG_State::GLState::ProgramObject> stage = Ctx().GetProgramObject(vertex);
|
|
ASSERT_TRUE(stage != nullptr);
|
|
pipeline->SetStageProgram(MobileGL::ShaderStage::Vertex, stage);
|
|
Walk();
|
|
ASSERT_EQ(Walk(), 0u) << "the fixture did not reach a steady state";
|
|
|
|
// A real relink, through the entry point glLinkProgram drives. It fails - the
|
|
// program has no shaders attached - and that is deliberate: Link()'s PROLOGUE is where
|
|
// the link-observable versions are bumped, before any early-out, precisely so that
|
|
// every memo keyed on them reads stale from the instant the relink is enqueued.
|
|
stage->Link();
|
|
const Uint32 dirty = Walk();
|
|
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewShader), 0u);
|
|
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewShaderBindings), 0u);
|
|
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewGlobalConstants), 0u);
|
|
EXPECT_EQ(Walk(), 0u);
|
|
}
|
|
|
|
// AND THE HANDOVER, which is the shape an application actually writes: a program is in
|
|
// use, a pipeline is bound underneath it, and glUseProgram(0) hands the draw to the
|
|
// pipeline (GL 4.6 core 7.4). The program in use wins while there is one - so the bits
|
|
// must move when the SOURCE changes - and the pipeline must drive them afterwards.
|
|
TEST_F(TrackerWalk, UseProgramZeroLeavesTheBoundPipelineDrivingTheProgramBits) {
|
|
Vector<Uint> names;
|
|
Ctx().GenProgramPipelineNames(1, names);
|
|
ASSERT_EQ(names.size(), 1u);
|
|
Ctx().CreateProgramPipelineObject(names[0]);
|
|
Ctx().BindProgramPipelineObject(names[0]);
|
|
const auto& pipeline = Ctx().GetBoundProgramPipeline();
|
|
ASSERT_TRUE(pipeline != nullptr);
|
|
|
|
const Uint installed = Ctx().CreateProgram();
|
|
Ctx().UseProgram(installed);
|
|
ASSERT_TRUE(Ctx().GetCurrentProgram() != nullptr);
|
|
Walk();
|
|
ASSERT_EQ(Walk(), 0u) << "the fixture did not reach a steady state";
|
|
|
|
Ctx().UseProgram(0);
|
|
const Uint32 handover = Walk();
|
|
EXPECT_NE(handover & MGPipeDirtyBit(MGPipeDirty::NewShader), 0u)
|
|
<< "the draw's program source changed and bit 6 did not fire";
|
|
ASSERT_EQ(Walk(), 0u);
|
|
|
|
// The pipeline is the source now, so a stage change has to reach the same bits.
|
|
const Uint vertex = Ctx().CreateProgram();
|
|
pipeline->SetStageProgram(MobileGL::ShaderStage::Vertex, Ctx().GetProgramObject(vertex));
|
|
const Uint32 dirty = Walk();
|
|
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewShader), 0u)
|
|
<< "with no program in use the bound pipeline has to drive the program family";
|
|
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewShaderBindings), 0u);
|
|
EXPECT_NE(dirty & MGPipeDirtyBit(MGPipeDirty::NewGlobalConstants), 0u);
|
|
}
|
|
|
|
// ===================================================================================
|
|
// set_vertex_attrib_defaults' payload (P2 brief D10)
|
|
// ===================================================================================
|
|
//
|
|
// A CurrentVertexAttributeValue is ONE value in three views and GLContext converts
|
|
// numerically between them, so four words on the wire are not the value unless the class
|
|
// travels with them. These pin exactly that, because nothing else can: the emission
|
|
// happens at step 3 and the residual fill re-pulls the field at step 4, so at a kDraw
|
|
// verb a wrong payload is overwritten before any comparator or backend read sees it -
|
|
// which is how a hard-coded ValueClass of 0 survived a green verify lane.
|
|
class TrackerAttribPayload : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
m_previous = Move(MG_State::pGLContext);
|
|
MG_State::pGLContext = MakeUnique<GLContext>();
|
|
}
|
|
void TearDown() override { MG_State::pGLContext = Move(m_previous); }
|
|
|
|
static GLContext& Ctx() { return *MG_State::pGLContext; }
|
|
|
|
static MGPAttribValue PayloadFor(Uint location) {
|
|
MGPAttribValue value{};
|
|
MGPipeFillAttribValue(static_cast<Uint32>(location), Ctx().GetCurrentVertexAttribute(location),
|
|
Ctx().GetCurrentVertexAttributeClass(location), value);
|
|
return value;
|
|
}
|
|
|
|
static Uint32 Word(Float value) {
|
|
Uint32 bits = 0;
|
|
std::memcpy(&bits, &value, sizeof(bits));
|
|
return bits;
|
|
}
|
|
|
|
UniquePtr<GLContext> m_previous;
|
|
};
|
|
|
|
TEST_F(TrackerAttribPayload, AFloatWriteCarriesTheFloatBitsAndNamesItsClass) {
|
|
Ctx().SetCurrentVertexAttributeFloat(3, Array<Float, 4>{1.5f, -2.5f, 3.0f, 4.0f});
|
|
const MGPAttribValue value = PayloadFor(3);
|
|
EXPECT_EQ(value.Location, 3u);
|
|
EXPECT_EQ(value.ValueClass, MG_State::GLState::kVertexAttribValueClassFloat);
|
|
EXPECT_EQ(value.Data[0], Word(1.5f));
|
|
EXPECT_EQ(value.Data[1], Word(-2.5f));
|
|
// The defect this exists to stop: 1.5f's int VIEW is 1, and a carrier that sent the
|
|
// float bits while calling them class 0 for every attribute would be sending
|
|
// 0x3FC00000 where the frontend holds 1.
|
|
EXPECT_NE(value.Data[0], static_cast<Uint32>(Ctx().GetCurrentVertexAttribute(3).intValue[0]));
|
|
}
|
|
|
|
TEST_F(TrackerAttribPayload, AnIntWriteCarriesTheIntWordsAndNamesItsClass) {
|
|
Ctx().SetCurrentVertexAttributeInt(5, Array<Int32, 4>{7, -9, 11, 13});
|
|
const MGPAttribValue value = PayloadFor(5);
|
|
EXPECT_EQ(value.ValueClass, MG_State::GLState::kVertexAttribValueClassInt);
|
|
EXPECT_EQ(static_cast<Int32>(value.Data[0]), 7);
|
|
EXPECT_EQ(static_cast<Int32>(value.Data[1]), -9);
|
|
// and NOT the float view the frontend converted it into
|
|
EXPECT_NE(value.Data[0], Word(7.0f));
|
|
}
|
|
|
|
TEST_F(TrackerAttribPayload, AUintWriteCarriesTheUintWordsAndNamesItsClass) {
|
|
Ctx().SetCurrentVertexAttributeUint(6, Array<Uint32, 4>{4000000000u, 2u, 3u, 4u});
|
|
const MGPAttribValue value = PayloadFor(6);
|
|
EXPECT_EQ(value.ValueClass, MG_State::GLState::kVertexAttribValueClassUint);
|
|
EXPECT_EQ(value.Data[0], 4000000000u);
|
|
EXPECT_NE(value.Data[0], Word(4000000000.0f));
|
|
}
|
|
|
|
// The class is PER ATTRIBUTE and it is the last writer's, not the context's - a payload
|
|
// that took one attribute's class for all 32 would be the same defect as a hard-coded 0.
|
|
TEST_F(TrackerAttribPayload, TheSameNumbersWrittenThroughADifferentClassAreADifferentValue) {
|
|
Ctx().SetCurrentVertexAttributeFloat(1, Array<Float, 4>{1.0f, 2.0f, 3.0f, 4.0f});
|
|
Ctx().SetCurrentVertexAttributeInt(2, Array<Int32, 4>{1, 2, 3, 4});
|
|
EXPECT_EQ(PayloadFor(1).ValueClass, MG_State::GLState::kVertexAttribValueClassFloat);
|
|
EXPECT_EQ(PayloadFor(2).ValueClass, MG_State::GLState::kVertexAttribValueClassInt);
|
|
// Same numbers, different classes, so the same four words mean different things:
|
|
// 1.0f is 0x3F800000 and the integer 1 is 0x00000001.
|
|
EXPECT_NE(PayloadFor(1).Data[0], PayloadFor(2).Data[0]);
|
|
// An attribute nobody wrote answers Float, which is what the GL default (0,0,0,1) is.
|
|
EXPECT_EQ(PayloadFor(7).ValueClass, MG_State::GLState::kVertexAttribValueClassFloat);
|
|
// and a later write of the other class moves the class of THAT attribute only
|
|
Ctx().SetCurrentVertexAttributeUint(1, Array<Uint32, 4>{1u, 2u, 3u, 4u});
|
|
EXPECT_EQ(PayloadFor(1).ValueClass, MG_State::GLState::kVertexAttribValueClassUint);
|
|
EXPECT_EQ(PayloadFor(2).ValueClass, MG_State::GLState::kVertexAttribValueClassInt);
|
|
}
|
|
|
|
// ===================================================================================
|
|
// The SHIPPED emitter, driven through MGPipeValidateForVerb itself
|
|
// ===================================================================================
|
|
//
|
|
// TrackerWalk above reproduces step 3 against a local tracker and cache, which cannot
|
|
// fail on a defect in the validate point itself (a bit gated on the wrong subsystem, an
|
|
// emission dropped). These drive the real entry point and read the real singletons back.
|
|
// Safe because ctest runs one gtest case per process and the fixture resets all three
|
|
// pieces of server-side state on both sides of every case.
|
|
class TrackerShippedEmitter : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
m_previous = Move(MG_State::pGLContext);
|
|
MG_State::pGLContext = MakeUnique<GLContext>();
|
|
m_savedPush = MG_Config::Features.PipePush;
|
|
MG_Config::Features.PipePush = kMGPipeSubsystemsMigratedAtP2;
|
|
ResetTheServerSideSingletons();
|
|
}
|
|
void TearDown() override {
|
|
MGPipeLeaveVerb();
|
|
MG_Config::Features.PipePush = m_savedPush;
|
|
ResetTheServerSideSingletons();
|
|
MG_State::pGLContext = Move(m_previous);
|
|
}
|
|
|
|
static GLContext& Ctx() { return *MG_State::pGLContext; }
|
|
static void Draw() { MGPipeValidateForVerb(MGPipeVerb::DrawArrays); }
|
|
static const MGPipeCsoCache::Counters& Cso() { return MGPipeCsoCacheInstance().GetCounters(); }
|
|
|
|
Uint64 m_savedPush = 0;
|
|
UniquePtr<GLContext> m_previous;
|
|
};
|
|
|
|
TEST_F(TrackerShippedEmitter, ABlendToggleThroughTheValidatePointMintsTwoCsos) {
|
|
constexpr int kToggles = 16;
|
|
Draw(); // prime: a fresh context resets the cache inside the emitter and mints once
|
|
MGPipeCsoCacheInstance().ResetCounters();
|
|
for (int i = 0; i < kToggles; ++i) {
|
|
Ctx().SetCapability(CapabilityInput::Blend, true);
|
|
Draw();
|
|
Ctx().SetCapability(CapabilityInput::Blend, false);
|
|
Draw();
|
|
}
|
|
EXPECT_EQ(Cso().Mints, 1u) << "the blend-disabled state was already cached by the priming draw";
|
|
EXPECT_EQ(Cso().Binds, static_cast<Uint64>(2 * kToggles));
|
|
EXPECT_EQ(Cso().Hits, static_cast<Uint64>(2 * kToggles - 1));
|
|
EXPECT_EQ(MGPipeCsoCacheInstance().Size(), 2u);
|
|
}
|
|
|
|
TEST_F(TrackerShippedEmitter, TheSteadyStateThroughTheValidatePointEmitsNothing) {
|
|
Draw();
|
|
// The positive half, so "nothing was emitted" cannot pass because nothing is wired:
|
|
// the first draw on a fresh context mints and binds exactly one CSO.
|
|
ASSERT_EQ(Cso().Mints, 1u) << "the priming draw emitted no create_render_state at all";
|
|
ASSERT_EQ(Cso().Binds, 1u);
|
|
MGPipeCsoCacheInstance().ResetCounters();
|
|
for (int i = 0; i < 8; ++i) {
|
|
Draw();
|
|
EXPECT_EQ(MGPipeTrackerInstance().LastDirty(), 0u) << "walk " << i << " fired with nothing moved";
|
|
}
|
|
EXPECT_EQ(Cso().Mints, 0u);
|
|
EXPECT_EQ(Cso().Binds, 0u) << "a steady-state draw bound a render-state CSO";
|
|
}
|
|
|
|
// The window the repair covers: a glVertexAttrib* write followed by a verb whose class
|
|
// does NOT read m_currentVertexAttribute. The call still goes out (the dirty bit and the
|
|
// subsystem bit are all step 3 looks at), and today's applier writes four words into all
|
|
// three views because it ignores ValueClass, so nothing in step 4 puts the converted
|
|
// value back and the client repairs the mirror itself.
|
|
//
|
|
// THE ASSERTION IS THE INVARIANT, NOT THE DEFECT. "repairs == before + 1" would pin
|
|
// today's applier and go red the day package A teaches
|
|
// MGPipeApplySetVertexAttribDefaults to switch on MGPAttribValue::ValueClass - which is
|
|
// the hand-off this package declares as blocking, and which is supposed to need no edit
|
|
// here. What must hold either way is that the call went out naming exactly the attribute
|
|
// that moved, and that the mirror ends up right by at most one repair: zero repairs once
|
|
// the applier reproduces the value, one until then.
|
|
TEST_F(TrackerShippedEmitter, APushedAttributeDefaultTheApplierCannotReproduceIsRepaired) {
|
|
Draw();
|
|
const Uint64 before = MGPipeVertexAttribDefaultRepairCount();
|
|
// 1.5f is the point: its int view is 1 and its bit pattern is 0x3FC00000, so the two
|
|
// cannot be the same four words whichever view the carrier picks.
|
|
Ctx().SetCurrentVertexAttributeFloat(0, Array<Float, 4>{1.5f, 2.5f, 3.5f, 4.5f});
|
|
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
|
|
const MGPVertexAttribDefaults header = MGPipeVertexAttribDefaultsLastHeader();
|
|
ASSERT_EQ(header.Count, 1u) << "the moved attribute default did not go out at all";
|
|
EXPECT_EQ(header.Mask, 1u) << "the call named an attribute that did not move";
|
|
const Uint64 repairs = MGPipeVertexAttribDefaultRepairCount() - before;
|
|
EXPECT_LE(repairs, 1u) << "one call cannot need two repairs";
|
|
// and the repair is not free-running: a second identical walk moves nothing, so it
|
|
// neither re-emits nor re-repairs.
|
|
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
|
|
EXPECT_EQ(MGPipeVertexAttribDefaultRepairCount() - before, repairs);
|
|
}
|
|
|
|
// MAJOR 1 of round 2's review, pinned. glEnable(GL_CLIP_DISTANCE0) is one of the 35
|
|
// capabilities the residual block carries AND one of the eight whose SetCapability arm
|
|
// deliberately does not BumpVersions(), so it moves m_version alone. An arming condition
|
|
// that reads the PIPELINE version - which is what this emitter used - never re-arms for
|
|
// those eight, and nothing can see it downstream: a block that is not emitted cannot
|
|
// diverge, so the trip wire is simply disarmed.
|
|
TEST_F(TrackerShippedEmitter, AClipDistanceEnableReArmsTheResidualBlock) {
|
|
Draw();
|
|
ASSERT_TRUE(MGPipeApplier().HasResidual) << "the priming draw sent no residual block";
|
|
// Poison the server's copy so a re-emission is the only thing that can restore it.
|
|
MGPipeApplier().Residual = ResidualValueBlock{};
|
|
MGPipeApplier().HasResidual = false;
|
|
|
|
Ctx().SetCapability(CapabilityInput::ClipDistance0, true);
|
|
// The premise: this moved the render-state counter and NOT the pipeline one.
|
|
const Uint16 pipelineBefore = static_cast<Uint16>(Ctx().GetPipelineStateVersion());
|
|
Draw();
|
|
ASSERT_EQ(MGPipeTrackerInstance().LastDirty() & MGPipeDirtyBit(MGPipeDirty::NewPipelineState), 0u)
|
|
<< "the premise is gone: a clip-distance enable now moves the pipeline version";
|
|
EXPECT_EQ(static_cast<Uint16>(Ctx().GetPipelineStateVersion()), pipelineBefore);
|
|
|
|
ASSERT_TRUE(MGPipeApplier().HasResidual)
|
|
<< "a capability change that moves only m_version never re-armed the residual block";
|
|
const Uint64 bit = Uint64{1} << static_cast<SizeT>(CapabilityInput::ClipDistance0);
|
|
EXPECT_NE(MGPipeApplier().Residual.CapabilityBits & bit, 0ull)
|
|
<< "the re-emitted block does not carry the capability that moved";
|
|
}
|
|
|
|
// MAJOR 3 of round 2's review, pinned. A fresh context resets the tracker's staging
|
|
// mirror to the GL defaults, which are exactly what a fresh GLContext holds - so the
|
|
// per-attribute diff is empty on the one walk that must publish everything, while the
|
|
// applier's mirror still holds the PREVIOUS context's defaults.
|
|
TEST_F(TrackerShippedEmitter, AFreshContextRepublishesEveryVertexAttributeDefault) {
|
|
// The fixture's context is itself fresh, so the priming draw is the first half of the
|
|
// same statement: a fresh context publishes the COMPLETE set, not a difference.
|
|
Draw();
|
|
ASSERT_EQ(MGPipeVertexAttribDefaultsLastHeader().Count, 32u)
|
|
<< "the first walk on a fresh context published an increment, not a complete state";
|
|
Ctx().SetCurrentVertexAttributeFloat(3, Array<Float, 4>{9.f, 8.f, 7.f, 6.f});
|
|
Draw();
|
|
ASSERT_EQ(MGPipeVertexAttribDefaultsLastHeader().Count, 1u)
|
|
<< "a steady context published more than the one attribute that moved";
|
|
|
|
// A different context, whose 32 defaults are the value-initialised {0,0,0,1} the
|
|
// tracker's own reset produces - so a diff against the staging mirror finds nothing.
|
|
MG_State::pGLContext = MakeUnique<GLContext>();
|
|
Draw();
|
|
const MGPVertexAttribDefaults header = MGPipeVertexAttribDefaultsLastHeader();
|
|
EXPECT_EQ(header.Count, 32u)
|
|
<< "a fresh context published " << header.Count
|
|
<< " attribute defaults; the server's mirror still holds the previous context's";
|
|
EXPECT_EQ(header.Mask, 0xFFFFFFFFu);
|
|
}
|
|
|
|
// Minor 4 of round 2's review. The fresh-context reset of the applier and the CSO cache
|
|
// used to sit inside EmitRenderState, i.e. behind bit 0 of MOBILEGL_PIPE_PUSH, so the
|
|
// per-subsystem A/B D14 invites gave a fresh context a never-reset applier holding the
|
|
// previous context's CSO records while every suppressor slot WAS invalidated.
|
|
TEST_F(TrackerShippedEmitter, AFreshContextResetsTheApplierWithTheRenderStateSubsystemOff) {
|
|
Draw();
|
|
ASSERT_FALSE(MGPipeApplier().RenderStateCsos.empty())
|
|
<< "the priming draw created no CSO record to leak into the next context";
|
|
|
|
MG_Config::Features.PipePush = kMGPipeSubsystemsMigratedAtP2 & ~kMGPipeSubsystemRenderState;
|
|
MG_State::pGLContext = MakeUnique<GLContext>();
|
|
Draw();
|
|
EXPECT_TRUE(MGPipeApplier().RenderStateCsos.empty())
|
|
<< "a fresh context kept the previous context's CSO records because the reset was "
|
|
"behind the render-state subsystem bit";
|
|
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundRenderStateCso));
|
|
}
|
|
|
|
TEST_F(TrackerShippedEmitter, AViewportThroughTheValidatePointMintsNoCso) {
|
|
Draw();
|
|
ASSERT_EQ(Cso().Mints, 1u) << "the priming draw emitted no create_render_state at all";
|
|
MGPipeCsoCacheInstance().ResetCounters();
|
|
for (Int i = 1; i <= 8; ++i) {
|
|
Ctx().SetViewport(IntVec4(0, 0, 64 + i, 48 + i));
|
|
Draw();
|
|
EXPECT_NE(MGPipeTrackerInstance().LastDirty() & MGPipeDirtyBit(MGPipeDirty::NewRenderState), 0u);
|
|
EXPECT_EQ(MGPipeTrackerInstance().LastDirty() & MGPipeDirtyBit(MGPipeDirty::NewPipelineState), 0u);
|
|
}
|
|
EXPECT_EQ(Cso().Mints, 0u);
|
|
EXPECT_EQ(Cso().Binds, 0u) << "glViewport reached the CSO cache";
|
|
}
|
|
|
|
// B-C1's scenario END TO END, through the shipped entry point and read back off the real
|
|
// applier: the setter runs, THEN a make-current happens inside the same fill (the fresh
|
|
// GLContext below is the switch), and what set_vertex_buffers carries has to be the 7 the
|
|
// draw entry point passed - not the 0 the tracker's context Reset used to leave behind.
|
|
//
|
|
// It also pins the two halves D-H2.3 makes one property: the applier's raw
|
|
// VertexFetchBaseInstance, and the ContentHash the suppressor keys on - which has to mix
|
|
// BaseInstance in, or the SECOND draw at a different base instance over the same buffer
|
|
// set would be suppressed as unchanged and the server would keep the first one's shift.
|
|
TEST_F(TrackerShippedEmitter, ABaseInstancedDrawAfterAMakeCurrentPublishesItsOwnBaseInstance) {
|
|
MG_Config::Features.PipePush = kMGPipeSubsystemsMigratedAtP2 | kMGPipeSubsystemResources |
|
|
kMGPipeSubsystemVertexInput;
|
|
Draw(); // prime this context, so the switch below is a real make-current
|
|
ASSERT_EQ(MGPipeApplier().VertexFetchBaseInstance, 0u);
|
|
|
|
// The make-current, then the entry point's one line, then the fill - in that order,
|
|
// which is the order GL_Drawing.cpp has.
|
|
MG_State::pGLContext = MakeUnique<GLContext>();
|
|
const SharedPtr<MG_State::GLState::BufferObject> vertices = Ctx().CreateBufferObject(1);
|
|
vertices->Respecify(4096, nullptr);
|
|
Ctx().GetBoundVertexArray()->SetAttributeFormat(0, 4, DataType::Float32, false, 16, 0, false, false, -1);
|
|
Ctx().GetBoundVertexArray()->BindAttributeBuffer(0, vertices);
|
|
Ctx().GetBoundVertexArray()->EnableAttribute(0);
|
|
|
|
MGPipeSetPendingBaseInstance(7);
|
|
ASSERT_EQ(MGPipePendingBaseInstance(), 7u) << "the setter did not take";
|
|
Draw();
|
|
|
|
EXPECT_EQ(MGPipeApplier().VertexFetchBaseInstance, 7u)
|
|
<< "the make-current between the setter and the fill ate the base instance";
|
|
EXPECT_EQ(MGPipeApplier().VertexBufferCount, 1u);
|
|
const Uint64 hashAtSeven = MGPipeVertexInputEmitterInstance().LastVertexBuffers().ContentHash;
|
|
EXPECT_EQ(hashAtSeven,
|
|
MGPipeVertexBufferSetContentHash(MGPipeVertexInputEmitterInstance().LastEntries().data(), 0,
|
|
1, 7))
|
|
<< "the emitted set's ContentHash does not include the base instance it went out with";
|
|
|
|
// CONSUMED by the verb that carried it: the next plain draw sees 0 again, and the
|
|
// clear that makes that true is the validate point's, not MGPipeLeaveVerb's (no GL
|
|
// entry point calls that one).
|
|
EXPECT_EQ(MGPipePendingBaseInstance(), 0u);
|
|
const Uint64 setsAtSeven = MGPipeVertexInputEmitterInstance().VertexBufferSetCount();
|
|
Draw();
|
|
EXPECT_EQ(MGPipeApplier().VertexFetchBaseInstance, 0u)
|
|
<< "a plain draw after a base-instanced one kept the previous fetch shift";
|
|
EXPECT_GT(MGPipeVertexInputEmitterInstance().VertexBufferSetCount(), setsAtSeven)
|
|
<< "the buffer set was suppressed on a base-instance-only change - the hash or the "
|
|
"bit-9 shutter is missing it";
|
|
EXPECT_NE(MGPipeVertexInputEmitterInstance().LastVertexBuffers().ContentHash, hashAtSeven);
|
|
|
|
// THE OTHER EXIT. MGPipeValidateForVerb returns early when there is no live context,
|
|
// which skips step 3 and therefore skips step 3's clear - and since Reset() no longer
|
|
// clears it either, that exit is the only path left on which a base instance could
|
|
// stand into the next verb. A draw with no context is a no-op; its argument must not
|
|
// outlive it.
|
|
MGPipeSetPendingBaseInstance(11);
|
|
UniquePtr<GLContext> parked = Move(MG_State::pGLContext);
|
|
Draw();
|
|
EXPECT_EQ(MGPipePendingBaseInstance(), 0u)
|
|
<< "the no-live-context exit left the draw's base instance standing for the next verb";
|
|
MG_State::pGLContext = Move(parked);
|
|
}
|
|
|
|
#endif // MOBILEGL_PIPE_PUSH
|
|
} // namespace
|