mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 00:28:31 +09:00
[Test] (MG_Test): one case per GPU-write row, the lifted live-host-writes pin from both sides, the widened INVALIDATE_RANGE edge and a non-zero persistent-map-push
This commit is contained in:
@@ -23,5 +23,33 @@ if (MSVC)
|
||||
target_compile_options(BufferTest PRIVATE /Zc:preprocessor)
|
||||
endif()
|
||||
|
||||
# P5 b1: the split buffer side - the client GPU-write set, the persistent-map block push and
|
||||
# tier 1 of the flush ladder. Built in EVERY configuration on purpose; the cases that need
|
||||
# MOBILEGL_BUILD_DISAGGREGATED skip elsewhere rather than disappearing, so the ctest name set
|
||||
# is the same in the pull and the push lanes (G2) and build-split removes none of them (G14).
|
||||
add_executable(
|
||||
SplitBufferTest
|
||||
SplitBufferTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(SplitBufferTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
${MGL_ROOT}/3rdparty/xxHash
|
||||
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
|
||||
${MGL_ROOT}/3rdparty/SPIRV-Reflect
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
SplitBufferTest PRIVATE
|
||||
GTest::gtest_main
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
if (MSVC)
|
||||
target_compile_options(SplitBufferTest PRIVATE /Zc:preprocessor)
|
||||
endif()
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(BufferTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(SplitBufferTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
// MobileGL - MobileGL/MG_Test/Buffer/SplitBufferTest.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
|
||||
|
||||
// P5 (b1): the buffer side of the split - the client-side conservative GPU-write set, the
|
||||
// block-granularity persistent-map push, and tier 1 of the flush ladder.
|
||||
//
|
||||
// EVERY CASE IS COMPILED IN ALL FOUR BUILDS AND SKIPS OUTSIDE build-split, deliberately. The
|
||||
// names then exist identically in the pull and the push lane (G2 stays at 0 diff lines) and
|
||||
// build-split adds none of its own (G14 stays at 0 removed), while a lane that cannot run a
|
||||
// case says so instead of quietly not having it.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <Config.h>
|
||||
#include <MG_State/GLState/BufferState/BufferObject.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
|
||||
#include <MG_State/GLState/TextureState/TextureState.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Backend/DirectGLES/Managers.h>
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Remote/Client/GpuWritePending.h>
|
||||
#include <MG_Remote/Client/PersistentMapTracker.h>
|
||||
#endif
|
||||
|
||||
using namespace MobileGL;
|
||||
|
||||
namespace {
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
using MG_State::GLState::BufferObject;
|
||||
using MG_Remote::Client::GpuWriteProducer;
|
||||
using MG_Remote::Client::PersistentMapTracker;
|
||||
|
||||
// Everything in this package is gated on `Transport != Monolith`, so every case has to
|
||||
// put the process into a split configuration and put it back. A fixture rather than a
|
||||
// lambda because the tracker is a process-wide singleton and a case that left an entry in
|
||||
// it would poison the next one through a raw pointer to a destroyed buffer - which is
|
||||
// exactly the failure mode the tracker's own Forget() exists to prevent.
|
||||
class SplitBufferSet : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
m_transport = MG_Config::Transport;
|
||||
m_blockKb = MG_Config::Ipc.PersistentBlockKb;
|
||||
m_adoptTier = MG_Config::Ipc.AdoptTier;
|
||||
m_pipeStats = MG_Config::Features.PipeStats;
|
||||
m_context = Move(MG_State::pGLContext);
|
||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
||||
MG_Config::Ipc.PersistentBlockKb = 64;
|
||||
MG_Config::Ipc.AdoptTier = 2;
|
||||
MG_Config::Features.PipeStats = true;
|
||||
MG_Util::PipeStats::Init();
|
||||
PersistentMapTracker::Instance().ClearForTest();
|
||||
MG_Remote::Client::ResetProducerMarkCountsForTest();
|
||||
}
|
||||
void TearDown() override {
|
||||
PersistentMapTracker::Instance().ClearForTest();
|
||||
MG_State::pGLContext = Move(m_context);
|
||||
MG_Config::Transport = m_transport;
|
||||
MG_Config::Ipc.PersistentBlockKb = m_blockKb;
|
||||
MG_Config::Ipc.AdoptTier = m_adoptTier;
|
||||
MG_Config::Features.PipeStats = m_pipeStats;
|
||||
MG_Util::PipeStats::Init();
|
||||
}
|
||||
|
||||
static SharedPtr<BufferObject> MakeBuffer(Uint index, SizeT size) {
|
||||
auto buffer = MakeShared<BufferObject>(index);
|
||||
buffer->Respecify(size, nullptr);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
MG_Config::TransportMode m_transport = MG_Config::TransportMode::Monolith;
|
||||
Uint32 m_blockKb = 64;
|
||||
Uint32 m_adoptTier = 2;
|
||||
Bool m_pipeStats = false;
|
||||
UniquePtr<MG_State::GLState::GLContext> m_context;
|
||||
};
|
||||
#endif
|
||||
|
||||
#define MGL_SPLIT_ONLY_OR_SKIP() \
|
||||
do { \
|
||||
GTEST_SKIP() << "the client-side GPU-write set and the persistent-map push exist only in " \
|
||||
"a MOBILEGL_BUILD_DISAGGREGATED build"; \
|
||||
} while (0)
|
||||
|
||||
} // namespace
|
||||
|
||||
// =====================================================================================
|
||||
// The GPU-write set: one case per row of CONTRACT-P5.md section 3's first table.
|
||||
// =====================================================================================
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
// Row 0 - DirectGLES.cpp:570 / UniformManager.cpp:1231.
|
||||
TEST_F(SplitBufferSet, Row0EverySsboBindingPointIsMarkedByADraw) {
|
||||
auto ssbo = MakeBuffer(11u, 256);
|
||||
MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, 0).Bind(ssbo);
|
||||
MG_State::pGLContext->TouchBufferBindingPoint(BufferTarget::ShaderStorage, 0);
|
||||
|
||||
MG_Remote::Client::MarkGpuWritesForDraw();
|
||||
|
||||
EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::ShaderStorageBinding), 1u)
|
||||
<< "a draw with an SSBO bound must mark it: the shader writes into the driver's buffer, "
|
||||
"behind the shadow glMapBuffer and glGetBufferSubData read";
|
||||
}
|
||||
|
||||
// Row 1 - DirectGLES.cpp:618. The point of a counter is that the shader increments it.
|
||||
TEST_F(SplitBufferSet, Row1EveryBoundAtomicCounterIsMarkedByADraw) {
|
||||
auto counter = MakeBuffer(12u, 64);
|
||||
MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::AtomicCounter, 0).Bind(counter);
|
||||
MG_State::pGLContext->TouchBufferBindingPoint(BufferTarget::AtomicCounter, 0);
|
||||
|
||||
MG_Remote::Client::MarkGpuWritesForDraw();
|
||||
|
||||
EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::AtomicCounterBinding), 1u);
|
||||
}
|
||||
|
||||
// Row 2's DISCRIMINATOR - DirectGLES.cpp:2354-2357. This is the one row whose backend twin is
|
||||
// narrow on purpose, so the narrowness is what the case is about: a GL_READ_ONLY image binding
|
||||
// must NOT be marked, or the next map waits on and re-reads a dispatch that cannot have changed
|
||||
// a byte of it.
|
||||
TEST_F(SplitBufferSet, Row2OnlyAWritableImageBufferTextureCounts) {
|
||||
MG_State::GLState::ImageTextureBinding empty{};
|
||||
EXPECT_FALSE(MG_Remote::Client::ImageUnitIsAWritableBufferTexture(empty))
|
||||
<< "an unbound image unit is not a GPU write";
|
||||
|
||||
auto texture = MakeShared<MG_State::GLState::TextureObjectBuffer>(7u);
|
||||
auto backing = MakeBuffer(13u, 128);
|
||||
texture->GetBufferBindingSlot().Bind(backing);
|
||||
|
||||
MG_State::GLState::ImageTextureBinding readOnly{};
|
||||
readOnly.Texture = texture;
|
||||
readOnly.Access = GL_READ_ONLY;
|
||||
EXPECT_FALSE(MG_Remote::Client::ImageUnitIsAWritableBufferTexture(readOnly))
|
||||
<< "a GL_READ_ONLY image binding is left alone by the backend twin and must be left "
|
||||
"alone here";
|
||||
|
||||
MG_State::GLState::ImageTextureBinding writable = readOnly;
|
||||
writable.Access = GL_READ_WRITE;
|
||||
EXPECT_TRUE(MG_Remote::Client::ImageUnitIsAWritableBufferTexture(writable));
|
||||
}
|
||||
|
||||
// Row 2's WALK, through the image unit the context actually holds.
|
||||
TEST_F(SplitBufferSet, Row2AWritableImageBufferTextureIsMarkedByADraw) {
|
||||
auto texture = MakeShared<MG_State::GLState::TextureObjectBuffer>(8u);
|
||||
auto backing = MakeBuffer(14u, 128);
|
||||
texture->GetBufferBindingSlot().Bind(backing);
|
||||
auto& binding = MG_State::pGLContext->GetImageTextureBinding(0);
|
||||
binding.Texture = texture;
|
||||
binding.Access = GL_WRITE_ONLY;
|
||||
|
||||
MG_Remote::Client::MarkGpuWritesForDraw();
|
||||
|
||||
EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::WritableImageBufferTexture), 1u);
|
||||
|
||||
binding = MG_State::GLState::ImageTextureBinding{};
|
||||
}
|
||||
|
||||
// Row 3 - VulkanRenderer.cpp:11618. With no capture active there is nothing to mark, and that
|
||||
// gate is the half worth pinning: a mark taken with no active capture would mark whatever the
|
||||
// binding points happened to hold from a previous one.
|
||||
TEST_F(SplitBufferSet, Row3TransformFeedbackTargetsAreOnlyMarkedWhileACaptureIsActive) {
|
||||
auto target = MakeBuffer(15u, 256);
|
||||
MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback, 0).Bind(target);
|
||||
MG_State::pGLContext->TouchBufferBindingPoint(BufferTarget::TransformFeedback, 0);
|
||||
|
||||
ASSERT_FALSE(MG_State::pGLContext->IsTransformFeedbackActive());
|
||||
MG_Remote::Client::MarkGpuWritesForDraw();
|
||||
EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::TransformFeedbackCapture), 0u);
|
||||
|
||||
// The marking itself, driven at the row rather than through the capture state machine.
|
||||
MG_Remote::Client::MarkBufferForProducer(target, GpuWriteProducer::TransformFeedbackCapture);
|
||||
EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::TransformFeedbackCapture), 1u);
|
||||
}
|
||||
|
||||
// Row 4 - P5's own: glReadPixels into a bound GL_PIXEL_PACK_BUFFER.
|
||||
TEST_F(SplitBufferSet, Row4AReadPixelsIntoAPackPboMarksThePbo) {
|
||||
MG_Remote::Client::MarkReadPixelsPackBuffer();
|
||||
EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::ReadPixelsPackBuffer), 0u)
|
||||
<< "a read into client memory binds no PBO and must mark nothing";
|
||||
|
||||
auto pbo = MakeBuffer(16u, 1024);
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).Bind(pbo);
|
||||
MG_Remote::Client::MarkReadPixelsPackBuffer();
|
||||
EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::ReadPixelsPackBuffer), 1u);
|
||||
}
|
||||
|
||||
// Row 5 - P5's own: glEndTransformFeedback, in place of the unbounded ClientWaitSync.
|
||||
TEST_F(SplitBufferSet, Row5EndTransformFeedbackMarksTheCaptureTargets) {
|
||||
auto target = MakeBuffer(17u, 256);
|
||||
ASSERT_FALSE(MG_State::pGLContext->IsTransformFeedbackActive());
|
||||
MG_Remote::Client::MarkEndTransformFeedbackCaptureTargets();
|
||||
EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::EndTransformFeedbackCapture), 0u);
|
||||
|
||||
MG_Remote::Client::MarkBufferForProducer(target, GpuWriteProducer::EndTransformFeedbackCapture);
|
||||
EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(GpuWriteProducer::EndTransformFeedbackCapture), 1u);
|
||||
}
|
||||
|
||||
// THE GATE ITSELF. On the monolith path the six backend sites are still the only producers and
|
||||
// a second marker would be new behaviour (D-J) - and rows 4 and 5 would remove a stall that
|
||||
// monolith is entitled to keep.
|
||||
TEST_F(SplitBufferSet, TheWholeSetIsInertOnTheMonolithPath) {
|
||||
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
||||
auto ssbo = MakeBuffer(18u, 256);
|
||||
MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::ShaderStorage, 0).Bind(ssbo);
|
||||
MG_State::pGLContext->TouchBufferBindingPoint(BufferTarget::ShaderStorage, 0);
|
||||
|
||||
MG_Remote::Client::MarkGpuWritesForDraw();
|
||||
MG_Remote::Client::MarkGpuWritesForDispatch();
|
||||
MG_Remote::Client::MarkReadPixelsPackBuffer();
|
||||
MG_Remote::Client::MarkEndTransformFeedbackCaptureTargets();
|
||||
|
||||
for (SizeT row = 0; row < static_cast<SizeT>(GpuWriteProducer::Count); ++row) {
|
||||
EXPECT_EQ(MG_Remote::Client::ProducerMarkCount(static_cast<GpuWriteProducer>(row)), 0u)
|
||||
<< "row " << row << " fired with Transport == Monolith";
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// The persistent-map push
|
||||
// =====================================================================================
|
||||
|
||||
// The membership predicate IS SyncPersistentMappedRange's early-out chain, and the two must
|
||||
// answer the same thing about the same buffer. A re-derived predicate that drifted would push
|
||||
// a buffer monolith stopped pushing, and no other test could see it.
|
||||
TEST_F(SplitBufferSet, MembershipIsSyncPersistentMappedRangesOwnEarlyOutChain) {
|
||||
auto buffer = MakeBuffer(20u, 4096);
|
||||
EXPECT_FALSE(PersistentMapTracker::IsLivePersistentMap(*buffer)) << "not mapped";
|
||||
|
||||
buffer->AcquireMemoryRange(Range1D{0, 4096},
|
||||
BufferMappingAccessBit::Write | BufferMappingAccessBit::Persistent);
|
||||
ASSERT_FALSE(buffer->IsBackendPersistentMapped())
|
||||
<< "the acquisition was minted, so this is the adopted arm and not the one under test";
|
||||
EXPECT_TRUE(PersistentMapTracker::IsLivePersistentMap(*buffer));
|
||||
EXPECT_EQ(PersistentMapTracker::Instance().MemberCount(), 1u);
|
||||
|
||||
buffer->ReleaseMemory(false);
|
||||
EXPECT_FALSE(PersistentMapTracker::IsLivePersistentMap(*buffer));
|
||||
EXPECT_EQ(PersistentMapTracker::Instance().MemberCount(), 0u);
|
||||
|
||||
// FLUSH_EXPLICIT is the early-out that is easiest to lose: the application announces its
|
||||
// own writes with glFlushMappedBufferRange, which already crosses as resource_flush_range.
|
||||
buffer->AcquireMemoryRange(Range1D{0, 4096}, BufferMappingAccessBit::Write |
|
||||
BufferMappingAccessBit::Persistent |
|
||||
BufferMappingAccessBit::FlushExplicit);
|
||||
EXPECT_FALSE(PersistentMapTracker::IsLivePersistentMap(*buffer));
|
||||
EXPECT_EQ(PersistentMapTracker::Instance().MemberCount(), 0u);
|
||||
buffer->ReleaseMemory(false);
|
||||
}
|
||||
|
||||
// pmap is non-zero, and it is non-zero in BLOCKS.
|
||||
TEST_F(SplitBufferSet, ThePushCutsTheMappedSpanIntoBlocksAndMovesPmap) {
|
||||
constexpr SizeT kSize = 4u * 64u * 1024u; // exactly four 64 KiB blocks
|
||||
auto buffer = MakeBuffer(21u, kSize);
|
||||
buffer->AcquireMemoryRange(Range1D{0, kSize},
|
||||
BufferMappingAccessBit::Write | BufferMappingAccessBit::Persistent);
|
||||
ASSERT_TRUE(PersistentMapTracker::IsLivePersistentMap(*buffer));
|
||||
|
||||
const Uint64 before = MG_Util::PipeStats::TotalBytes(MG_Util::PipeStats::ByteClass::PersistentMapPush);
|
||||
MG_Remote::Client::PushPersistentMapsBeforeVerb();
|
||||
|
||||
EXPECT_EQ(PersistentMapTracker::Instance().BlocksPushed(), 4u)
|
||||
<< "a 256 KiB span at a 64 KiB block size is four records, not one";
|
||||
EXPECT_EQ(PersistentMapTracker::Instance().BytesPushed(), static_cast<Uint64>(kSize));
|
||||
EXPECT_EQ(MG_Util::PipeStats::TotalBytes(MG_Util::PipeStats::ByteClass::PersistentMapPush) - before,
|
||||
static_cast<Uint64>(kSize))
|
||||
<< "persistent-map-push is wired and counts the bytes the client had to ship because "
|
||||
"MapPersistent declined";
|
||||
|
||||
buffer->ReleaseMemory(false);
|
||||
}
|
||||
|
||||
// A span that is not a whole number of blocks keeps its tail.
|
||||
TEST_F(SplitBufferSet, TheLastBlockIsTheRemainderAndNotAWholeBlock) {
|
||||
constexpr SizeT kSize = 64u * 1024u + 7u;
|
||||
auto buffer = MakeBuffer(22u, kSize);
|
||||
buffer->AcquireMemoryRange(Range1D{0, kSize},
|
||||
BufferMappingAccessBit::Write | BufferMappingAccessBit::Persistent);
|
||||
MG_Remote::Client::PushPersistentMapsBeforeVerb();
|
||||
EXPECT_EQ(PersistentMapTracker::Instance().BlocksPushed(), 2u);
|
||||
EXPECT_EQ(PersistentMapTracker::Instance().BytesPushed(), static_cast<Uint64>(kSize));
|
||||
buffer->ReleaseMemory(false);
|
||||
}
|
||||
|
||||
// E3(a)'s NEGATIVE CONTROL: 0 disables the push, it does not mean "one unlimited block".
|
||||
TEST_F(SplitBufferSet, AZeroBlockSizeTurnsThePushOffRatherThanMakingItUnlimited) {
|
||||
MG_Config::Ipc.PersistentBlockKb = 0;
|
||||
constexpr SizeT kSize = 128u * 1024u;
|
||||
auto buffer = MakeBuffer(23u, kSize);
|
||||
buffer->AcquireMemoryRange(Range1D{0, kSize},
|
||||
BufferMappingAccessBit::Write | BufferMappingAccessBit::Persistent);
|
||||
ASSERT_TRUE(PersistentMapTracker::IsLivePersistentMap(*buffer));
|
||||
|
||||
const Uint64 before = MG_Util::PipeStats::TotalBytes(MG_Util::PipeStats::ByteClass::PersistentMapPush);
|
||||
MG_Remote::Client::PushPersistentMapsBeforeVerb();
|
||||
EXPECT_EQ(PersistentMapTracker::Instance().BlocksPushed(), 0u);
|
||||
EXPECT_EQ(MG_Util::PipeStats::TotalBytes(MG_Util::PipeStats::ByteClass::PersistentMapPush), before)
|
||||
<< "MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 must ship nothing, so that "
|
||||
"PersistentCoherentMapScenario goes red under it";
|
||||
buffer->ReleaseMemory(false);
|
||||
}
|
||||
|
||||
// The set does not keep a pointer to a dead buffer.
|
||||
TEST_F(SplitBufferSet, ADestroyedBufferLeavesTheSet) {
|
||||
{
|
||||
auto buffer = MakeBuffer(24u, 4096);
|
||||
buffer->AcquireMemoryRange(Range1D{0, 4096},
|
||||
BufferMappingAccessBit::Write | BufferMappingAccessBit::Persistent);
|
||||
ASSERT_EQ(PersistentMapTracker::Instance().MemberCount(), 1u);
|
||||
}
|
||||
EXPECT_EQ(PersistentMapTracker::Instance().MemberCount(), 0u)
|
||||
<< "~BufferObject must Forget() itself: the set holds raw pointers keyed on the "
|
||||
"lifetime id, and an entry that outlives its object is the one failure it cannot have";
|
||||
}
|
||||
|
||||
// SyncGpuWrites' THIRD STATE. Monolith clears the flag before it emits, which is safe only
|
||||
// because the readback runs synchronously inside the applier; under a transport that clear
|
||||
// leaves the shadow silently stale for the object's life, so the WRITEBACK clears it instead.
|
||||
TEST_F(SplitBufferSet, UnderSplitTheWritebackClearsThePendingFlagAndNotTheRequest) {
|
||||
auto buffer = MakeBuffer(25u, 256);
|
||||
buffer->MarkGpuWritten();
|
||||
ASSERT_TRUE(buffer->HasOutstandingGpuWrite());
|
||||
|
||||
Vector<Uint8> bytes(256, static_cast<Uint8>(0x5A));
|
||||
buffer->WritebackFromBackend(DataPtr{bytes.data(), bytes.size()}, 0);
|
||||
EXPECT_FALSE(buffer->HasOutstandingGpuWrite())
|
||||
<< "the answer landing is what makes the shadow current, so the answer is what clears";
|
||||
|
||||
// A PARTIAL writeback is not an answer to a whole-buffer readback and must not clear:
|
||||
// GL_Drawing's transform-feedback strip fixup writes back three vertices at a time.
|
||||
buffer->MarkGpuWritten();
|
||||
buffer->WritebackFromBackend(DataPtr{bytes.data(), 16}, 0);
|
||||
EXPECT_TRUE(buffer->HasOutstandingGpuWrite());
|
||||
|
||||
// And with no readback route at all - no size, or a backend that registered no resource
|
||||
// ops - SyncGpuWrites must clear rather than block for ever. That is the ONE case
|
||||
// monolith's unconditional clear covers that a writeback cannot.
|
||||
EXPECT_FALSE(MG_Remote::Client::BufferWritebackIsReachable(*buffer));
|
||||
buffer->SyncGpuWrites();
|
||||
EXPECT_FALSE(buffer->HasOutstandingGpuWrite());
|
||||
}
|
||||
|
||||
// R-6's tier gate. T2 is the only tier P5 implements; the other two are a NAMED refusal and
|
||||
// their spelling exists now so the P11 negative control has one.
|
||||
TEST_F(SplitBufferSet, OnlyAdoptTierTwoIsImplemented) {
|
||||
EXPECT_TRUE(MG_Remote::Client::AdoptTierIsEmulate());
|
||||
MG_Config::Ipc.AdoptTier = 0;
|
||||
EXPECT_DEATH(MG_Remote::Client::AdoptTierIsEmulate(), "");
|
||||
MG_Config::Ipc.AdoptTier = 1;
|
||||
EXPECT_DEATH(MG_Remote::Client::AdoptTierIsEmulate(), "");
|
||||
MG_Config::Ipc.AdoptTier = 2;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
// THE SAME FIFTEEN NAMES, SO THE ctest NAME SET DOES NOT MOVE BETWEEN LANES. G2 compares the
|
||||
// pull and push name lists line for line and G14 allows build-split to ADD names but never to
|
||||
// remove one, so a case that exists only where it can run would break both gates for a reason
|
||||
// that has nothing to do with what it tests. It skips instead, and says why.
|
||||
TEST(SplitBufferSet, Row0EverySsboBindingPointIsMarkedByADraw) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
TEST(SplitBufferSet, Row1EveryBoundAtomicCounterIsMarkedByADraw) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
TEST(SplitBufferSet, Row2OnlyAWritableImageBufferTextureCounts) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
TEST(SplitBufferSet, Row2AWritableImageBufferTextureIsMarkedByADraw) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
TEST(SplitBufferSet, Row3TransformFeedbackTargetsAreOnlyMarkedWhileACaptureIsActive) {
|
||||
MGL_SPLIT_ONLY_OR_SKIP();
|
||||
}
|
||||
TEST(SplitBufferSet, Row4AReadPixelsIntoAPackPboMarksThePbo) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
TEST(SplitBufferSet, Row5EndTransformFeedbackMarksTheCaptureTargets) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
TEST(SplitBufferSet, TheWholeSetIsInertOnTheMonolithPath) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
TEST(SplitBufferSet, MembershipIsSyncPersistentMappedRangesOwnEarlyOutChain) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
TEST(SplitBufferSet, ThePushCutsTheMappedSpanIntoBlocksAndMovesPmap) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
TEST(SplitBufferSet, TheLastBlockIsTheRemainderAndNotAWholeBlock) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
TEST(SplitBufferSet, AZeroBlockSizeTurnsThePushOffRatherThanMakingItUnlimited) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
TEST(SplitBufferSet, ADestroyedBufferLeavesTheSet) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
TEST(SplitBufferSet, UnderSplitTheWritebackClearsThePendingFlagAndNotTheRequest) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
TEST(SplitBufferSet, OnlyAdoptTierTwoIsImplemented) { MGL_SPLIT_ONLY_OR_SKIP(); }
|
||||
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
// =====================================================================================
|
||||
// Tier 1 of the three-tier flush ladder - the INVALIDATE_RANGE edge.
|
||||
//
|
||||
// It is a PUSH-build case and not a split-build one: the widening hazard is real in every
|
||||
// build that compiles FlushPendingRangesFrom, and under split it simply gains a second cause
|
||||
// (a SEG_STAGE snapshot that no longer matches the queued range).
|
||||
// =====================================================================================
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
namespace {
|
||||
using MobileGL::MG_Backend::DirectGLES::BufferImpl::InvalidateFlushAccessFor;
|
||||
using MobileGL::MG_Backend::DirectGLES::BufferImpl::kEsprytInvalidateRangeMinBytes;
|
||||
constexpr SizeT kStore = 1024u * 1024u;
|
||||
} // namespace
|
||||
|
||||
TEST(EsprytFlushLadder, AWholeBufferRangeOrphansTheStore) {
|
||||
EXPECT_EQ(InvalidateFlushAccessFor(0, kStore, 0, kStore, kStore, kStore),
|
||||
static_cast<GLbitfield>(GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT));
|
||||
}
|
||||
|
||||
TEST(EsprytFlushLadder, ALargePartialRangeInvalidatesExactlyThatRange) {
|
||||
const SizeT start = 4096;
|
||||
const SizeT end = start + kEsprytInvalidateRangeMinBytes;
|
||||
EXPECT_EQ(InvalidateFlushAccessFor(start, end, start, end, kStore, kStore),
|
||||
static_cast<GLbitfield>(GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_RANGE_BIT));
|
||||
}
|
||||
|
||||
TEST(EsprytFlushLadder, ASmallPartialRangeFallsThroughToTheStagingRing) {
|
||||
EXPECT_EQ(InvalidateFlushAccessFor(4096, 4096 + 64, 4096, 4096 + 64, kStore, kStore), 0u)
|
||||
<< "below the threshold the map WAITS out the WAR hazard on the CPU instead of "
|
||||
"substituting pages, which is the whole reason tier 2 exists";
|
||||
}
|
||||
|
||||
// THE EDGE THAT HAS ALREADY DRAWN BLOOD (Managers.cpp:1125-1128): widening the map past the
|
||||
// queued range clobbered GPU-written data - an SSBO counter beside the app's SubData - with
|
||||
// the stale shadow, SILENTLY. Under split the same shape arrives with a different cause: the
|
||||
// server may hold no pointer into the client's shadow (R-11), so `hostBase` becomes a
|
||||
// SEG_STAGE snapshot, and a snapshot that does not cover exactly the queued range is the same
|
||||
// lie told by a thread boundary instead of by a page alignment.
|
||||
TEST(EsprytFlushLadder, AMapWiderThanTheQueuedRangeRefusesTierOne) {
|
||||
const SizeT queuedStart = 4096;
|
||||
const SizeT queuedEnd = queuedStart + kEsprytInvalidateRangeMinBytes;
|
||||
// Page-aligned outward, the exact widening the in-tree note records.
|
||||
EXPECT_EQ(InvalidateFlushAccessFor(queuedStart, queuedEnd, 0, queuedEnd + 4096, kStore, kStore), 0u)
|
||||
<< "a widened INVALIDATE_RANGE declares bytes dead that the shadow is not about to "
|
||||
"rewrite, and overwrites whatever the GPU put there";
|
||||
// And narrower, which is the same corruption read the other way round: bytes left
|
||||
// unwritten inside a range that has just been declared dead.
|
||||
EXPECT_EQ(InvalidateFlushAccessFor(queuedStart, queuedEnd, queuedStart, queuedEnd - 8, kStore, kStore), 0u);
|
||||
}
|
||||
|
||||
TEST(EsprytFlushLadder, AnEmptyRangeIsNeverTierOne) {
|
||||
EXPECT_EQ(InvalidateFlushAccessFor(4096, 4096, 4096, 4096, kStore, kStore), 0u);
|
||||
}
|
||||
#else
|
||||
// The same five names in a pull build, for the G2/G14 reason above: the ladder's push arm
|
||||
// (FlushPendingRangesFrom) is the only one that carries this decision as a function - the pull
|
||||
// arm's FlushPendingRangesNow is byte-frozen against 5cb826b0 (ID-15) and may not grow one.
|
||||
#define MGL_PUSH_ONLY_OR_SKIP() \
|
||||
GTEST_SKIP() << "the three-tier ladder's push arm (FlushPendingRangesFrom) is what carries " \
|
||||
"InvalidateFlushAccessFor; a pull build compiles the frozen arm instead"
|
||||
TEST(EsprytFlushLadder, AWholeBufferRangeOrphansTheStore) { MGL_PUSH_ONLY_OR_SKIP(); }
|
||||
TEST(EsprytFlushLadder, ALargePartialRangeInvalidatesExactlyThatRange) { MGL_PUSH_ONLY_OR_SKIP(); }
|
||||
TEST(EsprytFlushLadder, ASmallPartialRangeFallsThroughToTheStagingRing) { MGL_PUSH_ONLY_OR_SKIP(); }
|
||||
TEST(EsprytFlushLadder, AMapWiderThanTheQueuedRangeRefusesTierOne) { MGL_PUSH_ONLY_OR_SKIP(); }
|
||||
TEST(EsprytFlushLadder, AnEmptyRangeIsNeverTierOne) { MGL_PUSH_ONLY_OR_SKIP(); }
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
@@ -4652,6 +4652,92 @@ TEST(DirectGLESBufferDrawProbe, ALiveHostMapKeepsTheHandleArmProbeDirtyBetweenTw
|
||||
record = {};
|
||||
}
|
||||
|
||||
// P5 b1's half of the case above: THE PIN IS LIFTED, AND THIS IS WHAT REPLACED IT.
|
||||
//
|
||||
// The case above exists because answering the live-map question from HasLiveHostWrites alone
|
||||
// read draw-CLEAN forever. P5 gives that field a producer - MGPSubData::HasLiveHostWrites, set
|
||||
// by MGPipeEmitResourceSubData from BufferObject::HasLiveHostWritesForWire - and with the
|
||||
// producer in place the last frontend read in IsBufferDrawCleanByHandle retires under split,
|
||||
// because under a spawn there is no frontend object on that side to ask.
|
||||
//
|
||||
// So the same fixture is driven twice with the transport flipped, and the second half is the
|
||||
// one that would have been impossible before: the record ALONE has to be able to say both
|
||||
// answers. If it could not, this case reads CLEAN in both arms and the regression the case
|
||||
// above records comes back through the other door.
|
||||
TEST(DirectGLESBufferDrawProbe, UnderSplitTheRecordAloneAnswersTheLiveHostMapQuestion) {
|
||||
using namespace MobileGL;
|
||||
using namespace MobileGL::MG_Backend::DirectGLES;
|
||||
using namespace MobileGL::MG_State::GLState;
|
||||
|
||||
if (!EsprytSlotTablesEnabled()) {
|
||||
GTEST_SKIP() << "the handle-keyed resource table only exists on the {slot, gen} arm";
|
||||
}
|
||||
#if !MOBILEGL_BUILD_DISAGGREGATED
|
||||
GTEST_SKIP() << "MG_Config::Transport is a constexpr Monolith without the transport built in, "
|
||||
"so the split arm of this probe cannot be entered";
|
||||
#else
|
||||
auto owner = MakeShared<BufferObject>(0u);
|
||||
owner->Respecify(256, nullptr);
|
||||
const MG_Pipe::MGPipeHandle res =
|
||||
MG_Pipe::MGPipeSlots().Acquire(MG_Pipe::MGPipeKind::Buffer, owner->GetLifetimeId());
|
||||
ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(res));
|
||||
|
||||
auto& applier = MG_Pipe::MGPipeApplier();
|
||||
if (applier.Resources.size() <= static_cast<SizeT>(res.Slot)) {
|
||||
applier.Resources.resize(static_cast<SizeT>(res.Slot) + 1);
|
||||
}
|
||||
auto& record = applier.Resources[res.Slot];
|
||||
record = {};
|
||||
record.Gen = res.Gen;
|
||||
record.Live = true;
|
||||
record.Desc.Width = 256;
|
||||
record.Serial = 7;
|
||||
record.HasLiveHostWrites = false;
|
||||
|
||||
auto& twin = BufferImpl::g_backendBufferResources.GetOrCreate(res);
|
||||
twin = MakeShared<BufferImpl::GLESBufferResource>();
|
||||
auto* const resource = twin.get();
|
||||
resource->id = 1;
|
||||
resource->contextGeneration = BufferImpl::CurrentBufferContextGeneration();
|
||||
resource->storageInitialized = true;
|
||||
resource->storageSize = 256;
|
||||
resource->syncedChangeSerial = record.Serial;
|
||||
|
||||
const auto previousTransport = MG_Config::Transport;
|
||||
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
||||
|
||||
// The map is live and the object still says so - but the probe may no longer ask it.
|
||||
void* const mapped = owner->AcquireMemoryRange(
|
||||
Range1D{0, 256}, BufferMappingAccessBit::Write | BufferMappingAccessBit::Persistent);
|
||||
ASSERT_NE(mapped, nullptr);
|
||||
ASSERT_TRUE(owner->IsMapped());
|
||||
// AcquireMemoryRange's NotePersistentMapStateChanged may have emitted the falling/rising
|
||||
// edge record; re-stamp the fixture so the only question left is the flag.
|
||||
record.Serial = 9;
|
||||
resource->syncedChangeSerial = record.Serial;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||
resource->pendingRanges.clear();
|
||||
}
|
||||
|
||||
record.HasLiveHostWrites = false;
|
||||
EXPECT_TRUE(BufferImpl::IsBufferDrawCleanByHandle(res, resource, owner.get()))
|
||||
<< "the frontend IsMapped() read did NOT retire under split - it is still what answers, "
|
||||
"and a spawned server has no object to ask";
|
||||
|
||||
record.HasLiveHostWrites = true;
|
||||
EXPECT_FALSE(BufferImpl::IsBufferDrawCleanByHandle(res, resource, owner.get()))
|
||||
<< "the record alone cannot say 'a host writer is live', so the question has no answer "
|
||||
"on the server's side of a split at all";
|
||||
|
||||
MG_Config::Transport = previousTransport;
|
||||
owner->ReleaseMemory(false);
|
||||
BufferImpl::g_backendBufferResources.ReleaseByHandle(res);
|
||||
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Buffer, res);
|
||||
record = {};
|
||||
#endif
|
||||
}
|
||||
|
||||
// P3a REWORK M-1's gate (contract-review M2). The minting overload's symmetric `!=` is safe
|
||||
// because its handle comes out of the allocator and can never be behind the entry; the HANDLE
|
||||
// overload's input ARRIVES in a payload, so a generation BEHIND the live entry's is reachable -
|
||||
@@ -4782,4 +4868,8 @@ TEST(DirectGLESSlotTable, ADeathNoticeForEveryP4aKindIsIdempotent) {
|
||||
TEST(DirectGLESBufferDrawProbe, ALiveHostMapKeepsTheHandleArmProbeDirtyBetweenTwoDraws) {
|
||||
GTEST_SKIP() << "the handle-keyed resource table is compiled only under MOBILEGL_PIPE_PUSH";
|
||||
}
|
||||
|
||||
TEST(DirectGLESBufferDrawProbe, UnderSplitTheRecordAloneAnswersTheLiveHostMapQuestion) {
|
||||
GTEST_SKIP() << "the handle-keyed resource table is compiled only under MOBILEGL_PIPE_PUSH";
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
Reference in New Issue
Block a user