diff --git a/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp b/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp index 9d264d4f..060f72cb 100644 --- a/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/FramebufferEmitTest.cpp @@ -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(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(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(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(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(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(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(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(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(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(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(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(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 diff --git a/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp b/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp index 4853b9fe..9fae130f 100644 --- a/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp @@ -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(); - 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 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 MakeTexture2D(Uint name, Int size, Uint levels = 1) { - auto texture = MakeShared(name); - texture->SetInternalFormat(TextureInternalFormat::RGBA8); - for (Uint level = 0; level < levels; ++level) { - const Int extent = std::max(size >> level, 1); - texture->AllocateStorage(TextureUploadTarget::Texture2D, level, - MipmapInput{IntVec3{extent, extent, 1}, - static_cast(extent) * static_cast(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 seen; - for (Int i = 0; i < static_cast(TextureTarget::TextureTargetCount); ++i) { - const auto target = static_cast(i); - const Uint32 resourceTarget = MGPipeResourceTargetForTextureTarget(target); - EXPECT_NE(resourceTarget, kMGPipeResourceTargetUnmapped) - << "TextureTarget " << i << " has no MGPResourceDesc::Target row"; - EXPECT_NE(resourceTarget, static_cast(MGPipeResourceTarget::Buffer)) - << "TextureTarget " << i << " maps onto the BUFFER row, which the ack predicate reads"; - EXPECT_NE(resourceTarget, static_cast(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(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(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 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(record.UnionBox.W); - Int32 loY = record.UnionBox.Y + static_cast(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(region.W), - record.UnionBox.X + static_cast(record.UnionBox.W)) - << "a rect ends right of the union box"; - EXPECT_LE(region.Y + static_cast(region.H), - record.UnionBox.Y + static_cast(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(region.W)); - hiY = std::max(hiY, region.Y + static_cast(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(record.UnionBox.W)); - EXPECT_EQ(hiY, record.UnionBox.Y + static_cast(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 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(region.Y) * 256u * 4u + static_cast(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(MGPipeResourceTarget::Tex2D)); - EXPECT_EQ(MGPipeSubDataUploadTargetOf(record.Target), - static_cast(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(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(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(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::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(TextureSwizzleParam::Alpha)); - EXPECT_EQ(params.Swizzle[1], static_cast(TextureSwizzleParam::Blue)); - EXPECT_EQ(params.Swizzle[2], static_cast(TextureSwizzleParam::Green)); - EXPECT_EQ(params.Swizzle[3], static_cast(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(1); - const MGPipeHandle handle = Textures().FindRenderbuffer(*renderbuffer); - ASSERT_FALSE(MGPipeHandleIsNull(handle)); - EXPECT_TRUE(Textures().LastDesc().Resource == handle); - EXPECT_EQ(Textures().LastDesc().Target, static_cast(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(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(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");