diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index e891754b..8836cae2 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -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(level->Width), static_cast(level->Height), + static_cast(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(uploadTarget), static_cast(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; } diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 74c9c3bd..702f545b 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -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); } diff --git a/MobileGL/MG_Backend/DirectGLES/SlotTables.h b/MobileGL/MG_Backend/DirectGLES/SlotTables.h index 8cdca713..d61b9cff 100644 --- a/MobileGL/MG_Backend/DirectGLES/SlotTables.h +++ b/MobileGL/MG_Backend/DirectGLES/SlotTables.h @@ -14,6 +14,9 @@ #if MOBILEGL_PIPE_PUSH #include #endif +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#endif // Espryt 0b, the first Track H slice: the DENSE, {slot, gen}-keyed twin table that replaces // StateBackendObjectRegistry's UnorderedMap. @@ -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; diff --git a/MobileGL/MG_Impl/Pipe/TextureEmit.h b/MobileGL/MG_Impl/Pipe/TextureEmit.h index 3110692b..e6e1cb50 100644 --- a/MobileGL/MG_Impl/Pipe/TextureEmit.h +++ b/MobileGL/MG_Impl/Pipe/TextureEmit.h @@ -901,6 +901,18 @@ namespace MobileGL::MG_Pipe { MGPRespecifiedLevel key{}; key.UploadTarget = packedTarget; key.Level = static_cast(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(uploadTarget), key.Level); + key.Width = static_cast(exactLevelExtent.x()); + key.Height = static_cast(exactLevelExtent.y()); + key.Depth = static_cast(exactLevelExtent.z()); +#endif accepted = RespecifyOnce(texture, handle, entry, desc, &key, viewOf, bufferHandle, bufOffset, bufSize); if (!accepted) break; diff --git a/MobileGL/MG_Pipe/MGPipeTypes.h b/MobileGL/MG_Pipe/MGPipeTypes.h index 1fc23df3..55c5dea4 100644 --- a/MobileGL/MG_Pipe/MGPipeTypes.h +++ b/MobileGL/MG_Pipe/MGPipeTypes.h @@ -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(desc.BufOffset >> 32); + } + inline constexpr Uint32 MGPipeRespecifiedHeightOf(const MGPResourceDesc& desc) { + return MGPipeRespecifyIsWholeResource(desc) ? 0u : static_cast(desc.BufOffset); + } + inline constexpr Uint32 MGPipeRespecifiedDepthOf(const MGPResourceDesc& desc) { + return MGPipeRespecifyIsWholeResource(desc) ? 0u : static_cast(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(width) << 32) | static_cast(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(MGPipeResourceTarget::TexBuffer)) { + desc.BufOffset = 0; + desc.BufSize = 0; + } +#endif desc.HasRespecifiedLevel = 0; desc.RespecifiedUploadTarget = 0; desc.RespecifiedLevel = 0; diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index 726fcad7..df0e346a 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -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(level->Width), static_cast(level->Height), + static_cast(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(target), static_cast(mip)); + for (Uint32 mip = 0; mip < desc.Levels; ++mip) + define(static_cast(target), static_cast(mip), + MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, + desc.Height, desc.Depth, mip)); }; switch (static_cast(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(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(MGPipeResourceTarget::Renderbuffer) || + desc.Target == static_cast(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(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(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). diff --git a/MobileGL/MG_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index 9fc13ff8..ccd0f50f 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -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 diff --git a/MobileGL/MG_Pipe/PipeWireLayout.h b/MobileGL/MG_Pipe/PipeWireLayout.h index dfb7cfcf..65dbb857 100644 --- a/MobileGL/MG_Pipe/PipeWireLayout.h +++ b/MobileGL/MG_Pipe/PipeWireLayout.h @@ -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; diff --git a/MobileGL/MG_Remote/CapsCodec.cpp b/MobileGL/MG_Remote/CapsCodec.cpp index 0db7d3c3..322ee6d0 100644 --- a/MobileGL/MG_Remote/CapsCodec.cpp +++ b/MobileGL/MG_Remote/CapsCodec.cpp @@ -476,7 +476,14 @@ namespace MobileGL::MG_Remote { inputs.ProgramArtifactsCodecVersion = MG_State::GLState::kProgramArtifactsCodecVersion; inputs.ProgramArtifactsSchema = MG_State::GLState::ProgramArtifactsSchemaFingerprint(); inputs.OpCount = static_cast(MG_Pipe::MGPWireOp::kOpCount); +#if MOBILEGL_BUILD_DISAGGREGATED + inputs.ControlSchemaRevision = + (static_cast(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; diff --git a/MobileGL/MG_Remote/Client/WireTables.cpp b/MobileGL/MG_Remote/Client/WireTables.cpp index d2a5b1b1..4f96f46c 100644 --- a/MobileGL/MG_Remote/Client/WireTables.cpp +++ b/MobileGL/MG_Remote/Client/WireTables.cpp @@ -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); } diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index 242197dd..b9f86563 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -13,6 +13,7 @@ #include "ServerSession.h" #include "../Transport/ReplySlot.h" +#include "StagedTextureStore.h" #include #include @@ -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 { diff --git a/MobileGL/MG_Remote/Server/StagedTextureStore.h b/MobileGL/MG_Remote/Server/StagedTextureStore.h index 284979e0..53adfab7 100644 --- a/MobileGL/MG_Remote/Server/StagedTextureStore.h +++ b/MobileGL/MG_Remote/Server/StagedTextureStore.h @@ -105,12 +105,14 @@ // rather than left to StagedShadow.h's copy of the same include. #include #include +#include #include #include #include #include #include +#include #if MOBILEGL_BUILD_DISAGGREGATED #include @@ -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(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(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(internalFormat)); + if (bytesPerTexel == 0) return false; + const Uint64 x = static_cast(extent.x()); + const Uint64 y = static_cast(extent.y()); + const Uint64 z = static_cast(extent.z()); + const Uint64 bpp = static_cast(bytesPerTexel); + constexpr Uint64 max = std::numeric_limits::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(std::numeric_limits::max())) return false; + *outBound = bytes; + return true; + } + inline Int StagedTextureShrinkingAxisCount(Uint8 pipeResourceTarget) { switch (static_cast(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 lock(m_mutex); + m_deviceLimits = {maxTextureSize, std::min(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 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 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(MG_Pipe::MGPipeResourceTarget::Tex2D), + Uint32 internalFormat = static_cast(TextureInternalFormat::RGBA8)) { if (!m_copies) return; const std::lock_guard 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(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(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 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(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 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(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 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 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 Covered; + Uint32 InternalFormat = 0; + Uint8 ResourceTarget = static_cast(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(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(key), uploadTarget, level, + static_cast(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(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::max(); + const Uint64 runBytes = static_cast(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(key), uploadTarget, level, + static_cast(imageOffset), + static_cast(runBytes)); + } + const Uint64 end = imageOffset + runBytes; + if (end > shadow.DeclaredByteBound || + end > static_cast(std::numeric_limits::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(key), uploadTarget, level, + static_cast(imageOffset), + static_cast(runBytes), + static_cast(shadow.DeclaredByteBound)); + } const auto* raw = static_cast(bytes); - const SizeT needed = static_cast(imageOffset) + byteSize; + const SizeT begin = static_cast(imageOffset); + const SizeT needed = static_cast(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(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 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. diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp index c031c5d6..b1c7852a 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp @@ -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)); diff --git a/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp b/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp index 9038feae..e200348d 100644 --- a/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/ResourceEmitTest.cpp @@ -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); }); diff --git a/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp b/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp index a815c823..79a7ccef 100644 --- a/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/TextureEmitTest.cpp @@ -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. diff --git a/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp b/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp index a14f21fe..c54a8f63 100644 --- a/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp +++ b/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp @@ -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); } diff --git a/MobileGL/MG_Test/Wire/ServerSpawnTest.cpp b/MobileGL/MG_Test/Wire/ServerSpawnTest.cpp index 0ada60f0..8a7e0838 100644 --- a/MobileGL/MG_Test/Wire/ServerSpawnTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerSpawnTest.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include #include @@ -101,7 +102,9 @@ namespace { template 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 + 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::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::TextureInternalFormat::RGBA8); + state.Color[0].Kind = MobileGL::MG_Pipe::kMGPipeSurfaceKindTexture; + state.Color[0].UploadTarget = + static_cast(MobileGL::TextureUploadTarget::Texture2D); + state.Color[0].TextureTarget = + static_cast(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::MG_Pipe::MGPipeResourceTarget::Tex2D); + desc.StorageKind = static_cast(MobileGL::TextureStorageType::Mipmap); + desc.InternalFormat = static_cast(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(respecify.Target), + static_cast(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 staged(96, 0x5A); + MobileGL::MG_Pipe::MGPSubData upload{}; + upload.Res = desc.Resource; + upload.Target = MobileGL::MG_Pipe::MGPipePackSubDataTarget( + static_cast(MobileGL::MG_Pipe::MGPipeResourceTarget::Tex2D), + static_cast(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{®ion, 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(payloadBytes); + wireUpload->Blob.Size = 96; + auto* wireRegion = reinterpret_cast( + 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::MG_Pipe::MGPipeResourceTarget::Tex2D); + desc.StorageKind = static_cast(MobileGL::TextureStorageType::Mipmap); + desc.InternalFormat = static_cast(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(respecify.Target), + static_cast(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 staged(64, 0x5A); + MobileGL::MG_Pipe::MGPSubData upload{}; + upload.Res = desc.Resource; + upload.Target = MobileGL::MG_Pipe::MGPipePackSubDataTarget( + static_cast(MobileGL::MG_Pipe::MGPipeResourceTarget::Tex2D), + static_cast(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(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::MG_Pipe::MGPipeResourceTarget::Tex2DMS); + desc.StorageKind = static_cast(MobileGL::TextureStorageType::Mipmap); + desc.InternalFormat = static_cast(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 staged(64, 0x5A); + MobileGL::MG_Pipe::MGPSubData upload{}; + upload.Res = desc.Resource; + upload.Target = MobileGL::MG_Pipe::MGPipePackSubDataTarget( + static_cast(MobileGL::MG_Pipe::MGPipeResourceTarget::Tex2DMS), + static_cast(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::MG_Pipe::MGPipeResourceTarget::Tex2D); + desc.StorageKind = static_cast(MobileGL::TextureStorageType::Mipmap); + desc.InternalFormat = static_cast(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(respecify.Target), + static_cast(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(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::MG_Pipe::MGPipeResourceTarget::Tex2D); + desc.StorageKind = static_cast(MobileGL::TextureStorageType::Mipmap); + desc.InternalFormat = static_cast(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(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::MG_Pipe::MGPipeResourceTarget::Tex2D); + desc.StorageKind = static_cast(MobileGL::TextureStorageType::Mipmap); + desc.BindMask = MobileGL::MG_Pipe::kMGPipeBindRenderTarget; + desc.InternalFormat = static_cast(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"; diff --git a/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp b/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp index e4ee8020..d5c05f41 100644 --- a/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp +++ b/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp @@ -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(MG_Pipe::MGPWireOp::kOpCount)); +#if MOBILEGL_BUILD_DISAGGREGATED + EXPECT_EQ(inputs.ControlSchemaRevision, + (static_cast(MOBILEGL_PROTOCOL_CONTROL_REVISION) << 32) | + MG_Pipe::kMGPipeResourceRespecifyExtentCarrierRevision); +#else EXPECT_EQ(inputs.ControlSchemaRevision, static_cast(MOBILEGL_PROTOCOL_CONTROL_REVISION)); +#endif EXPECT_EQ(inputs.PointerBits, sizeof(void*) * 8); const auto perturbed = [&](auto mutate) { auto copy = inputs; diff --git a/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp b/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp index 488d32ae..b05374fa 100644 --- a/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp +++ b/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp @@ -92,6 +92,7 @@ TEST(StagedTextureStoreTest, TheSplitArmCopiesAndSurvivesTheSourceBeingPoisoned) Vector 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(MG_Pipe::MGPipeResourceTarget::Tex2D), + static_cast(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(MG_Pipe::MGPipeResourceTarget::Tex2DMS))); + EXPECT_FALSE(Server::StagedTextureTargetSupportsSubData( + static_cast(MG_Pipe::MGPipeResourceTarget::Tex2DMSArray))); + EXPECT_FALSE(Server::StagedTextureTargetSupportsSubData( + static_cast(MG_Pipe::MGPipeResourceTarget::TexBuffer))); + EXPECT_TRUE(Server::StagedTextureTargetSupportsSubData( + static_cast(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 bytes(16, 0x22); + Vector bytes4x4(64, 0x22); + Vector 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 lower(32, 0x33); Vector 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 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 oldRun(16, 0x44); - Vector newRun(16, 0x99); + Vector oldRun(64, 0x44); // the whole 4x4 RGBA8 level + Vector 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 newLower(16, 0xAA); + Vector 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 bytes(16, 0x33); + Vector 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 part(16, 0x55); Vector 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";