diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 19f70fb2..0cb22b2e 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -10692,7 +10692,8 @@ namespace MobileGL::MG_Backend::DirectGLES { MG_Pipe::MGPipeHandle destination, Uint32 destinationLevel, const IntVec3& destinationOrigin, const IntVec3& size) { auto& store = MG_Remote::Server::ServerStagedTexture(); - if (!store.CopiesIntoServerStorage()) return false; + // P13 W4b: an aliasing store (monolith's record arm) follows too; see FollowCopy. + if (!store.Holds()) return false; // The store's level key for an endpoint copy-image addresses as ONE image with its // layers on z: exactly one upload target (not a cube map, whose faces are six store // levels) and not a 1D array (the store keeps its layers as rows). A view's own levels diff --git a/MobileGL/MG_Remote/Server/StagedTextureStore.h b/MobileGL/MG_Remote/Server/StagedTextureStore.h index 19e5cc92..e1129ea6 100644 --- a/MobileGL/MG_Remote/Server/StagedTextureStore.h +++ b/MobileGL/MG_Remote/Server/StagedTextureStore.h @@ -304,9 +304,11 @@ namespace MobileGL::MG_Remote::Server { // in place: a run records the address it arrived at (minus its offset in the level) and // nothing is copied. The readers then ask exactly what they ask under split - is the // level covered, what are its bytes - and get the same answers from memory that is - // already there. The two copy-only operations (FollowCopy, AdoptClientBoxes) stay - // copy-only: this store never writes frontend memory, so a copy it cannot follow marks - // the destination driver-written, which sends its readers to the driver. + // already there. This store never writes frontend memory: a copy it follows + // (FollowCopy) first takes the destination level into a server-owned copy, which is then + // what AdoptClientBoxes lands the client's later boxes on - exactly the split store's + // shape for that level - and a copy it cannot follow marks the destination + // driver-written, which sends its readers to the driver. Bool Aliases() const { return !m_copies && MG_Config::RecordArmAliasesFrontend(); } // Does this store answer at all: a copying store, or an aliasing one. Bool Holds() const { return m_copies || Aliases(); } @@ -555,7 +557,7 @@ namespace MobileGL::MG_Remote::Server { Bool FollowCopy(Uint64 srcKey, Uint16 srcTarget, Uint16 srcLevel, const IntVec3& srcOrigin, Uint64 dstKey, Uint16 dstTarget, Uint16 dstLevel, const IntVec3& dstOrigin, const IntVec3& size) { - if (!m_copies) return false; + if (!Holds()) return false; const std::lock_guard lock(m_mutex); if (m_driverWritten.find(Bucketed(srcKey)) != m_driverWritten.end()) return false; const LevelShadow* src = FindLevel(srcKey, srcTarget, srcLevel); @@ -569,14 +571,22 @@ namespace MobileGL::MG_Remote::Server { // Gathered first, so a copy within one level cannot read rows it has already written. const SizeT rowBytes = static_cast(size.x()) * texel; Vector rows(rowBytes * static_cast(size.y()) * static_cast(size.z())); + const Uint8* srcBytes = LevelData(*src); for (Int z = 0; z < size.z(); ++z) { for (Int y = 0; y < size.y(); ++y) { std::memcpy(rows.data() + (static_cast(z) * size.y() + y) * rowBytes, - src->Bytes.data() + TexelOffset(src->Extent, srcOrigin.x(), srcOrigin.y() + y, - srcOrigin.z() + z, texel), + srcBytes + TexelOffset(src->Extent, srcOrigin.x(), srcOrigin.y() + y, + srcOrigin.z() + z, texel), rowBytes); } } + if (dst->Alias != nullptr) { + // P13 W4b, the aliasing mode: the destination's bytes are the frontend's level + // shadow, which never sees the copy and which this store does not write. The level + // becomes server-owned here, from the bytes it stood for, as under split. + dst->Bytes.assign(dst->Alias, dst->Alias + static_cast(dst->DeclaredByteBound)); + dst->Alias = nullptr; + } if (!LevelComplete(*dst)) { dst->Bytes.assign(static_cast(dst->DeclaredByteBound), Uint8{0}); dst->Covered.clear(); @@ -609,10 +619,13 @@ namespace MobileGL::MG_Remote::Server { Bool AdoptClientBoxes(Uint64 key, Uint16 uploadTarget, Uint16 level, const IntVec3& extent, Uint64 imageOffset, const void* bytes, SizeT byteSize, const ClientTexelBox* boxes, SizeT boxCount) { - if (!m_copies || bytes == nullptr || (boxCount != 0 && boxes == nullptr)) return false; + if (!Holds() || bytes == nullptr || (boxCount != 0 && boxes == nullptr)) return false; const std::lock_guard lock(m_mutex); LevelShadow* shadow = FindLevelMutable(key, uploadTarget, level); - if (shadow == nullptr || !shadow->HoldsFollowedCopy || !LevelComplete(*shadow) || shadow->Extent != extent) { + // Alias is null on a level holding a followed copy (FollowCopy took it server-owned); + // checked anyway, because the boxes below are written into Bytes. + if (shadow == nullptr || !shadow->HoldsFollowedCopy || shadow->Alias != nullptr || !LevelComplete(*shadow) || + shadow->Extent != extent) { return false; } const SizeT texel = LevelTexelBytes(*shadow); diff --git a/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp b/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp index 9e26de9c..8ada1530 100644 --- a/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp +++ b/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp @@ -454,6 +454,64 @@ TEST(StagedTextureStoreTest, AFollowedCopyMovesTheWindowAndLeavesTheSourceAlone) } } +// P13 W4b: THE ALIASING STORE FOLLOWS TOO, WITHOUT WRITING FRONTEND MEMORY. On monolith's record +// arm both levels are the frontend's own level shadows, held by address. The copy takes the +// destination into a server-owned copy and moves the window there; the frontend bytes it stood for +// stay exactly as they were, and a later client run lands only its boxes on the copy. +#if MOBILEGL_BUILD_DISAGGREGATED +TEST(StagedTextureStoreTest, AnAliasingStoreFollowsACopyIntoAServerOwnedDestination) { + const auto savedTransport = MG_Config::Transport; + const Bool savedArm = MG_Config::MonolithTakesRecordArm; + MG_Config::Transport = MG_Config::TransportMode::Monolith; + MG_Config::MonolithTakesRecordArm = true; + struct Restore { + MG_Config::TransportMode transport; + Bool arm; + ~Restore() { + MG_Config::Transport = transport; + MG_Config::MonolithTakesRecordArm = arm; + } + } restore{savedTransport, savedArm}; + Server::StagedTextureStore store(/*copies=*/false); + ASSERT_TRUE(store.Aliases()); + const Uint64 src = Server::StagedTextureStore::KeyForHandle(TestHandle(66, 1)); + const Uint64 dst = Server::StagedTextureStore::KeyForHandle(TestHandle(67, 1)); + const auto srcBytes = FollowPattern(0x00); + auto dstFrontend = FollowPattern(0x80); + const auto dstBefore = dstFrontend; + store.NoteLevelDefined(src, kTex2DTarget, 0, kFollowExtent); + store.Adopt(src, kTex2DTarget, 0, kFollowExtent, srcBytes.data(), srcBytes.size(), /*frontendShadow=*/true); + store.NoteLevelDefined(dst, kTex2DTarget, 0, kFollowExtent); + store.Adopt(dst, kTex2DTarget, 0, kFollowExtent, dstFrontend.data(), dstFrontend.size(), /*frontendShadow=*/true); + ASSERT_EQ(store.RequireLevelBytes(dst, kTex2DTarget, 0, "unit"), dstFrontend.data()) << "the level is aliased"; + + ASSERT_TRUE(store.FollowCopy(src, kTex2DTarget, 0, IntVec3{1, 1, 0}, dst, kTex2DTarget, 0, IntVec3{2, 0, 0}, + IntVec3{2, 3, 1})); + EXPECT_TRUE(store.LevelHoldsFollowedCopy(dst, kTex2DTarget, 0)); + EXPECT_EQ(dstFrontend, dstBefore) << "the copy wrote the frontend's level shadow"; + const Uint8* got = store.RequireLevelBytes(dst, kTex2DTarget, 0, "unit"); + ASSERT_NE(got, dstFrontend.data()) << "the destination is still aliased after a followed copy"; + for (Int y = 0; y < 4; ++y) { + for (Int x = 0; x < 4; ++x) { + const Bool copied = x >= 2 && y < 3; + for (Int c = 0; c < 4; ++c) { + const Uint8 want = copied ? srcBytes[At(x - 2 + 1, y + 1, c)] : dstBefore[At(x, y, c)]; + ASSERT_EQ(got[At(x, y, c)], want) << "destination texel (" << x << ", " << y << ")"; + } + } + } + + // The client then writes texel (0, 3) of its shadow: only that box lands on the copy. + dstFrontend[At(0, 3, 0)] = 0x11; + const Server::StagedTextureStore::ClientTexelBox box{0, 3, 0, 1, 1, 1}; + ASSERT_TRUE(store.AdoptClientBoxes(dst, kTex2DTarget, 0, kFollowExtent, 0, dstFrontend.data(), dstFrontend.size(), + &box, 1)); + got = store.RequireLevelBytes(dst, kTex2DTarget, 0, "unit"); + EXPECT_EQ(got[At(0, 3, 0)], 0x11); + EXPECT_EQ(got[At(2, 0, 0)], srcBytes[At(1, 1, 0)]) << "the client's box wiped the followed copy"; +} +#endif + // Every shape the store cannot address, and a source it can no longer vouch for, is REFUSED with // the destination untouched - the caller marks it driver-written instead. The monolith store // follows nothing at all.