[MG_Pipe, MG_Remote, MG_Backend] (P7): bind staged bytes to declared mip extents

This commit is contained in:
2026-09-23 13:43:44 -04:00
parent 501bdb2fed
commit 0535c4f07d
19 changed files with 1111 additions and 109 deletions
+39 -5
View File
@@ -2734,12 +2734,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!store.CopiesIntoServerStorage()) return;
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(res);
if (level != nullptr) {
// ONE glTexImage*D redefined one level: it exists from here on, at the
// derived extent (§1). NoteLevelDefined keeps a same-extent level's bytes.
// ONE glTexImage*D redefined one level: its exact extent crossed with the
// accepted scope carrier, including a legal noncanonical mutable mip.
store.NoteLevelDefined(
key, MG_Pipe::MGPipeSubDataUploadTargetOf(level->UploadTarget), level->Level,
MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
desc.Depth, level->Level));
IntVec3{static_cast<Int>(level->Width), static_cast<Int>(level->Height),
static_cast<Int>(level->Depth)},
desc.Target, desc.InternalFormat);
return;
}
// A whole-resource redefinition: every level's old coordinate system is gone.
@@ -2754,7 +2755,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
store.NoteLevelDefined(
key, static_cast<Uint16>(uploadTarget), static_cast<Uint16>(levelIndex),
MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
desc.Depth, levelIndex));
desc.Depth, levelIndex),
desc.Target, desc.InternalFormat);
}
}
}
@@ -3009,17 +3011,33 @@ namespace MobileGL::MG_Backend::DirectGLES {
// refusal, because MOBILEGL_ASSERT compiles out at INFO and a resource that silently
// stops being twinned is the failure mode the refusal exists to replace.
if (res.Slot >= BackendBufferResourceTable::kMaxHandleSlot) {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeSessionFail(
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.HandleSlot\"} - "
"buffer twin request named slot %u, past this table's %u bound",
res.Slot, BackendBufferResourceTable::kMaxHandleSlot);
#else
MGLOG_E_ONCE("MGPipe: resource handle slot %u is past the backend table's %u bound - "
"refusing to twin it",
res.Slot, BackendBufferResourceTable::kMaxHandleSlot);
return nullptr;
#endif
}
const Uint32 liveGen = g_backendBufferResources.LiveGenAt(res.Slot);
if (liveGen != 0 && liveGen > res.Gen) {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeSessionFail(
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.Generation\"} - "
"buffer twin request named generation %u at slot %u, behind live generation %u",
res.Gen, res.Slot, liveGen);
#else
MGLOG_E_ONCE("MGPipe: resource handle {%u, %u} names a generation BEHIND the live twin's "
"%u - refusing rather than dropping the incumbent's driver storage",
res.Slot, res.Gen, liveGen);
return nullptr;
#endif
}
auto& twin = g_backendBufferResources.GetOrCreate(res);
if (!twin) {
@@ -4329,13 +4347,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool PipeTwinHandleIsAdoptable(Registry& registry, MG_Pipe::MGPipeHandle handle, const char* kindName) {
if (MG_Pipe::MGPipeHandleIsNull(handle)) return false;
if (handle.Slot >= Registry::SlotTable::kMaxHandleSlot) {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeSessionFail(
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.HandleSlot\"} - "
"%s handle slot %u is past this table's %u bound",
kindName, handle.Slot, Registry::SlotTable::kMaxHandleSlot);
#else
MGLOG_E_ONCE("MGPipe: %s handle slot %u is past the backend table's %u bound - "
"refusing to twin it",
kindName, handle.Slot, Registry::SlotTable::kMaxHandleSlot);
return false;
#endif
}
const Uint32 liveGen = registry.LiveGenAt(handle.Slot);
if (liveGen != 0 && liveGen > handle.Gen) {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeSessionFail(
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.Generation\"} - "
"%s handle {%u, %u} names generation behind live generation %u",
kindName, handle.Slot, handle.Gen, liveGen);
#else
// SlotTables.h:301-321: forward is a recycle and resets the twin, BACKWARD is
// refused, because adopting it would release the incumbent LIVE twin's driver
// id and then stamp the slot back to the dead object's generation, after which
@@ -4347,6 +4380,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
"object",
kindName, handle.Slot, handle.Gen, liveGen);
return false;
#endif
}
return true;
}
+22 -2
View File
@@ -479,9 +479,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendPtr* GetOrCreateByHandle(MG_Pipe::MGPipeHandle handle) {
if (!EsprytSlotTablesEnabled()) return nullptr;
if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr;
if (handle.Slot >= SlotTable::kMaxHandleSlot) return nullptr;
if (handle.Slot >= SlotTable::kMaxHandleSlot) {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeSessionFail(
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.HandleSlot\"} - "
"GetOrCreateByHandle named slot %u, past this table's %u bound",
handle.Slot, SlotTable::kMaxHandleSlot);
#else
return nullptr;
#endif
}
const Uint32 liveGen = m_slotTable.LiveGenAt(handle.Slot);
if (liveGen != 0 && liveGen > handle.Gen) return nullptr;
if (liveGen != 0 && liveGen > handle.Gen) {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeSessionFail(
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.Generation\"} - "
"GetOrCreateByHandle named generation %u at slot %u, behind live generation %u",
handle.Gen, handle.Slot, liveGen);
#else
return nullptr;
#endif
}
return &m_slotTable.GetOrCreate(handle);
}
@@ -14,6 +14,9 @@
#if MOBILEGL_PIPE_PUSH
#include <MG_Impl/Pipe/SlotAllocator.h>
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Pipe/PipeSessionFail.h>
#endif
// Espryt 0b, the first Track H slice: the DENSE, {slot, gen}-keyed twin table that replaces
// StateBackendObjectRegistry's UnorderedMap<StateObject*, Entry>.
@@ -345,9 +348,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
// already orders of magnitude past any real GL object count, while a corrupt 32-bit
// slot asks for a four-billion-entry resize.
if (handle.Slot >= kMaxHandleSlot) {
#if MOBILEGL_BUILD_DISAGGREGATED
// PH-2: handle.Slot is peer supplied on the disaggregated arm. Release builds
// must publish a named protocol fault instead of silently returning m_nullTwin.
MG_Pipe::MGPipeSessionFail(
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.HandleSlot\"} - "
"GetOrCreate(handle) named slot %u, past this table's %u bound",
handle.Slot, kMaxHandleSlot);
#else
MOBILEGL_ASSERT(false, "GetOrCreate(handle) named slot %u, past this table's %u bound",
handle.Slot, kMaxHandleSlot);
return m_nullTwin;
#endif
}
// Same arming as the minting overload, and for the same reason: twin creation is
@@ -369,11 +382,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
// below already gives the same input.
Entry& entry = EntryAt(handle.Slot);
if (entry.Live && entry.Gen > handle.Gen) {
#if MOBILEGL_BUILD_DISAGGREGATED
// PH-2: a stale peer generation must not silently shed the incumbent twin in a
// release server. The funnel names the exact identity fault and notifies the peer.
MG_Pipe::MGPipeSessionFail(
MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"BackendSlotTable.Generation\"} - "
"GetOrCreate(handle) named generation %u at slot %u, behind live generation %u",
handle.Gen, handle.Slot, entry.Gen);
#else
MOBILEGL_ASSERT(false,
"GetOrCreate(handle) named generation %u at slot %u, which is BEHIND "
"the live entry's %u - refusing rather than destroying the incumbent",
handle.Gen, handle.Slot, entry.Gen);
return m_nullTwin;
#endif
}
if (entry.Live && entry.Gen != handle.Gen) entry.backend.reset();
entry.Gen = handle.Gen;
+12
View File
@@ -901,6 +901,18 @@ namespace MobileGL::MG_Pipe {
MGPRespecifiedLevel key{};
key.UploadTarget = packedTarget;
key.Level = static_cast<Uint16>(firstLevel + i);
#if MOBILEGL_BUILD_DISAGGREGATED
const auto* mipmap = MG_State::GLState::AsMipmapTexture(&texture);
if (mipmap == nullptr) {
MGLOG_E_ONCE("MGPipe: a per-level texture respecify had no mipmap storage; it was not emitted");
return;
}
const IntVec3 exactLevelExtent = mipmap->GetMipmapTexelSize(
static_cast<TextureUploadTarget>(uploadTarget), key.Level);
key.Width = static_cast<Uint32>(exactLevelExtent.x());
key.Height = static_cast<Uint32>(exactLevelExtent.y());
key.Depth = static_cast<Uint32>(exactLevelExtent.z());
#endif
accepted = RespecifyOnce(texture, handle, entry, desc, &key, viewOf, bufferHandle, bufOffset,
bufSize);
if (!accepted) break;
+50 -12
View File
@@ -342,16 +342,19 @@ namespace MobileGL::MG_Pipe {
// It exists because the applier ALREADY takes that scope - as a trailing
// const MGPRespecifiedLevel* (PipeApply.h:792-795) - and MGPResourceDesc could not
// express it, so it was the second of resource_respecify's two companions with no wire
// carrier. The difference is not cosmetic: a null scope drops EVERY pending upload,
// carrier. PH-4 extends that carrier with exact W/H/D for the named level, which the
// server validates before staging bytes. The difference is not cosmetic: a null scope drops EVERY pending upload,
// because every level's coordinate system has just been replaced, while a per-level
// one drops only that key. Crossing without the scope would make every mutable
// per-level glTexImage*D on the far side look like a whole-resource redefinition and
// silently eat the texels of every other level - exactly the loss the server-side
// pending-upload set exists to prevent.
//
// READ IT THROUGH THE THREE HELPERS BELOW, never by touching the fields: the
// presence byte and the pair are one value in three pieces, and an open-coded reader
// that forgets the presence byte reads level 0 of upload target 0 as a real scope.
// READ IT THROUGH THE HELPERS BELOW, never by touching the fields: the presence byte,
// target/level pair and exact per-level extent form one value. For a named image level,
// BufOffset/BufSize temporarily carry W/H/D; the target makes this safe because a
// texture-buffer range cannot legally name a mip level. The applier clears that carrier
// before storing the descriptor.
//
// NOT STORAGE-DEFINING, and not metadata either: it does not describe the resource at
// all, it describes what this CALL replaces. MGPipeResourceRespecifyNeedsAck and the
@@ -378,7 +381,9 @@ namespace MobileGL::MG_Pipe {
Uint16 RespecifiedLevel;
MGPipeHandle ViewOf; // storage owner for a texture view
MGPipeHandle BufferForTexBuffer; // texture-buffer backing store
Uint64 BufOffset, BufSize; // kWholeBuffer == ~0, resolved live
// TexBuffer's backing range; on a named non-buffer mip respecify the same words carry
// exact width/height/depth until the applier accepts and normalizes the descriptor.
Uint64 BufOffset, BufSize;
};
MGP_ASSERT_POD(MGPResourceDesc, 88);
// The scope fields went into the two existing pads, so the descriptor did not grow and this
@@ -401,9 +406,9 @@ namespace MobileGL::MG_Pipe {
return desc.HasRespecifiedLevel == 0;
}
// The single (uploadTarget, level) a per-level respecify replaces. Reading either half of a
// whole-resource descriptor is a caller error; both answer 0 so that a misuse is at least
// deterministic rather than whatever the pad happened to hold.
// The single (uploadTarget, level, width, height, depth) a per-level respecify replaces.
// Reading any component of a whole-resource descriptor is a caller error; helpers answer
// zero so a misuse is deterministic rather than whatever the pad/range held.
inline constexpr Uint16 MGPipeRespecifiedUploadTargetOf(const MGPResourceDesc& desc) {
return MGPipeRespecifyIsWholeResource(desc) ? Uint16(0) : desc.RespecifiedUploadTarget;
}
@@ -411,16 +416,49 @@ namespace MobileGL::MG_Pipe {
return MGPipeRespecifyIsWholeResource(desc) ? Uint16(0) : desc.RespecifiedLevel;
}
// The two writers. A producer sets the scope with one call so the presence byte cannot be
// left behind, and clears it with the other; a descriptor built by value-initialization is
// already whole-resource, which is the safe default and the only one P5 produces.
// A producer sets the scope and its exact mutable-level extent with one call so the
// presence byte cannot be left behind, and clears scope with the other. A descriptor built
// by value-initialization is already whole-resource, which is the safe default.
inline constexpr Uint32 MGPipeRespecifiedWidthOf(const MGPResourceDesc& desc) {
return MGPipeRespecifyIsWholeResource(desc) ? 0u : static_cast<Uint32>(desc.BufOffset >> 32);
}
inline constexpr Uint32 MGPipeRespecifiedHeightOf(const MGPResourceDesc& desc) {
return MGPipeRespecifyIsWholeResource(desc) ? 0u : static_cast<Uint32>(desc.BufOffset);
}
inline constexpr Uint32 MGPipeRespecifiedDepthOf(const MGPResourceDesc& desc) {
return MGPipeRespecifyIsWholeResource(desc) ? 0u : static_cast<Uint32>(desc.BufSize);
}
inline constexpr Bool MGPipeRespecifiedExtentCarrierIsCanonical(const MGPResourceDesc& desc) {
return MGPipeRespecifyIsWholeResource(desc) ||
((desc.BufSize >> 32) == 0 && MGPipeRespecifiedWidthOf(desc) != 0 &&
MGPipeRespecifiedHeightOf(desc) != 0 && MGPipeRespecifiedDepthOf(desc) != 0);
}
inline constexpr void MGPipeSetRespecifiedLevel(MGPResourceDesc& desc, Uint16 uploadTarget,
Uint16 level) {
Uint16 level, Uint32 width = 0, Uint32 height = 0,
Uint32 depth = 0) {
desc.HasRespecifiedLevel = 1;
desc.RespecifiedUploadTarget = uploadTarget;
desc.RespecifiedLevel = level;
#if MOBILEGL_BUILD_DISAGGREGATED
// Non-buffer texture targets leave this descriptor range unused. A named-level
// respecify repurposes it as exact W/H/D; TexBuffer cannot legally name a mip level.
desc.BufOffset = (static_cast<Uint64>(width) << 32) | static_cast<Uint64>(height);
desc.BufSize = depth;
#else
(void)width;
(void)height;
(void)depth;
#endif
}
inline constexpr void MGPipeClearRespecifiedLevel(MGPResourceDesc& desc) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (desc.HasRespecifiedLevel != 0 &&
desc.Target != static_cast<Uint8>(MGPipeResourceTarget::TexBuffer)) {
desc.BufOffset = 0;
desc.BufSize = 0;
}
#endif
desc.HasRespecifiedLevel = 0;
desc.RespecifiedUploadTarget = 0;
desc.RespecifiedLevel = 0;
+52 -13
View File
@@ -1138,18 +1138,22 @@ namespace MobileGL::MG_Pipe {
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
auto& store = MG_Remote::Server::ServerStagedTexture();
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(desc.Resource);
const auto define = [&](Uint16 target, Uint16 mip) {
store.NoteLevelDefined(key, target, mip, MG_Remote::Server::StagedTextureMipExtent(
desc.Target, desc.Width, desc.Height, desc.Depth, mip));
const auto define = [&](Uint16 target, Uint16 mip, const IntVec3& extent) {
store.NoteLevelDefined(key, target, mip, extent, desc.Target, desc.InternalFormat);
};
if (level) {
define(MGPipeSubDataUploadTargetOf(level->UploadTarget), level->Level);
define(MGPipeSubDataUploadTargetOf(level->UploadTarget), level->Level,
IntVec3{static_cast<Int>(level->Width), static_cast<Int>(level->Height),
static_cast<Int>(level->Depth)});
return;
}
store.ResetLevels(key);
if (!desc.Immutable || !desc.Levels) return;
const auto defineChain = [&](TextureUploadTarget target) {
for (Uint32 mip = 0; mip < desc.Levels; ++mip) define(static_cast<Uint16>(target), static_cast<Uint16>(mip));
for (Uint32 mip = 0; mip < desc.Levels; ++mip)
define(static_cast<Uint16>(target), static_cast<Uint16>(mip),
MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width,
desc.Height, desc.Depth, mip));
};
switch (static_cast<MGPipeResourceTarget>(desc.Target)) {
case MGPipeResourceTarget::Tex1D: defineChain(TextureUploadTarget::Texture1D); break;
@@ -1975,6 +1979,17 @@ namespace MobileGL::MG_Pipe {
// ================================================================================
Bool MGPipeApplyResourceCreate(const MGPResourceDesc& desc) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (!MGPipeRespecifyIsWholeResource(desc)) {
MGP_TRIP_WIRE_REPORT("MGPipe: Fatal{ProtocolCorruption, \"ResourceCreate.RespecifyScope\"} - "
"resource_create cannot carry a per-level respecify extent");
}
if (desc.Target != static_cast<Uint8>(MGPipeResourceTarget::TexBuffer) &&
(desc.BufOffset != 0 || desc.BufSize != 0)) {
MGP_TRIP_WIRE_REPORT("MGPipe: Fatal{ProtocolCorruption, \"ResourceCreate.BufferRange\"} - "
"only a texture-buffer resource may carry BufOffset/BufSize");
}
#endif
MOBILEGL_ASSERT(desc.Resource.Slot >= kMGPipeFirstAllocatableSlot,
"resource_create named the reserved slot 0");
if (desc.Resource.Slot < kMGPipeFirstAllocatableSlot) return false;
@@ -2055,6 +2070,25 @@ namespace MobileGL::MG_Pipe {
if (record == nullptr) return false;
PinNoLiveHostWrites(*record, desc.Resource, "resource_respecify");
PinRespecifyScopeCoversItsDeclaration(desc, level, desc.Resource, "resource_respecify");
#if MOBILEGL_BUILD_DISAGGREGATED
if (level != nullptr &&
(desc.Target == kMGPipeResourceTargetBuffer ||
desc.Target == static_cast<Uint8>(MGPipeResourceTarget::Renderbuffer) ||
desc.Target == static_cast<Uint8>(MGPipeResourceTarget::TexBuffer))) {
MGP_TRIP_WIRE_REPORT("MGPipe: Fatal{ProtocolCorruption, \"ResourceRespecify.Target\"} - "
"this resource target cannot name a texture mip level");
}
if (!MGPipeRespecifyIsWholeResource(desc) &&
!MGPipeRespecifiedExtentCarrierIsCanonical(desc)) {
MGP_TRIP_WIRE_REPORT("MGPipe: Fatal{ProtocolCorruption, \"ResourceRespecify.ExtentCarrier\"} - "
"a named image level must carry nonzero width/height/depth with no high BufSize bits");
}
if (level == nullptr && desc.Target != static_cast<Uint8>(MGPipeResourceTarget::TexBuffer) &&
(desc.BufOffset != 0 || desc.BufSize != 0)) {
MGP_TRIP_WIRE_REPORT("MGPipe: Fatal{ProtocolCorruption, \"ResourceRespecify.BufferRange\"} - "
"a whole non-buffer resource may not carry BufOffset/BufSize");
}
#endif
// IS THIS A REDEFINITION AT ALL? Asked BEFORE the descriptor is replaced, because the
// stored one is the only thing there is to compare against (ID-18 M4). See
@@ -2069,6 +2103,14 @@ namespace MobileGL::MG_Pipe {
// asking a frontend object for them. A metadata update replaces it too - that is how
// the mask arrives - and by construction only the non-storage fields differ.
record->Desc = desc;
#if MOBILEGL_BUILD_DISAGGREGATED
if (level != nullptr && desc.Target != static_cast<Uint8>(MGPipeResourceTarget::TexBuffer)) {
// Scope and BufOffset/BufSize are transient respecify carriers, not resource state.
MGPipeClearRespecifiedLevel(record->Desc);
record->Desc.BufOffset = 0;
record->Desc.BufSize = 0;
}
#endif
// THE SERIAL MOVES EITHER WAY, and for a metadata update it is the entire publication:
// the twin re-derives its storage flags from the new mask at its next sync and decides
// for itself whether the backend needs a recreate.
@@ -2107,14 +2149,11 @@ namespace MobileGL::MG_Pipe {
// glTexSubImage2D and the sync that consumes it; eating those texels there would be
// C1's bug with a different trigger, and just as silent.
//
// - A NAMED LEVEL IS DROPPED WHETHER OR NOT THE DESCRIPTOR MOVED (P4a final review
// C-1, refining wire's W11 clause). The level pointer is the CALLER's statement that
// it reallocated that level, and the descriptor cannot contradict it: a non-base
// level redefined at a new size moves no descriptor field at all (the descriptor
// carries the base extent and the level count), so "identical storage fields" says
// nothing about that level's coordinate system, and a box kept against the old
// level would be uploaded past the end of the new one. The client's mask republish
// passes null, so this arm can never eat a standing upload on its behalf.
// - A NAMED LEVEL IS DROPPED WHETHER OR NOT THE base descriptor moved (P4a final
// review C-1). The companion scope now carries that level's exact W/H/D as well as
// its (uploadTarget, level) key, so the texture twin and staged-byte store update the
// same coordinate system. A metadata-only mask republish passes null and therefore
// cannot consume a standing upload on its behalf.
//
// A buffer never has a pending upload at all, so all three arms are inert for P3a's
// half - which is also why a buffer is never classified as metadata-only (below).
+25
View File
@@ -1141,8 +1141,33 @@ namespace MobileGL::MG_Pipe {
struct MGPRespecifiedLevel {
Uint16 UploadTarget = 0;
Uint16 Level = 0;
#if MOBILEGL_BUILD_DISAGGREGATED
// P7 PH-4: mutable mip dimensions do not follow from the base descriptor. These fields
// are carried by the split resource_respecify record; the pull helper remains 4 bytes.
Uint32 Width = 0;
Uint32 Height = 0;
Uint32 Depth = 0;
#endif
};
inline MGPRespecifiedLevel MGPipeMakeRespecifiedLevel(Uint16 uploadTarget, Uint16 level,
Uint32 width = 0, Uint32 height = 0,
Uint32 depth = 0) {
MGPRespecifiedLevel value{};
value.UploadTarget = uploadTarget;
value.Level = level;
#if MOBILEGL_BUILD_DISAGGREGATED
value.Width = width;
value.Height = height;
value.Depth = depth;
#else
(void)width;
(void)height;
(void)depth;
#endif
return value;
}
// THE THREE ACCEPTANCE RETURNS, AND WHY ALL THREE (ID-18 M3, clientfb review M3). D-D5
// step 1 says the client clears a level's dirty flags "for the levels whose record the
// applier ACCEPTED", and the emitter cannot answer that for itself: an `if constexpr` that
+3
View File
@@ -33,6 +33,9 @@ namespace MobileGL::MG_Pipe {
}
inline constexpr Uint64 kMGPipeWireMemberLayoutDigest = WireMemberLayoutDigest(
kMGPipeWireLayoutMembers, sizeof(kMGPipeWireLayoutMembers) / sizeof(WireLayoutMember));
// Bump when a fixed-layout pipe carrier keeps its bytes but changes their meaning. The
// mutable-level extent carrier reuses BufOffset/BufSize only on named image respecifies.
inline constexpr Uint32 kMGPipeResourceRespecifyExtentCarrierRevision = 1;
constexpr Uint64 WireCatalogueDigest() {
Uint64 hash = 1469598103934665603ull;
+7
View File
@@ -476,7 +476,14 @@ namespace MobileGL::MG_Remote {
inputs.ProgramArtifactsCodecVersion = MG_State::GLState::kProgramArtifactsCodecVersion;
inputs.ProgramArtifactsSchema = MG_State::GLState::ProgramArtifactsSchemaFingerprint();
inputs.OpCount = static_cast<Uint64>(MG_Pipe::MGPWireOp::kOpCount);
#if MOBILEGL_BUILD_DISAGGREGATED
inputs.ControlSchemaRevision =
(static_cast<Uint64>(MOBILEGL_PROTOCOL_CONTROL_REVISION) << 32) |
MG_Pipe::kMGPipeResourceRespecifyExtentCarrierRevision;
#else
// The pull build has no respecify extent carrier; preserve its pre-P7 wire fingerprint.
inputs.ControlSchemaRevision = MOBILEGL_PROTOCOL_CONTROL_REVISION;
#endif
inputs.AbiVersion = MOBILEGL_ABI_VERSION(MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR);
inputs.PointerBits = sizeof(void*) * 8;
const Uint32 endian = 1;
+8 -4
View File
@@ -421,13 +421,17 @@ namespace MobileGL::MG_Remote::Client {
"resource_subdata; a caller that still passes it has bytes nothing will "
"carry");
}
// The scope rides in the descriptor's own pads (CONTRACT-P5 table 1 row 19b, LANDED)
// and is written only through MGPipeSetRespecifiedLevel - three fields are one
// value, and an open-coded writer that forgets the presence byte says "level 0 of
// upload target 0" where it meant "the whole resource".
// Scope and exact mutable mip extent travel together through the descriptor helper.
// It temporarily reuses BufOffset/BufSize on non-buffer image targets; the server
// checks and clears that carrier before persisting the resource descriptor.
MG_Pipe::MGPResourceDesc record = *desc;
if (level != nullptr) {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeSetRespecifiedLevel(record, level->UploadTarget, level->Level,
level->Width, level->Height, level->Depth);
#else
MG_Pipe::MGPipeSetRespecifiedLevel(record, level->UploadTarget, level->Level);
#endif
} else {
MG_Pipe::MGPipeClearRespecifiedLevel(record);
}
@@ -13,6 +13,7 @@
#include "ServerSession.h"
#include "../Transport/ReplySlot.h"
#include "StagedTextureStore.h"
#include <Config.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
@@ -87,6 +88,12 @@ namespace MobileGL::MG_Remote::Server {
ReleaseFences();
}
m_backend = backend;
if (backend != nullptr) {
const auto& limits = backend->GetDynamicParameters();
ServerStagedTexture().SetDeviceLimits(
limits.MaxTextureSize, limits.Max3DTextureSize, limits.MaxCubeMapTextureSize,
limits.MaxArrayTextureLayers, limits.MaxTextureBufferSize);
}
}
const MG_Backend::GlobalBackendFunctionsTable* ServerVerbSink::Table(const char* verb) const {
+214 -29
View File
@@ -105,12 +105,14 @@
// rather than left to StagedShadow.h's copy of the same include.
#include <MG_Remote/FatalFunnel.h>
#include <MG_Util/Debug/Log.h>
#include <MG_Util/Metrics/TextureMetrics.h>
#include <MG_Util/Math/VectorTypes.h>
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <mutex>
#include <limits>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Config.h>
@@ -125,6 +127,97 @@ namespace MobileGL::MG_Remote::Server {
// TextureCubeMapArray shrink x and y; every other target shrinks all three. This is the
// mip chain's definition (MG_State's IsMipmapCompleteForFilter applies the same split,
// TextureObject.cpp:705-734), so two honest ends compute the same number.
struct StagedTextureDeviceLimits {
Int MaxTextureSize = 16384;
Int MaxRectangleTextureSize = 16384;
Int Max3DTextureSize = 2048;
Int MaxCubeMapTextureSize = 16384;
Int MaxArrayTextureLayers = 2048;
Int MaxTextureBufferSize = 65536;
};
// Multisample and buffer textures have no legal glTexSubImage producer. Keep their
// definedness in the store, but never accept a byte-backed staged level for them.
inline Bool StagedTextureTargetSupportsSubData(Uint8 resourceTarget) {
switch (static_cast<MG_Pipe::MGPipeResourceTarget>(resourceTarget)) {
case MG_Pipe::MGPipeResourceTarget::Tex1D:
case MG_Pipe::MGPipeResourceTarget::Tex2D:
case MG_Pipe::MGPipeResourceTarget::Tex3D:
case MG_Pipe::MGPipeResourceTarget::Tex1DArray:
case MG_Pipe::MGPipeResourceTarget::Tex2DArray:
case MG_Pipe::MGPipeResourceTarget::TexCube:
case MG_Pipe::MGPipeResourceTarget::TexCubeArray:
case MG_Pipe::MGPipeResourceTarget::TexRect:
return true;
default:
return false;
}
}
inline Bool StagedTextureExtentWithinDeviceLimits(Uint8 resourceTarget, const IntVec3& extent,
const StagedTextureDeviceLimits& limits) {
if (extent.x() <= 0 || extent.y() <= 0 || extent.z() <= 0) return false;
const auto target = static_cast<MG_Pipe::MGPipeResourceTarget>(resourceTarget);
switch (target) {
case MG_Pipe::MGPipeResourceTarget::Tex1D:
return extent.x() <= limits.MaxTextureSize && extent.y() == 1 && extent.z() == 1;
case MG_Pipe::MGPipeResourceTarget::Tex2D:
return extent.x() <= limits.MaxTextureSize && extent.y() <= limits.MaxTextureSize &&
extent.z() == 1;
case MG_Pipe::MGPipeResourceTarget::TexRect:
return extent.x() <= limits.MaxRectangleTextureSize &&
extent.y() <= limits.MaxRectangleTextureSize && extent.z() == 1;
case MG_Pipe::MGPipeResourceTarget::Tex3D:
return extent.x() <= limits.Max3DTextureSize && extent.y() <= limits.Max3DTextureSize &&
extent.z() <= limits.Max3DTextureSize;
case MG_Pipe::MGPipeResourceTarget::Tex1DArray:
return extent.x() <= limits.MaxTextureSize && extent.y() <= limits.MaxArrayTextureLayers &&
extent.z() == 1;
case MG_Pipe::MGPipeResourceTarget::Tex2DArray:
return extent.x() <= limits.MaxTextureSize && extent.y() <= limits.MaxTextureSize &&
extent.z() <= limits.MaxArrayTextureLayers;
case MG_Pipe::MGPipeResourceTarget::Tex2DMSArray:
return false; // defined levels only; multisample subimage records are never legal
case MG_Pipe::MGPipeResourceTarget::TexCube:
return extent.x() <= limits.MaxCubeMapTextureSize &&
extent.y() <= limits.MaxCubeMapTextureSize && extent.z() == 1;
case MG_Pipe::MGPipeResourceTarget::TexCubeArray:
return extent.x() <= limits.MaxCubeMapTextureSize &&
extent.y() <= limits.MaxCubeMapTextureSize && extent.z() <= limits.MaxArrayTextureLayers;
case MG_Pipe::MGPipeResourceTarget::TexBuffer:
return false; // texture buffers have no glTexSubImage entry point
default:
return false;
}
}
// The descriptor's InternalFormat is already the server's uncompressed storage fallback.
// Compute the maximum byte coordinate of one declared level without trusting upload bytes.
inline Bool StagedTextureDeclaredLevelByteBound(Uint8 resourceTarget, Uint32 internalFormat,
const IntVec3& extent,
const StagedTextureDeviceLimits& limits,
Uint64* outBound) {
if (outBound == nullptr || !StagedTextureTargetSupportsSubData(resourceTarget) ||
!StagedTextureExtentWithinDeviceLimits(resourceTarget, extent, limits)) return false;
const SizeT bytesPerTexel = MG_Util::GetSizedInternalFormatSizeInBytes(
static_cast<TextureInternalFormat>(internalFormat));
if (bytesPerTexel == 0) return false;
const Uint64 x = static_cast<Uint64>(extent.x());
const Uint64 y = static_cast<Uint64>(extent.y());
const Uint64 z = static_cast<Uint64>(extent.z());
const Uint64 bpp = static_cast<Uint64>(bytesPerTexel);
constexpr Uint64 max = std::numeric_limits<Uint64>::max();
if (x > max / y) return false;
const Uint64 xy = x * y;
if (xy > max / z) return false;
const Uint64 texels = xy * z;
if (texels > max / bpp) return false;
const Uint64 bytes = texels * bpp;
if (bytes > static_cast<Uint64>(std::numeric_limits<SizeT>::max())) return false;
*outBound = bytes;
return true;
}
inline Int StagedTextureShrinkingAxisCount(Uint8 pipeResourceTarget) {
switch (static_cast<MG_Pipe::MGPipeResourceTarget>(pipeResourceTarget)) {
case MG_Pipe::MGPipeResourceTarget::Tex1DArray:
@@ -205,6 +298,12 @@ namespace MobileGL::MG_Remote::Server {
explicit StagedTextureStore(Bool copies) : m_copies(copies) {}
Bool CopiesIntoServerStorage() const { return m_copies; }
void SetDeviceLimits(Int maxTextureSize, Int max3DTextureSize, Int maxCubeMapTextureSize,
Int maxArrayTextureLayers, Int maxTextureBufferSize) {
const std::lock_guard<std::mutex> lock(m_mutex);
m_deviceLimits = {maxTextureSize, std::min<Int>(16384, maxTextureSize), max3DTextureSize,
maxCubeMapTextureSize, maxArrayTextureLayers, maxTextureBufferSize};
}
// The wire handle the record carried, tagged so it can never alias a twin address.
// Slot and Gen are the client allocator's identity for one live object, so a recycled
@@ -236,20 +335,23 @@ namespace MobileGL::MG_Remote::Server {
if (!m_copies) return nullptr;
const std::lock_guard<std::mutex> lock(m_mutex);
LevelShadow& shadow = m_shadows[key].Levels[PackLevel(uploadTarget, level)];
RequireDeclaredLevel(shadow, key, uploadTarget, level, extent);
#if MOBILEGL_BUILD_DISAGGREGATED
// A run can be the first piece of a redefined level. Old coverage
// belongs to the previous coordinate system even when the new run
// happens to touch its old range; retaining it invents bytes that
// the current definition never supplied.
// the current definition never supplied (B4). The redefinition itself
// is NoteLevelDefined's, which drops the old bytes and coverage on an
// extent move, and RequireDeclaredLevel above refuses a run whose
// extent disagrees with that declaration (PH-4) - so this reset is the
// belt that keeps B4's rule if that refusal is ever relaxed.
if (!shadow.Defined || shadow.Extent != extent) {
shadow.Bytes.clear();
shadow.Covered.clear();
}
#endif
shadow.Extent = extent;
shadow.Defined = true;
shadow.GpuDirty = false;
return CopyRunInto(shadow, imageOffset, bytes, byteSize);
return CopyRunInto(shadow, key, uploadTarget, level, imageOffset, bytes, byteSize);
}
// THE WHOLE-LEVEL SPELLING: the run is the level's COMPLETE current content, beginning at
@@ -263,25 +365,39 @@ namespace MobileGL::MG_Remote::Server {
if (!m_copies) return nullptr;
const std::lock_guard<std::mutex> lock(m_mutex);
LevelShadow& shadow = m_shadows[key].Levels[PackLevel(uploadTarget, level)];
shadow.Extent = extent;
shadow.Defined = true;
RequireDeclaredLevel(shadow, key, uploadTarget, level, extent);
shadow.GpuDirty = false;
shadow.Bytes.clear();
shadow.Covered.clear();
return CopyRunInto(shadow, 0, bytes, byteSize);
return CopyRunInto(shadow, key, uploadTarget, level, 0, bytes, byteSize);
}
// A storage-defining respecify named this level: it EXISTS from here on, at this
// extent (null-data glTexImage*D, a generated level). An extent move redefines the
// level's coordinate system, so the bytes and the dirty mark of the old one go with
// it; an extent-restating one keeps them, which is the same answer the driver gives
// (a same-shape redefinition that carries no new upload leaves the old texels in
// place - undefined content is allowed to be the old content).
void NoteLevelDefined(Uint64 key, Uint16 uploadTarget, Uint16 level, const IntVec3& extent) {
// A storage-defining respecify named this level: it EXISTS from here on, at the exact
// extent carried by that accepted operation (including noncanonical mutable mips and
// null-data glTexImage*D). Its byte bound is established here, before any upload arrives.
// An extent move replaces the coordinate system, so old bytes and coverage go with it.
void NoteLevelDefined(Uint64 key, Uint16 uploadTarget, Uint16 level, const IntVec3& extent,
Uint8 resourceTarget = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D),
Uint32 internalFormat = static_cast<Uint32>(TextureInternalFormat::RGBA8)) {
if (!m_copies) return;
const std::lock_guard<std::mutex> lock(m_mutex);
Uint64 byteBound = 0;
const Bool supportsSubData = StagedTextureTargetSupportsSubData(resourceTarget);
const Bool validExtent = extent.x() > 0 && extent.y() > 0 && extent.z() > 0;
if (MG_Util::GetSizedInternalFormatSizeInBytes(
static_cast<TextureInternalFormat>(internalFormat)) == 0 || !validExtent ||
(supportsSubData && !StagedTextureDeclaredLevelByteBound(
resourceTarget, internalFormat, extent, m_deviceLimits, &byteBound))) {
SessionFail(MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.NoteLevelDefined\"} - "
"the accepted respecify has no valid server-bounded texture level "
"(key=%llu, target=%u, level=%u, extent=%d,%d,%d, internalFormat=%u)",
static_cast<unsigned long long>(key), uploadTarget, level,
extent.x(), extent.y(), extent.z(), internalFormat);
}
LevelShadow& shadow = m_shadows[key].Levels[PackLevel(uploadTarget, level)];
if (!shadow.Defined || shadow.Extent != extent) {
if (!shadow.Defined || shadow.Extent != extent || shadow.InternalFormat != internalFormat ||
shadow.ResourceTarget != resourceTarget || shadow.DeclaredByteBound != byteBound) {
shadow.Bytes.clear();
// The covered set is a statement about THIS coordinate system, so it goes with
// the bytes it described.
@@ -289,6 +405,9 @@ namespace MobileGL::MG_Remote::Server {
shadow.GpuDirty = false;
}
shadow.Extent = extent;
shadow.InternalFormat = internalFormat;
shadow.ResourceTarget = resourceTarget;
shadow.DeclaredByteBound = byteBound; // zero for targets without glTexSubImage
shadow.Defined = true;
m_any.store(true, std::memory_order_release);
}
@@ -340,8 +459,10 @@ namespace MobileGL::MG_Remote::Server {
if (!m_copies) return nullptr;
const std::lock_guard<std::mutex> lock(m_mutex);
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
if (shadow != nullptr && shadow->Defined && !shadow->Bytes.empty() &&
StagedShadowStore::CoverageHas(shadow->Covered, 0, shadow->Bytes.size())) {
if (shadow != nullptr && shadow->Defined && shadow->DeclaredByteBound != 0 &&
shadow->Bytes.size() == shadow->DeclaredByteBound &&
StagedShadowStore::CoverageHas(shadow->Covered, 0,
static_cast<SizeT>(shadow->DeclaredByteBound))) {
return shadow->Bytes.data();
}
// Verbatim what the MGLOG_F said, through the funnel that publishes it (P7 wave 0).
@@ -363,12 +484,12 @@ namespace MobileGL::MG_Remote::Server {
Bool IsCovered(Uint64 key, Uint16 uploadTarget, Uint16 level) const {
const std::lock_guard<std::mutex> lock(m_mutex);
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
if (shadow == nullptr || !shadow->Defined || shadow->Bytes.empty()) return false;
// THE WHOLE LEVEL, not merely "some bytes": a level assembled out of stage-chunk
// pieces is covered only once every piece has landed, and both readers of this
// answer then read the image whole - the image-promotion merge and Magma's
// unbacked-level readback.
return StagedShadowStore::CoverageHas(shadow->Covered, 0, shadow->Bytes.size());
if (shadow == nullptr || !shadow->Defined || shadow->DeclaredByteBound == 0 ||
shadow->Bytes.size() != shadow->DeclaredByteBound) return false;
// THE WHOLE LEVEL, not merely the currently assembled prefix: all runs must cover
// the server-derived bound before a reader can treat the level as complete.
return StagedShadowStore::CoverageHas(
shadow->Covered, 0, static_cast<SizeT>(shadow->DeclaredByteBound));
}
// How many runs this level's coverage is made of: 1 for a whole-level adoption, one per
// piece for a level cut into stage-chunk slabs. Diagnostics for the unit cases, so a
@@ -406,6 +527,12 @@ namespace MobileGL::MG_Remote::Server {
if (shadow == nullptr || !shadow->Defined) return 0;
return shadow->Bytes.size();
}
Uint64 LevelDeclaredByteBound(Uint64 key, Uint16 uploadTarget, Uint16 level) const {
if (!m_any.load(std::memory_order_acquire)) return 0;
const std::lock_guard<std::mutex> lock(m_mutex);
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
return shadow == nullptr || !shadow->Defined ? 0 : shadow->DeclaredByteBound;
}
Bool HasShadow(Uint64 key) const {
if (!m_any.load(std::memory_order_acquire)) return false;
const std::lock_guard<std::mutex> lock(m_mutex);
@@ -436,6 +563,9 @@ namespace MobileGL::MG_Remote::Server {
// adopted for this level: a gap in it is a piece that never arrived, which is the
// Fatal below (StagedShadowStore's coverage rules, same helpers).
Vector<Range1D> Covered;
Uint32 InternalFormat = 0;
Uint8 ResourceTarget = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Count);
Uint64 DeclaredByteBound = 0;
Bool Defined = false;
Bool GpuDirty = false;
};
@@ -447,15 +577,69 @@ namespace MobileGL::MG_Remote::Server {
// hold the run, place it, and widen the covered set by exactly its range. An empty run
// (a level defined with no bytes, or a null pointer) covers nothing - emptiness is the
// coverage answer for such a level, not an error.
const Uint8* CopyRunInto(LevelShadow& shadow, Uint64 imageOffset, const void* bytes,
SizeT byteSize) {
void RequireDeclaredLevel(LevelShadow& shadow, Uint64 key, Uint16 uploadTarget,
Uint16 level, const IntVec3& extent) {
if (!shadow.Defined || shadow.InternalFormat == 0) {
SessionFail(MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.LevelExtent\"} - "
"resource_subdata arrived before the server declared texture level "
"(key=%llu, uploadTarget=%u, level=%u)",
static_cast<unsigned long long>(key), uploadTarget, level);
}
if (!StagedTextureTargetSupportsSubData(shadow.ResourceTarget) ||
shadow.DeclaredByteBound == 0) {
SessionFail(MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.Target\"} - "
"resource_subdata is not legal for the server-declared texture target "
"(key=%llu, uploadTarget=%u, level=%u, target=%u)",
static_cast<unsigned long long>(key), uploadTarget, level,
static_cast<Uint32>(shadow.ResourceTarget));
}
if (shadow.Extent == extent) return;
SessionFail(MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.LevelExtent\"} - "
"resource_subdata extent disagrees with the server-declared texture level "
"(key=%llu, uploadTarget=%u, level=%u, declared=%d,%d,%d, record=%d,%d,%d)",
static_cast<unsigned long long>(key), uploadTarget, level,
shadow.Extent.x(), shadow.Extent.y(), shadow.Extent.z(),
extent.x(), extent.y(), extent.z());
}
// Check the run's exclusive end against the extent recorded by the server's prior
// resource_respecify before any cast, coverage edit, or vector growth.
const Uint8* CopyRunInto(LevelShadow& shadow, Uint64 key, Uint16 uploadTarget, Uint16 level,
Uint64 imageOffset, const void* bytes, SizeT byteSize) {
if (bytes != nullptr && byteSize != 0) {
constexpr Uint64 max = std::numeric_limits<Uint64>::max();
const Uint64 runBytes = static_cast<Uint64>(byteSize);
if (imageOffset > max - runBytes) {
SessionFail(MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.CopyRunInto\"} - "
"texture run end overflows (handle=%llu, uploadTarget=%u, level=%u, "
"offset=%llu, size=%llu)",
static_cast<unsigned long long>(key), uploadTarget, level,
static_cast<unsigned long long>(imageOffset),
static_cast<unsigned long long>(runBytes));
}
const Uint64 end = imageOffset + runBytes;
if (end > shadow.DeclaredByteBound ||
end > static_cast<Uint64>(std::numeric_limits<SizeT>::max())) {
SessionFail(MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.CopyRunInto\"} - "
"texture run exceeds the server-declared level byte bound "
"(handle=%llu, uploadTarget=%u, level=%u, offset=%llu, size=%llu, "
"bound=%llu)",
static_cast<unsigned long long>(key), uploadTarget, level,
static_cast<unsigned long long>(imageOffset),
static_cast<unsigned long long>(runBytes),
static_cast<unsigned long long>(shadow.DeclaredByteBound));
}
const auto* raw = static_cast<const Uint8*>(bytes);
const SizeT needed = static_cast<SizeT>(imageOffset) + byteSize;
const SizeT begin = static_cast<SizeT>(imageOffset);
const SizeT needed = static_cast<SizeT>(end);
if (shadow.Bytes.size() < needed) shadow.Bytes.resize(needed, 0);
std::memcpy(shadow.Bytes.data() + imageOffset, raw, byteSize);
StagedShadowStore::CoverageAdd(shadow.Covered, static_cast<SizeT>(imageOffset),
needed);
std::memcpy(shadow.Bytes.data() + begin, raw, byteSize);
StagedShadowStore::CoverageAdd(shadow.Covered, begin, needed);
}
m_any.store(true, std::memory_order_release);
return shadow.Bytes.empty() ? nullptr : shadow.Bytes.data();
@@ -470,6 +654,7 @@ namespace MobileGL::MG_Remote::Server {
const Bool m_copies;
mutable std::mutex m_mutex;
StagedTextureDeviceLimits m_deviceLimits{};
ska::flat_hash_map<Uint64, TextureShadow> m_shadows;
// Read on every IsLevelGpuDirty / LevelExtentOrUndefined, so the monolith cost is one
// acquire load of a never-written flag rather than a mutex and two hash lookups.
+8 -5
View File
@@ -1671,14 +1671,17 @@ namespace MobileGL::MG_Remote::Wire {
// resource_subdata records immediately after this one, which is what the texture
// path already does (TextureEmit.h:1137).
//
// The SCOPE is contract table 1 row 19b's carrier, read only through the helpers:
// the presence byte and the pair are one value in three pieces, and an open-coded
// reader that forgets the byte reads level 0 of upload target 0 as a real scope.
// The scope and exact mutable-level extent are one carrier value. Read each part
// through the helpers; forgetting the presence byte would turn a whole-resource
// respecify into level 0 of upload target 0.
MGPRespecifiedLevel level{};
const MGPRespecifiedLevel* scope = nullptr;
if (!MGPipeRespecifyIsWholeResource(desc)) {
level.UploadTarget = MGPipeRespecifiedUploadTargetOf(desc);
level.Level = MGPipeRespecifiedLevelOf(desc);
level = MGPipeMakeRespecifiedLevel(MGPipeRespecifiedUploadTargetOf(desc),
MGPipeRespecifiedLevelOf(desc),
MGPipeRespecifiedWidthOf(desc),
MGPipeRespecifiedHeightOf(desc),
MGPipeRespecifiedDepthOf(desc));
scope = &level;
}
noteAcceptance(MGPipeApplyResourceRespecify(desc, nullptr, scope));
+4 -4
View File
@@ -1422,9 +1422,9 @@ namespace {
// The split shape, both halves in agreement: the carrier says (kTex2DUpload, 1) and the
// trailing pointer the codec rebuilt from it says the same.
MGPResourceDesc perLevel = Tex2DDesc(res, 4, 2, 91);
MGPipeSetRespecifiedLevel(perLevel, kTex2DUpload, 1);
MGPipeSetRespecifiedLevel(perLevel, kTex2DUpload, 1, 2, 2, 1);
ASSERT_FALSE(MGPipeRespecifyIsWholeResource(perLevel));
const MGPRespecifiedLevel covered{kTex2DUpload, 1};
const MGPRespecifiedLevel covered = MGPipeMakeRespecifiedLevel(kTex2DUpload, 1, 2, 2, 1);
EXPECT_TRUE(MGPipeApplyResourceRespecify(perLevel, nullptr, &covered));
EXPECT_TRUE(HasPendingUpload(res.Slot, kTex2DUpload, 0))
@@ -1476,8 +1476,8 @@ namespace {
for (const Drive& drive : drives) {
const ChildResult child = RunInChild([&res, &drive]() {
MGPResourceDesc perLevel = Tex2DDesc(res, 4, 2, 92);
MGPipeSetRespecifiedLevel(perLevel, kTex2DUpload, 1);
const MGPRespecifiedLevel named{drive.PointerTarget, drive.PointerLevel};
MGPipeSetRespecifiedLevel(perLevel, kTex2DUpload, 1, 2, 2, 1);
const MGPRespecifiedLevel named = MGPipeMakeRespecifiedLevel(drive.PointerTarget, drive.PointerLevel, 2, 2, 1);
MGPipeApplyResourceRespecify(perLevel, nullptr,
drive.HasPointer ? &named : nullptr);
});
+7 -7
View File
@@ -2149,14 +2149,14 @@ TEST(TextureEmit, ARespecifyOfOneLevelKeepsThePendingUploadsOfTheOthers) {
// glTexImage2D(level 0, data): the respecify names the level it defines, and the drain then
// emits level 0's shape, which the applier accepts.
const MGPRespecifiedLevel levelZero{kTex2D, 0};
const MGPRespecifiedLevel levelZero = MGPipeMakeRespecifiedLevel(kTex2D, 0, 64, 64, 1);
MGPipeApplyResourceRespecify(levelDesc(1, 0x8058u /*GL_RGBA8*/), nullptr, &levelZero);
ASSERT_TRUE(MGPipeApplyResourceSubData(TextureUpload(texture, 0, MGPBox{0, 0, 0, 64, 64, 1}, 0), texels));
// A SECOND FACE OF THE SAME LEVEL, keyed the way the packed Target keys it (ID-12: high
// byte = the cube-face upload target, low byte = the resource target), so what survives is
// a SET and not one lucky entry - and so that the level number alone cannot be what matched.
const Uint16 secondFace = MGPipePackSubDataTarget(kTex2D, 1u);
const MGPRespecifiedLevel faceOfLevelZero{secondFace, 0};
const MGPRespecifiedLevel faceOfLevelZero = MGPipeMakeRespecifiedLevel(secondFace, 0, 64, 64, 1);
MGPipeApplyResourceRespecify(levelDesc(1, 0x8058u), nullptr, &faceOfLevelZero);
MGPSubData otherFace = TextureUpload(texture, 0, MGPBox{0, 0, 0, 64, 64, 1}, 0);
otherFace.Target = secondFace;
@@ -2168,7 +2168,7 @@ TEST(TextureEmit, ARespecifyOfOneLevelKeepsThePendingUploadsOfTheOthers) {
//
// glTexImage2D(level 1, data): this redefines level 1 of the (kTex2D, *) face only, and the
// level count moves 1 -> 2 with it.
const MGPRespecifiedLevel levelOne{kTex2D, 1};
const MGPRespecifiedLevel levelOne = MGPipeMakeRespecifiedLevel(kTex2D, 1, 32, 32, 1);
MGPipeApplyResourceRespecify(levelDesc(2, 0x8058u), nullptr, &levelOne);
ASSERT_EQ(TextureRecordOf(10).PendingUploads.size(), 2u)
@@ -2256,10 +2256,10 @@ TEST(TextureEmit, ARespecifyOfOneCubeFaceKeepsTheOtherFacesUploadOfTheSameLevel)
// glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, level 0, data), then the same for -X. Espryt
// bails on both (a cube map with one face defined is not cube-complete), so both are still
// owed when the next call arrives.
const MGPRespecifiedLevel positiveXLevelZero{positiveX, 0};
const MGPRespecifiedLevel positiveXLevelZero = MGPipeMakeRespecifiedLevel(positiveX, 0, 64, 64, 1);
MGPipeApplyResourceRespecify(faceDesc(0x8058u /*GL_RGBA8*/), nullptr, &positiveXLevelZero);
ASSERT_TRUE(MGPipeApplyResourceSubData(faceUpload(positiveX), texels));
const MGPRespecifiedLevel negativeXLevelZero{negativeX, 0};
const MGPRespecifiedLevel negativeXLevelZero = MGPipeMakeRespecifiedLevel(negativeX, 0, 64, 64, 1);
MGPipeApplyResourceRespecify(faceDesc(0x8058u), nullptr, &negativeXLevelZero);
ASSERT_TRUE(MGPipeApplyResourceSubData(faceUpload(negativeX), texels));
ASSERT_EQ(TextureRecordOf(11).PendingUploads.size(), 2u)
@@ -2498,12 +2498,12 @@ TEST(TextureEmit, ARespecifyThatRedefinesNoStorageCarriesTheStickyMaskAndKeepsTh
// nothing about that level's coordinate system. Level 1's entry goes; level 0's stays.
ASSERT_TRUE(MGPipeApplyResourceSubData(TextureUpload(texture, 1, MGPBox{0, 0, 0, 32, 32, 1}, 0), texels));
ASSERT_EQ(TextureRecordOf(11).PendingUploads.size(), 2u);
const MGPRespecifiedLevel levelOne{kTex2D, 1};
const MGPRespecifiedLevel levelOne = MGPipeMakeRespecifiedLevel(kTex2D, 1, 32, 32, 1);
ASSERT_TRUE(MGPipeApplyResourceRespecify(maskedAgain, nullptr, &levelOne));
ASSERT_EQ(TextureRecordOf(11).PendingUploads.size(), 1u)
<< "a level-scoped respecify on an unchanged descriptor did not drop the level it named";
EXPECT_EQ(TextureRecordOf(11).PendingUploads[0].Level, 0u) << "it dropped the wrong level";
const MGPRespecifiedLevel levelZero{kTex2D, 0};
const MGPRespecifiedLevel levelZero = MGPipeMakeRespecifiedLevel(kTex2D, 0, 64, 64, 1);
// THE NEGATIVE CONTROL, in the same case: move ONE storage-defining field and the same call
// is a redefinition again, which takes the level it names with it.
+5 -2
View File
@@ -704,7 +704,7 @@ TEST_F(PipeWireCodecTest, KNeedsAckRespecifyCarriesItsRedefinitionScope) {
ASSERT_TRUE(wire.PumpOne(&applied));
MGPResourceDesc respecify = create;
MGPipeSetRespecifiedLevel(respecify, 0x0102u, 1u);
MGPipeSetRespecifiedLevel(respecify, 0x0102u, 1u, 4u, 4u, 1u);
EXPECT_FALSE(MGPipeRespecifyIsWholeResource(respecify));
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ResourceRespecify, &respecify, sizeof(respecify)),
kInvalidSeq);
@@ -836,7 +836,7 @@ TEST_F(PipeWireCodecTest, ResourceDescPadsCrossToo) {
desc.ArrayLayers = 6;
desc.Levels = 3;
desc.Samples = 1;
MGPipeSetRespecifiedLevel(desc, 0x0304u, 2u);
MGPipeSetRespecifiedLevel(desc, 0x0304u, 2u, 4u, 4u, 1u);
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ResourceRespecify, &desc, sizeof(desc)),
kInvalidSeq);
@@ -851,6 +851,9 @@ TEST_F(PipeWireCodecTest, ResourceDescPadsCrossToo) {
EXPECT_FALSE(MGPipeRespecifyIsWholeResource(*crossed));
EXPECT_EQ(MGPipeRespecifiedUploadTargetOf(*crossed), 0x0304u);
EXPECT_EQ(MGPipeRespecifiedLevelOf(*crossed), 2u);
EXPECT_EQ(MGPipeRespecifiedWidthOf(*crossed), 4u);
EXPECT_EQ(MGPipeRespecifiedHeightOf(*crossed), 4u);
EXPECT_EQ(MGPipeRespecifiedDepthOf(*crossed), 1u);
}
+554 -2
View File
@@ -44,6 +44,7 @@
#include <MG_Remote/Wire/PipeWireCodec.h>
#include <MG_Pipe/MGPipeRenderStateSpans.h>
#include <MG_Pipe/PipeApply.h>
#include <MG_State/GLState/TextureState/TextureEnum.h>
#include <MG_Util/Debug/Log.h>
#include <gtest/gtest.h>
@@ -101,7 +102,9 @@ namespace {
template <class Payload, class Mutator>
bool EncodeThenMutateHeaderAndPublish(Client::ClientSession& session, MobileGL::MG_Pipe::MGPWireOp op,
const Payload& payload, Mutator&& mutate,
std::uint64_t* outSeq = nullptr) {
std::uint64_t* outSeq = nullptr,
const Wire::WireTail* tails = nullptr,
MobileGL::Uint32 tailCount = 0) {
if (outSeq != nullptr) *outSeq = Wire::kInvalidSeq;
auto* link = session.DataLink();
if (link == nullptr || !link->Attached()) return false;
@@ -109,7 +112,9 @@ namespace {
if (!Wire::MGPipeWireRecordLayout(op, &payload, layout) ||
layout.PayloadBytes != sizeof(payload)) return false;
auto& encoder = session.Encoder();
const std::uint64_t seq = encoder.EncodeRecord(op, &payload, sizeof(payload));
const std::uint64_t seq = tailCount == 0
? encoder.EncodeRecord(op, &payload, sizeof(payload))
: encoder.EncodeRecord(op, &payload, sizeof(payload), tails, tailCount);
if (seq == Wire::kInvalidSeq) return false;
auto& commands = link->CommandsOut();
const auto* arena = link->RecordArena();
@@ -131,6 +136,53 @@ namespace {
return true;
}
template <class Payload>
bool PublishRawRecordAndWait(Client::ClientSession& session, MobileGL::MG_Pipe::MGPWireOp op,
const Payload& payload, std::uint64_t* outSeq = nullptr) {
std::uint64_t seq = Wire::kInvalidSeq;
if (!EncodeThenMutateHeaderAndPublish(
session, op, payload, [](Transport::RingRecordHeader&, std::uint8_t*) {}, &seq)) {
return false;
}
if (outSeq != nullptr) *outSeq = seq;
return session.WaitForApplied(seq, 5000) == Transport::SessionWait::Reached;
}
MobileGL::MG_Pipe::MGPFramebufferState RawColorAttachmentFramebuffer(
MobileGL::MG_Pipe::MGPipeHandle fbo, MobileGL::MG_Pipe::MGPipeHandle texture,
MobileGL::Uint64 contentHash) {
MobileGL::MG_Pipe::MGPFramebufferState state{};
state.Fbo = fbo;
state.Target = static_cast<MobileGL::Uint8>(
MobileGL::MG_Pipe::MGPipeFramebufferTarget::Both);
state.IsDefault = 0;
state.Complete = 1;
state.Width = state.Height = state.Layers = state.Samples = 1;
state.FixedSampleLocations = 1;
for (auto& drawBuffer : state.DrawBuffers) drawBuffer = -1;
state.DrawBuffers[0] = 0;
state.Color[0].Res = texture;
state.Color[0].InternalFormat =
static_cast<MobileGL::Uint32>(MobileGL::TextureInternalFormat::RGBA8);
state.Color[0].Kind = MobileGL::MG_Pipe::kMGPipeSurfaceKindTexture;
state.Color[0].UploadTarget =
static_cast<MobileGL::Uint16>(MobileGL::TextureUploadTarget::Texture2D);
state.Color[0].TextureTarget =
static_cast<MobileGL::Uint16>(MobileGL::TextureTarget::Texture2D);
state.ContentHash = contentHash;
return state;
}
MobileGL::MG_Pipe::MGPClear RawColorClear(MobileGL::MG_Pipe::MGPipeHandle fbo) {
MobileGL::MG_Pipe::MGPClear clear{};
clear.Fbo = fbo;
clear.Kind = MobileGL::MG_Pipe::kMGPipeClearKindWhole;
clear.DrawBufferIndex = -1;
clear.BufferMask = GL_COLOR_BUFFER_BIT;
clear.ValueClass = MobileGL::MG_Pipe::kMGPipeClearValueClassFloat;
return clear;
}
std::string ServerImage() {
if (const char* explicitPath = std::getenv("MOBILEGL_TEST_SERVER_PATH")) {
return explicitPath;
@@ -343,6 +395,506 @@ TEST(ServerSpawnTest, RawPeerOversizedReadbackGetsErrorAndSessionContinues) {
EXPECT_EQ(exitCode, 0);
}
TEST(ServerSpawnTest, RawPeerTextureRunPastDeclaredLevelBoundIsNamedProtocolCorruption) {
const std::string endpoint = Endpoint("bad-texture-run-bound");
const std::string logBase = endpoint + ".log";
ScopedEnvironment logPath("MOBILEGL_LOG_FILE_PATH", logBase);
Session session;
ScopedSessionCleanup cleanup{session};
ASSERT_TRUE(Bring("bad-texture-run-bound", &session));
ASSERT_EQ(Handshake(session), MOBILEGL_OK);
auto& client = Client::ClientSessionInstance();
MobileGL::MG_Pipe::MGPResourceDesc desc{};
desc.Resource = {73u, 1u};
desc.Target = static_cast<MobileGL::Uint8>(MobileGL::MG_Pipe::MGPipeResourceTarget::Tex2D);
desc.StorageKind = static_cast<MobileGL::Uint8>(MobileGL::TextureStorageType::Mipmap);
desc.InternalFormat = static_cast<MobileGL::Uint32>(MobileGL::TextureInternalFormat::RGBA8);
desc.Width = 4;
desc.Height = 4;
desc.Depth = 1;
desc.ArrayLayers = 1;
desc.Levels = 1;
desc.Samples = 1;
MobileGL::Int32 status = Wire::ReplySink::kStatusError;
auto seq = client.EmitAndWait(MobileGL::MG_Pipe::MGPWireOp::ResourceCreate, &desc, sizeof(desc),
nullptr, 0, nullptr, 0, &status);
ASSERT_NE(seq, Wire::kInvalidSeq);
ASSERT_EQ(status, Wire::ReplySink::kStatusOk);
ASSERT_EQ(client.WaitForApplied(seq, 5000), Transport::SessionWait::Reached);
MobileGL::MG_Pipe::MGPResourceDesc respecify = desc;
respecify.HasDefinedContent = 1;
MobileGL::MG_Pipe::MGPipeSetRespecifiedLevel(
respecify, MobileGL::MG_Pipe::MGPipePackSubDataTarget(
static_cast<MobileGL::Uint32>(respecify.Target),
static_cast<MobileGL::Uint32>(MobileGL::TextureUploadTarget::Texture2D)),
0, 4, 4, 1);
seq = client.EmitAndWait(MobileGL::MG_Pipe::MGPWireOp::ResourceRespecify, &respecify,
sizeof(respecify), nullptr, 0, nullptr, 0, &status);
ASSERT_NE(seq, Wire::kInvalidSeq);
ASSERT_EQ(status, Wire::ReplySink::kStatusOk);
ASSERT_EQ(client.WaitForApplied(seq, 5000), Transport::SessionWait::Reached);
// Reserve the post-mutation 96-byte stage run, but declare an encoder-accepted 48-byte
// 4x3 box from Y=1. The mutation changes only the declared run geometry/size.
std::vector<std::uint8_t> staged(96, 0x5A);
MobileGL::MG_Pipe::MGPSubData upload{};
upload.Res = desc.Resource;
upload.Target = MobileGL::MG_Pipe::MGPipePackSubDataTarget(
static_cast<MobileGL::Uint32>(MobileGL::MG_Pipe::MGPipeResourceTarget::Tex2D),
static_cast<MobileGL::Uint32>(MobileGL::TextureUploadTarget::Texture2D));
upload.Level = 0;
upload.UnionBox = MobileGL::MG_Pipe::MGPBox{0, 1, 0, 4, 3, 1};
upload.RegionCount = 1;
upload.Blob = client.Encoder().StageBytes(staged.data(), staged.size());
upload.Blob.Size = 48;
upload.LevelWidth = 4;
upload.LevelHeight = 4;
upload.LevelDepth = 1;
MobileGL::MG_Pipe::MGPSubRegion region{};
region.Y = 1;
region.W = 4;
region.H = 3;
region.D = 1;
region.SrcOffset = 0;
region.SrcRowStride = 16;
region.SrcSliceStride = 64;
const Wire::WireTail tail{&region, sizeof(region)};
std::uint64_t uploadSeq = Wire::kInvalidSeq;
ASSERT_TRUE(EncodeThenMutateHeaderAndPublish(
client, MobileGL::MG_Pipe::MGPWireOp::ResourceSubData, upload,
[](Transport::RingRecordHeader&, std::uint8_t* payloadBytes) {
auto* wireUpload = reinterpret_cast<MobileGL::MG_Pipe::MGPSubData*>(payloadBytes);
wireUpload->Blob.Size = 96;
auto* wireRegion = reinterpret_cast<MobileGL::MG_Pipe::MGPSubRegion*>(
payloadBytes + sizeof(MobileGL::MG_Pipe::MGPSubData));
wireRegion->SrcRowStride = 32;
wireRegion->SrcSliceStride = 96;
}, &uploadSeq, &tail, 1))
<< "the peer failed to publish the encoder-accepted texture run";
ASSERT_NE(uploadSeq, Wire::kInvalidSeq);
int exitCode = -1;
const auto reaped = Server::ReapServer(session.server, 5000, &exitCode);
if (reaped != MOBILEGL_OK) {
client.Stop();
if (session.server.pid > 0) {
::kill(session.server.pid, SIGKILL);
(void)Server::ReapServer(session.server, 5000, &exitCode);
}
}
ASSERT_EQ(reaped, MOBILEGL_OK) << "server did not reject the out-of-level run promptly";
EXPECT_EQ(exitCode, -SIGABRT);
const std::string log = MobileGL::MG_Util::Debug::ReadRoleLogs(logBase.c_str());
const auto firstProtocolFatal = log.find("Fatal{ProtocolCorruption");
const auto boundFatal = log.find("Fatal{ProtocolCorruption, \"StagedTextureStore.CopyRunInto\"}");
EXPECT_NE(boundFatal, std::string::npos)
<< "the server did not reject the run at the declared level byte bound";
EXPECT_EQ(firstProtocolFatal, boundFatal)
<< "the run was refused before it reached the staged-level byte bound";
client.Stop();
}
TEST(ServerSpawnTest, RawPeerTextureSubDataMustMatchTheAcceptedMutableLevelExtent) {
const std::string endpoint = Endpoint("bad-texture-level-extent");
const std::string logBase = endpoint + ".log";
ScopedEnvironment logPath("MOBILEGL_LOG_FILE_PATH", logBase);
Session session;
ScopedSessionCleanup cleanup{session};
ASSERT_TRUE(Bring("bad-texture-level-extent", &session));
ASSERT_EQ(Handshake(session), MOBILEGL_OK);
auto& client = Client::ClientSessionInstance();
MobileGL::MG_Pipe::MGPResourceDesc desc{};
desc.Resource = {74u, 1u};
desc.Target = static_cast<MobileGL::Uint8>(MobileGL::MG_Pipe::MGPipeResourceTarget::Tex2D);
desc.StorageKind = static_cast<MobileGL::Uint8>(MobileGL::TextureStorageType::Mipmap);
desc.InternalFormat = static_cast<MobileGL::Uint32>(MobileGL::TextureInternalFormat::RGBA8);
desc.Width = 4;
desc.Height = 4;
desc.Depth = 1;
desc.ArrayLayers = 1;
desc.Levels = 1;
desc.Samples = 1;
MobileGL::Int32 status = Wire::ReplySink::kStatusError;
auto seq = client.EmitAndWait(MobileGL::MG_Pipe::MGPWireOp::ResourceCreate, &desc, sizeof(desc),
nullptr, 0, nullptr, 0, &status);
ASSERT_NE(seq, Wire::kInvalidSeq);
ASSERT_EQ(status, Wire::ReplySink::kStatusOk);
ASSERT_EQ(client.WaitForApplied(seq, 5000), Transport::SessionWait::Reached);
MobileGL::MG_Pipe::MGPResourceDesc respecify = desc;
respecify.HasDefinedContent = 1;
MobileGL::MG_Pipe::MGPipeSetRespecifiedLevel(
respecify, MobileGL::MG_Pipe::MGPipePackSubDataTarget(
static_cast<MobileGL::Uint32>(respecify.Target),
static_cast<MobileGL::Uint32>(MobileGL::TextureUploadTarget::Texture2D)),
0, 4, 4, 1);
seq = client.EmitAndWait(MobileGL::MG_Pipe::MGPWireOp::ResourceRespecify, &respecify,
sizeof(respecify), nullptr, 0, nullptr, 0, &status);
ASSERT_NE(seq, Wire::kInvalidSeq);
ASSERT_EQ(status, Wire::ReplySink::kStatusOk);
ASSERT_EQ(client.WaitForApplied(seq, 5000), Transport::SessionWait::Reached);
std::vector<std::uint8_t> staged(64, 0x5A);
MobileGL::MG_Pipe::MGPSubData upload{};
upload.Res = desc.Resource;
upload.Target = MobileGL::MG_Pipe::MGPipePackSubDataTarget(
static_cast<MobileGL::Uint32>(MobileGL::MG_Pipe::MGPipeResourceTarget::Tex2D),
static_cast<MobileGL::Uint32>(MobileGL::TextureUploadTarget::Texture2D));
upload.Level = 0;
upload.UnionBox = MobileGL::MG_Pipe::MGPBox{0, 0, 0, 4, 4, 1};
upload.Blob = client.Encoder().StageBytes(staged.data(), staged.size());
upload.LevelWidth = 4;
upload.LevelHeight = 4;
upload.LevelDepth = 1;
std::uint64_t uploadSeq = Wire::kInvalidSeq;
ASSERT_TRUE(EncodeThenMutateHeaderAndPublish(
client, MobileGL::MG_Pipe::MGPWireOp::ResourceSubData, upload,
[](Transport::RingRecordHeader&, std::uint8_t* payloadBytes) {
auto* wireUpload = reinterpret_cast<MobileGL::MG_Pipe::MGPSubData*>(payloadBytes);
// Still within the device limit and still contains the dirty box; the server must
// refuse it because ResourceRespecify accepted 4x4 for this exact level.
wireUpload->LevelWidth = 8;
}, &uploadSeq))
<< "the peer failed to publish the encoder-accepted texture record";
ASSERT_NE(uploadSeq, Wire::kInvalidSeq);
int exitCode = -1;
const auto reaped = Server::ReapServer(session.server, 5000, &exitCode);
if (reaped != MOBILEGL_OK) {
client.Stop();
if (session.server.pid > 0) {
::kill(session.server.pid, SIGKILL);
(void)Server::ReapServer(session.server, 5000, &exitCode);
}
}
ASSERT_EQ(reaped, MOBILEGL_OK) << "server did not reject a changed mutable-level extent promptly";
EXPECT_EQ(exitCode, -SIGABRT);
const std::string log = MobileGL::MG_Util::Debug::ReadRoleLogs(logBase.c_str());
const auto firstProtocolFatal = log.find("Fatal{ProtocolCorruption");
const auto extentFatal = log.find("Fatal{ProtocolCorruption, \"StagedTextureStore.LevelExtent\"}");
EXPECT_NE(extentFatal, std::string::npos)
<< "server did not refuse the upload extent that disagreed with ResourceRespecify";
EXPECT_EQ(firstProtocolFatal, extentFatal)
<< "the record was refused before staged-level extent matching";
client.Stop();
}
TEST(ServerSpawnTest, RawPeerMultisampleTextureSubDataIsNamedProtocolCorruption) {
const std::string endpoint = Endpoint("bad-multisample-texture-subdata");
const std::string logBase = endpoint + ".log";
ScopedEnvironment logPath("MOBILEGL_LOG_FILE_PATH", logBase);
Session session;
ScopedSessionCleanup cleanup{session};
ASSERT_TRUE(Bring("bad-multisample-texture-subdata", &session));
ASSERT_EQ(Handshake(session), MOBILEGL_OK);
auto& client = Client::ClientSessionInstance();
MobileGL::MG_Pipe::MGPResourceDesc desc{};
desc.Resource = {75u, 1u};
desc.Target = static_cast<MobileGL::Uint8>(MobileGL::MG_Pipe::MGPipeResourceTarget::Tex2DMS);
desc.StorageKind = static_cast<MobileGL::Uint8>(MobileGL::TextureStorageType::Mipmap);
desc.InternalFormat = static_cast<MobileGL::Uint32>(MobileGL::TextureInternalFormat::RGBA8);
desc.Width = 4;
desc.Height = 4;
desc.Depth = 1;
desc.ArrayLayers = 1;
desc.Levels = 1;
desc.Samples = 4;
desc.Immutable = 1;
MobileGL::Int32 status = Wire::ReplySink::kStatusError;
auto seq = client.EmitAndWait(MobileGL::MG_Pipe::MGPWireOp::ResourceCreate, &desc, sizeof(desc),
nullptr, 0, nullptr, 0, &status);
ASSERT_NE(seq, Wire::kInvalidSeq);
ASSERT_EQ(status, Wire::ReplySink::kStatusOk);
ASSERT_EQ(client.WaitForApplied(seq, 5000), Transport::SessionWait::Reached);
MobileGL::MG_Pipe::MGPResourceDesc respecify = desc;
respecify.HasDefinedContent = 1;
seq = client.EmitAndWait(MobileGL::MG_Pipe::MGPWireOp::ResourceRespecify, &respecify,
sizeof(respecify), nullptr, 0, nullptr, 0, &status);
ASSERT_NE(seq, Wire::kInvalidSeq);
ASSERT_EQ(status, Wire::ReplySink::kStatusOk);
ASSERT_EQ(client.WaitForApplied(seq, 5000), Transport::SessionWait::Reached);
std::vector<std::uint8_t> staged(64, 0x5A);
MobileGL::MG_Pipe::MGPSubData upload{};
upload.Res = desc.Resource;
upload.Target = MobileGL::MG_Pipe::MGPipePackSubDataTarget(
static_cast<MobileGL::Uint32>(MobileGL::MG_Pipe::MGPipeResourceTarget::Tex2DMS),
static_cast<MobileGL::Uint32>(MobileGL::TextureUploadTarget::Texture2DMultisample));
upload.Level = 0;
upload.UnionBox = MobileGL::MG_Pipe::MGPBox{0, 0, 0, 4, 4, 1};
upload.Blob = client.Encoder().StageBytes(staged.data(), staged.size());
upload.LevelWidth = 4;
upload.LevelHeight = 4;
upload.LevelDepth = 1;
std::uint64_t uploadSeq = Wire::kInvalidSeq;
ASSERT_TRUE(EncodeThenMutateHeaderAndPublish(
client, MobileGL::MG_Pipe::MGPWireOp::ResourceSubData, upload,
[](Transport::RingRecordHeader&, std::uint8_t*) {}, &uploadSeq));
ASSERT_NE(uploadSeq, Wire::kInvalidSeq);
int exitCode = -1;
const auto reaped = Server::ReapServer(session.server, 5000, &exitCode);
if (reaped != MOBILEGL_OK) {
client.Stop();
if (session.server.pid > 0) {
::kill(session.server.pid, SIGKILL);
(void)Server::ReapServer(session.server, 5000, &exitCode);
}
}
ASSERT_EQ(reaped, MOBILEGL_OK) << "server accepted an illegal multisample subimage record";
EXPECT_EQ(exitCode, -SIGABRT);
const std::string log = MobileGL::MG_Util::Debug::ReadRoleLogs(logBase.c_str());
const auto firstProtocolFatal = log.find("Fatal{ProtocolCorruption");
const auto targetFatal = log.find("Fatal{ProtocolCorruption, \"StagedTextureStore.Target\"}");
EXPECT_NE(targetFatal, std::string::npos)
<< "server did not refuse multisample bytes through the staged texture target gate";
EXPECT_EQ(firstProtocolFatal, targetFatal)
<< "multisample record was refused before the staged target gate";
client.Stop();
}
TEST(ServerSpawnTest, RawPeerNoncanonicalMutableExtentCarrierIsRefused) {
const std::string endpoint = Endpoint("bad-respecify-extent-carrier");
const std::string logBase = endpoint + ".log";
ScopedEnvironment logPath("MOBILEGL_LOG_FILE_PATH", logBase);
Session session;
ScopedSessionCleanup cleanup{session};
ASSERT_TRUE(Bring("bad-respecify-extent-carrier", &session));
ASSERT_EQ(Handshake(session), MOBILEGL_OK);
auto& client = Client::ClientSessionInstance();
MobileGL::MG_Pipe::MGPResourceDesc desc{};
desc.Resource = {76u, 1u};
desc.Target = static_cast<MobileGL::Uint8>(MobileGL::MG_Pipe::MGPipeResourceTarget::Tex2D);
desc.StorageKind = static_cast<MobileGL::Uint8>(MobileGL::TextureStorageType::Mipmap);
desc.InternalFormat = static_cast<MobileGL::Uint32>(MobileGL::TextureInternalFormat::RGBA8);
desc.Width = 4;
desc.Height = 4;
desc.Depth = 1;
desc.ArrayLayers = 1;
desc.Levels = 1;
desc.Samples = 1;
MobileGL::Int32 status = Wire::ReplySink::kStatusError;
auto seq = client.EmitAndWait(MobileGL::MG_Pipe::MGPWireOp::ResourceCreate, &desc, sizeof(desc),
nullptr, 0, nullptr, 0, &status);
ASSERT_NE(seq, Wire::kInvalidSeq);
ASSERT_EQ(status, Wire::ReplySink::kStatusOk);
ASSERT_EQ(client.WaitForApplied(seq, 5000), Transport::SessionWait::Reached);
MobileGL::MG_Pipe::MGPResourceDesc respecify = desc;
respecify.HasDefinedContent = 1;
MobileGL::MG_Pipe::MGPipeSetRespecifiedLevel(
respecify, MobileGL::MG_Pipe::MGPipePackSubDataTarget(
static_cast<MobileGL::Uint32>(respecify.Target),
static_cast<MobileGL::Uint32>(MobileGL::TextureUploadTarget::Texture2D)),
0, 4, 4, 1);
std::uint64_t respecifySeq = Wire::kInvalidSeq;
ASSERT_TRUE(EncodeThenMutateHeaderAndPublish(
client, MobileGL::MG_Pipe::MGPWireOp::ResourceRespecify, respecify,
[](Transport::RingRecordHeader&, std::uint8_t* payloadBytes) {
auto* wireDesc = reinterpret_cast<MobileGL::MG_Pipe::MGPResourceDesc*>(payloadBytes);
wireDesc->BufSize |= (std::uint64_t{1} << 32);
}, &respecifySeq));
ASSERT_NE(respecifySeq, Wire::kInvalidSeq);
int exitCode = -1;
const auto reaped = Server::ReapServer(session.server, 5000, &exitCode);
if (reaped != MOBILEGL_OK) {
client.Stop();
if (session.server.pid > 0) {
::kill(session.server.pid, SIGKILL);
(void)Server::ReapServer(session.server, 5000, &exitCode);
}
}
ASSERT_EQ(reaped, MOBILEGL_OK) << "server accepted a noncanonical mip extent carrier";
EXPECT_EQ(exitCode, -SIGABRT);
const std::string log = MobileGL::MG_Util::Debug::ReadRoleLogs(logBase.c_str());
const auto firstProtocolFatal = log.find("Fatal{ProtocolCorruption");
const auto carrierFatal = log.find("Fatal{ProtocolCorruption, \"ResourceRespecify.ExtentCarrier\"}");
EXPECT_NE(carrierFatal, std::string::npos)
<< "server did not reject high BufSize bits in the mutable extent carrier";
EXPECT_EQ(firstProtocolFatal, carrierFatal)
<< "carrier was refused before the canonicality gate";
client.Stop();
}
TEST(ServerSpawnTest, RawPeerWholeImageRespecifyCannotSmuggleBufferRangeFields) {
const std::string endpoint = Endpoint("bad-whole-respecify-buffer-range");
const std::string logBase = endpoint + ".log";
ScopedEnvironment logPath("MOBILEGL_LOG_FILE_PATH", logBase);
Session session;
ScopedSessionCleanup cleanup{session};
ASSERT_TRUE(Bring("bad-whole-respecify-buffer-range", &session));
ASSERT_EQ(Handshake(session), MOBILEGL_OK);
auto& client = Client::ClientSessionInstance();
MobileGL::MG_Pipe::MGPResourceDesc desc{};
desc.Resource = {77u, 1u};
desc.Target = static_cast<MobileGL::Uint8>(MobileGL::MG_Pipe::MGPipeResourceTarget::Tex2D);
desc.StorageKind = static_cast<MobileGL::Uint8>(MobileGL::TextureStorageType::Mipmap);
desc.InternalFormat = static_cast<MobileGL::Uint32>(MobileGL::TextureInternalFormat::RGBA8);
desc.Width = 4;
desc.Height = 4;
desc.Depth = 1;
desc.ArrayLayers = 1;
desc.Levels = 1;
desc.Samples = 1;
MobileGL::Int32 status = Wire::ReplySink::kStatusError;
auto seq = client.EmitAndWait(MobileGL::MG_Pipe::MGPWireOp::ResourceCreate, &desc, sizeof(desc),
nullptr, 0, nullptr, 0, &status);
ASSERT_NE(seq, Wire::kInvalidSeq);
ASSERT_EQ(status, Wire::ReplySink::kStatusOk);
ASSERT_EQ(client.WaitForApplied(seq, 5000), Transport::SessionWait::Reached);
MobileGL::MG_Pipe::MGPResourceDesc respecify = desc;
respecify.HasDefinedContent = 1;
std::uint64_t respecifySeq = Wire::kInvalidSeq;
ASSERT_TRUE(EncodeThenMutateHeaderAndPublish(
client, MobileGL::MG_Pipe::MGPWireOp::ResourceRespecify, respecify,
[](Transport::RingRecordHeader&, std::uint8_t* payloadBytes) {
auto* wireDesc = reinterpret_cast<MobileGL::MG_Pipe::MGPResourceDesc*>(payloadBytes);
wireDesc->BufOffset = 1;
}, &respecifySeq));
ASSERT_NE(respecifySeq, Wire::kInvalidSeq);
int exitCode = -1;
const auto reaped = Server::ReapServer(session.server, 5000, &exitCode);
if (reaped != MOBILEGL_OK) {
client.Stop();
if (session.server.pid > 0) {
::kill(session.server.pid, SIGKILL);
(void)Server::ReapServer(session.server, 5000, &exitCode);
}
}
ASSERT_EQ(reaped, MOBILEGL_OK) << "server accepted a buffer range on a whole image texture";
EXPECT_EQ(exitCode, -SIGABRT);
const std::string log = MobileGL::MG_Util::Debug::ReadRoleLogs(logBase.c_str());
const auto firstProtocolFatal = log.find("Fatal{ProtocolCorruption");
const auto rangeFatal = log.find("Fatal{ProtocolCorruption, \"ResourceRespecify.BufferRange\"}");
EXPECT_NE(rangeFatal, std::string::npos)
<< "server did not reject BufOffset/BufSize on the whole image descriptor";
EXPECT_EQ(firstProtocolFatal, rangeFatal)
<< "descriptor was refused before the canonical buffer-range gate";
client.Stop();
}
TEST(ServerSpawnTest, RawPeerFramebufferAttachmentPastBackendSlotLimitIsNamedProtocolCorruption) {
const std::string endpoint = Endpoint("bad-fbo-texture-slot");
const std::string logBase = endpoint + ".log";
ScopedEnvironment logPath("MOBILEGL_LOG_FILE_PATH", logBase);
Session session;
ScopedSessionCleanup cleanup{session};
ASSERT_TRUE(Bring("bad-fbo-texture-slot", &session));
ASSERT_EQ(Handshake(session), MOBILEGL_OK);
auto& client = Client::ClientSessionInstance();
const MobileGL::MG_Pipe::MGPipeHandle fbo{20u, 1u};
const MobileGL::MG_Pipe::MGPipeHandle outOfRange{1u << 20, 1u};
const auto framebuffer = RawColorAttachmentFramebuffer(fbo, outOfRange, 1);
ASSERT_TRUE(PublishRawRecordAndWait(client, MobileGL::MG_Pipe::MGPWireOp::SetFramebufferState,
framebuffer));
const auto clear = RawColorClear(fbo);
std::uint64_t clearSeq = Wire::kInvalidSeq;
ASSERT_TRUE(EncodeThenMutateHeaderAndPublish(
client, MobileGL::MG_Pipe::MGPWireOp::Clear, clear,
[](Transport::RingRecordHeader&, std::uint8_t*) {}, &clearSeq));
ASSERT_NE(clearSeq, Wire::kInvalidSeq);
// This final clear is expected to kill the server, so there is no applied watermark to wait on.
int exitCode = -1;
const auto reaped = Server::ReapServer(session.server, 5000, &exitCode);
if (reaped != MOBILEGL_OK) {
client.Stop();
if (session.server.pid > 0) {
::kill(session.server.pid, SIGKILL);
(void)Server::ReapServer(session.server, 5000, &exitCode);
}
}
ASSERT_EQ(reaped, MOBILEGL_OK)
<< "server did not reject the FBO attachment whose texture slot exceeded BackendSlotTable's cap";
EXPECT_EQ(exitCode, -SIGABRT);
const std::string log = MobileGL::MG_Util::Debug::ReadRoleLogs(logBase.c_str());
const auto firstProtocolFatal = log.find("Fatal{ProtocolCorruption");
const auto slotFatal = log.find("Fatal{ProtocolCorruption, \"BackendSlotTable.HandleSlot\"}");
EXPECT_NE(slotFatal, std::string::npos) << "server did not name the backend texture slot bound";
EXPECT_EQ(firstProtocolFatal, slotFatal)
<< "the peer record was refused before the backend twin adoption guard";
client.Stop();
}
TEST(ServerSpawnTest, RawPeerFramebufferAttachmentBackwardGenerationIsNamedProtocolCorruption) {
const std::string endpoint = Endpoint("bad-fbo-texture-generation");
const std::string logBase = endpoint + ".log";
ScopedEnvironment logPath("MOBILEGL_LOG_FILE_PATH", logBase);
Session session;
ScopedSessionCleanup cleanup{session};
ASSERT_TRUE(Bring("bad-fbo-texture-generation", &session));
ASSERT_EQ(Handshake(session), MOBILEGL_OK);
auto& client = Client::ClientSessionInstance();
const MobileGL::MG_Pipe::MGPipeHandle texture{37u, 2u};
MobileGL::MG_Pipe::MGPResourceDesc desc{};
desc.Resource = texture;
desc.Target = static_cast<MobileGL::Uint8>(MobileGL::MG_Pipe::MGPipeResourceTarget::Tex2D);
desc.StorageKind = static_cast<MobileGL::Uint8>(MobileGL::TextureStorageType::Mipmap);
desc.BindMask = MobileGL::MG_Pipe::kMGPipeBindRenderTarget;
desc.InternalFormat = static_cast<MobileGL::Uint32>(MobileGL::TextureInternalFormat::RGBA8);
desc.Width = desc.Height = desc.Depth = 1;
desc.ArrayLayers = desc.Levels = desc.Samples = 1;
MobileGL::Int32 status = Wire::ReplySink::kStatusError;
auto seq = client.EmitAndWait(MobileGL::MG_Pipe::MGPWireOp::ResourceCreate, &desc, sizeof(desc),
nullptr, 0, nullptr, 0, &status);
ASSERT_NE(seq, Wire::kInvalidSeq);
ASSERT_EQ(status, Wire::ReplySink::kStatusOk);
ASSERT_EQ(client.WaitForApplied(seq, 5000), Transport::SessionWait::Reached);
const MobileGL::MG_Pipe::MGPipeHandle fbo{20u, 1u};
auto current = RawColorAttachmentFramebuffer(fbo, texture, 1);
ASSERT_TRUE(PublishRawRecordAndWait(client, MobileGL::MG_Pipe::MGPWireOp::SetFramebufferState,
current));
const auto clear = RawColorClear(fbo);
ASSERT_TRUE(PublishRawRecordAndWait(client, MobileGL::MG_Pipe::MGPWireOp::Clear, clear));
// PipeApply accepts this attachment verbatim; the server's already-live {37,2} twin is
// the only fact that lets the apply-side adoption guard identify the stale generation.
current.Color[0].Res.Gen = 1;
current.ContentHash = 2;
ASSERT_TRUE(PublishRawRecordAndWait(client, MobileGL::MG_Pipe::MGPWireOp::SetFramebufferState,
current));
std::uint64_t badClearSeq = Wire::kInvalidSeq;
ASSERT_TRUE(EncodeThenMutateHeaderAndPublish(
client, MobileGL::MG_Pipe::MGPWireOp::Clear, clear,
[](Transport::RingRecordHeader&, std::uint8_t*) {}, &badClearSeq));
ASSERT_NE(badClearSeq, Wire::kInvalidSeq);
int exitCode = -1;
const auto reaped = Server::ReapServer(session.server, 5000, &exitCode);
if (reaped != MOBILEGL_OK) {
client.Stop();
if (session.server.pid > 0) {
::kill(session.server.pid, SIGKILL);
(void)Server::ReapServer(session.server, 5000, &exitCode);
}
}
ASSERT_EQ(reaped, MOBILEGL_OK)
<< "server did not reject the FBO attachment whose generation moved backwards";
EXPECT_EQ(exitCode, -SIGABRT);
const std::string log = MobileGL::MG_Util::Debug::ReadRoleLogs(logBase.c_str());
const auto firstProtocolFatal = log.find("Fatal{ProtocolCorruption");
const auto generationFatal = log.find("Fatal{ProtocolCorruption, \"BackendSlotTable.Generation\"}");
EXPECT_NE(generationFatal, std::string::npos)
<< "server did not name the stale attachment generation";
EXPECT_EQ(firstProtocolFatal, generationFatal)
<< "the peer record was refused before the backend twin generation guard";
client.Stop();
}
TEST(ServerSpawnTest, StartsAServerProcessAndHandshakesAcrossIt) {
const int before = Server::CountOwnChildren();
ASSERT_GE(before, 0) << "/proc unreadable; the process-tree gate cannot run here";
@@ -130,7 +130,13 @@ TEST(SessionHandshakeTest, TheAbiFingerprintChangesWhenAnyOfItsInputsDoes) {
EXPECT_EQ(inputs.ProgramArtifactsCodecVersion, MG_State::GLState::kProgramArtifactsCodecVersion);
EXPECT_EQ(inputs.ProgramArtifactsSchema, MG_State::GLState::ProgramArtifactsSchemaFingerprint());
EXPECT_EQ(inputs.OpCount, static_cast<Uint64>(MG_Pipe::MGPWireOp::kOpCount));
#if MOBILEGL_BUILD_DISAGGREGATED
EXPECT_EQ(inputs.ControlSchemaRevision,
(static_cast<Uint64>(MOBILEGL_PROTOCOL_CONTROL_REVISION) << 32) |
MG_Pipe::kMGPipeResourceRespecifyExtentCarrierRevision);
#else
EXPECT_EQ(inputs.ControlSchemaRevision, static_cast<Uint64>(MOBILEGL_PROTOCOL_CONTROL_REVISION));
#endif
EXPECT_EQ(inputs.PointerBits, sizeof(void*) * 8);
const auto perturbed = [&](auto mutate) {
auto copy = inputs;
@@ -92,6 +92,7 @@ TEST(StagedTextureStoreTest, TheSplitArmCopiesAndSurvivesTheSourceBeingPoisoned)
Vector<Uint8> staged(64, 0xAB);
const IntVec3 extent{4, 4, 1};
splitStore.NoteLevelDefined(key, kTex2DTarget, 0, extent);
const Uint8* splitBase = splitStore.Adopt(key, kTex2DTarget, 0, extent, staged.data(), staged.size());
const Uint8* monolithBase =
monolithStore.Adopt(key, kTex2DTarget, 0, extent, staged.data(), staged.size());
@@ -148,6 +149,32 @@ TEST(StagedTextureStoreTest, DefinednessIsTrackedAndAnExtentMoveDropsTheBytes) {
EXPECT_EQ(store.LevelExtentOrUndefined(key, kTex2DTarget, 0), IntVec3(8, 8, 1));
}
TEST(StagedTextureStoreTest, AMutableNoncanonicalLevelKeepsItsExactDeclaredExtentAndBound) {
Server::StagedTextureStore store(/*copies=*/true);
const Uint64 key = Server::StagedTextureStore::KeyForHandle(TestHandle(41, 1));
const IntVec3 noncanonical{3, 2, 1};
store.NoteLevelDefined(key, kTex2DTarget, 2, noncanonical,
static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D),
static_cast<Uint32>(TextureInternalFormat::RGBA8));
EXPECT_TRUE(store.IsLevelDefined(key, kTex2DTarget, 2));
EXPECT_EQ(store.LevelExtentOrUndefined(key, kTex2DTarget, 2), noncanonical)
<< "a null-data mutable mip lost the exact extent accepted by resource_respecify";
EXPECT_EQ(store.LevelDeclaredByteBound(key, kTex2DTarget, 2), 24u);
EXPECT_FALSE(store.IsCovered(key, kTex2DTarget, 2));
}
TEST(StagedTextureStoreTest, MultisampleAndTextureBufferTargetsCannotAcceptStagedTexelBytes) {
EXPECT_FALSE(Server::StagedTextureTargetSupportsSubData(
static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2DMS)));
EXPECT_FALSE(Server::StagedTextureTargetSupportsSubData(
static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2DMSArray)));
EXPECT_FALSE(Server::StagedTextureTargetSupportsSubData(
static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::TexBuffer)));
EXPECT_TRUE(Server::StagedTextureTargetSupportsSubData(
static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D)));
}
// Keys are independent, ResetLevels drops one resource's whole chain, Drop one key and
// DropAll every one - the three events the contract names (respecify, destroy, context death)
// each have their call site, and these are the answers those call sites rely on.
@@ -155,11 +182,15 @@ TEST(StagedTextureStoreTest, ResetDropAndDropAllForgetExactlyWhatTheyName) {
Server::StagedTextureStore store(/*copies=*/true);
const Uint64 a = Server::StagedTextureStore::KeyForHandle(TestHandle(5, 1));
const Uint64 b = Server::StagedTextureStore::KeyForHandle(TestHandle(6, 1));
Vector<Uint8> bytes(16, 0x22);
Vector<Uint8> bytes4x4(64, 0x22);
Vector<Uint8> bytes2x2(16, 0x22);
store.Adopt(a, kTex2DTarget, 0, IntVec3{4, 4, 1}, bytes.data(), bytes.size());
store.Adopt(a, kTex2DTarget, 1, IntVec3{2, 2, 1}, bytes.data(), bytes.size());
store.Adopt(b, kTex2DTarget, 0, IntVec3{4, 4, 1}, bytes.data(), bytes.size());
store.NoteLevelDefined(a, kTex2DTarget, 0, IntVec3{4, 4, 1});
store.Adopt(a, kTex2DTarget, 0, IntVec3{4, 4, 1}, bytes4x4.data(), bytes4x4.size());
store.NoteLevelDefined(a, kTex2DTarget, 1, IntVec3{2, 2, 1});
store.Adopt(a, kTex2DTarget, 1, IntVec3{2, 2, 1}, bytes2x2.data(), bytes2x2.size());
store.NoteLevelDefined(b, kTex2DTarget, 0, IntVec3{4, 4, 1});
store.Adopt(b, kTex2DTarget, 0, IntVec3{4, 4, 1}, bytes4x4.data(), bytes4x4.size());
ASSERT_EQ(store.TrackedResources(), 2u);
ASSERT_EQ(store.TrackedLevelCount(a), 2u);
@@ -167,7 +198,8 @@ TEST(StagedTextureStoreTest, ResetDropAndDropAllForgetExactlyWhatTheyName) {
EXPECT_FALSE(store.HasShadow(a)) << "a whole-resource respecify forgets every level";
EXPECT_TRUE(store.IsCovered(b, kTex2DTarget, 0));
store.Adopt(a, kTex2DTarget, 0, IntVec3{4, 4, 1}, bytes.data(), bytes.size());
store.NoteLevelDefined(a, kTex2DTarget, 0, IntVec3{4, 4, 1});
store.Adopt(a, kTex2DTarget, 0, IntVec3{4, 4, 1}, bytes4x4.data(), bytes4x4.size());
store.Drop(a);
EXPECT_EQ(store.TrackedResources(), 1u);
EXPECT_TRUE(store.IsCovered(b, kTex2DTarget, 0));
@@ -247,6 +279,7 @@ TEST(StagedTextureStoreTest, StageChunkRunsAssembleTheLevelImageAndAGapIsNotCove
Vector<Uint8> lower(32, 0x33);
Vector<Uint8> upper(32, 0x77);
store.NoteLevelDefined(key, kTex2DTarget, 0, extent);
// THE SECOND RUN FIRST, deliberately: coverage is a SET and the placement comes from the
// record, so the order the pieces land in cannot matter.
EXPECT_EQ(monolithStore.AdoptRun(key, kTex2DTarget, 0, extent, 32, upper.data(), upper.size()),
@@ -277,22 +310,20 @@ TEST(StagedTextureStoreTest, StageChunkRunsAssembleTheLevelImageAndAGapIsNotCove
// AND A GAP IS NOT COVERAGE: it is the missing-piece case, which is the one thing the covered
// set exists to make visible instead of papered over.
const Uint64 holed = Server::StagedTextureStore::KeyForHandle(TestHandle(12, 1));
store.NoteLevelDefined(holed, kTex2DTarget, 0, extent);
Vector<Uint8> part(16, 0x11);
store.AdoptRun(holed, kTex2DTarget, 0, extent, 0, part.data(), part.size());
store.AdoptRun(holed, kTex2DTarget, 0, extent, 32, part.data(), part.size());
store.AdoptRun(holed, kTex2DTarget, 0, extent, 32, upper.data(), upper.size());
EXPECT_EQ(store.LevelCoveredRunCount(holed, kTex2DTarget, 0), 2u)
<< "two runs with a 16-byte gap merged into one";
EXPECT_FALSE(store.IsCovered(holed, kTex2DTarget, 0)) << "a span across the gap reads covered";
// AND WHAT THE CHECK CANNOT SEE, written down rather than left to be discovered: the store
// knows no format, so it cannot know how many bytes a level SHOULD have. A leading run that
// already reaches the image's high-water mark therefore reads as covered - the check is "the
// range the reader asks for is covered", not "the level is the size the client meant", and no
// reader runs between the pieces of one level anyway (they are records of ONE verb, and the
// texture sync runs at the barrier that closes it).
// A short leading run is not a complete level even if its current high-water mark ends at
// that run. The store checks coverage against the server-derived byte bound, not its prefix.
const Uint64 leading = Server::StagedTextureStore::KeyForHandle(TestHandle(14, 1));
store.NoteLevelDefined(leading, kTex2DTarget, 0, IntVec3{4, 4, 1});
store.AdoptRun(leading, kTex2DTarget, 0, IntVec3{4, 4, 1}, 0, part.data(), part.size());
EXPECT_TRUE(store.IsCovered(leading, kTex2DTarget, 0))
<< "a leading run is the whole image as far as this store can tell";
EXPECT_FALSE(store.IsCovered(leading, kTex2DTarget, 0))
<< "the leading prefix must not stand in for the declared level";
EXPECT_EQ(store.LevelByteSize(leading, kTex2DTarget, 0), 16u);
// Filling the gap merges all three into one run: adjacency, not proximity, is the rule.
store.AdoptRun(holed, kTex2DTarget, 0, extent, 16, part.data(), part.size());
@@ -308,23 +339,31 @@ TEST(StagedTextureStoreTest, StageChunkRunsAssembleTheLevelImageAndAGapIsNotCove
}
#if MOBILEGL_BUILD_DISAGGREGATED
// B4's rule under PH-4's declared extents: a level redefined at a new extent (the respecify's
// NoteLevelDefined, which every run now needs before it lands) starts with no coverage, so the
// first run of the new definition cannot be completed by the old definition's bytes. A run that
// still carries the old extent is StagedTextureStore.LevelExtent's refusal (ServerSpawnTest's
// raw-peer control), so the new-extent runs are the only ones that can land here.
TEST(StagedTextureStoreTest, AdoptRunExtentMoveDoesNotInheritOldCoverage) {
Server::StagedTextureStore store(/*copies=*/true);
const Uint64 key = Server::StagedTextureStore::KeyForHandle(TestHandle(15, 1));
Vector<Uint8> oldRun(16, 0x44);
Vector<Uint8> newRun(16, 0x99);
Vector<Uint8> oldRun(64, 0x44); // the whole 4x4 RGBA8 level
Vector<Uint8> newRun(64, 0x99); // the upper half of the 8x4 RGBA8 level
store.NoteLevelDefined(key, kTex2DTarget, 0, IntVec3{4, 4, 1});
store.AdoptRun(key, kTex2DTarget, 0, IntVec3{4, 4, 1}, 0, oldRun.data(), oldRun.size());
ASSERT_TRUE(store.IsCovered(key, kTex2DTarget, 0));
store.AdoptRun(key, kTex2DTarget, 0, IntVec3{8, 4, 1}, 16, newRun.data(), newRun.size());
store.NoteLevelDefined(key, kTex2DTarget, 0, IntVec3{8, 4, 1});
ASSERT_EQ(store.LevelDeclaredByteBound(key, kTex2DTarget, 0), 128u);
store.AdoptRun(key, kTex2DTarget, 0, IntVec3{8, 4, 1}, 64, newRun.data(), newRun.size());
EXPECT_FALSE(store.IsCovered(key, kTex2DTarget, 0))
<< "the old level's [0,16) run was mistaken for bytes of the new extent";
<< "the old level's [0,64) run was mistaken for bytes of the new extent";
EXPECT_EQ(store.LevelCoveredRunCount(key, kTex2DTarget, 0), 1u);
Vector<Uint8> newLower(16, 0xAA);
Vector<Uint8> newLower(64, 0xAA);
store.AdoptRun(key, kTex2DTarget, 0, IntVec3{8, 4, 1}, 0, newLower.data(), newLower.size());
const Uint8* base = store.RequireLevelBytes(key, kTex2DTarget, 0, "unit_extent_move");
ASSERT_NE(base, nullptr);
EXPECT_EQ(base[0], 0xAAu);
EXPECT_EQ(base[16], 0x99u);
EXPECT_EQ(base[64], 0x99u);
}
#endif
@@ -390,7 +429,8 @@ TEST(StagedTextureStoreTest, TheRunImageOffsetIsReadOffTheRecordsOwnRunAndBox) {
TEST(StagedTextureStoreTest, ATexelReadOutsideTheStagedCoverageIsFatalByName) {
Server::StagedTextureStore store(/*copies=*/true);
const Uint64 key = Server::StagedTextureStore::KeyForHandle(TestHandle(8, 1));
Vector<Uint8> bytes(16, 0x33);
Vector<Uint8> bytes(64, 0x33);
store.NoteLevelDefined(key, kTex2DTarget, 0, IntVec3{4, 4, 1});
store.Adopt(key, kTex2DTarget, 0, IntVec3{4, 4, 1}, bytes.data(), bytes.size());
// In coverage: no Fatal, asserted first so the death below cannot be a function that
@@ -434,6 +474,8 @@ TEST(StagedTextureStoreTest, ALevelWithAPieceMissingIsRefusedAsAWholeLevelReadBy
const Uint64 key = Server::StagedTextureStore::KeyForHandle(TestHandle(13, 1));
Vector<Uint8> part(16, 0x55);
Vector<Uint8> whole(64, 0x22);
store.NoteLevelDefined(key, kTex2DTarget, 0, IntVec3{4, 4, 1});
store.NoteLevelDefined(key, kTex2DTarget, 1, IntVec3{4, 4, 1});
// TWO RUNS WITH A HOLE BETWEEN THEM, which is what a missing piece looks like from here: the
// image's high-water mark is the level's size and the covered set does not reach across it.
store.AdoptRun(key, kTex2DTarget, 0, IntVec3{4, 4, 1}, 0, part.data(), part.size());
@@ -553,9 +595,8 @@ TEST(StagedTextureProductionTest, HooklessTextureConsumerOwnsBytesAndScopedStora
desc.Height = 4;
desc.Depth = 1;
desc.Levels = 3;
MG_Pipe::MGPRespecifiedLevel level{};
level.UploadTarget = MG_Pipe::MGPipePackSubDataTarget(desc.Target, kTex2DTarget);
level.Level = 0;
MG_Pipe::MGPRespecifiedLevel level = MG_Pipe::MGPipeMakeRespecifiedLevel(
MG_Pipe::MGPipePackSubDataTarget(desc.Target, kTex2DTarget), 0, 4, 4, 1);
ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceRespecify(desc, nullptr, &level));
EXPECT_EQ(store.LevelExtentOrUndefined(key, kTex2DTarget, 0), IntVec3(4, 4, 1))
<< "null-data glTexImage defines the level even though no upload follows";