mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 12:48:32 +09:00
[Fix, Test] (Pipe): reconcile the framebuffer and texture emit suites after the client replay - the union of wire's and the client's cases in one file, spelled against the contract's Named enumerator
This commit is contained in:
@@ -210,364 +210,6 @@ TEST(FramebufferEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// The APPLIER's half of set_framebuffer_state (the wire commits'). The emitter's half - the
|
||||
// resolved read surface, the draw-buffer array in the content hash, a recycled handle never
|
||||
// suppressed against its predecessor, an attachment point above the wire width refused rather
|
||||
// than truncated - is the client package's and lands beside these.
|
||||
// =========================================================================================
|
||||
|
||||
// THE WHOLE POINT OF THE Target BYTE. GL has two independent framebuffer bindings and this
|
||||
// record carries one Fbo and one ReadSurface, so a record says which binding it describes;
|
||||
// Both is one object bound to both and writes both. Deleting either store, or the serial bump,
|
||||
// leaves this red.
|
||||
TEST(FramebufferEmit, ADrawRecordAndAReadRecordAreKeptApartAndBothWritesBoth) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const Uint64 serialAtStart = MGPipeApplier().FramebufferSerial;
|
||||
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{4, 1}, MGPipeFramebufferTarget::Draw, 100));
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, (MGPipeHandle{4, 1}));
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Width, 100u);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Color[0].InternalFormat, 0x8058u);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->DrawBuffers[0], 0);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer(), nullptr)
|
||||
<< "a Draw record landed in the read binding as well";
|
||||
const Uint64 afterDraw = MGPipeApplier().FramebufferSerial;
|
||||
EXPECT_GT(afterDraw, serialAtStart) << "an applied record must move the serial the twin memoises";
|
||||
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{5, 2}, MGPipeFramebufferTarget::Read, 200));
|
||||
ASSERT_NE(MGPipeApplier().ReadFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer()->Fbo, (MGPipeHandle{5, 2}));
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer()->Width, 200u);
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr) << "a Read record overwrote the draw binding";
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, (MGPipeHandle{4, 1}));
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Width, 100u);
|
||||
EXPECT_GT(MGPipeApplier().FramebufferSerial, afterDraw);
|
||||
|
||||
// Both: one record, one serial bump, two destinations.
|
||||
const Uint64 beforeBoth = MGPipeApplier().FramebufferSerial;
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{6, 3}, MGPipeFramebufferTarget::Both, 300));
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
ASSERT_NE(MGPipeApplier().ReadFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, (MGPipeHandle{6, 3}));
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer()->Fbo, (MGPipeHandle{6, 3}));
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Width, 300u);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer()->Width, 300u);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferSerial, beforeBoth + 1)
|
||||
<< "a Both record is ONE record and moves the serial once";
|
||||
// The two earlier framebuffers keep their own records - the table is keyed by the handle,
|
||||
// so binding a third displaced neither (ID-19(b)).
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{4, 1}), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{4, 1})->Width, 100u);
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{5, 2}), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{5, 2})->Width, 200u);
|
||||
|
||||
// A framebuffer has a handle but NO wire lifetime, so there is no record to refuse against
|
||||
// and this entry point never counts an object refusal.
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// A target outside the FOUR is not a target this server has, and guessing one would put a
|
||||
// draw's attachments into the read binding or the other way round. Named (3) is legal since
|
||||
// ID-19(b) and has its own case below; the first refused value is the one above it.
|
||||
TEST(FramebufferEmit, ATargetOutsideTheThreeBindingsIsRefusedNamingTheRecord) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
MGPFramebufferState bad = FramebufferRecord(MGPipeHandle{7, 4}, MGPipeFramebufferTarget::Draw, 100);
|
||||
bad.Target = static_cast<Uint8>(MGPipeFramebufferTarget::Count);
|
||||
const Uint64 serialBefore = MGPipeApplier().FramebufferSerial;
|
||||
|
||||
ExpectRefusedNaming("set_framebuffer_state {slot=7, gen=4, target=4}: the record names no framebuffer "
|
||||
"binding target",
|
||||
[&bad]() { MGPipeApplySetFramebufferState(bad); });
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{7, 4}), nullptr)
|
||||
<< "a refused record was written into the per-object table anyway";
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferSerial, serialBefore)
|
||||
<< "a refused record must not move the serial";
|
||||
#endif
|
||||
}
|
||||
|
||||
// The draw-buffer array is an INDEX into this record's own Color[], and -1 is NONE. An entry
|
||||
// outside that range would have the server read a colour attachment the record does not carry,
|
||||
// which is the truncation the wire width's cap refusal exists to prevent upstream.
|
||||
TEST(FramebufferEmit, ADrawBufferEntryOutsideTheRecordsOwnArrayIsRefusedRatherThanRead) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
|
||||
// The positive control first: -1 everywhere and the last legal index are both fine, so
|
||||
// what follows is refusing the value and not the loop around it.
|
||||
MGPFramebufferState legal = FramebufferRecord(MGPipeHandle{8, 1}, MGPipeFramebufferTarget::Draw, 100);
|
||||
legal.DrawBuffers[7] = static_cast<Int8>(kMGPipeMaxColorAttachments - 1);
|
||||
MGPipeApplySetFramebufferState(legal);
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
ASSERT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, (MGPipeHandle{8, 1}));
|
||||
const Uint64 serialBefore = MGPipeApplier().FramebufferSerial;
|
||||
|
||||
MGPFramebufferState past = FramebufferRecord(MGPipeHandle{8, 1}, MGPipeFramebufferTarget::Draw, 111);
|
||||
past.DrawBuffers[3] = static_cast<Int8>(kMGPipeMaxColorAttachments);
|
||||
ExpectRefusedNaming("set_framebuffer_state {slot=8, gen=1, target=0}: a draw-buffer entry names a "
|
||||
"colour attachment outside the record's own array",
|
||||
[&past]() { MGPipeApplySetFramebufferState(past); });
|
||||
|
||||
MGPFramebufferState negative = FramebufferRecord(MGPipeHandle{8, 1}, MGPipeFramebufferTarget::Draw, 222);
|
||||
negative.DrawBuffers[0] = -2;
|
||||
ExpectRefusedNaming("set_framebuffer_state {slot=8, gen=1, target=0}: a draw-buffer entry names a "
|
||||
"colour attachment outside the record's own array",
|
||||
[&negative]() { MGPipeApplySetFramebufferState(negative); });
|
||||
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Width, 100u) << "a refused record was stored anyway";
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferSerial, serialBefore);
|
||||
#endif
|
||||
}
|
||||
|
||||
// D-J4, as ID-19(b) leaves it. The two framebuffer BINDINGS are per-context working state and a
|
||||
// make-current takes them - so both accessors answer null afterwards, exactly as the zeroed
|
||||
// records used to answer a null Fbo - while the per-object RECORD survives, because a
|
||||
// framebuffer that is only ever addressed BY NAME has no re-emission trigger at all. The serial
|
||||
// ADVANCES rather than restarting, because a counter that walks back through values it has
|
||||
// already stamped into a twin that outlived the switch is not a generation at all. Restoring
|
||||
// `= 0` anywhere in the reset, or clearing the table there, leaves this red.
|
||||
TEST(FramebufferEmit, AMakeCurrentClearsBothRecordsAndAdvancesTheSerialRatherThanZeroingIt) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{4, 1}, MGPipeFramebufferTarget::Both, 100));
|
||||
const Uint64 serialBefore = MGPipeApplier().FramebufferSerial;
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
ASSERT_EQ(MGPipeApplier().DrawFramebuffer()->Width, 100u);
|
||||
|
||||
MGPipeApplierReset(); // the make-current
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().BoundFramebuffer[0], kMGPipeNullHandle);
|
||||
EXPECT_EQ(MGPipeApplier().BoundFramebuffer[1], kMGPipeNullHandle);
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{4, 1}), nullptr)
|
||||
<< "the per-object record is not working state and a make-current may not take it";
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{4, 1})->Width, 100u);
|
||||
EXPECT_GT(MGPipeApplier().FramebufferSerial, serialBefore)
|
||||
<< "the serial was carried over or restarted; the cleared window is itself a change the "
|
||||
"twin has to hear about, and no stamped value may ever recur";
|
||||
|
||||
// And the teardown scope advances it again, for the same reason.
|
||||
const Uint64 afterReset = MGPipeApplier().FramebufferSerial;
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
EXPECT_GT(MGPipeApplier().FramebufferSerial, afterReset);
|
||||
#endif
|
||||
}
|
||||
|
||||
// THE TEARDOWN SCOPE DROPS THE OBJECT RECORDS, SO IT MUST DROP EVERY WORKING HANDLE THAT NAMES
|
||||
// ONE. The two framebuffer records hold eleven MGPSurface::Res naming texture and renderbuffer
|
||||
// records, and the three unit windows hold entries naming sampler-view, sampler-CSO and texture
|
||||
// records; a window left standing after the tables are emptied is a set of handles into empty
|
||||
// tables, which the next resolve either refuses and counts or - on a slot the next context
|
||||
// re-mints - resolves onto somebody else's record. Deleting any one of the eleven clears in
|
||||
// MGPipeApplierReleaseObjectRecords leaves this red.
|
||||
TEST(FramebufferEmit, AReleaseOfTheObjectRecordsAlsoClearsTheWorkingHandlesThatCouldNameThem) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{4, 1}, MGPipeFramebufferTarget::Both, 100));
|
||||
|
||||
// The three kVarTail sets, each with one entry naming a record the release is about to
|
||||
// drop, and each at a non-zero Start so the window itself is visible in the assertions.
|
||||
MGPBoundView view{};
|
||||
view.View = MGPipeHandle{3, 1};
|
||||
view.Texture = MGPipeHandle{9, 1};
|
||||
view.Unit = 2;
|
||||
MGPipeApplySetSamplerViews(MGPSamplerViews{2, 1, 0xAAAAu}, &view);
|
||||
|
||||
const MGPipeHandle samplerState{5, 1};
|
||||
MGPipeApplyBindSamplerStates(MGPSamplerStates{2, 1, 0xBBBBu}, &samplerState);
|
||||
|
||||
MGPImageView image{};
|
||||
image.Res = MGPipeHandle{9, 1};
|
||||
image.Unit = 2;
|
||||
image.InternalFormat = 0x8058u; // GL_RGBA8
|
||||
MGPipeApplySetShaderImages(MGPShaderImages{2, 1, 0xCCCCu}, &image);
|
||||
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
ASSERT_EQ(MGPipeApplier().DrawFramebuffer()->Color[0].Res, (MGPipeHandle{9, 1}));
|
||||
ASSERT_EQ(MGPipeApplier().SamplerViewCount, 1u);
|
||||
ASSERT_EQ(MGPipeApplier().BoundSamplerViews[2].View, (MGPipeHandle{3, 1}));
|
||||
ASSERT_EQ(MGPipeApplier().SamplerStateCount, 1u);
|
||||
ASSERT_EQ(MGPipeApplier().BoundSamplerStates[2], samplerState);
|
||||
ASSERT_EQ(MGPipeApplier().ShaderImageCount, 1u);
|
||||
ASSERT_EQ(MGPipeApplier().BoundShaderImages[2].Res, (MGPipeHandle{9, 1}));
|
||||
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{4, 1}), nullptr)
|
||||
<< "a framebuffer record holding eleven MGPSurface::Res into the emptied texture and "
|
||||
"renderbuffer tables survived the teardown";
|
||||
EXPECT_TRUE(MGPipeApplier().FramebufferRecords.empty());
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewStart, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCount, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundSamplerViews[2].View, kMGPipeNullHandle);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerStateStart, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerStateCount, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundSamplerStates[2], kMGPipeNullHandle);
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageStart, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageCount, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[2].Res, kMGPipeNullHandle);
|
||||
#endif
|
||||
}
|
||||
|
||||
// ID-19's CORRECTION, AND THE CASE THAT SAYS WHAT THE FOURTH TARGET IS FOR. Every DSA entry
|
||||
// point - BlitNamedFramebuffer and the four ClearNamedFramebuffer* - hands Espryt a framebuffer
|
||||
// BY NAME, and that framebuffer is very often bound to neither binding. With only the two bound
|
||||
// records the server had no description of it at all, bound its driver FBO with no attachments
|
||||
// and cleared or blitted into nothing (esprytobj C-1). A Named record fixes that WITHOUT lying
|
||||
// about the bindings: the record is written and addressable by handle, and BoundFramebuffer
|
||||
// does not move. Making the Named arm touch either binding leaves this red.
|
||||
TEST(FramebufferEmit, ANamedRecordDescribesTheFramebufferItNamesWithoutMovingEitherBinding) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
// TWO DIFFERENT FRAMEBUFFERS ON THE TWO BINDINGS FIRST, so "the bindings did not move" is an
|
||||
// assertion about values rather than about null.
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{4, 1}, MGPipeFramebufferTarget::Draw, 100));
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{5, 2}, MGPipeFramebufferTarget::Read, 200));
|
||||
const Uint64 serialBefore = MGPipeApplier().FramebufferSerial;
|
||||
|
||||
MGPFramebufferState named = FramebufferRecord(MGPipeHandle{6, 3}, MGPipeFramebufferTarget::Draw, 300);
|
||||
named.Target = static_cast<Uint8>(MGPipeFramebufferTarget::Named);
|
||||
named.Color[0].Res = MGPipeHandle{21, 1};
|
||||
MGPipeApplySetFramebufferState(named);
|
||||
|
||||
// (a) THE DSA LOOKUP FINDS IT, BY HANDLE, WITH ITS ATTACHMENTS. This is the call package D
|
||||
// makes at every named blit and clear.
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{6, 3}), nullptr)
|
||||
<< "a framebuffer handed to the server by name has no record, which is the state that "
|
||||
"clears into a driver framebuffer with no attachments";
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{6, 3})->Width, 300u);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{6, 3})->Color[0].Res, (MGPipeHandle{21, 1}));
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{6, 3})->Target, static_cast<Uint8>(MGPipeFramebufferTarget::Named));
|
||||
|
||||
// (b) AND NEITHER BINDING MOVED.
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
ASSERT_NE(MGPipeApplier().ReadFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, (MGPipeHandle{4, 1}))
|
||||
<< "a Named record claimed the draw binding";
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer()->Fbo, (MGPipeHandle{5, 2}))
|
||||
<< "a Named record claimed the read binding";
|
||||
EXPECT_EQ(MGPipeApplier().BoundFramebuffer[0], (MGPipeHandle{4, 1}));
|
||||
EXPECT_EQ(MGPipeApplier().BoundFramebuffer[1], (MGPipeHandle{5, 2}));
|
||||
|
||||
// (c) The serial moves for a Named record too: a twin memoising a framebuffer's attachments
|
||||
// has to hear that they moved, and whether it is bound is a different question.
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferSerial, serialBefore + 1);
|
||||
|
||||
// (d) And the same framebuffer can then be BOUND, which moves the binding and restates the
|
||||
// record - the two targets are not two tables.
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{6, 3}, MGPipeFramebufferTarget::Draw, 400));
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, (MGPipeHandle{6, 3}));
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Width, 400u);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer()->Fbo, (MGPipeHandle{5, 2}));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// STALE-GENERATION REFUSAL, ON THE ONE TABLE WHOSE OBJECT HAS NO WIRE LIFETIME. A framebuffer is
|
||||
// never destroyed on the wire, so its slot is simply overwritten by its successor - and until
|
||||
// that successor describes itself, a handle naming the DEAD one must be refused rather than
|
||||
// answered with the predecessor's attachments. That answer would be a blit or a clear into
|
||||
// somebody else's colour buffer. It is LOUD (counted, and logged once) because the only way to
|
||||
// reach it is an emitter defect, and it is counted APART from RefusedObjectCalls because this is
|
||||
// a read by the server's own sync path and not a call the applier refused.
|
||||
TEST(FramebufferEmit, AFramebufferHandleWhoseGenerationHasMovedOnIsRefusedRatherThanAnswered) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{12, 1}, MGPipeFramebufferTarget::Draw, 100));
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{12, 1}), nullptr);
|
||||
const Uint64 staleBefore = MGPipeApplier().StaleFramebufferRecordLookups;
|
||||
|
||||
// The slot has been recycled and the successor has not described itself yet.
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{12, 2}), nullptr)
|
||||
<< "a handle at a recycled slot was answered with its predecessor's record";
|
||||
EXPECT_EQ(MGPipeApplier().StaleFramebufferRecordLookups, staleBefore + 1);
|
||||
|
||||
// Now it does, and the predecessor's handle becomes the stale one - in the other direction.
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{12, 2}, MGPipeFramebufferTarget::Draw, 200));
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{12, 2}), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{12, 2})->Width, 200u);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{12, 1}), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().StaleFramebufferRecordLookups, staleBefore + 2);
|
||||
|
||||
// THE TWO SILENT NULLS, and they are silent on purpose. "Nothing is bound to this binding"
|
||||
// is what a make-current leaves behind and arrives on every draw of a context that has not
|
||||
// described its framebuffers; "no record at this slot" is what every framebuffer looks like
|
||||
// before its first set_framebuffer_state. Counting either would bury the one that matters.
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(kMGPipeNullHandle), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{99, 1}), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().StaleFramebufferRecordLookups, staleBefore + 2)
|
||||
<< "an unbound binding or an undescribed slot was counted as a stale generation";
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u)
|
||||
<< "the framebuffer family may never move the object-refusal counter";
|
||||
#endif
|
||||
}
|
||||
|
||||
// THE TWO REFUSALS THE PER-OBJECT TABLE ADDED. The null handle is what "nothing is bound" reads
|
||||
// as, so a record installed at {0,0} would be answered to every caller asking about an EMPTY
|
||||
// binding; and Slot is a client-supplied Uint32 that now reaches an allocator, so it takes the
|
||||
// same bound the five object tables take. Every emitter has a handle for every framebuffer it
|
||||
// describes - kMGPipeDefaultFramebuffer {0,1} for the default one - so neither value is
|
||||
// producible by a correct client, which is why both are Fatal rather than counted refusals.
|
||||
TEST(FramebufferEmit, AFramebufferRecordThatNamesNoUsableHandleIsRefusedRatherThanStored) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
// The positive control first: the DEFAULT framebuffer is slot 0 at generation 1 and is
|
||||
// perfectly legal, so what follows refuses the null handle and not slot 0.
|
||||
MGPipeApplySetFramebufferState(
|
||||
FramebufferRecord(kMGPipeDefaultFramebuffer, MGPipeFramebufferTarget::Both, 128));
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(kMGPipeDefaultFramebuffer), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(kMGPipeDefaultFramebuffer)->Width, 128u);
|
||||
const Uint64 serialBefore = MGPipeApplier().FramebufferSerial;
|
||||
|
||||
MGPFramebufferState nullHandle = FramebufferRecord(kMGPipeNullHandle, MGPipeFramebufferTarget::Draw, 300);
|
||||
ExpectRefusedNaming("set_framebuffer_state {slot=0, gen=0, target=0}: the record names the null "
|
||||
"framebuffer handle",
|
||||
[&nullHandle]() { MGPipeApplySetFramebufferState(nullHandle); });
|
||||
|
||||
MGPFramebufferState pastTheBound = FramebufferRecord(
|
||||
MGPipeHandle{kMGPipeMaxFramebufferSlots, 1}, MGPipeFramebufferTarget::Draw, 400);
|
||||
ExpectRefusedNaming("set_framebuffer_state {slot=65536, gen=1, target=0}: the framebuffer slot is "
|
||||
"outside the record table's bound",
|
||||
[&pastTheBound]() { MGPipeApplySetFramebufferState(pastTheBound); });
|
||||
static_assert(kMGPipeMaxFramebufferSlots == 65536u,
|
||||
"the refusal line above names the bound; move both together");
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferSerial, serialBefore) << "a refused record moved the serial";
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, kMGPipeDefaultFramebuffer)
|
||||
<< "a refused record took the draw binding";
|
||||
EXPECT_LT(MGPipeApplier().FramebufferRecords.size(),
|
||||
static_cast<SizeT>(kMGPipeMaxFramebufferSlots))
|
||||
<< "an out-of-range slot resized the table instead of being refused";
|
||||
#endif
|
||||
}
|
||||
=======
|
||||
// ============================================================================
|
||||
// P4a package B's EMITTER-SIDE cases. The contract commit landed the file, its ctest
|
||||
// registration and one shape pin; the wire package's applier-side cases and these are disjoint
|
||||
@@ -1126,7 +768,365 @@ TEST(FramebufferEmit, EveryNonTexturePointCarriesTheUnknownSentinelsRatherThanZe
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
// ==================================================================================
|
||||
// =========================================================================================
|
||||
// The APPLIER's half of set_framebuffer_state (the wire commits'). The emitter's half - the
|
||||
// resolved read surface, the draw-buffer array in the content hash, a recycled handle never
|
||||
// suppressed against its predecessor, an attachment point above the wire width refused rather
|
||||
// than truncated - is the client package's and lands beside these.
|
||||
// =========================================================================================
|
||||
|
||||
// THE WHOLE POINT OF THE Target BYTE. GL has two independent framebuffer bindings and this
|
||||
// record carries one Fbo and one ReadSurface, so a record says which binding it describes;
|
||||
// Both is one object bound to both and writes both. Deleting either store, or the serial bump,
|
||||
// leaves this red.
|
||||
TEST(FramebufferEmit, ADrawRecordAndAReadRecordAreKeptApartAndBothWritesBoth) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
const Uint64 serialAtStart = MGPipeApplier().FramebufferSerial;
|
||||
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{4, 1}, MGPipeFramebufferTarget::Draw, 100));
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, (MGPipeHandle{4, 1}));
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Width, 100u);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Color[0].InternalFormat, 0x8058u);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->DrawBuffers[0], 0);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer(), nullptr)
|
||||
<< "a Draw record landed in the read binding as well";
|
||||
const Uint64 afterDraw = MGPipeApplier().FramebufferSerial;
|
||||
EXPECT_GT(afterDraw, serialAtStart) << "an applied record must move the serial the twin memoises";
|
||||
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{5, 2}, MGPipeFramebufferTarget::Read, 200));
|
||||
ASSERT_NE(MGPipeApplier().ReadFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer()->Fbo, (MGPipeHandle{5, 2}));
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer()->Width, 200u);
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr) << "a Read record overwrote the draw binding";
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, (MGPipeHandle{4, 1}));
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Width, 100u);
|
||||
EXPECT_GT(MGPipeApplier().FramebufferSerial, afterDraw);
|
||||
|
||||
// Both: one record, one serial bump, two destinations.
|
||||
const Uint64 beforeBoth = MGPipeApplier().FramebufferSerial;
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{6, 3}, MGPipeFramebufferTarget::Both, 300));
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
ASSERT_NE(MGPipeApplier().ReadFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, (MGPipeHandle{6, 3}));
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer()->Fbo, (MGPipeHandle{6, 3}));
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Width, 300u);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer()->Width, 300u);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferSerial, beforeBoth + 1)
|
||||
<< "a Both record is ONE record and moves the serial once";
|
||||
// The two earlier framebuffers keep their own records - the table is keyed by the handle,
|
||||
// so binding a third displaced neither (ID-19(b)).
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{4, 1}), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{4, 1})->Width, 100u);
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{5, 2}), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{5, 2})->Width, 200u);
|
||||
|
||||
// A framebuffer has a handle but NO wire lifetime, so there is no record to refuse against
|
||||
// and this entry point never counts an object refusal.
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// A target outside the FOUR is not a target this server has, and guessing one would put a
|
||||
// draw's attachments into the read binding or the other way round. Named (3) is legal since
|
||||
// ID-19(b) and has its own case below; the first refused value is the one above it.
|
||||
TEST(FramebufferEmit, ATargetOutsideTheThreeBindingsIsRefusedNamingTheRecord) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
MGPFramebufferState bad = FramebufferRecord(MGPipeHandle{7, 4}, MGPipeFramebufferTarget::Draw, 100);
|
||||
bad.Target = static_cast<Uint8>(MGPipeFramebufferTarget::Count);
|
||||
const Uint64 serialBefore = MGPipeApplier().FramebufferSerial;
|
||||
|
||||
ExpectRefusedNaming("set_framebuffer_state {slot=7, gen=4, target=4}: the record names no framebuffer "
|
||||
"binding target",
|
||||
[&bad]() { MGPipeApplySetFramebufferState(bad); });
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{7, 4}), nullptr)
|
||||
<< "a refused record was written into the per-object table anyway";
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferSerial, serialBefore)
|
||||
<< "a refused record must not move the serial";
|
||||
#endif
|
||||
}
|
||||
|
||||
// The draw-buffer array is an INDEX into this record's own Color[], and -1 is NONE. An entry
|
||||
// outside that range would have the server read a colour attachment the record does not carry,
|
||||
// which is the truncation the wire width's cap refusal exists to prevent upstream.
|
||||
TEST(FramebufferEmit, ADrawBufferEntryOutsideTheRecordsOwnArrayIsRefusedRatherThanRead) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
|
||||
// The positive control first: -1 everywhere and the last legal index are both fine, so
|
||||
// what follows is refusing the value and not the loop around it.
|
||||
MGPFramebufferState legal = FramebufferRecord(MGPipeHandle{8, 1}, MGPipeFramebufferTarget::Draw, 100);
|
||||
legal.DrawBuffers[7] = static_cast<Int8>(kMGPipeMaxColorAttachments - 1);
|
||||
MGPipeApplySetFramebufferState(legal);
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
ASSERT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, (MGPipeHandle{8, 1}));
|
||||
const Uint64 serialBefore = MGPipeApplier().FramebufferSerial;
|
||||
|
||||
MGPFramebufferState past = FramebufferRecord(MGPipeHandle{8, 1}, MGPipeFramebufferTarget::Draw, 111);
|
||||
past.DrawBuffers[3] = static_cast<Int8>(kMGPipeMaxColorAttachments);
|
||||
ExpectRefusedNaming("set_framebuffer_state {slot=8, gen=1, target=0}: a draw-buffer entry names a "
|
||||
"colour attachment outside the record's own array",
|
||||
[&past]() { MGPipeApplySetFramebufferState(past); });
|
||||
|
||||
MGPFramebufferState negative = FramebufferRecord(MGPipeHandle{8, 1}, MGPipeFramebufferTarget::Draw, 222);
|
||||
negative.DrawBuffers[0] = -2;
|
||||
ExpectRefusedNaming("set_framebuffer_state {slot=8, gen=1, target=0}: a draw-buffer entry names a "
|
||||
"colour attachment outside the record's own array",
|
||||
[&negative]() { MGPipeApplySetFramebufferState(negative); });
|
||||
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Width, 100u) << "a refused record was stored anyway";
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferSerial, serialBefore);
|
||||
#endif
|
||||
}
|
||||
|
||||
// D-J4, as ID-19(b) leaves it. The two framebuffer BINDINGS are per-context working state and a
|
||||
// make-current takes them - so both accessors answer null afterwards, exactly as the zeroed
|
||||
// records used to answer a null Fbo - while the per-object RECORD survives, because a
|
||||
// framebuffer that is only ever addressed BY NAME has no re-emission trigger at all. The serial
|
||||
// ADVANCES rather than restarting, because a counter that walks back through values it has
|
||||
// already stamped into a twin that outlived the switch is not a generation at all. Restoring
|
||||
// `= 0` anywhere in the reset, or clearing the table there, leaves this red.
|
||||
TEST(FramebufferEmit, AMakeCurrentClearsBothRecordsAndAdvancesTheSerialRatherThanZeroingIt) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{4, 1}, MGPipeFramebufferTarget::Both, 100));
|
||||
const Uint64 serialBefore = MGPipeApplier().FramebufferSerial;
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
ASSERT_EQ(MGPipeApplier().DrawFramebuffer()->Width, 100u);
|
||||
|
||||
MGPipeApplierReset(); // the make-current
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().BoundFramebuffer[0], kMGPipeNullHandle);
|
||||
EXPECT_EQ(MGPipeApplier().BoundFramebuffer[1], kMGPipeNullHandle);
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{4, 1}), nullptr)
|
||||
<< "the per-object record is not working state and a make-current may not take it";
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{4, 1})->Width, 100u);
|
||||
EXPECT_GT(MGPipeApplier().FramebufferSerial, serialBefore)
|
||||
<< "the serial was carried over or restarted; the cleared window is itself a change the "
|
||||
"twin has to hear about, and no stamped value may ever recur";
|
||||
|
||||
// And the teardown scope advances it again, for the same reason.
|
||||
const Uint64 afterReset = MGPipeApplier().FramebufferSerial;
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
EXPECT_GT(MGPipeApplier().FramebufferSerial, afterReset);
|
||||
#endif
|
||||
}
|
||||
|
||||
// THE TEARDOWN SCOPE DROPS THE OBJECT RECORDS, SO IT MUST DROP EVERY WORKING HANDLE THAT NAMES
|
||||
// ONE. The two framebuffer records hold eleven MGPSurface::Res naming texture and renderbuffer
|
||||
// records, and the three unit windows hold entries naming sampler-view, sampler-CSO and texture
|
||||
// records; a window left standing after the tables are emptied is a set of handles into empty
|
||||
// tables, which the next resolve either refuses and counts or - on a slot the next context
|
||||
// re-mints - resolves onto somebody else's record. Deleting any one of the eleven clears in
|
||||
// MGPipeApplierReleaseObjectRecords leaves this red.
|
||||
TEST(FramebufferEmit, AReleaseOfTheObjectRecordsAlsoClearsTheWorkingHandlesThatCouldNameThem) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{4, 1}, MGPipeFramebufferTarget::Both, 100));
|
||||
|
||||
// The three kVarTail sets, each with one entry naming a record the release is about to
|
||||
// drop, and each at a non-zero Start so the window itself is visible in the assertions.
|
||||
MGPBoundView view{};
|
||||
view.View = MGPipeHandle{3, 1};
|
||||
view.Texture = MGPipeHandle{9, 1};
|
||||
view.Unit = 2;
|
||||
MGPipeApplySetSamplerViews(MGPSamplerViews{2, 1, 0xAAAAu}, &view);
|
||||
|
||||
const MGPipeHandle samplerState{5, 1};
|
||||
MGPipeApplyBindSamplerStates(MGPSamplerStates{2, 1, 0xBBBBu}, &samplerState);
|
||||
|
||||
MGPImageView image{};
|
||||
image.Res = MGPipeHandle{9, 1};
|
||||
image.Unit = 2;
|
||||
image.InternalFormat = 0x8058u; // GL_RGBA8
|
||||
MGPipeApplySetShaderImages(MGPShaderImages{2, 1, 0xCCCCu}, &image);
|
||||
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
ASSERT_EQ(MGPipeApplier().DrawFramebuffer()->Color[0].Res, (MGPipeHandle{9, 1}));
|
||||
ASSERT_EQ(MGPipeApplier().SamplerViewCount, 1u);
|
||||
ASSERT_EQ(MGPipeApplier().BoundSamplerViews[2].View, (MGPipeHandle{3, 1}));
|
||||
ASSERT_EQ(MGPipeApplier().SamplerStateCount, 1u);
|
||||
ASSERT_EQ(MGPipeApplier().BoundSamplerStates[2], samplerState);
|
||||
ASSERT_EQ(MGPipeApplier().ShaderImageCount, 1u);
|
||||
ASSERT_EQ(MGPipeApplier().BoundShaderImages[2].Res, (MGPipeHandle{9, 1}));
|
||||
|
||||
MGPipeApplierReleaseObjectRecords();
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{4, 1}), nullptr)
|
||||
<< "a framebuffer record holding eleven MGPSurface::Res into the emptied texture and "
|
||||
"renderbuffer tables survived the teardown";
|
||||
EXPECT_TRUE(MGPipeApplier().FramebufferRecords.empty());
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewStart, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerViewCount, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundSamplerViews[2].View, kMGPipeNullHandle);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerStateStart, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().SamplerStateCount, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundSamplerStates[2], kMGPipeNullHandle);
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageStart, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().ShaderImageCount, 0u);
|
||||
EXPECT_EQ(MGPipeApplier().BoundShaderImages[2].Res, kMGPipeNullHandle);
|
||||
#endif
|
||||
}
|
||||
|
||||
// ID-19's CORRECTION, AND THE CASE THAT SAYS WHAT THE FOURTH TARGET IS FOR. Every DSA entry
|
||||
// point - BlitNamedFramebuffer and the four ClearNamedFramebuffer* - hands Espryt a framebuffer
|
||||
// BY NAME, and that framebuffer is very often bound to neither binding. With only the two bound
|
||||
// records the server had no description of it at all, bound its driver FBO with no attachments
|
||||
// and cleared or blitted into nothing (esprytobj C-1). A Named record fixes that WITHOUT lying
|
||||
// about the bindings: the record is written and addressable by handle, and BoundFramebuffer
|
||||
// does not move. Making the Named arm touch either binding leaves this red.
|
||||
TEST(FramebufferEmit, ANamedRecordDescribesTheFramebufferItNamesWithoutMovingEitherBinding) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
// TWO DIFFERENT FRAMEBUFFERS ON THE TWO BINDINGS FIRST, so "the bindings did not move" is an
|
||||
// assertion about values rather than about null.
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{4, 1}, MGPipeFramebufferTarget::Draw, 100));
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{5, 2}, MGPipeFramebufferTarget::Read, 200));
|
||||
const Uint64 serialBefore = MGPipeApplier().FramebufferSerial;
|
||||
|
||||
MGPFramebufferState named = FramebufferRecord(MGPipeHandle{6, 3}, MGPipeFramebufferTarget::Draw, 300);
|
||||
named.Target = static_cast<Uint8>(MGPipeFramebufferTarget::Named);
|
||||
named.Color[0].Res = MGPipeHandle{21, 1};
|
||||
MGPipeApplySetFramebufferState(named);
|
||||
|
||||
// (a) THE DSA LOOKUP FINDS IT, BY HANDLE, WITH ITS ATTACHMENTS. This is the call package D
|
||||
// makes at every named blit and clear.
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{6, 3}), nullptr)
|
||||
<< "a framebuffer handed to the server by name has no record, which is the state that "
|
||||
"clears into a driver framebuffer with no attachments";
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{6, 3})->Width, 300u);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{6, 3})->Color[0].Res, (MGPipeHandle{21, 1}));
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{6, 3})->Target, static_cast<Uint8>(MGPipeFramebufferTarget::Named));
|
||||
|
||||
// (b) AND NEITHER BINDING MOVED.
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
ASSERT_NE(MGPipeApplier().ReadFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, (MGPipeHandle{4, 1}))
|
||||
<< "a Named record claimed the draw binding";
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer()->Fbo, (MGPipeHandle{5, 2}))
|
||||
<< "a Named record claimed the read binding";
|
||||
EXPECT_EQ(MGPipeApplier().BoundFramebuffer[0], (MGPipeHandle{4, 1}));
|
||||
EXPECT_EQ(MGPipeApplier().BoundFramebuffer[1], (MGPipeHandle{5, 2}));
|
||||
|
||||
// (c) The serial moves for a Named record too: a twin memoising a framebuffer's attachments
|
||||
// has to hear that they moved, and whether it is bound is a different question.
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferSerial, serialBefore + 1);
|
||||
|
||||
// (d) And the same framebuffer can then be BOUND, which moves the binding and restates the
|
||||
// record - the two targets are not two tables.
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{6, 3}, MGPipeFramebufferTarget::Draw, 400));
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, (MGPipeHandle{6, 3}));
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Width, 400u);
|
||||
EXPECT_EQ(MGPipeApplier().ReadFramebuffer()->Fbo, (MGPipeHandle{5, 2}));
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u);
|
||||
#endif
|
||||
}
|
||||
|
||||
// STALE-GENERATION REFUSAL, ON THE ONE TABLE WHOSE OBJECT HAS NO WIRE LIFETIME. A framebuffer is
|
||||
// never destroyed on the wire, so its slot is simply overwritten by its successor - and until
|
||||
// that successor describes itself, a handle naming the DEAD one must be refused rather than
|
||||
// answered with the predecessor's attachments. That answer would be a blit or a clear into
|
||||
// somebody else's colour buffer. It is LOUD (counted, and logged once) because the only way to
|
||||
// reach it is an emitter defect, and it is counted APART from RefusedObjectCalls because this is
|
||||
// a read by the server's own sync path and not a call the applier refused.
|
||||
TEST(FramebufferEmit, AFramebufferHandleWhoseGenerationHasMovedOnIsRefusedRatherThanAnswered) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{12, 1}, MGPipeFramebufferTarget::Draw, 100));
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{12, 1}), nullptr);
|
||||
const Uint64 staleBefore = MGPipeApplier().StaleFramebufferRecordLookups;
|
||||
|
||||
// The slot has been recycled and the successor has not described itself yet.
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{12, 2}), nullptr)
|
||||
<< "a handle at a recycled slot was answered with its predecessor's record";
|
||||
EXPECT_EQ(MGPipeApplier().StaleFramebufferRecordLookups, staleBefore + 1);
|
||||
|
||||
// Now it does, and the predecessor's handle becomes the stale one - in the other direction.
|
||||
MGPipeApplySetFramebufferState(FramebufferRecord(MGPipeHandle{12, 2}, MGPipeFramebufferTarget::Draw, 200));
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{12, 2}), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{12, 2})->Width, 200u);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{12, 1}), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().StaleFramebufferRecordLookups, staleBefore + 2);
|
||||
|
||||
// THE TWO SILENT NULLS, and they are silent on purpose. "Nothing is bound to this binding"
|
||||
// is what a make-current leaves behind and arrives on every draw of a context that has not
|
||||
// described its framebuffers; "no record at this slot" is what every framebuffer looks like
|
||||
// before its first set_framebuffer_state. Counting either would bury the one that matters.
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(kMGPipeNullHandle), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(MGPipeHandle{99, 1}), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().StaleFramebufferRecordLookups, staleBefore + 2)
|
||||
<< "an unbound binding or an undescribed slot was counted as a stale generation";
|
||||
EXPECT_EQ(MGPipeApplier().RefusedObjectCalls, 0u)
|
||||
<< "the framebuffer family may never move the object-refusal counter";
|
||||
#endif
|
||||
}
|
||||
|
||||
// THE TWO REFUSALS THE PER-OBJECT TABLE ADDED. The null handle is what "nothing is bound" reads
|
||||
// as, so a record installed at {0,0} would be answered to every caller asking about an EMPTY
|
||||
// binding; and Slot is a client-supplied Uint32 that now reaches an allocator, so it takes the
|
||||
// same bound the five object tables take. Every emitter has a handle for every framebuffer it
|
||||
// describes - kMGPipeDefaultFramebuffer {0,1} for the default one - so neither value is
|
||||
// producible by a correct client, which is why both are Fatal rather than counted refusals.
|
||||
TEST(FramebufferEmit, AFramebufferRecordThatNamesNoUsableHandleIsRefusedRatherThanStored) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
|
||||
#else
|
||||
ApplierGuard guard;
|
||||
// The positive control first: the DEFAULT framebuffer is slot 0 at generation 1 and is
|
||||
// perfectly legal, so what follows refuses the null handle and not slot 0.
|
||||
MGPipeApplySetFramebufferState(
|
||||
FramebufferRecord(kMGPipeDefaultFramebuffer, MGPipeFramebufferTarget::Both, 128));
|
||||
ASSERT_NE(MGPipeApplier().FramebufferRecordFor(kMGPipeDefaultFramebuffer), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferRecordFor(kMGPipeDefaultFramebuffer)->Width, 128u);
|
||||
const Uint64 serialBefore = MGPipeApplier().FramebufferSerial;
|
||||
|
||||
MGPFramebufferState nullHandle = FramebufferRecord(kMGPipeNullHandle, MGPipeFramebufferTarget::Draw, 300);
|
||||
ExpectRefusedNaming("set_framebuffer_state {slot=0, gen=0, target=0}: the record names the null "
|
||||
"framebuffer handle",
|
||||
[&nullHandle]() { MGPipeApplySetFramebufferState(nullHandle); });
|
||||
|
||||
MGPFramebufferState pastTheBound = FramebufferRecord(
|
||||
MGPipeHandle{kMGPipeMaxFramebufferSlots, 1}, MGPipeFramebufferTarget::Draw, 400);
|
||||
ExpectRefusedNaming("set_framebuffer_state {slot=65536, gen=1, target=0}: the framebuffer slot is "
|
||||
"outside the record table's bound",
|
||||
[&pastTheBound]() { MGPipeApplySetFramebufferState(pastTheBound); });
|
||||
static_assert(kMGPipeMaxFramebufferSlots == 65536u,
|
||||
"the refusal line above names the bound; move both together");
|
||||
|
||||
EXPECT_EQ(MGPipeApplier().FramebufferSerial, serialBefore) << "a refused record moved the serial";
|
||||
ASSERT_NE(MGPipeApplier().DrawFramebuffer(), nullptr);
|
||||
EXPECT_EQ(MGPipeApplier().DrawFramebuffer()->Fbo, kMGPipeDefaultFramebuffer)
|
||||
<< "a refused record took the draw binding";
|
||||
EXPECT_LT(MGPipeApplier().FramebufferRecords.size(),
|
||||
static_cast<SizeT>(kMGPipeMaxFramebufferSlots))
|
||||
<< "an out-of-range slot resized the table instead of being refused";
|
||||
#endif
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// Before anything logs: the logger reads this variable once, on its first write, and
|
||||
// caches the handle. The name carries this process's pid, and the file is removed on the
|
||||
|
||||
@@ -1612,485 +1612,7 @@ TEST(TextureEmit, TheCreateAndRespecifyCallsAnswerWhetherTheRecordWasAccepted) {
|
||||
#endif
|
||||
}
|
||||
|
||||
=======
|
||||
// ============================================================================
|
||||
// P4a package B's EMITTER-SIDE cases. The contract commit landed the file and its ctest
|
||||
// registration and one shape pin; the wire package's applier-side cases and these are disjoint
|
||||
// TEST bodies in one file, and a collision between them is resolved by UNION, never by
|
||||
// choosing a side.
|
||||
// ============================================================================
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
#define MGL_TEXTURE_EMIT_CLIENT_TEST_LIST(X) \
|
||||
X(TextureEmit, EveryTextureTargetMapsToItsOwnResourceTarget) \
|
||||
X(TextureEmit, EveryBindKindSetsItsBindMaskBit) \
|
||||
X(TextureEmit, ABindMaskBitIsStickyAcrossARespecify) \
|
||||
X(TextureEmit, AnImageBoundTextureCarriesTheImageBindableHintForever) \
|
||||
X(TextureEmit, TheUnionBoxAndTheRegionListDescribeTheSameTexels) \
|
||||
X(TextureEmit, AScatteredUploadCarriesTheLevelShadowsStridesAndNotZero) \
|
||||
X(TextureEmit, AWholeLevelUploadCarriesZeroStrides) \
|
||||
X(TextureEmit, MoreThanKMaxDirtyRectsCollapsesToTheBoxWithRegionCountZero) \
|
||||
X(TextureEmit, AnUploadThroughAViewKeysOnTheStorageOwner) \
|
||||
X(TextureEmit, EveryTexturesParamsNameItsBuiltinSamplerCso) \
|
||||
X(TextureEmit, TwoTexturesWithIdenticalSamplingShareOneBuiltinCso) \
|
||||
X(TextureEmit, ADestroyedTextureReleasesItsResourceViewAndBuiltinSamplerSlots) \
|
||||
X(TextureEmit, ARenderbufferRespecifyPublishesItsExtentWithoutAVersionCounter) \
|
||||
X(TextureEmit, ABailedLevelStaysDirtyAndStaysOnTheDrainList)
|
||||
|
||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||
MGL_TEXTURE_EMIT_CLIENT_TEST_LIST(MGL_DECLARE_PULL_SKIP)
|
||||
#undef MGL_DECLARE_PULL_SKIP
|
||||
#else
|
||||
namespace {
|
||||
using GLContext = MG_State::GLState::GLContext;
|
||||
using MG_State::GLState::MipmapDirtyRegion;
|
||||
using MG_State::GLState::MipmapInput;
|
||||
using MG_State::GLState::MipmapStorage;
|
||||
using MG_State::GLState::RenderbufferObject;
|
||||
using MG_State::GLState::TextureObject2D;
|
||||
using MG_State::GLState::TextureObjectView;
|
||||
|
||||
// AN RAII SCOPE RATHER THAN A gtest FIXTURE, for VertexInputEmitTest's reason: both gates
|
||||
// grep `ctest -R 'TextureEmit\.'`, a TEST_F files its cases under the FIXTURE's name, and
|
||||
// gtest refuses to mix TEST and TEST_F under one suite name - so a fixture would rename
|
||||
// every case out of the gate's reach.
|
||||
//
|
||||
// It ARMS THE SUBSYSTEM BIT, which a unit binary otherwise has cleared:
|
||||
// MG_Config::Features.PipePush defaults to 0 and only ConfigLoader ever sets the phase mask,
|
||||
// so without this every emission below would be correctly skipped and every assertion would
|
||||
// be green for the wrong reason.
|
||||
struct TextureScope {
|
||||
TextureScope() {
|
||||
m_previousPush = MG_Config::Features.PipePush;
|
||||
MG_Config::Features.PipePush |= kMGPipeSubsystemTextureResources;
|
||||
m_previousContext = Move(MG_State::pGLContext);
|
||||
MG_State::pGLContext = MakeUnique<GLContext>();
|
||||
MGPipeTextureEmitterInstance().ResetForTest();
|
||||
// ARMED EXPLICITLY. kMGPipeWiredTextureSubsystem is still 0 on this base - P3a's four
|
||||
// resource_* apply bodies index ONE slot space and validate every record as a buffer
|
||||
// write, so a texture record there is a dropped buffer write or a
|
||||
// Fatal{ProtocolCorruption}, and the flip is the wire package's w1 to unblock. The
|
||||
// conversion itself is finished, so it is gated HERE: the emitter is armed, a frontend
|
||||
// mutation is driven, and the assertions read the record the emitter BUILT rather than
|
||||
// any applier state.
|
||||
MGPipeTextureEmitterInstance().ArmForTest(true);
|
||||
}
|
||||
~TextureScope() {
|
||||
MGPipeTextureEmitterInstance().ResetForTest();
|
||||
MG_State::pGLContext.reset();
|
||||
MG_State::pGLContext = Move(m_previousContext);
|
||||
MG_Config::Features.PipePush = m_previousPush;
|
||||
}
|
||||
TextureScope(const TextureScope&) = delete;
|
||||
TextureScope& operator=(const TextureScope&) = delete;
|
||||
|
||||
UniquePtr<GLContext> m_previousContext;
|
||||
Uint64 m_previousPush = 0;
|
||||
};
|
||||
|
||||
MGPipeTextureEmitter& Textures() { return MGPipeTextureEmitterInstance(); }
|
||||
GLContext& Ctx() { return *MG_State::pGLContext; }
|
||||
|
||||
// A 2D texture with `levels` levels, each RGBA8 and each half the previous one, so the
|
||||
// bytes-per-texel the emitter derives from (byteSize / texelCount) is exactly 4 and every
|
||||
// stride assertion below is an exact number rather than a range.
|
||||
SharedPtr<TextureObject2D> MakeTexture2D(Uint name, Int size, Uint levels = 1) {
|
||||
auto texture = MakeShared<TextureObject2D>(name);
|
||||
texture->SetInternalFormat(TextureInternalFormat::RGBA8);
|
||||
for (Uint level = 0; level < levels; ++level) {
|
||||
const Int extent = std::max<Int>(size >> level, 1);
|
||||
texture->AllocateStorage(TextureUploadTarget::Texture2D, level,
|
||||
MipmapInput{IntVec3{extent, extent, 1},
|
||||
static_cast<SizeT>(extent) * static_cast<SizeT>(extent) * 4});
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ============================ D-A3 ============================
|
||||
//
|
||||
// The exhaustiveness the contract's static_assert already pins, walked at RUNTIME over every
|
||||
// enumerator - because the assert answers "is every target mapped" and this answers the
|
||||
// stronger "does every target map to its OWN row". Folding rectangle onto 2D is the one
|
||||
// collapse anybody would be tempted by, and it is a distinction the frontend keeps and both
|
||||
// backends switch on.
|
||||
TEST(TextureEmit, EveryTextureTargetMapsToItsOwnResourceTarget) {
|
||||
Vector<Uint32> seen;
|
||||
for (Int i = 0; i < static_cast<Int>(TextureTarget::TextureTargetCount); ++i) {
|
||||
const auto target = static_cast<TextureTarget>(i);
|
||||
const Uint32 resourceTarget = MGPipeResourceTargetForTextureTarget(target);
|
||||
EXPECT_NE(resourceTarget, kMGPipeResourceTargetUnmapped)
|
||||
<< "TextureTarget " << i << " has no MGPResourceDesc::Target row";
|
||||
EXPECT_NE(resourceTarget, static_cast<Uint32>(MGPipeResourceTarget::Buffer))
|
||||
<< "TextureTarget " << i << " maps onto the BUFFER row, which the ack predicate reads";
|
||||
EXPECT_NE(resourceTarget, static_cast<Uint32>(MGPipeResourceTarget::Renderbuffer))
|
||||
<< "TextureTarget " << i << " maps onto the RENDERBUFFER row";
|
||||
for (const Uint32 previous : seen) {
|
||||
EXPECT_NE(previous, resourceTarget)
|
||||
<< "TextureTarget " << i << " shares its resource target with an earlier one";
|
||||
}
|
||||
seen.push_back(resourceTarget);
|
||||
}
|
||||
EXPECT_EQ(seen.size(), static_cast<SizeT>(TextureTarget::TextureTargetCount));
|
||||
}
|
||||
|
||||
// ============================ D-A4 ============================
|
||||
TEST(TextureEmit, EveryBindKindSetsItsBindMaskBit) {
|
||||
TextureScope scope;
|
||||
const auto texture = MakeTexture2D(1, 8);
|
||||
const MGPipeHandle handle = Textures().FindTexture(*texture);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(handle));
|
||||
|
||||
const Uint16 bits[] = {kMGPipeBindSampler, kMGPipeBindShaderImage, kMGPipeBindRenderTarget,
|
||||
kMGPipeBindDepthStencil};
|
||||
Uint16 expected = 0;
|
||||
for (const Uint16 bit : bits) {
|
||||
Textures().NoteTextureBoundAs(handle, bit);
|
||||
expected = static_cast<Uint16>(expected | bit);
|
||||
EXPECT_EQ(Textures().TextureBindMask(handle), expected) << "bind bit " << bit;
|
||||
}
|
||||
// ORed, never cleared: re-noting one bit cannot drop the others.
|
||||
Textures().NoteTextureBoundAs(handle, kMGPipeBindSampler);
|
||||
EXPECT_EQ(Textures().TextureBindMask(handle), expected);
|
||||
}
|
||||
|
||||
TEST(TextureEmit, ABindMaskBitIsStickyAcrossARespecify) {
|
||||
TextureScope scope;
|
||||
const auto texture = MakeTexture2D(2, 8);
|
||||
const MGPipeHandle handle = Textures().FindTexture(*texture);
|
||||
Textures().NoteTextureBoundAs(handle, kMGPipeBindRenderTarget);
|
||||
|
||||
// A storage definition REPUBLISHES the mask; it does not rebuild it.
|
||||
texture->AllocateStorage(TextureUploadTarget::Texture2D, 0,
|
||||
MipmapInput{IntVec3{16, 16, 1}, 16 * 16 * 4});
|
||||
EXPECT_TRUE(Textures().LastDesc().Resource == handle);
|
||||
EXPECT_NE(Textures().LastDesc().BindMask & kMGPipeBindRenderTarget, 0)
|
||||
<< "MGPResourceDesc::BindMask lost the RENDER_TARGET bit across a respecify";
|
||||
EXPECT_EQ(Textures().LastDesc().Width, 16u);
|
||||
}
|
||||
|
||||
TEST(TextureEmit, AnImageBoundTextureCarriesTheImageBindableHintForever) {
|
||||
TextureScope scope;
|
||||
const auto texture = MakeTexture2D(3, 8);
|
||||
const MGPipeHandle handle = Textures().FindTexture(*texture);
|
||||
EXPECT_EQ(Textures().LastDesc().ImageBindableHint, 0);
|
||||
|
||||
Textures().NoteTextureBoundAs(handle, kMGPipeBindShaderImage);
|
||||
// The next respecify carries the hint - and it is the PREVENTION half of the texture-remint
|
||||
// stall class, so it must never go back to 0 afterwards.
|
||||
texture->AllocateStorage(TextureUploadTarget::Texture2D, 1, MipmapInput{IntVec3{4, 4, 1}, 4 * 4 * 4});
|
||||
EXPECT_EQ(Textures().LastDesc().ImageBindableHint, 1);
|
||||
texture->AllocateStorage(TextureUploadTarget::Texture2D, 2, MipmapInput{IntVec3{2, 2, 1}, 2 * 2 * 4});
|
||||
EXPECT_EQ(Textures().LastDesc().ImageBindableHint, 1);
|
||||
|
||||
// And the transition armed the ONE resync the client is allowed to ask for (D-E2): the
|
||||
// widened-channel carrier needs a swizzle override the frontend params version never moves
|
||||
// for. It is one-shot - the server clears its own copy, the client never clears a server
|
||||
// flag, and the client must not keep asking.
|
||||
texture->SetSwizzleParam(TextureSwizzleParam::Red, TextureSwizzleParam::Blue);
|
||||
EXPECT_EQ(Textures().LastParams().ForceResync, 1);
|
||||
texture->SetSwizzleParam(TextureSwizzleParam::Green, TextureSwizzleParam::Blue);
|
||||
EXPECT_EQ(Textures().LastParams().ForceResync, 0);
|
||||
}
|
||||
|
||||
// ============================ D-D3 / D-D6 ============================
|
||||
//
|
||||
// THE INVARIANT THAT MAKES THE SERVER'S CHOICE SAFE, and nothing else in the tree can see it:
|
||||
// SSIM is completely blind to whether the server uploaded one union box or N rects, and the
|
||||
// Mali cliff behind that choice is ~+6 ms/frame for a hundred one-rect jobs against one box. So
|
||||
// the two representations have to describe the SAME texels - every rect inside the box, and
|
||||
// their union exactly the box.
|
||||
TEST(TextureEmit, TheUnionBoxAndTheRegionListDescribeTheSameTexels) {
|
||||
TextureScope scope;
|
||||
const auto texture = MakeTexture2D(4, 256);
|
||||
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, IntVec3{1, 1, 1});
|
||||
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{255, 255, 0},
|
||||
IntVec3{1, 1, 1});
|
||||
ASSERT_EQ(Textures().DrainListSize(), 1u);
|
||||
|
||||
Textures().DrainTextureSubData(Ctx());
|
||||
const MGPSubData record = Textures().LastSubData();
|
||||
const Vector<MGPSubRegion> regions = Textures().LastRegions();
|
||||
ASSERT_EQ(record.RegionCount, regions.size());
|
||||
ASSERT_GE(record.RegionCount, 2u) << "two far-apart one-texel writes must survive as two rects";
|
||||
|
||||
Int32 loX = record.UnionBox.X + static_cast<Int32>(record.UnionBox.W);
|
||||
Int32 loY = record.UnionBox.Y + static_cast<Int32>(record.UnionBox.H);
|
||||
Int32 hiX = record.UnionBox.X;
|
||||
Int32 hiY = record.UnionBox.Y;
|
||||
for (const MGPSubRegion& region : regions) {
|
||||
EXPECT_GE(region.X, record.UnionBox.X) << "a rect starts left of the union box";
|
||||
EXPECT_GE(region.Y, record.UnionBox.Y) << "a rect starts above the union box";
|
||||
EXPECT_LE(region.X + static_cast<Int32>(region.W),
|
||||
record.UnionBox.X + static_cast<Int32>(record.UnionBox.W))
|
||||
<< "a rect ends right of the union box";
|
||||
EXPECT_LE(region.Y + static_cast<Int32>(region.H),
|
||||
record.UnionBox.Y + static_cast<Int32>(record.UnionBox.H))
|
||||
<< "a rect ends below the union box";
|
||||
loX = std::min(loX, region.X);
|
||||
loY = std::min(loY, region.Y);
|
||||
hiX = std::max(hiX, region.X + static_cast<Int32>(region.W));
|
||||
hiY = std::max(hiY, region.Y + static_cast<Int32>(region.H));
|
||||
}
|
||||
EXPECT_EQ(loX, record.UnionBox.X) << "the rects' union does not reach the box's left edge";
|
||||
EXPECT_EQ(loY, record.UnionBox.Y) << "the rects' union does not reach the box's top edge";
|
||||
EXPECT_EQ(hiX, record.UnionBox.X + static_cast<Int32>(record.UnionBox.W));
|
||||
EXPECT_EQ(hiY, record.UnionBox.Y + static_cast<Int32>(record.UnionBox.H));
|
||||
}
|
||||
|
||||
TEST(TextureEmit, AScatteredUploadCarriesTheLevelShadowsStridesAndNotZero) {
|
||||
TextureScope scope;
|
||||
const auto texture = MakeTexture2D(5, 256);
|
||||
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{4, 8, 0}, IntVec3{2, 2, 1});
|
||||
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{200, 220, 0},
|
||||
IntVec3{2, 2, 1});
|
||||
Textures().DrainTextureSubData(Ctx());
|
||||
const Vector<MGPSubRegion> regions = Textures().LastRegions();
|
||||
ASSERT_GE(regions.size(), 2u);
|
||||
// A SUB-RECT'S ROWS ARE NOT CONTIGUOUS IN THE SHADOW, so it must carry the LEVEL's pitches -
|
||||
// not its own width - or the staging planner on the far side repacks the wrong bytes.
|
||||
for (const MGPSubRegion& region : regions) {
|
||||
EXPECT_EQ(region.SrcRowStride, 256u * 4u) << "SrcRowStride is not the LEVEL's row pitch";
|
||||
EXPECT_EQ(region.SrcSliceStride, 256u * 4u * 256u)
|
||||
<< "SrcSliceStride is not the LEVEL's slice pitch";
|
||||
const Uint64 expectedOffset =
|
||||
static_cast<Uint64>(region.Y) * 256u * 4u + static_cast<Uint64>(region.X) * 4u;
|
||||
EXPECT_EQ(region.SrcOffset, expectedOffset) << "SrcOffset is not the first texel's byte offset";
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TextureEmit, AWholeLevelUploadCarriesZeroStrides) {
|
||||
TextureScope scope;
|
||||
const auto texture = MakeTexture2D(6, 64);
|
||||
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0},
|
||||
IntVec3{64, 64, 1});
|
||||
Textures().DrainTextureSubData(Ctx());
|
||||
const MGPSubData record = Textures().LastSubData();
|
||||
EXPECT_EQ(record.RegionCount, 0u) << "a single whole-level rect IS the union box, not a list";
|
||||
EXPECT_EQ(record.UnionBox.X, 0);
|
||||
EXPECT_EQ(record.UnionBox.Y, 0);
|
||||
EXPECT_EQ(record.UnionBox.W, 64u);
|
||||
EXPECT_EQ(record.UnionBox.H, 64u);
|
||||
EXPECT_EQ(record.SourceIsVerbatimLevelShadow, 1)
|
||||
<< "the client always declares the level shadow; the server clears it when it converts";
|
||||
EXPECT_EQ(record.Blob.Seg, kMGHostSpanSegNone);
|
||||
EXPECT_EQ(record.Blob.Size, 0u) << "a monolith record does not declare its blob";
|
||||
EXPECT_NE(record.Blob.Offset, 0u) << "Blob.Offset is the level shadow's address in monolith";
|
||||
// The upload target rides in the record's Target byte beside the resource target, which is
|
||||
// the only place a cube face could ever be carried.
|
||||
EXPECT_EQ(MGPipeSubDataResourceTargetOf(record.Target),
|
||||
static_cast<Uint8>(MGPipeResourceTarget::Tex2D));
|
||||
EXPECT_EQ(MGPipeSubDataUploadTargetOf(record.Target),
|
||||
static_cast<Uint8>(TextureUploadTarget::Texture2D));
|
||||
// And the whole-level builder really does say TIGHTLY PACKED.
|
||||
const MGPipeLevelPitch pitch = MGPipeLevelPitchOf(IntVec3{64, 64, 1}, 64 * 64 * 4);
|
||||
const MGPSubRegion whole = MGPipeBuildSubRegion(record.UnionBox, IntVec3{64, 64, 1}, pitch);
|
||||
EXPECT_EQ(whole.SrcRowStride, 0u);
|
||||
EXPECT_EQ(whole.SrcSliceStride, 0u);
|
||||
EXPECT_EQ(whole.SrcOffset, 0u);
|
||||
}
|
||||
|
||||
TEST(TextureEmit, MoreThanKMaxDirtyRectsCollapsesToTheBoxWithRegionCountZero) {
|
||||
TextureScope scope;
|
||||
const auto texture = MakeTexture2D(7, 128);
|
||||
// Far more writes than the rect cap. The storage MERGES rather than truncates - a dropped
|
||||
// rect is a dropped write - and degrades toward the union box; what the emitter must never
|
||||
// do is invent or truncate. So the storage's own answer is the oracle, read BEFORE the drain
|
||||
// clears the level, and 0 means "the union box is the whole story".
|
||||
for (Int i = 0; i < 4 * static_cast<Int>(MipmapStorage::kMaxDirtyRects); ++i) {
|
||||
const Int x = (i * 7) % 120;
|
||||
const Int y = (i * 11) % 120;
|
||||
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{x, y, 0},
|
||||
IntVec3{2, 2, 1});
|
||||
}
|
||||
MipmapDirtyRegion oracle[MipmapStorage::kMaxDirtyRects];
|
||||
const SizeT oracleCount = texture->GetStorageDirtyRects(TextureUploadTarget::Texture2D, 0, oracle,
|
||||
MipmapStorage::kMaxDirtyRects);
|
||||
EXPECT_LE(oracleCount, MipmapStorage::kMaxDirtyRects);
|
||||
Textures().DrainTextureSubData(Ctx());
|
||||
EXPECT_EQ(Textures().LastSubData().RegionCount, static_cast<Uint32>(oracleCount))
|
||||
<< "the emitted region count is not the storage's own answer";
|
||||
EXPECT_EQ(Textures().LastRegions().size(), oracleCount);
|
||||
}
|
||||
|
||||
// ============================ D-D4 ============================
|
||||
TEST(TextureEmit, AnUploadThroughAViewKeysOnTheStorageOwner) {
|
||||
TextureScope scope;
|
||||
const auto owner = MakeTexture2D(8, 64, 3);
|
||||
owner->SetImmutableLevels(3);
|
||||
const MGPipeHandle ownerHandle = Textures().FindTexture(*owner);
|
||||
const auto view = MakeShared<TextureObjectView>(9, TextureTarget::Texture2D, owner, 1, 2, 0, 1);
|
||||
const MGPipeHandle viewHandle = Textures().FindTexture(*view);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(ownerHandle));
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(viewHandle));
|
||||
ASSERT_FALSE(ownerHandle == viewHandle);
|
||||
// ViewOf names the storage owner, and ONE HOP always reaches storage.
|
||||
EXPECT_TRUE(Textures().LastDesc().Resource == viewHandle);
|
||||
EXPECT_TRUE(Textures().LastDesc().ViewOf == ownerHandle)
|
||||
<< "the view's descriptor does not name its storage owner";
|
||||
|
||||
// An upload through the VIEW: TextureObjectView forwards the mark to the OWNER's method
|
||||
// after remapping the level, so the drain list holds exactly one entry and it is the
|
||||
// owner's - which is what makes an upload through a view and an upload through the owner one
|
||||
// key rather than two.
|
||||
view->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, IntVec3{4, 4, 1});
|
||||
ASSERT_EQ(Textures().DrainListSize(), 1u);
|
||||
Textures().DrainTextureSubData(Ctx());
|
||||
EXPECT_TRUE(Textures().LastSubData().Res == ownerHandle)
|
||||
<< "an upload through a view was keyed on the view instead of on its storage owner";
|
||||
// The view's level 0 IS the owner's level 1.
|
||||
EXPECT_EQ(Textures().LastSubData().Level, 1u);
|
||||
}
|
||||
|
||||
// ============================ D-E1 ============================
|
||||
TEST(TextureEmit, EveryTexturesParamsNameItsBuiltinSamplerCso) {
|
||||
TextureScope scope;
|
||||
const auto texture = MakeTexture2D(10, 8);
|
||||
const MGPipeHandle handle = Textures().FindTexture(*texture);
|
||||
texture->SetSwizzleParamRGBA(Vec4<TextureSwizzleParam>{TextureSwizzleParam::Alpha,
|
||||
TextureSwizzleParam::Blue,
|
||||
TextureSwizzleParam::Green,
|
||||
TextureSwizzleParam::Red});
|
||||
const MGPTextureParams params = Textures().LastParams();
|
||||
EXPECT_TRUE(params.Res == handle);
|
||||
// kMGPipeNullHandle is ILLEGAL here: every texture object owns a sampler object, so a null
|
||||
// is Fatal{ProtocolCorruption} on the far side rather than "no sampler".
|
||||
EXPECT_FALSE(MGPipeHandleIsNull(params.BuiltinSampler))
|
||||
<< "MGPTextureParams::BuiltinSampler must never be the null handle";
|
||||
EXPECT_TRUE(params.BuiltinSampler ==
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::SamplerCso,
|
||||
texture->GetSamplerObject()->GetLifetimeId()))
|
||||
<< "the built-in sampler CSO is not keyed on the SamplerObject's own lifetime id, which is "
|
||||
"the key ~SamplerObject's death helper resolves through";
|
||||
EXPECT_EQ(params.Swizzle[0], static_cast<Uint8>(TextureSwizzleParam::Alpha));
|
||||
EXPECT_EQ(params.Swizzle[1], static_cast<Uint8>(TextureSwizzleParam::Blue));
|
||||
EXPECT_EQ(params.Swizzle[2], static_cast<Uint8>(TextureSwizzleParam::Green));
|
||||
EXPECT_EQ(params.Swizzle[3], static_cast<Uint8>(TextureSwizzleParam::Red));
|
||||
EXPECT_EQ(params.DepthStencilMode, kMGPipeDepthStencilModeDepth);
|
||||
// D-E3's deliverable at the one site that proves it: a texture nothing has bound - no
|
||||
// sampler view, no image unit, only ever an attachment - still publishes the mode, because
|
||||
// set_texture_params is addressed by RESOURCE and is independent of every binding.
|
||||
texture->SetDepthStencilTextureMode(GL_STENCIL_INDEX);
|
||||
EXPECT_EQ(Textures().LastParams().DepthStencilMode, kMGPipeDepthStencilModeStencil);
|
||||
}
|
||||
|
||||
TEST(TextureEmit, TwoTexturesWithIdenticalSamplingShareOneBuiltinCso) {
|
||||
TextureScope scope;
|
||||
if (kMGPipeWiredSamplerSubsystem == 0) {
|
||||
GTEST_SKIP() << "the sampler family's content-addressed CSO cache is not wired on this tree. "
|
||||
"This package resolves MGPTextureParams::BuiltinSampler IDENTITY-addressed, "
|
||||
"off the SamplerObject's own lifetime id - which is the key "
|
||||
"~SamplerObject's death helper already resolves through - so two textures "
|
||||
"get two CSOs here. Sharing is the sampler package's cache (capacity 256, "
|
||||
"hashed and memcmp-confirmed field-wise over a zero-initialised canonical "
|
||||
"copy), and this case is its gate on the integrated tree.";
|
||||
}
|
||||
const auto first = MakeTexture2D(11, 8);
|
||||
const auto second = MakeTexture2D(12, 8);
|
||||
first->SetSwizzleParam(TextureSwizzleParam::Red, TextureSwizzleParam::Green);
|
||||
const MGPipeHandle firstCso = Textures().LastParams().BuiltinSampler;
|
||||
second->SetSwizzleParam(TextureSwizzleParam::Red, TextureSwizzleParam::Green);
|
||||
EXPECT_TRUE(Textures().LastParams().BuiltinSampler == firstCso);
|
||||
}
|
||||
|
||||
// ============================ D-I1 ============================
|
||||
TEST(TextureEmit, ADestroyedTextureReleasesItsResourceViewAndBuiltinSamplerSlots) {
|
||||
TextureScope scope;
|
||||
const Uint32 texturesBefore = MGPipeSlots().LiveCount(MGPipeKind::Texture);
|
||||
const Uint32 viewsBefore = MGPipeSlots().LiveCount(MGPipeKind::SamplerViewCso);
|
||||
MGPipeHandle handle{};
|
||||
{
|
||||
const auto texture = MakeTexture2D(13, 8);
|
||||
handle = Textures().FindTexture(*texture);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(handle));
|
||||
EXPECT_TRUE(MGPipeSlots().IsLive(MGPipeKind::Texture, handle));
|
||||
// The sampler VIEW is minted off the TEXTURE's own lifetime id - one per ITextureObject
|
||||
// (D-F2) - so the texture's death is the only thing that can release it.
|
||||
(void)MGPipeSlots().Acquire(MGPipeKind::SamplerViewCso, texture->GetLifetimeId());
|
||||
}
|
||||
EXPECT_FALSE(MGPipeSlots().IsLive(MGPipeKind::Texture, handle))
|
||||
<< "~TextureObjectBase did not return the texture's slot";
|
||||
EXPECT_EQ(MGPipeSlots().LiveCount(MGPipeKind::Texture), texturesBefore)
|
||||
<< "a destroyed texture leaked its resource slot";
|
||||
EXPECT_EQ(MGPipeSlots().LiveCount(MGPipeKind::SamplerViewCso), viewsBefore)
|
||||
<< "a destroyed texture leaked the sampler view minted off its lifetime id";
|
||||
// THE BUILT-IN SAMPLER'S CSO SLOT IS NOT CHECKED HERE, and the absence is a statement rather
|
||||
// than an omission: it belongs to a real SamplerObject with its OWN lifetime id and its own
|
||||
// #if MOBILEGL_PIPE_PUSH destructor, so releasing it from the texture's id would resolve the
|
||||
// wrong slot - possibly a live one belonging to another object. ~SamplerObject runs
|
||||
// immediately after ~TextureObjectBase's body (a member's destructor follows its owner's) and
|
||||
// is where the sampler package wires MGPipeEmitSamplerCsoDestroyAndFree; until that lands,
|
||||
// this tree's SamplerCso slots are released by nobody, which is exactly the ordered
|
||||
// dependency the integration order (clientfb, then clientsp) exists for.
|
||||
//
|
||||
// A second release on the same handle is a proven no-op: Free bumps no generation of its
|
||||
// own, so a double free cannot skip one.
|
||||
MGPipeSlots().Free(MGPipeKind::Texture, handle);
|
||||
EXPECT_EQ(MGPipeSlots().LiveCount(MGPipeKind::Texture), texturesBefore);
|
||||
}
|
||||
|
||||
// ============================ D-D2 ============================
|
||||
TEST(TextureEmit, ARenderbufferRespecifyPublishesItsExtentWithoutAVersionCounter) {
|
||||
TextureScope scope;
|
||||
const auto renderbuffer = MakeShared<RenderbufferObject>(1);
|
||||
const MGPipeHandle handle = Textures().FindRenderbuffer(*renderbuffer);
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(handle));
|
||||
EXPECT_TRUE(Textures().LastDesc().Resource == handle);
|
||||
EXPECT_EQ(Textures().LastDesc().Target, static_cast<Uint8>(MGPipeResourceTarget::Renderbuffer));
|
||||
EXPECT_EQ(Textures().LastDesc().HasDefinedContent, 0) << "a create carries no storage";
|
||||
|
||||
// The three setters bump no version and raise no notice, and the framebuffer bit's shutter
|
||||
// does not move for an ALREADY-ATTACHED renderbuffer - which is exactly why the hole is
|
||||
// closed by EMISSION from the storage entry point rather than by a new counter (a member
|
||||
// would resize the pull build's object) or a wider shutter.
|
||||
renderbuffer->SetInternalFormat(TextureInternalFormat::Depth24Stencil8);
|
||||
renderbuffer->AllocateStorage(IntVec2{320, 240});
|
||||
renderbuffer->SetSamples(4);
|
||||
const MGPResourceDesc desc = Textures().LastDesc();
|
||||
EXPECT_TRUE(desc.Resource == handle);
|
||||
EXPECT_EQ(desc.Width, 320u);
|
||||
EXPECT_EQ(desc.Height, 240u);
|
||||
EXPECT_EQ(desc.Samples, 4u);
|
||||
EXPECT_EQ(desc.HasDefinedContent, 1);
|
||||
EXPECT_EQ(desc.InternalFormat, static_cast<Uint32>(TextureInternalFormat::Depth24Stencil8));
|
||||
|
||||
// THE DEDUPE, which is what keeps one glRenderbufferStorage one record rather than three.
|
||||
const Uint64 respecifiesBefore = Textures().RespecifyCount();
|
||||
renderbuffer->AllocateStorage(IntVec2{320, 240});
|
||||
EXPECT_EQ(Textures().RespecifyCount(), respecifiesBefore);
|
||||
}
|
||||
|
||||
// ============================ D-D5 ============================
|
||||
TEST(TextureEmit, ABailedLevelStaysDirtyAndStaysOnTheDrainList) {
|
||||
TextureScope scope;
|
||||
// A level with no storage at all: there is nothing to upload, the record cannot be built,
|
||||
// and the texels are still owed. Naively clearing the flag here is exactly how a bail loses
|
||||
// texels, which is the failure D-D5's three steps exist to make impossible.
|
||||
// A level with a real EXTENT and no BYTES: the region is non-empty so the level is genuinely
|
||||
// dirty, and the emitter cannot derive a bytes-per-texel or find a shadow to point at.
|
||||
const auto texture = MakeShared<TextureObject2D>(14);
|
||||
texture->SetInternalFormat(TextureInternalFormat::RGBA8);
|
||||
texture->AllocateStorage(TextureUploadTarget::Texture2D, 0, MipmapInput{IntVec3{8, 8, 1}, 0});
|
||||
texture->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, IntVec3{4, 4, 1});
|
||||
ASSERT_TRUE(texture->IsStorageDirty(TextureUploadTarget::Texture2D, 0));
|
||||
ASSERT_EQ(Textures().DrainListSize(), 1u);
|
||||
Textures().DrainTextureSubData(Ctx());
|
||||
EXPECT_EQ(Textures().DrainListSize(), 1u)
|
||||
<< "a level the emitter could not describe was dropped from the drain list";
|
||||
EXPECT_EQ(Textures().SubDataCount(), 0u);
|
||||
|
||||
// And a level that WAS emitted leaves both the flag and the list entry behind it.
|
||||
const auto good = MakeTexture2D(15, 16);
|
||||
good->MarkStorageDirtyRegion(TextureUploadTarget::Texture2D, 0, IntVec3{0, 0, 0}, IntVec3{4, 4, 1});
|
||||
Textures().DrainTextureSubData(Ctx());
|
||||
EXPECT_EQ(Textures().SubDataCount(), 1u);
|
||||
EXPECT_FALSE(good->IsStorageDirty(TextureUploadTarget::Texture2D, 0))
|
||||
<< "the client must clear its own flag for a level whose record the applier accepted";
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
|
||||
// ==================================================================================int main(int argc, char** argv) {
|
||||
int main(int argc, char** argv) {
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
fs::temp_directory_path() / ("mobilegl-textureemit-test-" + std::to_string(ProcessId()) + ".log");
|
||||
|
||||
Reference in New Issue
Block a user