mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 12:48:32 +09:00
[Feat, Test] (Pipe): wire the framebuffer subsystem bit and pin the resolved read surface, the sticky bind mask, the level-shadow strides and the union-box/region-list invariant
This commit is contained in:
@@ -41,12 +41,19 @@
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// WHICH SUBSYSTEM BIT THIS BUILD ACTUALLY EMITS FOR, and it is 0 until the emitter below
|
||||
// has a body. PipeFill.cpp ORs the four per-family constants into kMGPipeWiredSubsystems,
|
||||
// so the bit is added by the commit that gives the emitters their bodies, with no file
|
||||
// touched twice - and a Coverage.def row can never silently drop a field on the floor
|
||||
// before the call that carries it exists.
|
||||
inline constexpr Uint64 kMGPipeWiredFramebufferSubsystem = 0;
|
||||
// WHICH SUBSYSTEM BIT THIS BUILD ACTUALLY EMITS FOR. PipeFill.cpp ORs the four per-family
|
||||
// constants into kMGPipeWiredSubsystems, so the bit is added by the commit that gives the
|
||||
// emitters their bodies, with no file touched twice - and a Coverage.def row can never
|
||||
// silently drop a field on the floor before the call that carries it exists.
|
||||
//
|
||||
// TURNING IT ON RETIRES NO PULL. GetFramebufferBindingSlot is the family's one
|
||||
// Coverage.def emitted row and PipeFill.cpp's EmittedCallSuppliesTheWholeField answers
|
||||
// FALSE for it, with the reason: the field's storage is a BindingSlot<FramebufferObject> -
|
||||
// a frontend heap reference - and the call that supplies it carries eight-byte {slot, gen}
|
||||
// handles and a fully resolved descriptor. So this bit switches the EMISSION on and the
|
||||
// residual fill keeps writing the mirror, which is what keeps the verify lane at zero
|
||||
// divergence.
|
||||
inline constexpr Uint64 kMGPipeWiredFramebufferSubsystem = kMGPipeSubsystemFramebuffer;
|
||||
|
||||
inline Bool MGPipeFramebufferSubsystemEnabled() {
|
||||
return (kMGPipeWiredFramebufferSubsystem & kMGPipeSubsystemFramebuffer) != 0 &&
|
||||
|
||||
@@ -67,22 +67,61 @@
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
// WHICH SUBSYSTEM BIT THIS BUILD ACTUALLY EMITS FOR, and it is 0 until the emitter below
|
||||
// has a body. PipeFill.cpp ORs the four per-family constants into kMGPipeWiredSubsystems,
|
||||
// so the bit is added by the commit that gives the emitters their bodies, with no file
|
||||
// touched twice - and a Coverage.def row can never silently drop a field on the floor
|
||||
// before the call that carries it exists.
|
||||
// WHICH SUBSYSTEM BIT THIS BUILD ACTUALLY EMITS FOR. PipeFill.cpp ORs the four per-family
|
||||
// constants into kMGPipeWiredSubsystems, so the bit is added by the commit that gives the
|
||||
// emitters their bodies, with no file touched twice - and a Coverage.def row can never
|
||||
// silently drop a field on the floor before the call that carries it exists.
|
||||
//
|
||||
// IT IS STILL 0, AND THAT IS A BLOCKED FLIP RATHER THAN AN UNFINISHED ONE. Unlike the other
|
||||
// three P4a families, this one does not get four fresh apply entry points: the catalogue is
|
||||
// closed and a texture rides P3a's OWN resource_create / resource_respecify /
|
||||
// resource_subdata / resource_destroy rows. On a base without the wire package's `w1` those
|
||||
// four have P3a's BUFFER bodies, and two of their properties make a texture record actively
|
||||
// harmful rather than merely ignored:
|
||||
//
|
||||
// * MGPipeApplierState::Resources is ONE vector indexed by SLOT (D-B2 makes it three, one
|
||||
// per resource kind). Buffer, Texture and Renderbuffer slot spaces are independent, so
|
||||
// a texture create at slot 12 OVERWRITES the buffer record at slot 12, and the next
|
||||
// write to that buffer is refused against the texture's extent - a dropped content
|
||||
// write with no diagnostic beyond the refusal counter;
|
||||
// * SubDataBoxFault validates every record as the buffer half of MGPSubData, so a texture
|
||||
// sub-data record is Fatal{ProtocolCorruption} on `record.Level != 0` alone (D-D4's
|
||||
// drain cases and TextureTest.GetTexImageReadsALevelWhoseLowerLevelsWereNeverDefined
|
||||
// abort in a verify build, which is how this was found rather than argued).
|
||||
//
|
||||
// So the flip is `w1`'s to unblock and the integrator's to make, in the rebase of this
|
||||
// branch onto the wire branch: change the 0 below to kMGPipeSubsystemTextureResources, and
|
||||
// nothing else. The whole conversion is already gated by TextureEmit's cases, which arm the
|
||||
// emitter directly (ArmForTest) and assert on the EMITTED records rather than on applier
|
||||
// state - so the flip cannot land untested, and until it lands nothing this file builds
|
||||
// reaches an applier that cannot hold it.
|
||||
inline constexpr Uint64 kMGPipeWiredTextureSubsystem = 0;
|
||||
|
||||
// BOTH HALVES MATTER, exactly as MGPipeResourceSubsystemEnabled()'s two do. The bit is the
|
||||
// operator's per-subsystem A/B; the constant above is "has the client's texture family been
|
||||
// given a body in this build at all". There is no third half - no MGPipeResourceOps member
|
||||
// and no backend op table (D-B1: every P4a call is an object record or working state the
|
||||
// applier stores, and none of them dispatches to a backend function pointer) - which is
|
||||
// what makes the A/B a pure configuration question rather than a bring-up-order one.
|
||||
inline Bool MGPipeTextureSubsystemEnabled() {
|
||||
return (kMGPipeWiredTextureSubsystem & kMGPipeSubsystemTextureResources) != 0 &&
|
||||
(MG_Config::Features.PipePush & kMGPipeSubsystemTextureResources) != 0;
|
||||
// operator's per-subsystem A/B; the emitter's arm is "has this build's texture family been
|
||||
// switched on at all", and it is initialised from the constant above. There is no third
|
||||
// half - no MGPipeResourceOps member and no backend op table (D-B1: every P4a call is an
|
||||
// object record or working state the applier stores, and none of them dispatches to a
|
||||
// backend function pointer) - which is what makes the A/B a pure configuration question
|
||||
// rather than a bring-up-order one.
|
||||
inline Bool MGPipeTextureSubsystemEnabled();
|
||||
|
||||
// DO THE RECORDS THIS EMITTER BUILDS REACH THE APPLIER ON THIS BASE? It is the wired
|
||||
// constant asked as a predicate, and it is a SECOND gate rather than the same one because
|
||||
// the two questions really are different while the flip is blocked:
|
||||
//
|
||||
// * the emitter's ARM decides whether the family runs at all - the handles, the sticky
|
||||
// bind mask, the descriptor dedupe, the drain list and the record construction;
|
||||
// * this decides whether the four resource_* records are handed to P3a's apply bodies,
|
||||
// which cannot hold them until the wire package's w1 gives them their Desc.Target
|
||||
// branch and their three per-kind vectors.
|
||||
//
|
||||
// set_texture_params is deliberately NOT behind it: MGPipeApplySetTextureParams is one of
|
||||
// P4a's OWN fifteen entry points and is a stub at the contract tag, so a record handed to it
|
||||
// is stored by nobody and refused by nobody. Sending it is what keeps that seam exercised
|
||||
// rather than merely declared.
|
||||
inline constexpr Bool MGPipeTextureRecordsReachTheApplier() {
|
||||
return (kMGPipeWiredTextureSubsystem & kMGPipeSubsystemTextureResources) != 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
@@ -455,8 +494,8 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
void NoteRenderbufferBoundAs(MGPipeHandle handle, Uint16 bit) {
|
||||
if (MGPipeHandleIsNull(handle)) return;
|
||||
EntryFor(m_renderbuffers, handle).BindMask = static_cast<Uint16>(
|
||||
EntryFor(m_renderbuffers, handle).BindMask | bit);
|
||||
Entry& entry = EntryFor(m_renderbuffers, handle);
|
||||
entry.BindMask = static_cast<Uint16>(entry.BindMask | bit);
|
||||
}
|
||||
Uint16 TextureBindMask(MGPipeHandle handle) const { return MaskOf(m_textures, handle); }
|
||||
Uint16 RenderbufferBindMask(MGPipeHandle handle) const { return MaskOf(m_renderbuffers, handle); }
|
||||
@@ -489,7 +528,7 @@ namespace MobileGL::MG_Pipe {
|
||||
entry.LastDesc = desc;
|
||||
entry.HasLastDesc = true;
|
||||
NoteDesc(desc, /*isCreate=*/true);
|
||||
MGPipeApplyResourceCreate(desc);
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) MGPipeApplyResourceCreate(desc);
|
||||
}
|
||||
|
||||
// resource_create for a texture whose DERIVED object does not exist yet - the base
|
||||
@@ -510,7 +549,7 @@ namespace MobileGL::MG_Pipe {
|
||||
entry.LastDesc = desc;
|
||||
entry.HasLastDesc = true;
|
||||
NoteDesc(desc, /*isCreate=*/true);
|
||||
MGPipeApplyResourceCreate(desc);
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) MGPipeApplyResourceCreate(desc);
|
||||
}
|
||||
|
||||
// resource_respecify, from every storage-defining entry point. DEDUPED ON THE
|
||||
@@ -565,7 +604,7 @@ namespace MobileGL::MG_Pipe {
|
||||
bufOffset, bufSize);
|
||||
entry.Published = true;
|
||||
NoteDesc(createDesc, /*isCreate=*/true);
|
||||
MGPipeApplyResourceCreate(createDesc);
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) MGPipeApplyResourceCreate(createDesc);
|
||||
}
|
||||
entry.LastDesc = desc;
|
||||
entry.HasLastDesc = true;
|
||||
@@ -573,7 +612,7 @@ namespace MobileGL::MG_Pipe {
|
||||
// NO initial bytes: a texture's texels travel as resource_subdata out of the drain
|
||||
// list, never inside its storage definition. This is what keeps glTexImage2D's
|
||||
// "define the level and upload it" one allocation and one upload rather than two.
|
||||
MGPipeApplyResourceRespecify(desc, nullptr);
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) MGPipeApplyResourceRespecify(desc, nullptr);
|
||||
}
|
||||
|
||||
void EmitTextureParams(ITextureObject& texture) {
|
||||
@@ -614,7 +653,7 @@ namespace MobileGL::MG_Pipe {
|
||||
entry.LastDesc = desc;
|
||||
entry.HasLastDesc = true;
|
||||
NoteDesc(desc, /*isCreate=*/true);
|
||||
MGPipeApplyResourceCreate(desc);
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) MGPipeApplyResourceCreate(desc);
|
||||
}
|
||||
|
||||
// D-D2: THE RENDERBUFFER PUBLICATION HOLE, CLOSED BY EMISSION.
|
||||
@@ -639,12 +678,12 @@ namespace MobileGL::MG_Pipe {
|
||||
renderbuffer, handle, entry.BindMask, /*storageDefined=*/false);
|
||||
entry.Published = true;
|
||||
NoteDesc(createDesc, /*isCreate=*/true);
|
||||
MGPipeApplyResourceCreate(createDesc);
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) MGPipeApplyResourceCreate(createDesc);
|
||||
}
|
||||
entry.LastDesc = desc;
|
||||
entry.HasLastDesc = true;
|
||||
NoteDesc(desc, /*isCreate=*/false);
|
||||
MGPipeApplyResourceRespecify(desc, nullptr);
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) MGPipeApplyResourceRespecify(desc, nullptr);
|
||||
}
|
||||
|
||||
// ---- the drain list (D-D4) ----
|
||||
@@ -755,6 +794,18 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
void ResetCounters() { m_creates = m_respecifies = m_paramSets = m_subDatas = 0; }
|
||||
|
||||
// ---- the arm (see kMGPipeWiredTextureSubsystem) ----
|
||||
//
|
||||
// "Does this build's texture family emit at all", initialised from the wired constant.
|
||||
// It is a RUNTIME latch and not a constant only because the flip is blocked on the wire
|
||||
// package's `w1` while the conversion below is finished: a unit case arms it, drives a
|
||||
// frontend mutation and asserts on the record the emitter built, so the conversion is
|
||||
// gated by a test on a base whose applier could not yet hold that record. Once the
|
||||
// constant is flipped this stays true for the life of the process and ArmForTest is
|
||||
// redundant rather than wrong.
|
||||
Bool Armed() const { return m_armed; }
|
||||
void ArmForTest(Bool armed) { m_armed = armed; }
|
||||
|
||||
// A unit fixture's per-case reset; the library never calls it. See
|
||||
// MGPipeResourceTracker::ResetForTest for the rule this restates: a texture handle and
|
||||
// the applier record it names are SHARE-GROUP OBJECT STATE, so nothing here is
|
||||
@@ -768,6 +819,7 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastDesc = MGPResourceDesc{};
|
||||
m_lastParams = MGPTextureParams{};
|
||||
m_lastSubData = MGPSubData{};
|
||||
m_armed = (kMGPipeWiredTextureSubsystem & kMGPipeSubsystemTextureResources) != 0;
|
||||
ResetCounters();
|
||||
}
|
||||
|
||||
@@ -884,7 +936,9 @@ namespace MobileGL::MG_Pipe {
|
||||
m_lastSubData.Blob.Offset = static_cast<Uint64>(reinterpret_cast<std::uintptr_t>(shadow));
|
||||
m_lastSubData.Blob.Size = 0;
|
||||
|
||||
MGPipeApplyResourceSubData(m_lastSubData, shadow);
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
|
||||
MGPipeApplyResourceSubData(m_lastSubData, shadow);
|
||||
}
|
||||
++m_subDatas;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ClientTextureUploadEmissions, 1);
|
||||
@@ -911,6 +965,7 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 m_respecifies = 0;
|
||||
Uint64 m_paramSets = 0;
|
||||
Uint64 m_subDatas = 0;
|
||||
Bool m_armed = (kMGPipeWiredTextureSubsystem & kMGPipeSubsystemTextureResources) != 0;
|
||||
};
|
||||
|
||||
inline MGPipeTextureEmitter& MGPipeTextureEmitterInstance() {
|
||||
@@ -922,6 +977,11 @@ namespace MobileGL::MG_Pipe {
|
||||
return *emitter;
|
||||
}
|
||||
|
||||
inline Bool MGPipeTextureSubsystemEnabled() {
|
||||
return MGPipeTextureEmitterInstance().Armed() &&
|
||||
(MG_Config::Features.PipePush & kMGPipeSubsystemTextureResources) != 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The six entry points MG_State calls. See this file's header comment for why they are
|
||||
// here rather than in MG_Pipe/PipeMutation.h.
|
||||
@@ -1004,7 +1064,7 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPHandleOnly only{};
|
||||
only.Handle = handle;
|
||||
only.Kind = static_cast<Uint32>(MGPipeKind::Texture);
|
||||
MGPipeApplyResourceDestroy(only);
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) MGPipeApplyResourceDestroy(only);
|
||||
emitter.NoteTextureRecordDestroyed(handle);
|
||||
return true;
|
||||
}
|
||||
@@ -1016,7 +1076,7 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPHandleOnly only{};
|
||||
only.Handle = handle;
|
||||
only.Kind = static_cast<Uint32>(MGPipeKind::Renderbuffer);
|
||||
MGPipeApplyResourceDestroy(only);
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) MGPipeApplyResourceDestroy(only);
|
||||
emitter.NoteRenderbufferRecordDestroyed(handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,14 @@
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/FramebufferEmit.h>
|
||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||
#include <MG_State/GLState/RenderbufferState/RenderbufferObject.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject2D.h>
|
||||
|
||||
#include <algorithm>
|
||||
#endif
|
||||
|
||||
using namespace MobileGL;
|
||||
@@ -202,7 +209,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
|
||||
@@ -560,7 +566,367 @@ TEST(FramebufferEmit, AFramebufferRecordThatNamesNoUsableHandleIsRefusedRatherTh
|
||||
<< "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
|
||||
// TEST bodies in one file, and a collision between them is resolved by UNION, never by
|
||||
// choosing a side.
|
||||
//
|
||||
// EVERY CASE FAILS BY FIELD NAME, never by a bare count: G7's scripted control stops the
|
||||
// conversion copying exactly one member (MGPSurface::Layered) and expects this suite to go red
|
||||
// NAMING that field, and a case that reported only "the records differ" could not answer it.
|
||||
// ============================================================================
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
#define MGL_FRAMEBUFFER_EMIT_CLIENT_TEST_LIST(X) \
|
||||
X(FramebufferEmit, TheResolvedReadSurfaceComesFromTheReadFramebuffersOwnReadBuffer) \
|
||||
X(FramebufferEmit, OneObjectBoundToBothTargetsEmitsOneRecordWithTargetBoth) \
|
||||
X(FramebufferEmit, ADrawBufferChangeAloneStillMovesTheContentHash) \
|
||||
X(FramebufferEmit, ARecycledFramebufferHandleIsNeverSuppressedAgainstItsPredecessor) \
|
||||
X(FramebufferEmit, EveryAttachmentFieldSurvivesTheSurfaceConversion) \
|
||||
X(FramebufferEmit, AnAttachmentPointAboveTheWireWidthIsRefusedNotTruncated) \
|
||||
X(FramebufferEmit, ARestoragedAttachedRenderbufferPublishesItsNewExtent) \
|
||||
X(FramebufferEmit, AnUnchangedBindingPairEmitsNothing)
|
||||
|
||||
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
|
||||
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
|
||||
MGL_FRAMEBUFFER_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::FramebufferObject;
|
||||
using MG_State::GLState::MipmapInput;
|
||||
using MG_State::GLState::RenderbufferObject;
|
||||
using MG_State::GLState::TextureObject2D;
|
||||
|
||||
// AN RAII SCOPE RATHER THAN A gtest FIXTURE, for VertexInputEmitTest's reason, and it arms
|
||||
// BOTH bits this family needs: the framebuffer bit for the emission itself, and the
|
||||
// texture-resource bit because MGPSurface::Res names a texture or renderbuffer handle and
|
||||
// bit 9 requires bit 10 for exactly that reason.
|
||||
struct FramebufferScope {
|
||||
FramebufferScope() {
|
||||
m_previousPush = MG_Config::Features.PipePush;
|
||||
MG_Config::Features.PipePush |=
|
||||
kMGPipeSubsystemFramebuffer | kMGPipeSubsystemTextureResources;
|
||||
m_previousContext = Move(MG_State::pGLContext);
|
||||
MG_State::pGLContext = MakeUnique<GLContext>();
|
||||
MGPipeFramebufferEmitterInstance().ResetForTest();
|
||||
MGPipeTextureEmitterInstance().ResetForTest();
|
||||
MGPipeSetHashSuppressorInstance().InvalidateAll();
|
||||
// See TextureEmitTest's twin: the texture-resource family's wired constant is still
|
||||
// blocked on the wire package, and this suite drives a renderbuffer respecify through
|
||||
// it, so the emitter is armed explicitly here.
|
||||
MGPipeTextureEmitterInstance().ArmForTest(true);
|
||||
}
|
||||
~FramebufferScope() {
|
||||
MGPipeFramebufferEmitterInstance().ResetForTest();
|
||||
MGPipeTextureEmitterInstance().ResetForTest();
|
||||
MGPipeSetHashSuppressorInstance().InvalidateAll();
|
||||
MG_State::pGLContext.reset();
|
||||
MG_State::pGLContext = Move(m_previousContext);
|
||||
MG_Config::Features.PipePush = m_previousPush;
|
||||
}
|
||||
FramebufferScope(const FramebufferScope&) = delete;
|
||||
FramebufferScope& operator=(const FramebufferScope&) = delete;
|
||||
|
||||
UniquePtr<GLContext> m_previousContext;
|
||||
Uint64 m_previousPush = 0;
|
||||
};
|
||||
|
||||
MGPipeFramebufferEmitter& Framebuffers() { return MGPipeFramebufferEmitterInstance(); }
|
||||
GLContext& Ctx() { return *MG_State::pGLContext; }
|
||||
|
||||
SharedPtr<TextureObject2D> MakeColorTexture(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;
|
||||
}
|
||||
|
||||
void BindDrawAndRead(const SharedPtr<FramebufferObject>& draw, const SharedPtr<FramebufferObject>& read) {
|
||||
Ctx().GetFramebufferBindingSlot(FramebufferTarget::Draw).Bind(draw);
|
||||
Ctx().GetFramebufferBindingSlot(FramebufferTarget::Read).Bind(read);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ============================ D-C1 / D-C2 ============================
|
||||
//
|
||||
// THE RESOLVED READ SURFACE IS WHAT STRUCTURALLY CLOSES THE read-buffer-shared-FBO DEFECT
|
||||
// CLASS. The record carries the surface itself rather than an index, and it is resolved from the
|
||||
// READ framebuffer's OWN read buffer - so the "same FBO as draw" skip that used to lose it
|
||||
// cannot be expressed at all.
|
||||
TEST(FramebufferEmit, TheResolvedReadSurfaceComesFromTheReadFramebuffersOwnReadBuffer) {
|
||||
FramebufferScope scope;
|
||||
const auto drawColor = MakeColorTexture(1, 32);
|
||||
const auto readColor0 = MakeColorTexture(2, 32);
|
||||
const auto readColor1 = MakeColorTexture(3, 32);
|
||||
|
||||
const auto drawFbo = MakeShared<FramebufferObject>(1);
|
||||
drawFbo->AttachTexture(FramebufferAttachmentType::Color0, drawColor, TextureUploadTarget::Texture2D);
|
||||
const auto readFbo = MakeShared<FramebufferObject>(2);
|
||||
readFbo->AttachTexture(FramebufferAttachmentType::Color0, readColor0, TextureUploadTarget::Texture2D);
|
||||
readFbo->AttachTexture(FramebufferAttachmentType::Color1, readColor1, TextureUploadTarget::Texture2D);
|
||||
readFbo->SetReadBuffer(FramebufferAttachmentType::Color1);
|
||||
BindDrawAndRead(drawFbo, readFbo);
|
||||
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
EXPECT_EQ(Framebuffers().EmissionCount(), 2u) << "two distinct bindings are two records";
|
||||
|
||||
const MGPipeHandle readColor1Handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, readColor1->GetLifetimeId());
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(readColor1Handle));
|
||||
EXPECT_EQ(Framebuffers().LastDraw().Target, static_cast<Uint8>(MGPipeFramebufferTarget::Draw));
|
||||
EXPECT_TRUE(Framebuffers().LastDraw().ReadSurface.Res == readColor1Handle)
|
||||
<< "MGPFramebufferState::ReadSurface on the DRAW record did not come from the READ "
|
||||
"framebuffer's own read buffer";
|
||||
EXPECT_EQ(Framebuffers().LastRead().Target, static_cast<Uint8>(MGPipeFramebufferTarget::Read));
|
||||
EXPECT_TRUE(Framebuffers().LastRead().ReadSurface.Res == readColor1Handle);
|
||||
// And the draw-buffer array belongs to the DRAW object, whichever record carries it.
|
||||
EXPECT_EQ(Framebuffers().LastDraw().DrawBuffers[0], 0);
|
||||
}
|
||||
|
||||
TEST(FramebufferEmit, OneObjectBoundToBothTargetsEmitsOneRecordWithTargetBoth) {
|
||||
FramebufferScope scope;
|
||||
const auto color0 = MakeColorTexture(4, 32);
|
||||
const auto color1 = MakeColorTexture(5, 32);
|
||||
const auto fbo = MakeShared<FramebufferObject>(3);
|
||||
fbo->AttachTexture(FramebufferAttachmentType::Color0, color0, TextureUploadTarget::Texture2D);
|
||||
fbo->AttachTexture(FramebufferAttachmentType::Color1, color1, TextureUploadTarget::Texture2D);
|
||||
fbo->SetReadBuffer(FramebufferAttachmentType::Color1);
|
||||
BindDrawAndRead(fbo, fbo);
|
||||
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
EXPECT_EQ(Framebuffers().EmissionCount(), 1u) << "one object on both targets is ONE record";
|
||||
EXPECT_EQ(Framebuffers().LastDraw().Target, static_cast<Uint8>(MGPipeFramebufferTarget::Both));
|
||||
const MGPipeHandle color1Handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, color1->GetLifetimeId());
|
||||
EXPECT_TRUE(Framebuffers().LastDraw().ReadSurface.Res == color1Handle)
|
||||
<< "the shared-FBO record's ReadSurface is not that object's own read buffer";
|
||||
}
|
||||
|
||||
// ============================ D-C4 ============================
|
||||
//
|
||||
// THE fragColor BROADCAST TRAP. The backend derives the broadcast count from the draw-buffer
|
||||
// array, at the verb, from the framebuffer state it then holds - precisely so a program can
|
||||
// relink inside the same draw. A hash that did not cover the array would let a suppressed
|
||||
// set_framebuffer_state mean "the draw buffers did not move" when they had, and the shader
|
||||
// would be specialised for the previous output shape.
|
||||
TEST(FramebufferEmit, ADrawBufferChangeAloneStillMovesTheContentHash) {
|
||||
FramebufferScope scope;
|
||||
const auto color0 = MakeColorTexture(6, 32);
|
||||
const auto color1 = MakeColorTexture(7, 32);
|
||||
const auto fbo = MakeShared<FramebufferObject>(4);
|
||||
fbo->AttachTexture(FramebufferAttachmentType::Color0, color0, TextureUploadTarget::Texture2D);
|
||||
fbo->AttachTexture(FramebufferAttachmentType::Color1, color1, TextureUploadTarget::Texture2D);
|
||||
BindDrawAndRead(fbo, fbo);
|
||||
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
ASSERT_EQ(Framebuffers().EmissionCount(), 1u);
|
||||
const Uint64 before = Framebuffers().LastDraw().ContentHash;
|
||||
const Int8 slot1Before = Framebuffers().LastDraw().DrawBuffers[1];
|
||||
|
||||
// NOTHING BUT THE DRAW-BUFFER ARRAY MOVES: the same attachments, the same extent, the same
|
||||
// completeness answer, the same handle.
|
||||
fbo->SetDrawBuffer(1, FramebufferAttachmentType::Color1);
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
EXPECT_EQ(Framebuffers().EmissionCount(), 2u)
|
||||
<< "a draw-buffer change alone was suppressed, which would freeze the fragColor "
|
||||
"broadcast count at the previous output shape";
|
||||
EXPECT_NE(Framebuffers().LastDraw().ContentHash, before)
|
||||
<< "MGPFramebufferState::ContentHash does not cover DrawBuffers[]";
|
||||
EXPECT_NE(Framebuffers().LastDraw().DrawBuffers[1], slot1Before);
|
||||
EXPECT_EQ(Framebuffers().LastDraw().DrawBuffers[1], 1);
|
||||
}
|
||||
|
||||
TEST(FramebufferEmit, ARecycledFramebufferHandleIsNeverSuppressedAgainstItsPredecessor) {
|
||||
FramebufferScope scope;
|
||||
// The SAME attachment set on both objects, so every other field of the record is identical
|
||||
// and Fbo is the only thing that can move the hash.
|
||||
const auto color = MakeColorTexture(8, 32);
|
||||
Uint64 firstHash = 0;
|
||||
MGPipeHandle firstHandle{};
|
||||
{
|
||||
const auto first = MakeShared<FramebufferObject>(5);
|
||||
first->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D);
|
||||
BindDrawAndRead(first, first);
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
ASSERT_EQ(Framebuffers().EmissionCount(), 1u);
|
||||
firstHash = Framebuffers().LastDraw().ContentHash;
|
||||
firstHandle = Framebuffers().LastDraw().Fbo;
|
||||
BindDrawAndRead(nullptr, nullptr);
|
||||
}
|
||||
const auto second = MakeShared<FramebufferObject>(5);
|
||||
second->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D);
|
||||
BindDrawAndRead(second, second);
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
const MGPFramebufferState& record = Framebuffers().LastDraw();
|
||||
EXPECT_EQ(record.Fbo.Slot, firstHandle.Slot) << "the slot was not recycled; the case proves nothing";
|
||||
EXPECT_NE(record.Fbo.Gen, firstHandle.Gen) << "a recycled slot must carry a new generation";
|
||||
EXPECT_NE(record.ContentHash, firstHash)
|
||||
<< "MGPFramebufferState::ContentHash does not cover Fbo, so a recycled framebuffer handle "
|
||||
"would be suppressed against its predecessor's record";
|
||||
}
|
||||
|
||||
// ============================ G6 ============================
|
||||
//
|
||||
// "For every framebuffer configuration the emitted MGPFramebufferState reproduces exactly the
|
||||
// values the backend's SyncToBackend family reads from the frontend today, field by field."
|
||||
// The oracle is the frontend attachment itself, read back through the same getters the twin
|
||||
// uses, so this cannot drift into asserting what the emitter happens to do.
|
||||
TEST(FramebufferEmit, EveryAttachmentFieldSurvivesTheSurfaceConversion) {
|
||||
FramebufferScope scope;
|
||||
const auto fbo = MakeShared<FramebufferObject>(6);
|
||||
Vector<SharedPtr<TextureObject2D>> colors;
|
||||
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||
colors.push_back(MakeColorTexture(static_cast<Uint>(20 + i), 32, 2));
|
||||
// Distinct level, layer and layered flag per point, so a conversion that dropped one
|
||||
// field could not be masked by another point's value.
|
||||
fbo->AttachTexture(static_cast<FramebufferAttachmentType>(
|
||||
static_cast<Int>(FramebufferAttachmentType::Color0) + static_cast<Int>(i)),
|
||||
colors.back(), TextureUploadTarget::Texture2D,
|
||||
static_cast<int>(i % 2), static_cast<int>(i), (i % 2) == 0);
|
||||
}
|
||||
const auto depth = MakeShared<RenderbufferObject>(2);
|
||||
depth->SetInternalFormat(TextureInternalFormat::Depth24Stencil8);
|
||||
depth->AllocateStorage(IntVec2{32, 32});
|
||||
fbo->AttachRenderbuffer(FramebufferAttachmentType::Depth, depth);
|
||||
const auto stencil = MakeColorTexture(40, 32);
|
||||
fbo->AttachTexture(FramebufferAttachmentType::Stencil, stencil, TextureUploadTarget::Texture2D);
|
||||
BindDrawAndRead(fbo, fbo);
|
||||
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
ASSERT_EQ(Framebuffers().EmissionCount(), 1u);
|
||||
const MGPFramebufferState& record = Framebuffers().LastDraw();
|
||||
|
||||
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
|
||||
const auto type = static_cast<FramebufferAttachmentType>(
|
||||
static_cast<Int>(FramebufferAttachmentType::Color0) + static_cast<Int>(i));
|
||||
const auto& attachment = fbo->GetAttachment(type);
|
||||
const MGPSurface& surface = record.Color[i];
|
||||
const MGPipeHandle expected =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, attachment.GetTexture()->GetLifetimeId());
|
||||
EXPECT_TRUE(surface.Res == expected) << "MGPSurface::Res at colour point " << i;
|
||||
EXPECT_EQ(surface.Kind, kMGPipeSurfaceKindTexture) << "MGPSurface::Kind at colour point " << i;
|
||||
EXPECT_EQ(surface.InternalFormat, static_cast<Uint32>(attachment.GetTexture()->GetFormat()))
|
||||
<< "MGPSurface::InternalFormat at colour point " << i;
|
||||
EXPECT_EQ(surface.Level, static_cast<Uint16>(attachment.GetTextureLevel()))
|
||||
<< "MGPSurface::Level at colour point " << i;
|
||||
EXPECT_EQ(surface.Layer, static_cast<Uint32>(attachment.GetTextureLayer()))
|
||||
<< "MGPSurface::Layer at colour point " << i;
|
||||
EXPECT_EQ(surface.Layered, attachment.IsLayered() ? 1 : 0)
|
||||
<< "MGPSurface::Layered at colour point " << i;
|
||||
EXPECT_EQ(surface.UploadTarget, static_cast<Uint16>(TextureUploadTarget::Texture2D))
|
||||
<< "MGPSurface::UploadTarget at colour point " << i;
|
||||
}
|
||||
|
||||
EXPECT_EQ(record.Depth.Kind, kMGPipeSurfaceKindRenderbuffer) << "MGPSurface::Kind on the depth point";
|
||||
EXPECT_EQ(record.Depth.InternalFormat, static_cast<Uint32>(depth->GetInternalFormat()))
|
||||
<< "MGPSurface::InternalFormat on the depth point";
|
||||
EXPECT_TRUE(record.Depth.Res ==
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Renderbuffer, depth->GetLifetimeId()))
|
||||
<< "MGPSurface::Res on the depth point";
|
||||
EXPECT_EQ(record.Depth.Layered, 0) << "MGPSurface::Layered on the depth point";
|
||||
EXPECT_EQ(record.Stencil.Kind, kMGPipeSurfaceKindTexture) << "MGPSurface::Kind on the stencil point";
|
||||
EXPECT_TRUE(record.Stencil.Res ==
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, stencil->GetLifetimeId()))
|
||||
<< "MGPSurface::Res on the stencil point";
|
||||
|
||||
EXPECT_EQ(record.Width, 32u) << "MGPFramebufferState::Width";
|
||||
EXPECT_EQ(record.Height, 32u) << "MGPFramebufferState::Height";
|
||||
EXPECT_EQ(record.IsDefault, 0) << "MGPFramebufferState::IsDefault";
|
||||
// Complete is FramebufferObject::CheckCompleteness(), the FRONTEND-only answer, and never
|
||||
// glCheckFramebufferStatus's - that entry point additionally consults the backend's probed
|
||||
// format-capability cache, which a client emitting it would be reading from the wrong side.
|
||||
EXPECT_EQ(record.Complete, fbo->CheckCompleteness() ? 1 : 0) << "MGPFramebufferState::Complete";
|
||||
// And an empty point really is {null, None} and every other field zero.
|
||||
const auto emptyFbo = MakeShared<FramebufferObject>(7);
|
||||
emptyFbo->AttachTexture(FramebufferAttachmentType::Color0, colors[0], TextureUploadTarget::Texture2D);
|
||||
BindDrawAndRead(emptyFbo, emptyFbo);
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(Framebuffers().LastDraw().Color[1].Res));
|
||||
EXPECT_EQ(Framebuffers().LastDraw().Color[1].Kind, kMGPipeSurfaceKindNone);
|
||||
EXPECT_EQ(Framebuffers().LastDraw().Color[1].InternalFormat, 0u);
|
||||
EXPECT_EQ(Framebuffers().LastDraw().Color[1].UploadTarget, 0u);
|
||||
}
|
||||
|
||||
// ============================ D-C3 ============================
|
||||
TEST(FramebufferEmit, AnAttachmentPointAboveTheWireWidthIsRefusedNotTruncated) {
|
||||
FramebufferScope scope;
|
||||
const auto color = MakeColorTexture(50, 32);
|
||||
const auto beyond = MakeColorTexture(51, 32);
|
||||
const auto fbo = MakeShared<FramebufferObject>(8);
|
||||
fbo->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D);
|
||||
BindDrawAndRead(fbo, fbo);
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
ASSERT_EQ(Framebuffers().EmissionCount(), 1u);
|
||||
ASSERT_EQ(Framebuffers().RefusedCount(), 0u);
|
||||
|
||||
// MGPFramebufferState::Color[] is eight wide and MaxColorAttachments is the driver's raw ES
|
||||
// cap, not clamped to eight on the GLES path. Truncating silently is the bug class this
|
||||
// phase is closing, so the record is REFUSED and the legacy arm runs.
|
||||
fbo->AttachTexture(static_cast<FramebufferAttachmentType>(
|
||||
static_cast<Int>(FramebufferAttachmentType::Color0) +
|
||||
static_cast<Int>(kMGPipeMaxColorAttachments)),
|
||||
beyond, TextureUploadTarget::Texture2D);
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
EXPECT_EQ(Framebuffers().EmissionCount(), 1u)
|
||||
<< "an attachment above the wire width was truncated into a record instead of refused";
|
||||
EXPECT_GE(Framebuffers().RefusedCount(), 1u) << "the refusal was not counted";
|
||||
}
|
||||
|
||||
// ============================ D-D2 ============================
|
||||
//
|
||||
// RED BEFORE THIS PACKAGE LANDED. RenderbufferObject's three storage setters bump no version
|
||||
// and raise no notice, and the framebuffer bit's shutter does not move when an ALREADY-ATTACHED
|
||||
// renderbuffer is re-storaged - so `glBindRenderbuffer; glRenderbufferStorage(newSize)` on an
|
||||
// attached renderbuffer published nothing at all.
|
||||
TEST(FramebufferEmit, ARestoragedAttachedRenderbufferPublishesItsNewExtent) {
|
||||
FramebufferScope scope;
|
||||
const auto renderbuffer = MakeShared<RenderbufferObject>(3);
|
||||
renderbuffer->SetInternalFormat(TextureInternalFormat::Depth24Stencil8);
|
||||
renderbuffer->AllocateStorage(IntVec2{64, 64});
|
||||
const auto fbo = MakeShared<FramebufferObject>(9);
|
||||
fbo->AttachRenderbuffer(FramebufferAttachmentType::Depth, renderbuffer);
|
||||
BindDrawAndRead(fbo, fbo);
|
||||
Framebuffers().EmitFramebufferState(Ctx());
|
||||
|
||||
const MGPipeHandle handle =
|
||||
MGPipeSlots().FindByLifetimeId(MGPipeKind::Renderbuffer, renderbuffer->GetLifetimeId());
|
||||
ASSERT_FALSE(MGPipeHandleIsNull(handle));
|
||||
const Uint64 respecifiesBefore = MGPipeTextureEmitterInstance().RespecifyCount();
|
||||
|
||||
// NO BIND, NO ATTACHMENT CHANGE, NO DIRTY BIT - only the storage entry point.
|
||||
renderbuffer->AllocateStorage(IntVec2{128, 96});
|
||||
EXPECT_GT(MGPipeTextureEmitterInstance().RespecifyCount(), respecifiesBefore)
|
||||
<< "a re-storaged attached renderbuffer published nothing";
|
||||
const MGPResourceDesc desc = MGPipeTextureEmitterInstance().LastDesc();
|
||||
EXPECT_TRUE(desc.Resource == handle);
|
||||
EXPECT_EQ(desc.Width, 128u);
|
||||
EXPECT_EQ(desc.Height, 96u);
|
||||
}
|
||||
|
||||
TEST(FramebufferEmit, AnUnchangedBindingPairEmitsNothing) {
|
||||
FramebufferScope scope;
|
||||
const auto color = MakeColorTexture(60, 32);
|
||||
const auto fbo = MakeShared<FramebufferObject>(10);
|
||||
fbo->AttachTexture(FramebufferAttachmentType::Color0, color, TextureUploadTarget::Texture2D);
|
||||
BindDrawAndRead(fbo, fbo);
|
||||
const Uint64 bytes = Framebuffers().EmitFramebufferState(Ctx());
|
||||
EXPECT_EQ(bytes, sizeof(MGPFramebufferState));
|
||||
EXPECT_EQ(Framebuffers().EmissionCount(), 1u);
|
||||
// The version-first skip: nothing moved, so nothing goes out and nothing is hashed twice.
|
||||
EXPECT_EQ(Framebuffers().EmitFramebufferState(Ctx()), 0u);
|
||||
EXPECT_EQ(Framebuffers().EmissionCount(), 1u);
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
|
||||
// ==================================================================================
|
||||
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
|
||||
|
||||
@@ -57,8 +57,15 @@
|
||||
#include "Includes.h"
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SamplerEmit.h>
|
||||
#include <MG_Impl/Pipe/TextureEmit.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/RenderbufferState/RenderbufferObject.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject2D.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObjectView.h>
|
||||
|
||||
#include <algorithm>
|
||||
#endif
|
||||
|
||||
using namespace MobileGL;
|
||||
@@ -175,7 +182,6 @@ TEST(TextureEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// =========================================================================================
|
||||
// The APPLIER's half of the texture family (the wire commits'): set_texture_params on the
|
||||
// texture's own record, and the sub-data validator plus the pending-upload set that replaces
|
||||
// the frontend dirty flags the client clears at emission. The emitter's half - the descriptor
|
||||
@@ -795,7 +801,485 @@ TEST(TextureEmit, TheCreateAndRespecifyCallsAnswerWhetherTheRecordWasAccepted) {
|
||||
#endif
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
=======
|
||||
// ============================================================================
|
||||
// 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) {
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
fs::temp_directory_path() / ("mobilegl-textureemit-test-" + std::to_string(ProcessId()) + ".log");
|
||||
|
||||
@@ -32,7 +32,11 @@
|
||||
#include "Includes.h"
|
||||
|
||||
#include <MG_State/GLState/BufferState/BufferObject.h>
|
||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||
#include <MG_State/GLState/RenderbufferState/RenderbufferObject.h>
|
||||
#include <MG_State/GLState/SamplerState/SamplerObject.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject2D.h>
|
||||
#include <MG_State/GLState/VertexArrayState/VertexArrayObject.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
@@ -156,3 +160,69 @@ TEST(ObjectLifetimeIdTest, RenderbufferObjectAtARecycledAddressCarriesAFreshLife
|
||||
TEST(ObjectLifetimeIdTest, LiveRenderbufferObjectsHaveDistinctLifetimeIds) {
|
||||
ExpectDistinctIdsWhileBothAlive<MG_State::GLState::RenderbufferObject>("RenderbufferObject");
|
||||
}
|
||||
|
||||
// P4a mints a client handle off the lifetime id of FOUR more object classes - a texture, a
|
||||
// framebuffer, a sampler and a program - and resolves every one of their death paths through
|
||||
// the same lifetimeId -> slot map. So the property these cases prove stops being a statement
|
||||
// about two backend memos and becomes the thing the whole handle space rests on: a recycled
|
||||
// heap address must not reproduce a handle, because the applier's record and the backend's twin
|
||||
// are both keyed on one.
|
||||
//
|
||||
// The texture case is additionally a CHURN check that costs nothing extra: under a push build
|
||||
// each round mints a Texture slot in the constructor and returns it in the destructor, so 64
|
||||
// rounds that leaked would be visible to the leak cases the gates package owns.
|
||||
TEST(ObjectLifetimeIdTest, TextureObjectAtARecycledAddressCarriesAFreshLifetimeId) {
|
||||
using MG_State::GLState::TextureObject2D;
|
||||
const int reuseCount = ProbeLifetimeIdAcrossAddressReuse<TextureObject2D>("TextureObject2D");
|
||||
if (reuseCount == 0) {
|
||||
GTEST_SKIP() << "inconclusive, not proven: this allocator never handed the same address back across 64 "
|
||||
"construct/destroy rounds, so the recycled-address case was never exercised";
|
||||
}
|
||||
RecordProperty("address_reuses_observed", reuseCount);
|
||||
}
|
||||
|
||||
TEST(ObjectLifetimeIdTest, LiveTextureObjectsHaveDistinctLifetimeIds) {
|
||||
ExpectDistinctIdsWhileBothAlive<MG_State::GLState::TextureObject2D>("TextureObject2D");
|
||||
}
|
||||
|
||||
TEST(ObjectLifetimeIdTest, FramebufferObjectAtARecycledAddressCarriesAFreshLifetimeId) {
|
||||
using MG_State::GLState::FramebufferObject;
|
||||
const int reuseCount = ProbeLifetimeIdAcrossAddressReuse<FramebufferObject>("FramebufferObject");
|
||||
if (reuseCount == 0) {
|
||||
GTEST_SKIP() << "inconclusive, not proven: this allocator never handed the same address back across 64 "
|
||||
"construct/destroy rounds, so the recycled-address case was never exercised";
|
||||
}
|
||||
RecordProperty("address_reuses_observed", reuseCount);
|
||||
}
|
||||
|
||||
TEST(ObjectLifetimeIdTest, LiveFramebufferObjectsHaveDistinctLifetimeIds) {
|
||||
ExpectDistinctIdsWhileBothAlive<MG_State::GLState::FramebufferObject>("FramebufferObject");
|
||||
}
|
||||
|
||||
TEST(ObjectLifetimeIdTest, SamplerObjectAtARecycledAddressCarriesAFreshLifetimeId) {
|
||||
using MG_State::GLState::SamplerObject;
|
||||
const int reuseCount = ProbeLifetimeIdAcrossAddressReuse<SamplerObject>("SamplerObject");
|
||||
if (reuseCount == 0) {
|
||||
GTEST_SKIP() << "inconclusive, not proven: this allocator never handed the same address back across 64 "
|
||||
"construct/destroy rounds, so the recycled-address case was never exercised";
|
||||
}
|
||||
RecordProperty("address_reuses_observed", reuseCount);
|
||||
}
|
||||
|
||||
TEST(ObjectLifetimeIdTest, LiveSamplerObjectsHaveDistinctLifetimeIds) {
|
||||
ExpectDistinctIdsWhileBothAlive<MG_State::GLState::SamplerObject>("SamplerObject");
|
||||
}
|
||||
|
||||
TEST(ObjectLifetimeIdTest, ProgramObjectAtARecycledAddressCarriesAFreshLifetimeId) {
|
||||
using MG_State::GLState::ProgramObject;
|
||||
const int reuseCount = ProbeLifetimeIdAcrossAddressReuse<ProgramObject>("ProgramObject");
|
||||
if (reuseCount == 0) {
|
||||
GTEST_SKIP() << "inconclusive, not proven: this allocator never handed the same address back across 64 "
|
||||
"construct/destroy rounds, so the recycled-address case was never exercised";
|
||||
}
|
||||
RecordProperty("address_reuses_observed", reuseCount);
|
||||
}
|
||||
|
||||
TEST(ObjectLifetimeIdTest, LiveProgramObjectsHaveDistinctLifetimeIds) {
|
||||
ExpectDistinctIdsWhileBothAlive<MG_State::GLState::ProgramObject>("ProgramObject");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user