[Fix] (P13 W4b): the aliasing texture store follows glCopyImageSubData into a server-owned destination.

This commit is contained in:
2026-10-06 09:29:11 -04:00
parent 4f5b4fa25d
commit cfd6c8730b
3 changed files with 81 additions and 9 deletions
+2 -1
View File
@@ -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
+21 -8
View File
@@ -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<std::mutex> 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<SizeT>(size.x()) * texel;
Vector<Uint8> rows(rowBytes * static_cast<SizeT>(size.y()) * static_cast<SizeT>(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<SizeT>(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<SizeT>(dst->DeclaredByteBound));
dst->Alias = nullptr;
}
if (!LevelComplete(*dst)) {
dst->Bytes.assign(static_cast<SizeT>(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<std::mutex> 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);
@@ -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.