mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 12:48:32 +09:00
[Test] (Pipe): wire the sampler and program subsystems and pin the padding-proof CSO identity, the program-resolved unit sets, the linked-snapshot stage mask and the composite band's single release
This commit is contained in:
@@ -47,8 +47,12 @@
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// 0 until the emitters below have bodies; see FramebufferEmit.h's note.
|
||||
inline constexpr Uint64 kMGPipeWiredProgramSubsystem = 0;
|
||||
// WIRED. create/bind/delete_shader_state, set_draw_program, set_dispatch_program and
|
||||
// set_global_constants all have bodies, so this family contributes its bit to
|
||||
// kMGPipeWiredSubsystems. See SamplerEmit.h's note for what the bit does and does not do:
|
||||
// it states what this build emits for, and the EMISSION gate is the runtime
|
||||
// MOBILEGL_PIPE_PUSH mask through the validate point's `wants()`, not this constant.
|
||||
inline constexpr Uint64 kMGPipeWiredProgramSubsystem = kMGPipeSubsystemPrograms;
|
||||
|
||||
// D-H6. ~0u is the BACKENDS' "never uploaded" sentinel for a global-constants version, and
|
||||
// ProgramObject::MarkUBOContentDirty skips it on the wrap for exactly that reason. The
|
||||
|
||||
@@ -57,8 +57,19 @@
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// 0 until the emitters below and in ImageEmit.h have bodies; see FramebufferEmit.h's note.
|
||||
inline constexpr Uint64 kMGPipeWiredSamplerSubsystem = 0;
|
||||
// WIRED. The sampler CSO, the sampler view and all three unit sets - set_shader_images
|
||||
// included, whose emitter lives in ImageEmit.h - have bodies, so this file's family
|
||||
// contributes its bit to kMGPipeWiredSubsystems. ONE bit for the whole family, because an
|
||||
// operator switching samplers off has to get the whole family's legacy arm rather than two
|
||||
// thirds of it.
|
||||
//
|
||||
// WHAT THE BIT DOES AND DOES NOT DO, said plainly because it is not what a reader expects:
|
||||
// it is the honest statement of what this build emits for, and it feeds
|
||||
// EmittedCallSuppliesTheWholeField's guard. It is NOT the emission gate - the validate
|
||||
// point's `wants()` asks MGPipeSubsystemForDirty and the runtime MOBILEGL_PIPE_PUSH mask,
|
||||
// so these emitters go live the moment their bodies exist and the mask carries bit 11. The
|
||||
// A/B that switches this family off is the MASK, not this constant.
|
||||
inline constexpr Uint64 kMGPipeWiredSamplerSubsystem = kMGPipeSubsystemSamplers;
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// D-F1: the canonical SamplerParameters copy, and why it is not a memcpy
|
||||
@@ -83,10 +94,15 @@ namespace MobileGL::MG_Pipe {
|
||||
//
|
||||
// ONE COPY PER MINT ATTEMPT, never per draw: the version-first skip in the two emitters
|
||||
// below decides whether to come here at all.
|
||||
inline SamplerParameters MGPipeCanonicalSamplerParameters(const SamplerParameters& src) {
|
||||
// THE PRIMITIVE TAKES AN OUT-PARAMETER, and that is not a style preference either. A
|
||||
// returned SamplerParameters is copied, and a copy of a trivially copyable type leaves the
|
||||
// padding UNSPECIFIED - so a canonicaliser that returned by value would hand its caller a
|
||||
// value whose three trailing bytes are whatever the copy left there, which is the very
|
||||
// thing this function exists to make deterministic. Every producer of canonical bytes in
|
||||
// this file, and every consumer that stores them, goes through this and through memcpy.
|
||||
inline void MGPipeCanonicaliseSamplerParameters(const SamplerParameters& src, SamplerParameters& canon) {
|
||||
static_assert(std::is_trivially_copyable_v<SamplerParameters>,
|
||||
"the canonical copy is memset and then assigned field by field");
|
||||
SamplerParameters canon;
|
||||
std::memset(static_cast<void*>(&canon), 0, sizeof(canon));
|
||||
canon.wrapS = src.wrapS;
|
||||
canon.wrapT = src.wrapT;
|
||||
@@ -109,13 +125,19 @@ namespace MobileGL::MG_Pipe {
|
||||
// of Espryt's redundancy filters compare all four. Dropping this one line is G7's
|
||||
// scripted negative control and SamplerEmit's suite must go red naming it.
|
||||
canon.borderColorForm = src.borderColorForm;
|
||||
return canon;
|
||||
}
|
||||
|
||||
inline Uint64 MGPipeHashSamplerParameters(const SamplerParameters& canon) {
|
||||
return XXH64(&canon, sizeof(canon), 0);
|
||||
}
|
||||
|
||||
// Canonicalise and hash in one step, for a caller that wants only the hash.
|
||||
inline Uint64 MGPipeHashOfSamplerParameters(const SamplerParameters& src) {
|
||||
SamplerParameters canon;
|
||||
MGPipeCanonicaliseSamplerParameters(src, canon);
|
||||
return MGPipeHashSamplerParameters(canon);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// D-F1: the content-addressed sampler CSO cache, capacity 256
|
||||
// ---------------------------------------------------------------------------------
|
||||
@@ -169,7 +191,8 @@ namespace MobileGL::MG_Pipe {
|
||||
// exactly the sharing that makes content addressing the right answer for this kind.
|
||||
MGPipeHandle Acquire(const SamplerParameters& params, Uint64& payloadBytes) {
|
||||
++m_counters.Acquisitions;
|
||||
const SamplerParameters canon = MGPipeCanonicalSamplerParameters(params);
|
||||
SamplerParameters canon;
|
||||
MGPipeCanonicaliseSamplerParameters(params, canon);
|
||||
const Uint64 hash = MGPipeHashSamplerParameters(canon);
|
||||
for (SizeT i = 0; i < m_entries.size(); ++i) {
|
||||
if (m_entries[i].Hash != hash) continue;
|
||||
@@ -245,12 +268,20 @@ namespace MobileGL::MG_Pipe {
|
||||
desc.Parameters.Offset = 0;
|
||||
desc.Parameters.Size = 0;
|
||||
|
||||
Entry entry;
|
||||
// BUILT IN PLACE AND FILLED WITH A MEMCPY, not assigned from a local. This is the
|
||||
// padding trap one level deeper than the one the design names: an assignment copies
|
||||
// the sixteen MEMBERS and leaves the three trailing padding bytes of the
|
||||
// destination at whatever was there, so the next probe's memcmp would reject its
|
||||
// own entry, count a collision that never happened, evict and mint a fresh CSO - a
|
||||
// cache with a hit rate of zero whose failure depends on heap contents, which is
|
||||
// why it passes a case run alone and fails the same case run in a suite. The bytes
|
||||
// stored here have to be the bytes compared later, padding included.
|
||||
m_entries.push_back(Entry{});
|
||||
Entry& entry = m_entries.back();
|
||||
entry.Hash = hash;
|
||||
entry.LastUsed = ++m_clock;
|
||||
entry.Cso = cso;
|
||||
entry.Params = canon;
|
||||
m_entries.push_back(entry);
|
||||
std::memcpy(static_cast<void*>(&entry.Params), &canon, sizeof(canon));
|
||||
// The applier is handed the CACHE's copy, so the pointer stays valid for the whole
|
||||
// call and the bytes it stores are provably the bytes the memcmp will confirm
|
||||
// against later.
|
||||
|
||||
@@ -50,8 +50,14 @@
|
||||
#include "Includes.h"
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include "Init.h"
|
||||
#include <MG_Impl/GLImpl/Program/GL_Program.h>
|
||||
#include <MG_Impl/GLImpl/Program/GL_ProgramPipeline.h>
|
||||
#include <MG_Impl/Pipe/CompositeResolver.h>
|
||||
#include <MG_Impl/Pipe/ProgramEmit.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
// create_shader_state takes the two artefact structs by pointer beside the record, so a case
|
||||
// that mints a composite record needs their definitions.
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifacts.h>
|
||||
@@ -347,6 +353,211 @@ TEST(CompositeResolver, ASlotAtTheShaderCsoLimitIsRefusedWhileTheLastBandSlotIsN
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
// G2 requires the pull and push ctest name sets to be identical, name for name.
|
||||
#define MGL_COMPOSITE_RESOLVER_TEST_LIST(X) \
|
||||
X(CompositeResolver, ACompositeIsMintedFromTheReservedBand) \
|
||||
X(CompositeResolver, ASignatureThatHasNotMovedReusesOneComposite) \
|
||||
X(CompositeResolver, TwoPipelinesWithTheSameSignatureKeepTheirOwnComposite) \
|
||||
X(CompositeResolver, EvictionThenDestructionFreesTheSlotExactlyOnce) \
|
||||
X(CompositeResolver, DestructionThenEvictionFreesTheSlotExactlyOnce)
|
||||
|
||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||
MGL_COMPOSITE_RESOLVER_TEST_LIST(MGL_DECLARE_PULL_SKIP)
|
||||
#undef MGL_DECLARE_PULL_SKIP
|
||||
#else
|
||||
|
||||
namespace {
|
||||
namespace GL = MobileGL::MG_Impl::GLImpl;
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
using MG_State::GLState::ProgramObject;
|
||||
|
||||
struct ResolverScope {
|
||||
ResolverScope() { Clear(); }
|
||||
~ResolverScope() {
|
||||
GL::BindProgramPipeline(0);
|
||||
GL::UseProgram(0);
|
||||
Clear();
|
||||
}
|
||||
ResolverScope(const ResolverScope&) = delete;
|
||||
ResolverScope& operator=(const ResolverScope&) = delete;
|
||||
|
||||
static void Clear() {
|
||||
MGPipeProgramEmitterInstance().Reset();
|
||||
MGPipeProgramEmitterInstance().ResetCounters();
|
||||
MGPipeCompositeResolverInstance().ResetCounters();
|
||||
}
|
||||
};
|
||||
|
||||
GLContext& Ctx() { return *MG_State::pGLContext; }
|
||||
|
||||
const char* kVs = R"(#version 430 core
|
||||
out gl_PerVertex { vec4 gl_Position; };
|
||||
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
const char* kFs = R"(#version 430 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(1.0); }
|
||||
)";
|
||||
|
||||
// Built by hand rather than through glCreateShaderProgramv, for ProgramPipelineCompositeTest's
|
||||
// reason: that entry point detaches the shader right after linking, so a relink would leave
|
||||
// the stage program with nothing to composite from.
|
||||
GLuint MakeSeparableProgram(GLenum stage, const char* source) {
|
||||
const GLuint shader = GL::CreateShader(stage);
|
||||
GL::ShaderSource(shader, 1, &source, nullptr);
|
||||
GL::CompileShader(shader);
|
||||
const GLuint program = GL::CreateProgram();
|
||||
GL::ProgramParameteri(program, GL_PROGRAM_SEPARABLE, GL_TRUE);
|
||||
GL::AttachShader(program, shader);
|
||||
GL::LinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
GL::GetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
EXPECT_EQ(linked, GL_TRUE) << "separable stage program did not link";
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint MakeBoundPipeline(GLuint vs, GLuint fs) {
|
||||
GLuint pipeline = 0;
|
||||
GL::GenProgramPipelines(1, &pipeline);
|
||||
GL::BindProgramPipeline(pipeline);
|
||||
GL::UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
|
||||
GL::UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
|
||||
GL::UseProgram(0);
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
TEST(CompositeResolver, ACompositeIsMintedFromTheReservedBand) {
|
||||
ResolverScope scope;
|
||||
const GLuint vs = MakeSeparableProgram(GL_VERTEX_SHADER, kVs);
|
||||
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kFs);
|
||||
MakeBoundPipeline(vs, fs);
|
||||
|
||||
const SharedPtr<ProgramObject> composite = Ctx().GetProgramForDraw();
|
||||
ASSERT_TRUE(composite) << "the frontend has to flatten the pipeline for this to mean anything";
|
||||
// The composite is the one ProgramObject in the system with external index 0: it is
|
||||
// deliberately not a named program, must not answer glIsProgram and must not consume a
|
||||
// name, and glCreateProgram never returns 0.
|
||||
EXPECT_TRUE(MGPipeProgramIsPipelineComposite(*composite));
|
||||
EXPECT_EQ(composite->GetExternalIndex(), 0u);
|
||||
|
||||
ASSERT_GT(MGPipeProgramEmitterInstance().EmitShaderState(Ctx()), 0u);
|
||||
const MGPipeHandle cso = MGPipeProgramEmitterInstance().DrawCso();
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(cso));
|
||||
EXPECT_TRUE(MGPipeIsCompositeShaderSlot(cso.Slot))
|
||||
<< "a composite's slot comes out of the reserved band and nowhere else";
|
||||
// AND IT IS AN ORDINARY create_shader_state. The server never learns it is a composite.
|
||||
EXPECT_EQ(MGPipeProgramEmitterInstance().LastProgramDesc().Cso, cso);
|
||||
EXPECT_TRUE(MGPipeProgramEmitterInstance().RecordIsPublished(cso));
|
||||
EXPECT_EQ(MGPipeCompositeResolverInstance().GetCounters().Mints, 1u);
|
||||
}
|
||||
|
||||
TEST(CompositeResolver, ASignatureThatHasNotMovedReusesOneComposite) {
|
||||
ResolverScope scope;
|
||||
const GLuint vs = MakeSeparableProgram(GL_VERTEX_SHADER, kVs);
|
||||
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kFs);
|
||||
MakeBoundPipeline(vs, fs);
|
||||
|
||||
ASSERT_GT(MGPipeProgramEmitterInstance().EmitShaderState(Ctx()), 0u);
|
||||
const MGPipeHandle first = MGPipeProgramEmitterInstance().DrawCso();
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(first));
|
||||
ASSERT_EQ(MGPipeProgramEmitterInstance().CreateCount(), 1u);
|
||||
|
||||
// The stage set did not move, so the frontend hands back the cached composite and the
|
||||
// resolver reuses its handle - no second identity, no second record, and nothing
|
||||
// released. THE KEY IS ComputeDrawProgramSignature's {lifetimeId, linkVersion} array
|
||||
// and deliberately NOT GetBackendStateVersion, which a glUniform1i to a sampler moves
|
||||
// and which used to rebuild the composite on every draw.
|
||||
MGPipeProgramEmitterInstance().EmitShaderState(Ctx());
|
||||
EXPECT_EQ(MGPipeProgramEmitterInstance().DrawCso(), first);
|
||||
EXPECT_EQ(MGPipeProgramEmitterInstance().CreateCount(), 1u);
|
||||
EXPECT_EQ(MGPipeCompositeResolverInstance().GetCounters().Releases, 0u);
|
||||
EXPECT_GE(MGPipeCompositeResolverInstance().GetCounters().Reuses, 1u);
|
||||
}
|
||||
|
||||
// [deviation] The brief names this case "TwoPipelinesWithTheSameSignatureShareOneComposite".
|
||||
// Sharing one HANDLE between two pipeline objects is not implementable safely and the
|
||||
// property that is true is the opposite one, so the case is named for what it asserts.
|
||||
//
|
||||
// The reason is the death path: a composite is an ordinary ProgramObject with its OWN
|
||||
// lifetime id, and the client-side death helper resolves the handle FROM that lifetime id.
|
||||
// Two composites sharing one handle would put only one of the two ids in the allocator's
|
||||
// map, so the first ~ProgramObject would free a slot the second still names - a premature
|
||||
// free that reappears later as slot theft, which is the exact class this whole band exists
|
||||
// to prevent. The frontend does not share either: each ProgramPipelineObject carries its
|
||||
// own one-slot draw-program cache, so two pipeline objects with identical stage sets are
|
||||
// two composites in the frontend too.
|
||||
TEST(CompositeResolver, TwoPipelinesWithTheSameSignatureKeepTheirOwnComposite) {
|
||||
ResolverScope scope;
|
||||
const GLuint vs = MakeSeparableProgram(GL_VERTEX_SHADER, kVs);
|
||||
const GLuint fs = MakeSeparableProgram(GL_FRAGMENT_SHADER, kFs);
|
||||
|
||||
MakeBoundPipeline(vs, fs);
|
||||
ASSERT_GT(MGPipeProgramEmitterInstance().EmitShaderState(Ctx()), 0u);
|
||||
const MGPipeHandle firstCso = MGPipeProgramEmitterInstance().DrawCso();
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(firstCso));
|
||||
|
||||
MakeBoundPipeline(vs, fs); // a SECOND pipeline object, the same stage set
|
||||
MGPipeProgramEmitterInstance().EmitShaderState(Ctx());
|
||||
const MGPipeHandle secondCso = MGPipeProgramEmitterInstance().DrawCso();
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(secondCso));
|
||||
|
||||
EXPECT_NE(firstCso, secondCso);
|
||||
EXPECT_TRUE(MGPipeIsCompositeShaderSlot(firstCso.Slot));
|
||||
EXPECT_TRUE(MGPipeIsCompositeShaderSlot(secondCso.Slot));
|
||||
// Both are still resolvable, which is the property a shared handle would have broken.
|
||||
EXPECT_TRUE(MGPipeSlots().IsLive(MGPipeKind::ShaderCso, firstCso));
|
||||
EXPECT_TRUE(MGPipeSlots().IsLive(MGPipeKind::ShaderCso, secondCso));
|
||||
}
|
||||
|
||||
// THE TWO RELEASE ORDERS, driven at the level where both of them are representable. Either
|
||||
// order frees the slot exactly once and the second call is a PROVEN no-op, because
|
||||
// MGPipeSlotAllocator::Free refuses a slot that is not live at that generation and bumps no
|
||||
// generation of its own - so a double release cannot skip a generation either.
|
||||
TEST(CompositeResolver, EvictionThenDestructionFreesTheSlotExactlyOnce) {
|
||||
ResolverScope scope;
|
||||
constexpr Uint64 kCompositeLifetimeId = 918273645ull;
|
||||
const Uint32 liveBefore = MGPipeSlots().LiveCount(MGPipeKind::ShaderCso);
|
||||
const MGPipeHandle cso = MGPipeSlots().AllocateComposite(kCompositeLifetimeId);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(cso));
|
||||
ASSERT_TRUE(MGPipeIsCompositeShaderSlot(cso.Slot));
|
||||
ASSERT_EQ(MGPipeSlots().LiveCount(MGPipeKind::ShaderCso), liveBefore + 1);
|
||||
|
||||
// 1. the pipeline cache drops it
|
||||
MGPipeEmitShaderCsoDestroyAndFree(kCompositeLifetimeId);
|
||||
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::ShaderCso, cso));
|
||||
EXPECT_EQ(MGPipeSlots().LiveCount(MGPipeKind::ShaderCso), liveBefore);
|
||||
// 2. and then ~ProgramObject runs, and finds nothing to do
|
||||
MGPipeEmitShaderCsoDestroyAndFree(kCompositeLifetimeId);
|
||||
EXPECT_EQ(MGPipeSlots().LiveCount(MGPipeKind::ShaderCso), liveBefore);
|
||||
}
|
||||
|
||||
TEST(CompositeResolver, DestructionThenEvictionFreesTheSlotExactlyOnce) {
|
||||
ResolverScope scope;
|
||||
constexpr Uint64 kCompositeLifetimeId = 918273646ull;
|
||||
const Uint32 liveBefore = MGPipeSlots().LiveCount(MGPipeKind::ShaderCso);
|
||||
const MGPipeHandle cso = MGPipeSlots().AllocateComposite(kCompositeLifetimeId);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(cso));
|
||||
|
||||
// The mirror order, and it is the one that actually happens today: the frontend's
|
||||
// one-slot cache drops its SharedPtr as it overwrites it, so ~ProgramObject usually
|
||||
// runs first and the resolver's release is the second, redundant path.
|
||||
MGPipeEmitShaderCsoDestroyAndFree(kCompositeLifetimeId);
|
||||
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::ShaderCso, cso));
|
||||
MGPipeEmitShaderCsoDestroyAndFree(kCompositeLifetimeId);
|
||||
EXPECT_EQ(MGPipeSlots().LiveCount(MGPipeKind::ShaderCso), liveBefore);
|
||||
|
||||
// And the slot really came back: the next composite is handed the same slot with a
|
||||
// bumped generation, so the stale handle can never resolve to it.
|
||||
const MGPipeHandle recycled = MGPipeSlots().AllocateComposite(kCompositeLifetimeId + 1);
|
||||
EXPECT_EQ(recycled.Slot, cso.Slot);
|
||||
EXPECT_NE(recycled.Gen, cso.Gen);
|
||||
MGPipeSlots().Free(MGPipeKind::ShaderCso, recycled);
|
||||
}
|
||||
} // namespace
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
@@ -358,6 +569,9 @@ int main(int argc, char** argv) {
|
||||
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
||||
#else
|
||||
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
MobileGL::Initialize();
|
||||
#endif
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
const int rc = RUN_ALL_TESTS();
|
||||
|
||||
@@ -6,20 +6,16 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// P4a's image-unit set: set_shader_images, the third of the three kVarTail unit sets. It rides
|
||||
// the sampler family's subsystem bit - one family, one A/B - and has its own suite because its
|
||||
// content hash has to cover two fields the other two sets do not carry.
|
||||
// P4a's third kVarTail unit set, set_shader_images. It rides the sampler family's subsystem
|
||||
// bit: one family, one A/B.
|
||||
//
|
||||
// THE TWO CASES THIS SUITE EXISTS FOR: an ACCESS-mode change alone, and an INTERNAL-FORMAT
|
||||
// change alone, each has to move the hash and emit the set. Both are live glBindImageTexture
|
||||
// state, the format-less image bake keys on the format the shader was built against, and a
|
||||
// hash over the bindings alone would suppress exactly the record that says the bake is stale.
|
||||
// The behavioural gates beside them are the format-less bake and non-core-format scenarios,
|
||||
// and the photon fixture on desktop retrace - the only fixture that has ever caught an
|
||||
// image-binding-semantics regression, and one that must never be run on the Adreno.
|
||||
// WHAT THIS SUITE IS FOR. The image set is the one whose ContentHash has to cover more than
|
||||
// the binding: InternalFormat and Access are live glBindImageTexture state that the
|
||||
// format-less image bake keys on, so a set whose only movement is an access mode still has to
|
||||
// go out. And the zero early-out is the property an optimisation deletes by accident - it is
|
||||
// what makes every draw of every application that never binds an image pay one integer test.
|
||||
//
|
||||
// THE SUITE IS `ImageEmit`, not `ImageEmitTest`: the file is XTest.cpp and the suite is X,
|
||||
// this directory's convention, and it is what the gates grep for.
|
||||
// THE SUITE IS `ImageEmit`, not `ImageEmitTest`: the file is XTest.cpp and the suite is X.
|
||||
//
|
||||
// THE TARGET AND ITS ctest REGISTRATION ARE THE CONTRACT COMMIT'S; THE CONTENTS ARE NOT.
|
||||
//
|
||||
@@ -48,8 +44,14 @@
|
||||
#include "Includes.h"
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include "Init.h"
|
||||
#include <MG_Impl/GLImpl/Program/GL_Program.h>
|
||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||
#include <MG_Impl/Pipe/ImageEmit.h>
|
||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#endif
|
||||
|
||||
using namespace MobileGL;
|
||||
@@ -157,10 +159,9 @@ namespace {
|
||||
TEST(ImageEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
EXPECT_EQ(&MGPipeImageEmitterInstance(), &MGPipeImageEmitterInstance());
|
||||
// The image set's window is bounded by the same merged unit space the other two sets use;
|
||||
// there is no separate image-unit capacity and there must not be one, because a record
|
||||
// whose window is checked against a different bound from the array it indexes is the shape
|
||||
// the applier's Fatal{ProtocolCorruption} exists to make impossible.
|
||||
// The image set has no bit of its own: set_shader_images rides the SAMPLER subsystem,
|
||||
// because the three unit sets are one family and an operator switching them off has to get
|
||||
// the whole family's legacy arm.
|
||||
EXPECT_EQ(kMGPipeMaxImageUnits, kMGPipeMaxTextureUnits);
|
||||
#else
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no client emitter in a pull build";
|
||||
@@ -323,6 +324,184 @@ TEST(ImageEmit, AMakeCurrentClearsTheImageSetAndAdvancesItsSerial) {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
// G2 requires the pull and push ctest name sets to be identical, name for name.
|
||||
#define MGL_IMAGE_EMIT_TEST_LIST(X) \
|
||||
X(ImageEmit, AZeroHighWaterMarkEmitsNothingWithoutHashing) \
|
||||
X(ImageEmit, AnAccessModeChangeAloneStillEmitsTheSet) \
|
||||
X(ImageEmit, AnInternalFormatChangeAloneStillEmitsTheSet) \
|
||||
X(ImageEmit, TheApplicationsFormatAndAccessTravelUnrecast)
|
||||
|
||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||
MGL_IMAGE_EMIT_TEST_LIST(MGL_DECLARE_PULL_SKIP)
|
||||
#undef MGL_DECLARE_PULL_SKIP
|
||||
#else
|
||||
|
||||
namespace {
|
||||
namespace GL = MobileGL::MG_Impl::GLImpl;
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
|
||||
struct EmitterScope {
|
||||
EmitterScope() { Clear(); }
|
||||
~EmitterScope() { Clear(); }
|
||||
EmitterScope(const EmitterScope&) = delete;
|
||||
EmitterScope& operator=(const EmitterScope&) = delete;
|
||||
|
||||
static void Clear() {
|
||||
MGPipeImageEmitterInstance().Reset();
|
||||
MGPipeImageEmitterInstance().ResetCounters();
|
||||
MGPipeProgramOpaqueUnitsShared().Invalidate();
|
||||
MGPipeSetHashSuppressorInstance().InvalidateAll();
|
||||
}
|
||||
};
|
||||
|
||||
GLContext& Ctx() { return *MG_State::pGLContext; }
|
||||
MGPipeImageEmitter& Emitter() { return MGPipeImageEmitterInstance(); }
|
||||
|
||||
GLuint MakeComputeProgram(const char* source) {
|
||||
const GLuint shader = GL::CreateShader(GL_COMPUTE_SHADER);
|
||||
GL::ShaderSource(shader, 1, &source, nullptr);
|
||||
GL::CompileShader(shader);
|
||||
const GLuint program = GL::CreateProgram();
|
||||
GL::AttachShader(program, shader);
|
||||
GL::LinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
GL::GetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
EXPECT_EQ(linked, GL_TRUE) << "the compute program did not link";
|
||||
return program;
|
||||
}
|
||||
|
||||
GLuint MakeImageTexture() {
|
||||
GLuint name = 0;
|
||||
GL::GenTextures(1, &name);
|
||||
GL::BindTexture(GL_TEXTURE_2D, name);
|
||||
GL::TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4);
|
||||
return name;
|
||||
}
|
||||
|
||||
// PROPERTY 1 OF D-G4, and it is the one an optimisation deletes: an application that never
|
||||
// binds an image pays one integer test per draw, taken BEFORE any hash and before any
|
||||
// 192-entry walk.
|
||||
//
|
||||
// The frontend has no image-unit high-water mark of its own - NoteUnitTouched is the
|
||||
// TEXTURE-unit path and glBindImageTexture does not reach it - so the window is derived
|
||||
// from the highest image unit the CURRENT PROGRAM names. A program with no image uniform
|
||||
// names none, and the set is not emitted at all.
|
||||
TEST(ImageEmit, AZeroHighWaterMarkEmitsNothingWithoutHashing) {
|
||||
EmitterScope scope;
|
||||
static const char* kNoImages = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(std430, binding = 0) buffer Out { uint value; } outBuf;
|
||||
void main() { outBuf.value = 1u; }
|
||||
)";
|
||||
const GLuint program = MakeComputeProgram(kNoImages);
|
||||
GL::UseProgram(program);
|
||||
|
||||
// A texture IS bound to an image unit. The set still does not go out, because no
|
||||
// shader can read it - which is the whole point of deriving the window from the
|
||||
// program rather than walking 192 units to find out.
|
||||
const GLuint texture = MakeImageTexture();
|
||||
GL::BindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
|
||||
EXPECT_EQ(Emitter().EmitShaderImages(Ctx()), 0u);
|
||||
EXPECT_EQ(Emitter().ImageSetCount(), 0u);
|
||||
EXPECT_EQ(Emitter().Window(), 0u);
|
||||
GL::UseProgram(0);
|
||||
}
|
||||
|
||||
const char* kImageCompute = R"(#version 430 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(binding = 1, rgba8) uniform image2D img;
|
||||
void main() { imageStore(img, ivec2(0), vec4(1.0)); }
|
||||
)";
|
||||
|
||||
// The record carries the APPLICATION's format and access verbatim. The bind-format recast -
|
||||
// a GL_RG32F bind is INVALID_VALUE on most non-core formats on Adreno - and the
|
||||
// buffer-texture split view are SERVER-side and stay there, so a client that pre-applied
|
||||
// either of them would be answering a driver question from the wrong side of the boundary.
|
||||
TEST(ImageEmit, TheApplicationsFormatAndAccessTravelUnrecast) {
|
||||
EmitterScope scope;
|
||||
const GLuint program = MakeComputeProgram(kImageCompute);
|
||||
GL::UseProgram(program);
|
||||
const GLuint texture = MakeImageTexture();
|
||||
GL::BindImageTexture(1, texture, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8);
|
||||
|
||||
// THE FIXTURE HAS TO SET UP WHAT THE CASE IS ABOUT, and it is asserted rather than
|
||||
// assumed: the window comes from the program's own image uniforms, so a shader whose
|
||||
// image uniform did not reach the reflection would make every case below pass for the
|
||||
// wrong reason - by emitting nothing at all.
|
||||
const SharedPtr<MG_State::GLState::ProgramObject>& object = Ctx().GetProgramObject(program);
|
||||
ASSERT_TRUE(object);
|
||||
const auto& resolution = MGPipeProgramOpaqueUnitsShared().For(object.get());
|
||||
ASSERT_EQ(resolution.MaxImageUnit, 1)
|
||||
<< "maxUniformLocation=" << object->GetMaxUniformLocation()
|
||||
<< " unit@0=" << object->GetUniformSamplerOrImageUnitIndex(0)
|
||||
<< " linked=" << object->GetLinkStatus();
|
||||
|
||||
// ASSERTED ON THE COUNTER, NOT ON THIS CALL'S RETURN VALUE, and the reason is worth
|
||||
// writing down because it surprised this suite: the glBindImageTexture above ALREADY
|
||||
// reached the validate point and emitted the set, so a direct call afterwards is
|
||||
// correctly suppressed as unchanged. What the case is about is what went out, not who
|
||||
// sent it.
|
||||
Emitter().EmitShaderImages(Ctx());
|
||||
ASSERT_GE(Emitter().ImageSetCount(), 1u);
|
||||
ASSERT_GE(Emitter().LastShaderImages().Count, 2u);
|
||||
const MGPImageView& view = Emitter().LastImageViews()[1];
|
||||
EXPECT_EQ(view.Unit, 1u);
|
||||
EXPECT_FALSE(MGPipeHandleIsNull(view.Res));
|
||||
EXPECT_EQ(view.InternalFormat, static_cast<Uint32>(GL_RGBA8));
|
||||
EXPECT_EQ(view.Access, 1u) << "GL_WRITE_ONLY, folded into the one byte the wire carries";
|
||||
EXPECT_EQ(view.Level, 0u);
|
||||
EXPECT_EQ(view.Layered, 0u);
|
||||
EXPECT_EQ(view.Layer, 0u);
|
||||
GL::UseProgram(0);
|
||||
}
|
||||
|
||||
TEST(ImageEmit, AnAccessModeChangeAloneStillEmitsTheSet) {
|
||||
EmitterScope scope;
|
||||
const GLuint program = MakeComputeProgram(kImageCompute);
|
||||
GL::UseProgram(program);
|
||||
const GLuint texture = MakeImageTexture();
|
||||
GL::BindImageTexture(1, texture, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
Emitter().EmitShaderImages(Ctx());
|
||||
const Uint64 before = Emitter().ImageSetCount();
|
||||
ASSERT_GE(before, 1u);
|
||||
ASSERT_EQ(Emitter().LastImageViews()[1].Access, 0u) << "GL_READ_ONLY";
|
||||
|
||||
// The same texture, the same unit, the same format - only the access mode moves. The
|
||||
// hash has to cover it, or a shader that now writes where it used to read is bound with
|
||||
// the previous barrier and coherence semantics.
|
||||
GL::BindImageTexture(1, texture, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8);
|
||||
Emitter().EmitShaderImages(Ctx());
|
||||
EXPECT_GT(Emitter().ImageSetCount(), before);
|
||||
EXPECT_EQ(Emitter().LastImageViews()[1].Access, 2u);
|
||||
GL::UseProgram(0);
|
||||
}
|
||||
|
||||
TEST(ImageEmit, AnInternalFormatChangeAloneStillEmitsTheSet) {
|
||||
EmitterScope scope;
|
||||
const GLuint program = MakeComputeProgram(kImageCompute);
|
||||
GL::UseProgram(program);
|
||||
const GLuint texture = MakeImageTexture();
|
||||
GL::BindImageTexture(1, texture, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
|
||||
Emitter().EmitShaderImages(Ctx());
|
||||
const Uint64 before = Emitter().ImageSetCount();
|
||||
ASSERT_GE(before, 1u);
|
||||
ASSERT_EQ(Emitter().LastImageViews()[1].InternalFormat, static_cast<Uint32>(GL_RGBA8));
|
||||
|
||||
// The format the shader was built against is live glBindImageTexture state, and the
|
||||
// format-less image bake keys on it: a set suppressed because "the binding did not
|
||||
// move" would leave the server baking against the previous format.
|
||||
GL::BindImageTexture(1, texture, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8UI);
|
||||
Emitter().EmitShaderImages(Ctx());
|
||||
EXPECT_GT(Emitter().ImageSetCount(), before);
|
||||
EXPECT_EQ(Emitter().LastImageViews()[1].InternalFormat, static_cast<Uint32>(GL_RGBA8UI));
|
||||
GL::UseProgram(0);
|
||||
}
|
||||
} // namespace
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
@@ -334,6 +513,9 @@ int main(int argc, char** argv) {
|
||||
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
||||
#else
|
||||
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
MobileGL::Initialize();
|
||||
#endif
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
const int rc = RUN_ALL_TESTS();
|
||||
|
||||
@@ -6,20 +6,23 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// P4a's program family: create/bind/delete_shader_state, set_draw_program,
|
||||
// P4a's program family on the client: create/bind/delete_shader_state, set_draw_program,
|
||||
// set_dispatch_program and set_global_constants.
|
||||
//
|
||||
// THE ONE PIN THAT IS EASIEST TO LOSE AND WORST TO LOSE: set_global_constants' Version is
|
||||
// GetUBOContentVersion(), and ~0u is the BACKENDS' "never uploaded" sentinel - the wrap skips
|
||||
// it - so the client must never emit it. A record carrying the sentinel would tell a backend
|
||||
// that a block it has just been handed was never uploaded.
|
||||
// THE TWO PROPERTIES THIS SUITE EXISTS FOR, and both are invisible from the emitted bytes:
|
||||
// * THE STAGE MASK COMES FROM THE LINKED SNAPSHOT, never from the live attach list.
|
||||
// glAttachShader and glCompileShader take effect only at the NEXT link and neither moves
|
||||
// the link version, so a descriptor built from the attach list describes a program that
|
||||
// does not exist yet - and it would agree with nothing, because the SPIR-V array beside it
|
||||
// is indexed by the snapshot.
|
||||
// * THE EMITTER JOINS AND THE TRACKER DOES NOT. Bit 6's shutter reads GetCurrentProgram()
|
||||
// deliberately and not GetProgramForDraw(), because the tracker must not force a compile
|
||||
// just to answer "did the shader move"; the join belongs to the emitter, which makes the
|
||||
// same call the verb is about to make anyway.
|
||||
//
|
||||
// THE SUITE IS `ProgramEmit`, not `ProgramEmitTest`: the file is XTest.cpp and the suite is X,
|
||||
// this directory's convention, and it is what the gates grep for.
|
||||
// THE SUITE IS `ProgramEmit`, not `ProgramEmitTest`: the file is XTest.cpp and the suite is X.
|
||||
//
|
||||
// THE TARGET AND ITS ctest REGISTRATION ARE THE CONTRACT COMMIT'S; THE CONTENTS ARE NOT: the
|
||||
// applier-side cases are the wire commits' and the emitter-side cases are the client
|
||||
// package's, and neither has to come back to MG_Test/Pipe/CMakeLists.txt to add one.
|
||||
// THE TARGET AND ITS ctest REGISTRATION ARE THE CONTRACT COMMIT'S; THE CONTENTS ARE NOT.
|
||||
//
|
||||
// IT HAS ITS OWN main() for ResourceEmitTest's reason. Every case is a visible SKIP in a pull
|
||||
// build rather than a vanishing test, so `ctest -N` stays name-for-name identical between the
|
||||
@@ -46,12 +49,17 @@
|
||||
#include "Includes.h"
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include "Init.h"
|
||||
#include <MG_Impl/GLImpl/Program/GL_Program.h>
|
||||
#include <MG_Impl/Pipe/ProgramEmit.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Impl/Pipe/Tracker.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
// The applier takes the two artefact structs BY POINTER beside the record, so a case that
|
||||
// drives create_shader_state needs their definitions - the applier's own header deliberately
|
||||
// only forward-declares them.
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifacts.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#endif
|
||||
|
||||
using namespace MobileGL;
|
||||
@@ -161,10 +169,6 @@ TEST(ProgramEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||
EXPECT_EQ(&MGPipeProgramEmitterInstance(), &MGPipeProgramEmitterInstance());
|
||||
EXPECT_TRUE(kMGPipeWiredProgramSubsystem == 0 ||
|
||||
kMGPipeWiredProgramSubsystem == kMGPipeSubsystemPrograms);
|
||||
// The record the applier starts from carries the sentinel, not 0: a program that has never
|
||||
// published a default-uniform-block image must not look like one that published version 0.
|
||||
const MGPipeShaderCsoRecord fresh{};
|
||||
EXPECT_EQ(fresh.GlobalConstantsVersion, ~Uint32{0});
|
||||
#else
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no client emitter in a pull build";
|
||||
#endif
|
||||
@@ -471,6 +475,230 @@ TEST(ProgramEmit, TheProgramRecordSurvivesAMakeCurrentWhileTheThreeBindingsDoNot
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
// G2 requires the pull and push ctest name sets to be identical, name for name.
|
||||
#define MGL_PROGRAM_EMIT_TEST_LIST(X) \
|
||||
X(ProgramEmit, TheStageMaskComesFromTheLinkedSnapshotAndNotTheAttachList) \
|
||||
X(ProgramEmit, TheNeverUploadedSentinelIsNeverEmitted) \
|
||||
X(ProgramEmit, TheEmitterJoinsAndTheTrackerDoesNot) \
|
||||
X(ProgramEmit, AReLinkReIssuesOnTheSameHandle) \
|
||||
X(ProgramEmit, TheDrawAndDispatchProgramsAreTwoIndependentSlots) \
|
||||
X(ProgramEmit, AnUnchangedProgramEmitsNothingAtAll)
|
||||
|
||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||
MGL_PROGRAM_EMIT_TEST_LIST(MGL_DECLARE_PULL_SKIP)
|
||||
#undef MGL_DECLARE_PULL_SKIP
|
||||
#else
|
||||
|
||||
namespace {
|
||||
namespace GL = MobileGL::MG_Impl::GLImpl;
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
using MG_State::GLState::ProgramObject;
|
||||
|
||||
struct EmitterScope {
|
||||
EmitterScope() { Clear(); }
|
||||
~EmitterScope() {
|
||||
GL::UseProgram(0);
|
||||
Clear();
|
||||
}
|
||||
EmitterScope(const EmitterScope&) = delete;
|
||||
EmitterScope& operator=(const EmitterScope&) = delete;
|
||||
|
||||
static void Clear() {
|
||||
MGPipeProgramEmitterInstance().Reset();
|
||||
MGPipeProgramEmitterInstance().ResetCounters();
|
||||
}
|
||||
};
|
||||
|
||||
GLContext& Ctx() { return *MG_State::pGLContext; }
|
||||
MGPipeProgramEmitter& Emitter() { return MGPipeProgramEmitterInstance(); }
|
||||
|
||||
const char* kVs = R"(#version 430 core
|
||||
uniform vec4 u_value;
|
||||
void main() { gl_Position = u_value; }
|
||||
)";
|
||||
const char* kFs = R"(#version 430 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(1.0); }
|
||||
)";
|
||||
const char* kGs = R"(#version 430 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
void main() { gl_Position = vec4(0.0); EmitVertex(); }
|
||||
)";
|
||||
|
||||
GLuint MakeShader(GLenum stage, const char* source) {
|
||||
const GLuint shader = GL::CreateShader(stage);
|
||||
GL::ShaderSource(shader, 1, &source, nullptr);
|
||||
GL::CompileShader(shader);
|
||||
return shader;
|
||||
}
|
||||
|
||||
GLuint MakeVsFsProgram() {
|
||||
const GLuint program = GL::CreateProgram();
|
||||
GL::AttachShader(program, MakeShader(GL_VERTEX_SHADER, kVs));
|
||||
GL::AttachShader(program, MakeShader(GL_FRAGMENT_SHADER, kFs));
|
||||
GL::LinkProgram(program);
|
||||
GLint linked = GL_FALSE;
|
||||
GL::GetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
EXPECT_EQ(linked, GL_TRUE) << "the vertex/fragment program did not link";
|
||||
return program;
|
||||
}
|
||||
|
||||
constexpr Uint32 StageBit(ShaderStage stage) { return Uint32{1} << static_cast<Uint32>(stage); }
|
||||
|
||||
// ProgramObject.h says it in as many words and this is the case that holds it: a stage mask
|
||||
// built from the ATTACH list would describe a program that does not exist yet, because
|
||||
// glAttachShader takes effect only at the next link and does not move the link version.
|
||||
// The SPIR-V array beside the mask is indexed by the same snapshot, so the two halves of
|
||||
// the descriptor agree by construction rather than by care.
|
||||
TEST(ProgramEmit, TheStageMaskComesFromTheLinkedSnapshotAndNotTheAttachList) {
|
||||
EmitterScope scope;
|
||||
const GLuint name = MakeVsFsProgram();
|
||||
const SharedPtr<ProgramObject>& program = Ctx().GetProgramObject(name);
|
||||
ASSERT_TRUE(program);
|
||||
const Uint32 linkedMask = MGPipeStageMaskOf(*program);
|
||||
EXPECT_EQ(linkedMask, StageBit(ShaderStage::Vertex) | StageBit(ShaderStage::Fragment));
|
||||
|
||||
// A third stage is attached and NOT linked. The live attach list now has three
|
||||
// shaders; the mask must not move.
|
||||
GL::AttachShader(name, MakeShader(GL_GEOMETRY_SHADER, kGs));
|
||||
EXPECT_EQ(program->GetAttachedShaders().size(), 3u) << "the attach really has to land";
|
||||
EXPECT_EQ(MGPipeStageMaskOf(*program), linkedMask)
|
||||
<< "glAttachShader takes effect at the NEXT link and moves no link version";
|
||||
|
||||
GL::LinkProgram(name);
|
||||
GLint linked = GL_FALSE;
|
||||
GL::GetProgramiv(name, GL_LINK_STATUS, &linked);
|
||||
if (linked == GL_TRUE) {
|
||||
EXPECT_EQ(MGPipeStageMaskOf(*program), linkedMask | StageBit(ShaderStage::Geometry))
|
||||
<< "and after the relink the snapshot really does carry it";
|
||||
}
|
||||
}
|
||||
|
||||
// D-H6. ~0u is the backends' "never uploaded" sentinel and the frontend's own wrap skips
|
||||
// it; the client must never put it on the wire either, or a server would read its own
|
||||
// record as "nothing has ever been uploaded here" and re-upload for ever.
|
||||
//
|
||||
// PINNED AS A PREDICATE rather than by driving the counter to ~0u, and the reason is
|
||||
// written down instead of hidden: reaching that value takes four billion
|
||||
// MarkUBOContentDirty calls, which is not a test. The predicate is the thing
|
||||
// EmitGlobalConstants consults, so pinning it pins the behaviour, and a deletion of the
|
||||
// guard is a compile error here.
|
||||
TEST(ProgramEmit, TheNeverUploadedSentinelIsNeverEmitted) {
|
||||
EmitterScope scope;
|
||||
EXPECT_EQ(kMGPipeGlobalConstantsNeverUploaded, ~Uint32{0});
|
||||
EXPECT_FALSE(MGPipeGlobalConstantsVersionIsEmittable(kMGPipeGlobalConstantsNeverUploaded));
|
||||
EXPECT_TRUE(MGPipeGlobalConstantsVersionIsEmittable(0u));
|
||||
EXPECT_TRUE(MGPipeGlobalConstantsVersionIsEmittable(1u));
|
||||
EXPECT_TRUE(MGPipeGlobalConstantsVersionIsEmittable(~Uint32{0} - 1u));
|
||||
|
||||
// And the live path never produces it either: whatever the frontend's counter is at,
|
||||
// the record the emitter last built carries an emittable version.
|
||||
const GLuint name = MakeVsFsProgram();
|
||||
GL::UseProgram(name);
|
||||
const SharedPtr<ProgramObject>& program = Ctx().GetProgramObject(name);
|
||||
ASSERT_TRUE(program);
|
||||
program->MarkUBOContentDirty();
|
||||
if (Emitter().EmitGlobalConstants(Ctx()) > 0u) {
|
||||
EXPECT_TRUE(MGPipeGlobalConstantsVersionIsEmittable(Emitter().LastGlobalConstants().Version));
|
||||
EXPECT_EQ(Emitter().LastGlobalConstants().Version, program->GetUBOContentVersion());
|
||||
EXPECT_EQ(Emitter().LastGlobalConstants().Blob.Size, 0u)
|
||||
<< "the one Blob rule: a monolith emission does not declare its blob";
|
||||
}
|
||||
}
|
||||
|
||||
// D-H4, and it is a statement about the TRACKER as much as about the emitter: Update() may
|
||||
// not move a program's link completeness in either direction, because answering "did the
|
||||
// shader move" from a version counter is what keeps a compile off the dirty walk. The
|
||||
// emitter is where the join belongs, and it is the same GetProgramForDraw() the verb is
|
||||
// about to make anyway.
|
||||
TEST(ProgramEmit, TheEmitterJoinsAndTheTrackerDoesNot) {
|
||||
EmitterScope scope;
|
||||
const GLuint name = MakeVsFsProgram();
|
||||
GL::UseProgram(name);
|
||||
const SharedPtr<ProgramObject>& program = Ctx().GetProgramObject(name);
|
||||
ASSERT_TRUE(program);
|
||||
|
||||
const Bool completeBefore = program->IsLinkComplete();
|
||||
MGPipeTrackerInstance().Update(Ctx(), MGPipeVerbClass::kDraw);
|
||||
EXPECT_EQ(program->IsLinkComplete(), completeBefore)
|
||||
<< "the dirty walk must not force a compile, in either direction";
|
||||
|
||||
Emitter().EmitShaderState(Ctx());
|
||||
EXPECT_TRUE(program->IsLinkComplete()) << "the emitter joins, because the verb would";
|
||||
MGPipeTrackerInstance().Reset();
|
||||
}
|
||||
|
||||
TEST(ProgramEmit, AReLinkReIssuesOnTheSameHandle) {
|
||||
EmitterScope scope;
|
||||
const GLuint name = MakeVsFsProgram();
|
||||
GL::UseProgram(name);
|
||||
const SharedPtr<ProgramObject>& program = Ctx().GetProgramObject(name);
|
||||
ASSERT_TRUE(program);
|
||||
|
||||
ASSERT_GT(Emitter().EmitShaderState(Ctx()), 0u);
|
||||
ASSERT_EQ(Emitter().CreateCount(), 1u);
|
||||
const MGPipeHandle cso = Emitter().LastProgramDesc().Cso;
|
||||
EXPECT_FALSE(MGPipeHandleIsNull(cso));
|
||||
EXPECT_EQ(Emitter().DrawCso(), cso);
|
||||
|
||||
// Nothing moved: no second record, no second bind.
|
||||
Emitter().EmitShaderState(Ctx());
|
||||
EXPECT_EQ(Emitter().CreateCount(), 1u);
|
||||
EXPECT_EQ(Emitter().BindCount(), 1u);
|
||||
|
||||
const Uint32 linkVersionBefore = program->GetLinkVersion();
|
||||
GL::LinkProgram(name);
|
||||
ASSERT_NE(program->GetLinkVersion(), linkVersionBefore) << "the relink really has to move it";
|
||||
|
||||
Emitter().EmitShaderState(Ctx());
|
||||
EXPECT_EQ(Emitter().CreateCount(), 2u);
|
||||
// THE SAME HANDLE. Gen increments only on slot reuse and never on a respecify, so a
|
||||
// relinked program is the same GL object and the server's twin table must not be asked
|
||||
// to mint a second identity for it.
|
||||
EXPECT_EQ(Emitter().LastProgramDesc().Cso, cso);
|
||||
EXPECT_EQ(Emitter().BindCount(), 1u) << "and a re-issue on the bound handle is not a rebind";
|
||||
}
|
||||
|
||||
// Two calls because the frontend has two joins and two PipeInputs slots. With a plain
|
||||
// glUseProgram they name one object, and the record has to say so rather than leaving the
|
||||
// server to guess which of the two a verb meant.
|
||||
TEST(ProgramEmit, TheDrawAndDispatchProgramsAreTwoIndependentSlots) {
|
||||
EmitterScope scope;
|
||||
const GLuint name = MakeVsFsProgram();
|
||||
GL::UseProgram(name);
|
||||
ASSERT_GT(Emitter().EmitShaderState(Ctx()), 0u);
|
||||
EXPECT_EQ(Emitter().DrawCso(), Emitter().DispatchCso());
|
||||
EXPECT_EQ(Emitter().BoundCso(), Emitter().DrawCso());
|
||||
EXPECT_EQ(Emitter().DrawProgramSetCount(), 1u);
|
||||
EXPECT_EQ(Emitter().DispatchProgramSetCount(), 1u);
|
||||
|
||||
// A null handle is legal and means exactly "nothing bound".
|
||||
GL::UseProgram(0);
|
||||
Emitter().EmitShaderState(Ctx());
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(Emitter().DrawCso()));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(Emitter().DispatchCso()));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(Emitter().BoundCso()));
|
||||
EXPECT_EQ(Emitter().DrawProgramSetCount(), 2u);
|
||||
}
|
||||
|
||||
TEST(ProgramEmit, AnUnchangedProgramEmitsNothingAtAll) {
|
||||
EmitterScope scope;
|
||||
const GLuint name = MakeVsFsProgram();
|
||||
GL::UseProgram(name);
|
||||
ASSERT_GT(Emitter().EmitShaderState(Ctx()), 0u);
|
||||
// The version-first skip: nothing hashed, nothing copied, nothing emitted, and the
|
||||
// return value is the bytes that went on the wire - zero.
|
||||
EXPECT_EQ(Emitter().EmitShaderState(Ctx()), 0u);
|
||||
EXPECT_EQ(Emitter().CreateCount(), 1u);
|
||||
EXPECT_EQ(Emitter().BindCount(), 1u);
|
||||
EXPECT_EQ(Emitter().DrawProgramSetCount(), 1u);
|
||||
}
|
||||
} // namespace
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
@@ -482,6 +710,9 @@ int main(int argc, char** argv) {
|
||||
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
||||
#else
|
||||
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
MobileGL::Initialize();
|
||||
#endif
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
const int rc = RUN_ALL_TESTS();
|
||||
|
||||
@@ -53,8 +53,16 @@
|
||||
#include "Includes.h"
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <cstring>
|
||||
|
||||
#include "Init.h"
|
||||
#include <MG_Impl/GLImpl/Program/GL_Program.h>
|
||||
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
|
||||
#include <MG_Impl/Pipe/SamplerEmit.h>
|
||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#endif
|
||||
|
||||
using namespace MobileGL;
|
||||
@@ -524,6 +532,365 @@ TEST(SamplerEmit, AMakeCurrentTakesTheUnitSetsAndLeavesTheCsoAndViewRecordsStand
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
// G2 requires the pull and push ctest name sets to be identical, name for name, so every
|
||||
// push-only case is present here and SKIPS rather than being absent.
|
||||
#define MGL_SAMPLER_EMIT_TEST_LIST(X) \
|
||||
X(SamplerEmit, EverySamplerParameterFieldSurvivesTheBlobConversion) \
|
||||
X(SamplerEmit, PaddingCannotChangeTheHash) \
|
||||
X(SamplerEmit, TwoIdenticalSamplersShareOneCso) \
|
||||
X(SamplerEmit, ABorderColorFormChangeAloneMintsANewCso) \
|
||||
X(SamplerEmit, AHashCollisionDoesNotAliasTwoSamplerStates) \
|
||||
X(SamplerEmit, AViewIsReIssuedOnTheSameHandleWhenItsRestrictionsMove) \
|
||||
X(SamplerEmit, AnUnchangedTextureReIssuesNothing) \
|
||||
X(SamplerEmit, OnlyTheProgramResolvedUnitsAreEmitted) \
|
||||
X(SamplerEmit, AnUnchangedSetEmitsNothing) \
|
||||
X(SamplerEmit, ARedundantRebindOfTheSameSamplerEmitsNothing)
|
||||
|
||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||
MGL_SAMPLER_EMIT_TEST_LIST(MGL_DECLARE_PULL_SKIP)
|
||||
#undef MGL_DECLARE_PULL_SKIP
|
||||
#else
|
||||
|
||||
namespace {
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
using MG_State::GLState::ITextureObject;
|
||||
using MG_State::GLState::SamplerObject;
|
||||
|
||||
// AN RAII SCOPE RATHER THAN A gtest FIXTURE, for VertexInputEmitTest's reason: both gates
|
||||
// grep `ctest -R 'SamplerEmit\.'`, a TEST_F files its cases under the FIXTURE's name, and
|
||||
// gtest refuses to mix TEST and TEST_F under one suite name.
|
||||
//
|
||||
// UNLIKE VertexInputEmitTest's, this scope does NOT replace pGLContext: half these cases
|
||||
// need a really linked program, so the process is initialised once in main() and the cases
|
||||
// share that context, each using GL names of its own. What the scope does is put the
|
||||
// emitter, its counters and the suppressor back to a known state.
|
||||
struct EmitterScope {
|
||||
EmitterScope() { Clear(); }
|
||||
~EmitterScope() { Clear(); }
|
||||
EmitterScope(const EmitterScope&) = delete;
|
||||
EmitterScope& operator=(const EmitterScope&) = delete;
|
||||
|
||||
static void Clear() {
|
||||
MGPipeSamplerEmitterInstance().Reset();
|
||||
MGPipeSamplerEmitterInstance().ResetCounters();
|
||||
MGPipeSamplerCsoCacheInstance().ResetForTest();
|
||||
MGPipeSamplerCsoCacheInstance().ResetCounters();
|
||||
MGPipeSetHashSuppressorInstance().InvalidateAll();
|
||||
}
|
||||
};
|
||||
|
||||
GLContext& Ctx() { return *MG_State::pGLContext; }
|
||||
MGPipeSamplerEmitter& Emitter() { return MGPipeSamplerEmitterInstance(); }
|
||||
MGPipeSamplerCsoCache& Cache() { return MGPipeSamplerCsoCacheInstance(); }
|
||||
|
||||
// Every field of SamplerParameters set to a value that is not its default, so a conversion
|
||||
// that dropped one is caught by the field's own EXPECT rather than by a count.
|
||||
SamplerParameters DistinctParameters() {
|
||||
SamplerParameters params{};
|
||||
params.wrapS = SamplerWrapMode::ClampToBorder;
|
||||
params.wrapT = SamplerWrapMode::MirroredRepeat;
|
||||
params.wrapR = SamplerWrapMode::MirrorClampToEdge;
|
||||
params.minFilter = SamplerFilterMode::Linear;
|
||||
params.magFilter = SamplerFilterMode::Nearest;
|
||||
params.mipmapMode = SamplerMipmapMode::Nearest;
|
||||
params.minLod = -3.5f;
|
||||
params.maxLod = 7.25f;
|
||||
params.lodBias = 1.5f;
|
||||
params.maxAnisotropy = 2.0f;
|
||||
params.compareFunc = SamplerCompareFunc::Greater;
|
||||
params.compareMode = SamplerCompareMode::CompareToTexture;
|
||||
params.borderColor = {0.25f, 0.5f, 0.75f, 1.0f};
|
||||
params.borderColorI = {-1, 2, -3, 4};
|
||||
params.borderColorUI = {5u, 6u, 7u, 8u};
|
||||
params.borderColorForm = BorderColorForm::Int;
|
||||
return params;
|
||||
}
|
||||
|
||||
// A 2D texture with a single level and a mipmap mode of None, so it is mipmap-complete for
|
||||
// its filter and therefore actually reaches the emitted set. A texture that samples as
|
||||
// incomplete is dropped to a null view on purpose, which is the resolution DirectGLES
|
||||
// performs by leaving the native target unbound.
|
||||
const SharedPtr<ITextureObject>& MakeCompleteTexture(GLuint& name, Int width) {
|
||||
namespace GL = MobileGL::MG_Impl::GLImpl;
|
||||
GL::GenTextures(1, &name);
|
||||
GL::BindTexture(GL_TEXTURE_2D, name);
|
||||
GL::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, width, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
GL::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
GL::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
return Ctx().GetTextureObject(name);
|
||||
}
|
||||
|
||||
void BindTextureToUnit(Uint32 unit, const SharedPtr<ITextureObject>& texture) {
|
||||
Ctx().GetTextureUnitObject(static_cast<Int>(unit))
|
||||
.GetBindingSlot(TextureTarget::Texture2D)
|
||||
.Bind(texture);
|
||||
Ctx().NoteTextureUnitTouched(static_cast<Int>(unit));
|
||||
}
|
||||
|
||||
// ============================ D-F1: the CSO cache ============================
|
||||
|
||||
// G6 for this family: every one of SamplerParameters' sixteen members survives the
|
||||
// canonical copy the cache hashes and confirms over. Each member is its own EXPECT naming
|
||||
// that member, which is what G7's scripted control needs - it stops the conversion copying
|
||||
// borderColorForm and expects this case to go red NAMING it.
|
||||
TEST(SamplerEmit, EverySamplerParameterFieldSurvivesTheBlobConversion) {
|
||||
EmitterScope scope;
|
||||
const SamplerParameters source = DistinctParameters();
|
||||
SamplerParameters canon;
|
||||
MGPipeCanonicaliseSamplerParameters(source, canon);
|
||||
|
||||
EXPECT_EQ(canon.wrapS, source.wrapS);
|
||||
EXPECT_EQ(canon.wrapT, source.wrapT);
|
||||
EXPECT_EQ(canon.wrapR, source.wrapR);
|
||||
EXPECT_EQ(canon.minFilter, source.minFilter);
|
||||
EXPECT_EQ(canon.magFilter, source.magFilter);
|
||||
EXPECT_EQ(canon.mipmapMode, source.mipmapMode);
|
||||
EXPECT_EQ(canon.minLod, source.minLod);
|
||||
EXPECT_EQ(canon.maxLod, source.maxLod);
|
||||
EXPECT_EQ(canon.lodBias, source.lodBias);
|
||||
EXPECT_EQ(canon.maxAnisotropy, source.maxAnisotropy);
|
||||
EXPECT_EQ(canon.compareFunc, source.compareFunc);
|
||||
EXPECT_EQ(canon.compareMode, source.compareMode);
|
||||
EXPECT_EQ(canon.borderColor, source.borderColor);
|
||||
EXPECT_EQ(canon.borderColorI, source.borderColorI);
|
||||
EXPECT_EQ(canon.borderColorUI, source.borderColorUI);
|
||||
EXPECT_EQ(canon.borderColorForm, source.borderColorForm)
|
||||
<< "borderColorForm decides which of glSamplerParameterIiv / fv applies, and the "
|
||||
"three representations are always numerically populated, so the value alone "
|
||||
"cannot say";
|
||||
}
|
||||
|
||||
// THE PADDING TRAP. SamplerParameters is 100 bytes and its members occupy 97 of them, so
|
||||
// bytes 97..99 are padding no writer ever touches. A cache that hashed the object's own
|
||||
// bytes would read them, miss on every probe and mint a fresh CSO per call - a 256-entry
|
||||
// cache with a hit rate of zero that nobody notices, because the pixels are right.
|
||||
TEST(SamplerEmit, PaddingCannotChangeTheHash) {
|
||||
EmitterScope scope;
|
||||
SamplerParameters clean = DistinctParameters();
|
||||
SamplerParameters dirty = clean;
|
||||
auto* bytes = reinterpret_cast<Uint8*>(&dirty);
|
||||
// Written THROUGH A BYTE POINTER, past the last member and inside the object, which is
|
||||
// the only way to make the difference this case is about.
|
||||
for (SizeT i = 97; i < sizeof(SamplerParameters); ++i) bytes[i] = static_cast<Uint8>(0xA5 + i);
|
||||
|
||||
SamplerParameters canonClean;
|
||||
SamplerParameters canonDirty;
|
||||
MGPipeCanonicaliseSamplerParameters(clean, canonClean);
|
||||
MGPipeCanonicaliseSamplerParameters(dirty, canonDirty);
|
||||
EXPECT_EQ(MGPipeHashSamplerParameters(canonClean), MGPipeHashSamplerParameters(canonDirty));
|
||||
// And the CONFIRM, not only the hash: the cache reuses a handle on a memcmp over these
|
||||
// same canonical bytes, so the padding has to be deterministically zero in both.
|
||||
EXPECT_EQ(std::memcmp(&canonClean, &canonDirty, sizeof(SamplerParameters)), 0);
|
||||
|
||||
Uint64 payload = 0;
|
||||
const MGPipeHandle first = Cache().Acquire(clean, payload);
|
||||
const MGPipeHandle second = Cache().Acquire(dirty, payload);
|
||||
EXPECT_EQ(first, second) << "uninitialised padding must not mint a second CSO";
|
||||
EXPECT_EQ(Cache().GetCounters().Mints, 1u);
|
||||
EXPECT_EQ(Cache().GetCounters().Hits, 1u);
|
||||
}
|
||||
|
||||
TEST(SamplerEmit, TwoIdenticalSamplersShareOneCso) {
|
||||
EmitterScope scope;
|
||||
Uint64 payload = 0;
|
||||
const SamplerParameters params = DistinctParameters();
|
||||
const MGPipeHandle a = Cache().Acquire(params, payload);
|
||||
const MGPipeHandle b = Cache().Acquire(params, payload);
|
||||
EXPECT_FALSE(MGPipeHandleIsNull(a));
|
||||
EXPECT_EQ(a, b);
|
||||
EXPECT_EQ(Cache().Size(), 1u);
|
||||
EXPECT_EQ(Cache().GetCounters().Mints, 1u);
|
||||
// A SamplerObject is a pure value with no driver-side per-object binding state, so two
|
||||
// identical samplers really can share one CSO and one server-side twin. That is what
|
||||
// makes content addressing right for this kind and wrong for vertex elements.
|
||||
EXPECT_GT(payload, 0u);
|
||||
}
|
||||
|
||||
TEST(SamplerEmit, ABorderColorFormChangeAloneMintsANewCso) {
|
||||
EmitterScope scope;
|
||||
Uint64 payload = 0;
|
||||
SamplerParameters params = DistinctParameters();
|
||||
const MGPipeHandle asInt = Cache().Acquire(params, payload);
|
||||
params.borderColorForm = BorderColorForm::Uint;
|
||||
const MGPipeHandle asUint = Cache().Acquire(params, payload);
|
||||
EXPECT_NE(asInt, asUint) << "the form is the only thing that says which driver entry "
|
||||
"point applies; the three colour values did not move";
|
||||
EXPECT_EQ(Cache().GetCounters().Mints, 2u);
|
||||
}
|
||||
|
||||
// The memcmp confirm exists because a bare 64-bit equality would alias two DIFFERENT
|
||||
// sampler states onto one CSO - silent wrong filtering with no gate that can see it. Two
|
||||
// states whose hashes happen to agree cannot be manufactured here, so the property is
|
||||
// driven the other way: two states that differ in ONE field never share a handle, however
|
||||
// small the difference is.
|
||||
TEST(SamplerEmit, AHashCollisionDoesNotAliasTwoSamplerStates) {
|
||||
EmitterScope scope;
|
||||
Uint64 payload = 0;
|
||||
SamplerParameters base = DistinctParameters();
|
||||
const MGPipeHandle first = Cache().Acquire(base, payload);
|
||||
base.maxLod = base.maxLod + 0.0009765625f; // one representable step, nothing else moves
|
||||
const MGPipeHandle second = Cache().Acquire(base, payload);
|
||||
EXPECT_NE(first, second);
|
||||
EXPECT_EQ(Cache().GetCounters().Collisions, 0u)
|
||||
<< "a genuine collision would have been rejected by the memcmp, not accepted";
|
||||
}
|
||||
|
||||
// ============================ D-F2: the sampler view ============================
|
||||
|
||||
TEST(SamplerEmit, AnUnchangedTextureReIssuesNothing) {
|
||||
EmitterScope scope;
|
||||
GLuint name = 0;
|
||||
const SharedPtr<ITextureObject> texture = MakeCompleteTexture(name, 4);
|
||||
ASSERT_TRUE(texture);
|
||||
Uint64 payload = 0;
|
||||
const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::Texture, texture->GetLifetimeId());
|
||||
const MGPipeHandle view = Emitter().AcquireSamplerView(*texture, handle, payload);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(view));
|
||||
EXPECT_EQ(Emitter().ViewCreateCount(), 1u);
|
||||
// THE VERSION-FIRST SKIP: nothing moved, so nothing is hashed, copied or emitted.
|
||||
EXPECT_EQ(Emitter().AcquireSamplerView(*texture, handle, payload), view);
|
||||
EXPECT_EQ(Emitter().ViewCreateCount(), 1u);
|
||||
}
|
||||
|
||||
TEST(SamplerEmit, AViewIsReIssuedOnTheSameHandleWhenItsRestrictionsMove) {
|
||||
EmitterScope scope;
|
||||
namespace GL = MobileGL::MG_Impl::GLImpl;
|
||||
GLuint name = 0;
|
||||
const SharedPtr<ITextureObject> texture = MakeCompleteTexture(name, 4);
|
||||
ASSERT_TRUE(texture);
|
||||
Uint64 payload = 0;
|
||||
const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::Texture, texture->GetLifetimeId());
|
||||
const MGPipeHandle view = Emitter().AcquireSamplerView(*texture, handle, payload);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(view));
|
||||
ASSERT_EQ(Emitter().ViewCreateCount(), 1u);
|
||||
const Uint64 shapeBefore = texture->GetShapeVersion();
|
||||
|
||||
GL::BindTexture(GL_TEXTURE_2D, name);
|
||||
GL::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
ASSERT_NE(texture->GetShapeVersion(), shapeBefore) << "the storage really has to move";
|
||||
|
||||
// THE SAME HANDLE, a second record. Gen increments only on slot reuse and never on a
|
||||
// respecify, so re-issuing on the same handle is legal and is what keeps a server's
|
||||
// twin table from minting a second identity for one texture.
|
||||
const MGPipeHandle reissued = Emitter().AcquireSamplerView(*texture, handle, payload);
|
||||
EXPECT_EQ(reissued, view);
|
||||
EXPECT_EQ(Emitter().ViewCreateCount(), 2u);
|
||||
EXPECT_EQ(Emitter().LastCreatedView().Cso, view);
|
||||
EXPECT_EQ(Emitter().LastCreatedView().Texture, handle);
|
||||
EXPECT_EQ(Emitter().LastCreatedView().Target, static_cast<Uint8>(TextureTarget::Texture2D));
|
||||
EXPECT_EQ(Emitter().LastCreatedView().InternalFormat, static_cast<Uint32>(texture->GetFormat()));
|
||||
// An ordinary texture carries no view restriction, and zero is that statement rather
|
||||
// than a second spelling of it: glTextureView always writes NumLevels >= 1.
|
||||
EXPECT_EQ(Emitter().LastCreatedView().NumLevels, 0u);
|
||||
EXPECT_EQ(Emitter().LastCreatedView().NumLayers, 0u);
|
||||
}
|
||||
|
||||
// ============================ D-G: the two unit sets ============================
|
||||
|
||||
Uint MakeSamplerProgram() {
|
||||
namespace GL = MobileGL::MG_Impl::GLImpl;
|
||||
static const char* kVs = "#version 330 core\nvoid main(){ gl_Position = vec4(0.0); }\n";
|
||||
static const char* kFs =
|
||||
"#version 330 core\n"
|
||||
"uniform sampler2D sampled;\n"
|
||||
"out vec4 color;\n"
|
||||
"void main(){ color = texture(sampled, vec2(0.0)); }\n";
|
||||
const Uint program = GL::CreateProgram();
|
||||
const Uint vs = GL::CreateShader(GL_VERTEX_SHADER);
|
||||
GL::ShaderSource(vs, 1, &kVs, nullptr);
|
||||
GL::CompileShader(vs);
|
||||
GL::AttachShader(program, vs);
|
||||
const Uint fs = GL::CreateShader(GL_FRAGMENT_SHADER);
|
||||
GL::ShaderSource(fs, 1, &kFs, nullptr);
|
||||
GL::CompileShader(fs);
|
||||
GL::AttachShader(program, fs);
|
||||
GL::LinkProgram(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
// ARCHITECTURE's third merge rule: the client emits the PROGRAM-RESOLVED set. A unit the
|
||||
// shader does not sample carries no view, whatever is bound to it - which is exactly the
|
||||
// gallium one-view-per-slot resolved form, and exactly what DirectGLES arrives at by
|
||||
// asking the program which of a unit's aliased bindings is the sampled one.
|
||||
TEST(SamplerEmit, OnlyTheProgramResolvedUnitsAreEmitted) {
|
||||
EmitterScope scope;
|
||||
namespace GL = MobileGL::MG_Impl::GLImpl;
|
||||
const Uint program = MakeSamplerProgram();
|
||||
GLint linked = 0;
|
||||
GL::GetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
ASSERT_EQ(linked, GL_TRUE);
|
||||
GL::UseProgram(program);
|
||||
const GLint location = GL::GetUniformLocation(program, "sampled");
|
||||
ASSERT_GE(location, 0);
|
||||
GL::Uniform1i(location, 3);
|
||||
|
||||
GLuint sampledName = 0;
|
||||
GLuint unsampledName = 0;
|
||||
const SharedPtr<ITextureObject> sampled = MakeCompleteTexture(sampledName, 4);
|
||||
const SharedPtr<ITextureObject> unsampled = MakeCompleteTexture(unsampledName, 4);
|
||||
BindTextureToUnit(3, sampled);
|
||||
BindTextureToUnit(5, unsampled);
|
||||
|
||||
ASSERT_GT(Emitter().EmitSamplerViews(Ctx()), 0u);
|
||||
ASSERT_EQ(Emitter().ViewSetCount(), 1u);
|
||||
ASSERT_GE(Emitter().LastSamplerViews().Count, 6u);
|
||||
EXPECT_EQ(Emitter().LastSamplerViews().Start, 0u);
|
||||
|
||||
const MGPBoundView& resolved = Emitter().LastBoundViews()[3];
|
||||
EXPECT_EQ(resolved.Unit, 3u);
|
||||
EXPECT_FALSE(MGPipeHandleIsNull(resolved.Texture)) << "unit 3 is what the shader samples";
|
||||
EXPECT_FALSE(MGPipeHandleIsNull(resolved.View));
|
||||
|
||||
const MGPBoundView& ignored = Emitter().LastBoundViews()[5];
|
||||
EXPECT_EQ(ignored.Unit, 5u);
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(ignored.Texture))
|
||||
<< "unit 5 has a texture bound but no sampler uniform resolves to it";
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(ignored.View));
|
||||
|
||||
GL::UseProgram(0);
|
||||
}
|
||||
|
||||
TEST(SamplerEmit, AnUnchangedSetEmitsNothing) {
|
||||
EmitterScope scope;
|
||||
GLuint name = 0;
|
||||
const SharedPtr<ITextureObject> texture = MakeCompleteTexture(name, 4);
|
||||
BindTextureToUnit(1, texture);
|
||||
Emitter().EmitSamplerViews(Ctx());
|
||||
const Uint64 after = Emitter().ViewSetCount();
|
||||
// The suppressor's whole job: an unchanged resolved set is not sent again. Without it
|
||||
// this would be a several-hundred-byte variable-length record per batch, because a
|
||||
// redundant re-bind moves the bind generation and the dirty bit with it.
|
||||
Emitter().EmitSamplerViews(Ctx());
|
||||
EXPECT_EQ(Emitter().ViewSetCount(), after);
|
||||
}
|
||||
|
||||
TEST(SamplerEmit, ARedundantRebindOfTheSameSamplerEmitsNothing) {
|
||||
EmitterScope scope;
|
||||
const SharedPtr<MG_State::GLState::SamplerObject>& sampler = Ctx().CreateSamplerObject(4131);
|
||||
ASSERT_TRUE(sampler);
|
||||
Ctx().GetTextureUnitObject(2).SetSamplerObject(sampler);
|
||||
Ctx().NoteTextureUnitTouched(2);
|
||||
|
||||
ASSERT_GT(Emitter().EmitSamplerStates(Ctx()), 0u);
|
||||
ASSERT_EQ(Emitter().StateSetCount(), 1u);
|
||||
ASSERT_GE(Emitter().LastSamplerStates().Count, 3u);
|
||||
EXPECT_FALSE(MGPipeHandleIsNull(Emitter().LastSamplerStateHandles()[2]));
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(Emitter().LastSamplerStateHandles()[0]))
|
||||
<< "a unit with no sampler object carries the null handle, and the texture's "
|
||||
"built-in sampler then applies exactly as today";
|
||||
|
||||
// 26.2's idiom: the same sampler object re-bound at every texture-unit switch. The bind
|
||||
// generation moves, so the dirty bit fires and this emitter runs - and emits nothing.
|
||||
Ctx().BumpTextureBindGeneration();
|
||||
Emitter().EmitSamplerStates(Ctx());
|
||||
EXPECT_EQ(Emitter().StateSetCount(), 1u);
|
||||
EXPECT_EQ(Cache().GetCounters().Mints, 1u) << "and it mints no second CSO either";
|
||||
}
|
||||
} // namespace
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
@@ -535,6 +902,11 @@ int main(int argc, char** argv) {
|
||||
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
||||
#else
|
||||
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// ONE process-wide context for the whole suite, because half these cases need a really
|
||||
// linked program and glslang lives behind this call. Each case uses GL names of its own.
|
||||
MobileGL::Initialize();
|
||||
#endif
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
const int rc = RUN_ALL_TESTS();
|
||||
|
||||
Reference in New Issue
Block a user