[Fix] (Espryt, P8-E): a texture readback the driver refuses is answered from the server's staged store, which now follows glCopyImageSubData (FollowCopy, client-box merge, sticky driver-written mark, named decline); CopyImageStoreReadbackScenario on both backends x four arms plus the extent-scoped store-read knob; the dead CopyImageSubData+RENDERBUFFER latch goes (census 79 -> 78)

This commit is contained in:
2026-09-29 22:43:02 -04:00
parent 8d3e2317a6
commit f823422bfa
9 changed files with 1080 additions and 39 deletions
+44 -32
View File
@@ -13109,25 +13109,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// The renderbuffer twin of TextureImpl::SyncTextureObjectToBackend: the same
// find-or-create-then-sync the framebuffer attachment walk does (see SyncAttachmentObject),
// reachable from a path that has a renderbuffer but no framebuffer.
// reachable from a path that has a renderbuffer but no framebuffer. The MONOLITH arm's only:
// under a transport MakeGLESCopyImageEndpoint resolves a renderbuffer endpoint by handle and
// never calls this. (P8-E removed the `CopyImageSubData+RENDERBUFFER` latch that stood here -
// its transport condition could not hold at the only call site - and
// CopyImageStoreReadbackScenario's renderbuffer case runs the handle arm on every transport.)
static SharedPtr<RenderbufferImpl::BackendRenderbufferObject> SyncRenderbufferObjectToBackend(
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbufferObject) {
if (!renderbufferObject) return nullptr;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (fb, CONTRACT-P5E.md §4.4): the scope at this site is DELETED, and with it the
// arm that needed it. The only caller is the glCopyImageSubData endpoint builder, and a
// RENDERBUFFER endpoint is refused on the wire before it is emitted and again at the
// sink (PipeApplier.cpp's ServerUnmigratedVerbFatal("CopyImageSubData+RENDERBUFFER")),
// so under a transport nothing can reach here with one. Saying so by name is better
// than a find-or-MINT against the frontend address that the refusal already proved
// unreachable - if it ever becomes reachable, the name is where to start.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"CopyImageSubData+RENDERBUFFER\"} a renderbuffer "
"endpoint reached the backend under an active transport, where both the client "
"emitter and the sink refuse one");
std::abort();
}
#endif
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> backendRenderbufferObject;
if (auto* slot = RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) {
backendRenderbufferObject = *slot;
@@ -13248,19 +13237,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto* dstMipmap = MG_State::GLState::AsMipmapTexture(dstEndpoint.Texture.get());
if (!srcMipmap || !dstMipmap) return;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5b package i1's ONE backend edit, and the contract names this site
// (MG_Remote/CONTRACT-P5B.md §2 i1 "the copy-image-shadow-mirror emulation", ruling
// §6.7). Migrating glCopyImageSubData moves the first blocker off the client's
// Fatal{UnmigratedVerb} and onto the Fatal below, on every Espryt copy between two
// textures with CPU shadows - so the ruling is: UNDER A REAL TRANSPORT THE SERVER SKIPS
// THE MIRROR, and the client-side mirror ROADMAP P8 names ("CopyImage 镜像搬到 client")
// stays P8's.
//
// WHAT THE SKIP LOSES IS BOUNDED BY TWO FATALS, which is the whole reason it is allowed
// to be a skip rather than a port: a later glGetTexImage of the destination served from
// the shadow is class C wave 3 (Fatal{UnmigratedVerb, "GetTexImage"}, P9), and a texture
// re-mint under a transport reads the GPU level itself (RequireImageBindableStorageByHandle,
// P9 W2) rather than any shadow. Neither can silently read the un-mirrored shadow.
// UNDER A REAL TRANSPORT THE SERVER HAS NO FRONTEND SHADOW TO MIRROR INTO (P5b i1, ruling
// §6.7), and P8-E settled where the move lives instead - not on the client (ROADMAP P8's
// original row) but in the server's staged store: CopyImageSubData's handle arm makes the
// same texel-block move there (TextureImpl::FollowCopyImageInStagedStore) for EVERY
// format, not only the redundant-encoding ones this mirror serves, and marks the store
// driver-written where it cannot. The store is what answers a split glGetTexImage the
// driver refuses to read back (WireTextureReadback.inc's store route); every other split
// read of the destination reads the GPU level itself (notes/p8/E.md).
//
// BEHIND #if MOBILEGL_BUILD_DISAGGREGATED so the pull build's code does not move (G1),
// and the arm is the TRANSPORT and not the build - build-split runs its unit and
@@ -13347,9 +13331,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
#if MOBILEGL_BUILD_DISAGGREGATED
// The handle arm's endpoint names its texture by handle, and the server has no frontend
// object to note.
if (!dstEndpoint.IsRenderbuffer() && !MG_Pipe::MGPipeHandleIsNull(dstEndpoint.TextureHandle)) {
TextureImpl::NoteDriverSideTextureWriteByHandle(dstEndpoint.TextureHandle);
// object to note. The staged store's half of the note waits for the copy (P8-E, below).
const Bool handleArmDestination =
!dstEndpoint.IsRenderbuffer() && !MG_Pipe::MGPipeHandleIsNull(dstEndpoint.TextureHandle);
if (handleArmDestination) {
TextureImpl::NoteDriverSideTextureWriteByHandle(dstEndpoint.TextureHandle, /*storeFollowsTheWrite=*/true);
}
#endif
if (!MakeGLESCopyImageEndpoint(srcEndpoint, srcTarget, srcX, srcY, srcZ, src) ||
@@ -13357,6 +13343,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__);
return;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-E (notes/p8/E.md): THE SERVER'S STAGED STORE FOLLOWS THE COPY. Both endpoints are synced,
// so every byte the store holds for them is on the driver, and the copy below is a raw
// texel-block move - the store makes the same move, which keeps the destination level
// answerable from here when the driver will not read it back (ReadTextureImageWire's store
// route) and keeps a whole-level replay of it (a re-mint's refused readback, a full-level
// re-upload) from putting the pre-copy texels back. Where the store cannot follow (a
// renderbuffer, a cube or 1D-array endpoint, a source the driver has written, a level whose
// bytes did not all cross) it is marked driver-written instead, so it declines rather than
// answering with stale bytes. Nothing moves to the client (ROADMAP P8's original row).
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && handleArmDestination) {
const Bool followed =
!srcEndpoint.IsRenderbuffer() && !MG_Pipe::MGPipeHandleIsNull(srcEndpoint.TextureHandle) &&
TextureImpl::FollowCopyImageInStagedStore(
srcEndpoint.TextureHandle, static_cast<Uint32>(srcLevel), IntVec3{srcX, srcY, srcZ},
dstEndpoint.TextureHandle, static_cast<Uint32>(dstLevel), IntVec3{dstX, dstY, dstZ},
IntVec3{srcWidth, srcHeight, srcDepth});
if (!followed) TextureImpl::NoteDriverSideTextureWriteByHandle(dstEndpoint.TextureHandle);
}
#endif
// Verbatim: GL already spells a 1D array's extent the way the ES 2D array it maps onto
// wants it (height 1, layers on depth) - see MakeGLESCopyImageEndpoint.
@@ -13430,6 +13436,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_Util::ConvertGLEnumToString(dst.target).c_str(),
MG_Util::ConvertGLEnumToString(dstTarget).c_str());
MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request.");
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-E: the store already made the move the driver just refused.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && handleArmDestination) {
TextureImpl::NoteDriverSideTextureWriteByHandle(dstEndpoint.TextureHandle);
}
#endif
return;
}
// The copy landed on the GPU. For a destination whose readback cannot be bit-exact the
+75 -1
View File
@@ -3185,6 +3185,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Uint16 uploadTarget = MG_Pipe::MGPipeSubDataUploadTargetOf(record.Target);
const IntVec3 extent = MG_Remote::Server::StagedTextureUploadExtent(stored->Desc, record);
const SizeT runBytes = static_cast<SizeT>(record.Blob.Size);
// P8-E (notes/p8/E.md): THE LEVEL HOLDS A COPY THE CLIENT NEVER SAW
// (FollowCopyImageInStagedStore). The run is the client's shadow, stale everywhere it
// did not write, so only the texels it names land - its regions, or its union box
// when it carries none (D-D3's "the box is the whole story"). A run the store cannot
// place that way is adopted as ever, and the store stops standing for the texture.
if (store.LevelHoldsFollowedCopy(key, uploadTarget, record.Level) &&
(record.RegionCount == 0 || regions != nullptr)) {
Vector<MG_Remote::Server::StagedTextureStore::ClientTexelBox> boxes;
if (record.RegionCount == 0) {
const auto& box = record.UnionBox;
boxes.push_back({box.X, box.Y, box.Z, box.W, box.H, box.D});
} else {
for (Uint32 i = 0; i < record.RegionCount; ++i) {
const auto& region = regions[i];
boxes.push_back({region.X, region.Y, region.Z, region.W, region.H, region.D});
}
}
const Uint64 imageOffset = record.RegionCount == 0
? 0 : MG_Remote::Server::StagedTextureRunImageOffset(record, regions);
if (store.AdoptClientBoxes(key, uploadTarget, record.Level, extent, imageOffset, bytes, runBytes,
boxes.data(), boxes.size())) {
return;
}
store.MarkDriverWritten(key);
}
// RegionCount == 0 IS the whole-level spelling - "the run is the level shadow",
// beginning at the level's first byte - so it is adopted with the spelling that
// replaces the level; every other record names a RUN of the level by its own box.
@@ -10873,7 +10898,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return slot->get();
}
void NoteDriverSideTextureWriteByHandle(MG_Pipe::MGPipeHandle res) {
void NoteDriverSideTextureWriteByHandle(MG_Pipe::MGPipeHandle res, Bool storeFollowsTheWrite) {
// The storage owner's handle, one hop per view like the frontend walk above (the
// client composes a view of a view onto its root, so one hop is the real shape).
MG_Pipe::MGPipeHandle storage = res;
@@ -10883,6 +10908,55 @@ namespace MobileGL::MG_Backend::DirectGLES {
storage = record->Desc.ViewOf;
}
if (auto* twin = ResolveTextureTwin(storage)) twin->NoteDriverSideWrite();
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-E: the staged store stops standing for this texture's content (a no-op on a
// monolith arm, where the store does not copy).
if (!storeFollowsTheWrite && !MG_Pipe::MGPipeHandleIsNull(storage)) {
MG_Remote::Server::ServerStagedTexture().MarkDriverWritten(
MG_Remote::Server::StagedTextureStore::KeyForHandle(storage));
}
#else
(void)storeFollowsTheWrite;
#endif
}
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
Bool FollowCopyImageInStagedStore(MG_Pipe::MGPipeHandle source, Uint32 sourceLevel, const IntVec3& sourceOrigin,
MG_Pipe::MGPipeHandle destination, Uint32 destinationLevel,
const IntVec3& destinationOrigin, const IntVec3& size) {
auto& store = MG_Remote::Server::ServerStagedTexture();
if (!store.CopiesIntoServerStorage()) 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
// are byteless, so FollowCopy answers false for it by itself.
const auto storeTarget = [](MG_Pipe::MGPipeHandle handle, Uint16& target) {
const auto* record = PipeTextureRecordForHandle(handle);
if (record == nullptr ||
record->Desc.Target == static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex1DArray)) {
return false;
}
const auto& targets = BufferImpl::StagedUploadTargetsForPipeTarget(record->Desc.Target);
if (targets.size() != 1) return false;
target = static_cast<Uint16>(targets[0]);
return true;
};
Uint16 sourceTarget = 0, destinationTarget = 0;
if (!storeTarget(source, sourceTarget) || !storeTarget(destination, destinationTarget)) return false;
// ONE FORMAT BOTH ENDS. Copy-image moves the GL texel bits; the store holds each
// format's canonical shadow, which for most formats IS those bits and for some is not
// (the 16-bit packed formats are kept as UNorm8, RGB10 / RGB12 as UNorm16). Between two
// levels of one format the canonical move and the GL move agree whatever the format;
// across two compatible formats they need not, so that copy is not followed.
if (PipeTextureRecordForHandle(source)->Desc.InternalFormat !=
PipeTextureRecordForHandle(destination)->Desc.InternalFormat) {
return false;
}
return store.FollowCopy(MG_Remote::Server::StagedTextureStore::KeyForHandle(source), sourceTarget,
static_cast<Uint16>(sourceLevel), sourceOrigin,
MG_Remote::Server::StagedTextureStore::KeyForHandle(destination),
destinationTarget, static_cast<Uint16>(destinationLevel), destinationOrigin,
size);
}
#endif
} // namespace TextureImpl
+18 -1
View File
@@ -2321,7 +2321,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
// VerbCopyTexDst, a copy-image endpoint): the twin the handle's record names is marked, or
// its storage owner's (Desc.ViewOf) for a view. Nothing on the apply thread reads a
// frontend object for it.
void NoteDriverSideTextureWriteByHandle(MG_Pipe::MGPipeHandle texture);
//
// P8-E: under a transport the same storage owner's staged store is marked driver-written
// too (StagedTextureStore::MarkDriverWritten), so a refused readback does not answer from
// bytes the driver has since overwritten - unless `storeFollowsTheWrite`, which only
// glCopyImageSubData passes: it tries FollowCopyImageInStagedStore first and marks the store
// itself when that fails.
void NoteDriverSideTextureWriteByHandle(MG_Pipe::MGPipeHandle texture, Bool storeFollowsTheWrite = false);
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-E (notes/p8/E.md): make glCopyImageSubData's texel-block move in the server's staged
// store too (StagedTextureStore::FollowCopy), so the destination level's bytes stay its
// content. Box in each image's own texel grid. False when the store cannot follow: a cube
// map or 1D array endpoint (copy-image puts faces / layers on z, the store does not), a view,
// two different internal formats, a level whose bytes did not all cross, a source the
// driver has written, or a monolith arm.
Bool FollowCopyImageInStagedStore(MG_Pipe::MGPipeHandle source, Uint32 sourceLevel, const IntVec3& sourceOrigin,
MG_Pipe::MGPipeHandle destination, Uint32 destinationLevel,
const IntVec3& destinationOrigin, const IntVec3& size);
#endif
// Brings every texture the next draw reads - the touched units' bindings and the draw
// FBO's texture attachments - onto the backend, through the two borrowed-pair memos
@@ -320,6 +320,105 @@
return width == extent.x() && height == extent.y() && (!layered || depth == extent.z());
}
// P8-E (notes/p8/E.md). MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT=<w>x<h>, READ BY THE SERVER:
// a colour level of exactly that extent is treated as one the driver refused to attach - what an
// Adreno does for RGB16 and the 16-bit SNORM formats, and what no host driver can be made to do.
// Scoped by the extent so the tcp lane's shared supervisor can carry it for the whole lane: only
// the scenario that builds that extent (CopyImageStoreReadbackScenario) ever meets it.
static Bool WireTextureReadbackRefusedForTesting(const IntVec3& extent) {
static const IntVec3 s_refused = [] {
const char* value = std::getenv("MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT");
if (value == nullptr) return IntVec3{0, 0, 0};
char* end = nullptr;
const long width = std::strtol(value, &end, 10);
if (end == value || *end != 'x') return IntVec3{0, 0, 0};
const long height = std::strtol(end + 1, nullptr, 10);
if (width <= 0 || height <= 0) return IntVec3{0, 0, 0};
return IntVec3{static_cast<Int>(width), static_cast<Int>(height), 1};
}();
return s_refused.x() > 0 && extent == s_refused;
}
// P8-E: THE STORE ROUTE, for a level the driver refused to read back. The server's staged store
// is the only CPU copy of the level on this side, and it answers only while its bytes ARE the
// level: every byte crossed (IsCovered), at the extent asked for, and the texture was never
// written by the driver except by copies the store followed (!IsDriverWritten - see
// StagedTextureStore's P8-E block and FollowCopyImageInStagedStore). The conversion is
// GetTexImageViaShadowConversion's, into tight rows: a packed format read with its own type
// takes the stored words verbatim, everything else decodes to wide RGBA and converts row by row.
// Anything else DECLINES BY NAME: the reply is an error, and the client's
// Fatal{ReplyError, "ReadPixels"} ends the session instead of handing back bytes the driver has
// since overwritten.
static Bool ReadWireLevelFromStagedStore(MG_Pipe::MGPipeHandle res, TextureInternalFormat logicalFormat,
TextureUploadTarget uploadTarget, GLint level, const IntVec3& extent,
GLenum format, GLenum type, Vector<Uint8>& bytes) {
auto& store = MG_Remote::Server::ServerStagedTexture();
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(res);
const auto target = static_cast<Uint16>(uploadTarget);
const auto mip = static_cast<Uint16>(level);
ReadbackChannelMapping mapping{};
const SizeT pixelBytes = GetReadbackChannelMapping(format, mapping) ? GetReadbackDstPixelSize(mapping, type) : 0;
const SizeT texels = static_cast<SizeT>(std::max(extent.x(), 0)) * static_cast<SizeT>(std::max(extent.y(), 0)) *
static_cast<SizeT>(std::max(extent.z(), 0));
const char* declined = nullptr;
if (!store.CopiesIntoServerStorage()) {
declined = "this arm keeps no staged store";
} else if (store.IsDriverWritten(key)) {
declined = "the driver has written the texture since its bytes crossed";
} else if (!store.IsCovered(key, target, mip)) {
declined = "not every byte of the level crossed";
} else if (store.LevelExtentOrUndefined(key, target, mip) != extent) {
declined = "the staged level is not the extent asked for";
} else if (pixelBytes == 0 || texels == 0) {
declined = "the format/type pair has no readback mapping";
}
Vector<Uint8> out;
if (declined == nullptr) {
const Uint8* staged = store.RequireLevelBytes(key, target, mip, "texture-readback-from-store");
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(format),
MG_Util::ConvertGLEnumToTexturePixelDataType(type))) {
if (store.LevelByteSize(key, target, mip) != texels * pixelBytes) {
declined = "the staged words are not the client's word size";
} else {
out.assign(staged, staged + texels * pixelBytes);
}
} else {
Vector<Uint8> wide;
Bool isInteger = false;
Bool isSigned = false;
if (!MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA(logicalFormat, staged, texels, wide,
isInteger, isSigned)) {
declined = "the staged format has no wide decoding";
} else if (mapping.isInteger != isInteger) {
declined = "an integer read of a normalized level, or the reverse";
} else {
const GLenum wideType = isInteger ? (isSigned ? GL_INT : GL_UNSIGNED_INT) : GL_FLOAT;
const SizeT rowTexels = static_cast<SizeT>(extent.x());
const SizeT rows = texels / rowTexels;
const SizeT wideRowBytes = rowTexels * 4 * GetReadbackComponentSize(wideType);
out.resize(texels * pixelBytes);
for (SizeT row = 0; row < rows; ++row) {
ReadbackImpl::ConvertWideReadbackRow(wide.data() + row * wideRowBytes,
out.data() + row * rowTexels * pixelBytes, rowTexels,
wideType, mapping, type);
}
}
}
}
if (declined != nullptr) {
MGLOG_E_ONCE("MGPipe: texture {%u,%u} level %d: the driver refused the readback and the staged store "
"cannot answer it (%s) - DECLINED (texture-readback-store-declined)",
res.Slot, res.Gen, level, declined);
return false;
}
MGLOG_W_ONCE("MGPipe: texture {%u,%u} level %d: the driver refused the readback (internal format %u); "
"answered from the server's staged store (texture-readback-from-store)",
res.Slot, res.Gen, level, static_cast<unsigned>(logicalFormat));
bytes = Move(out);
return true;
}
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes) {
const auto* record = PipeTextureRecordForHandle(info.Res);
if (!record) return false;
@@ -333,15 +432,29 @@
const Bool wholeCube = info.Target == GL_TEXTURE_CUBE_MAP;
const IntVec3 extent{static_cast<Int>(info.Box.W), static_cast<Int>(info.Box.H),
wholeCube ? 1 : static_cast<Int>(info.Box.D)};
const auto logicalFormat = static_cast<TextureInternalFormat>(record->Desc.InternalFormat);
// The R32UI word leg never goes through a framebuffer, so the knob leaves it alone (it is
// ReadTextureLevelTight's first choice for exactly these reads).
const Bool packedWordRead =
!twin->RequiresImageBindableStorage() &&
MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)) &&
MG_Util::GetInputBytesPerPixel(MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)) == sizeof(Uint32);
const Bool refusedForTesting = !packedWordRead && WireTextureReadbackRefusedForTesting(extent);
Vector<Uint8> result;
for (Int face = 0; face < (wholeCube ? 6 : 1); ++face) {
const auto uploadTarget = wholeCube
? static_cast<TextureUploadTarget>(static_cast<Int>(TextureUploadTarget::CubeMapPositiveX) + face)
: MG_Util::ConvertGLEnumToTextureUploadTarget(info.Target);
Vector<Uint8> slice;
if (!ReadTextureLevelTight(twin->GetBackendTextureId(), target, uploadTarget,
static_cast<TextureInternalFormat>(record->Desc.InternalFormat), info.Level, extent,
twin->RequiresImageBindableStorage(), info.Format, info.Type, slice)) return false;
if (refusedForTesting ||
!ReadTextureLevelTight(twin->GetBackendTextureId(), target, uploadTarget, logicalFormat, info.Level,
extent, twin->RequiresImageBindableStorage(), info.Format, info.Type, slice)) {
if (!ReadWireLevelFromStagedStore(info.Res, logicalFormat, uploadTarget, info.Level, extent,
info.Format, info.Type, slice)) return false;
}
result.insert(result.end(), slice.begin(), slice.end());
}
bytes = Move(result);
@@ -137,6 +137,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/CopyImageLevelRangeScenario.cpp
Scenarios/CopyImageLayeredScenario.cpp
Scenarios/CopyImagePacked16Scenario.cpp
Scenarios/CopyImageStoreReadbackScenario.cpp
Scenarios/TextureViewScenario.cpp
# P3b/P4b wave 2-D package D2: the verification half of the owner-keyed emission cursor.
Scenarios/TextureViewAliasScenario.cpp
@@ -3225,6 +3226,72 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# case that produced no record RED for having nothing to say - which is the rule working, not
# a defect. They stay unregistered for that reason and are listed in the census note.
# P8-E (docs/Disaggregated/notes/p8/E.md): glCopyImageSubData into a level the driver will not
# read back, then glGetTexImage. The knob-free cases on Espryt's three arms; Magma's gated arms
# are the `DirectVulkan.<arm>.CopyStore.` entries just below; both monolith entries come from
# the whole-binary discovery.
mgl_itest_register_split_arms("CopyImageStoreReadbackScenario.*" "")
# The same cases with MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT pinned: the Espryt server
# treats a colour level of exactly that extent as one the driver refused to attach (what an
# Adreno does for RGB16 and 16-bit SNORM), so its staged store has to answer. READ BY THE
# SERVER, and it reaches all three arms: inproc sees this entry's environment, spawn copies it
# into the child, and the tcp supervisor carries it in its own fixture ENVIRONMENT (appended to
# TcpServer.Start here). Lane-wide there is harmless because the knob names an EXTENT and the
# scenario builds that extent only when it finds the knob in its own environment - every other
# tcp entry, this scenario's knob-free ones included, reads levels of other sizes. The
# renderbuffer case is not here: a renderbuffer's texels were never uploaded, so no store can
# answer for them, and a declined readback ends the session by design.
set(MGL_P8E_REFUSE_EXTENT "13x7")
set_property(TEST TcpServer.Start APPEND PROPERTY ENVIRONMENT
"MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT=${MGL_P8E_REFUSE_EXTENT}")
foreach(mglItestArm IN ITEMS Split Spawn Tcp)
string(TOUPPER "${mglItestArm}" mglItestArmUpper)
string(TOLOWER "${mglItestArm}" mglItestArmLower)
set(mglItestStoreReadProperties)
if(mglItestArm STREQUAL "Tcp")
set(mglItestStoreReadProperties FIXTURES_REQUIRED mobilegl-tcp RESOURCE_LOCK mobilegl-tcp)
endif()
foreach(storeReadCase AnUploadedLevelReadsBackExactly
ACopiedWindowReadsBackOverTheUpload
ACopiedWindowReadsBackFromAnImmutableLevel
APartialUploadAfterTheCopyKeepsTheCopiedWindow)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.${mglItestArm}.StoreRead."
TEST_LIST "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}"
TEST_FILTER "CopyImageStoreReadbackScenario.${storeReadCase}"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\\;integration-${mglItestArmLower}"
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_${mglItestArmUpper}_ENVIRONMENT}\;MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT=${MGL_P8E_REFUSE_EXTENT}"
${mglItestStoreReadProperties}
)
# Into the accumulated list, so SplitLogPaths gives each entry a private log: the
# store-route assertion reads the server's half of it.
list(APPEND MGL_SPLIT_ARM_TEST_LISTS "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}")
math(EXPR MGL_SPLIT_ARM_INDEX "${MGL_SPLIT_ARM_INDEX} + 1")
endforeach()
# Magma, the control backend, GATED on its three arms: it reads the VkImage back whatever
# the format, so it has no store route and no knob - only the copy and the read.
set(mglItestMagmaCopyStoreProperties)
if(mglItestArm STREQUAL "Tcp")
set(mglItestMagmaCopyStoreProperties FIXTURES_REQUIRED mobilegl-tcp RESOURCE_LOCK mobilegl-tcp)
endif()
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.${mglItestArm}.CopyStore."
TEST_LIST "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}"
TEST_FILTER "CopyImageStoreReadbackScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-magma-${mglItestArmLower}"
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_VULKAN_${mglItestArmUpper}_ENVIRONMENT}"
${mglItestMagmaCopyStoreProperties}
)
list(APPEND MGL_SPLIT_ARM_TEST_LISTS "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}")
math(EXPR MGL_SPLIT_ARM_INDEX "${MGL_SPLIT_ARM_INDEX} + 1")
endforeach()
# The split half of the counting pair. MGITEST_PERSISTENT_MAP_ARM=emulated is R-6: under split
# the adopt tier is pinned at T2, the resource owner declines every acquisition and the client
# pushes the mapping's dirty blocks - so pmap must be non-zero and mpr must be the monolith
@@ -0,0 +1,373 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImageStoreReadbackScenario.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - glCopyImageSubData INTO A LEVEL THE DRIVER WILL NOT READ BACK (P8-E,
// docs/Disaggregated/notes/p8/E.md).
//
// The formats are the ones an ES driver keeps as texture-only storage: GL_RGB16 (EXT_texture_norm16
// makes the three-channel spelling sampleable, never colour-renderable) and the 16-bit SNORM family
// (renderable only with EXT_render_snorm, which Adreno does not have). Their glGetTexImage cannot go
// through a framebuffer on such a driver, so something other than the GPU image has to answer it:
// the CPU shadow on the monolith arm, the server's staged store on a split one. A copy writes the
// GPU image and nothing else, which is exactly the write such a byte store never sees.
//
// GL_RGB9_E5 is the positive control: a 32-bit packed format, read through the scratch GL_R32UI
// image whoever wrote the level, so it must stay green under every configuration below.
//
// ARMS. Both backends on the monolith arm and on Split / Spawn / Tcp, every case, with no knob:
// on llvmpipe every one of these formats is attachable, so these runs pin the GPU route and the
// copy itself. Magma is the control backend - it reads the VkImage back whatever the format, so it
// has no store route to exercise.
//
// THE KNOB. MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT=<w>x<h> is read BY THE ESPRYT SERVER
// (WireTextureReadback.inc, ReadTextureImageWire): a colour level of exactly that extent is
// treated as a level the driver refused to attach, which is what an Adreno does for every one of
// these formats and what llvmpipe can be made to do no other way. The `DirectGLES.<arm>.StoreRead.`
// entries set it, and this scenario then builds its textures at THAT extent instead of the default
// one - so the knob is scoped by the extent, and the tcp lane's shared supervisor can carry it
// (the TcpServer.Start fixture's ENVIRONMENT) without touching any other entry, this scenario's
// own knob-free tcp entries included. Under the knob every read must still be right, and the
// server log must name the store route - otherwise the entry would be pinning the GPU route a
// second time under a name that claims otherwise.
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/PipeStatsWindow.h"
#include "../Harness/ScenarioFixture.h"
#include "../Harness/SplitRuntimePeek.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr const char* kRefuseExtentKnob = "MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT";
// What the Espryt server logs (once per process) when it answers a read from its store.
constexpr const char* kStoreReadMarker = "texture-readback-from-store";
// The knob-free extent. Deliberately not the extent the knob entries use, so the tcp
// supervisor - which carries the knob for the whole lane - leaves these textures alone.
constexpr int kDefaultWidth = 12;
constexpr int kDefaultHeight = 6;
struct CopyFormat {
GLenum internalFormat;
GLenum format;
GLenum type;
int components; // 16-bit components per texel; 0 for the one 32-bit packed format
bool isSigned;
const char* name;
};
const CopyFormat kFormats[] = {
{GL_RGB16, GL_RGB, GL_UNSIGNED_SHORT, 3, false, "rgb16"},
{GL_RGBA16_SNORM, GL_RGBA, GL_SHORT, 4, true, "rgba16_snorm"},
{GL_RGB16_SNORM, GL_RGB, GL_SHORT, 3, true, "rgb16_snorm"},
{GL_RGBA16, GL_RGBA, GL_UNSIGNED_SHORT, 4, false, "rgba16"},
{GL_RGB9_E5, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV, 0, false, "rgb9_e5"},
};
int TexelBytes(const CopyFormat& format) { return format.components == 0 ? 4 : format.components * 2; }
// Every word differs from its neighbours and from the other patterns, so a mis-addressed
// row, a swapped endpoint or a copy that never happened cannot cancel out. SNORM never
// gets -32768: it and -32767 are the same value (-1.0), and a read that goes through a
// normalized float is entitled to answer either.
std::vector<std::uint8_t> MakeTexels(const CopyFormat& format, int width, int height, std::uint32_t seed) {
const std::size_t texels = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
std::vector<std::uint8_t> bytes(texels * static_cast<std::size_t>(TexelBytes(format)));
if (format.components == 0) {
for (std::size_t i = 0; i < texels; ++i) {
std::uint32_t word = (static_cast<std::uint32_t>(i) + 1u) * 2654435761u ^ seed * 40503u;
word ^= word >> 13;
std::memcpy(bytes.data() + i * 4, &word, 4);
}
return bytes;
}
const std::size_t words = texels * static_cast<std::size_t>(format.components);
for (std::size_t i = 0; i < words; ++i) {
std::uint32_t mix = (static_cast<std::uint32_t>(i) + 1u) * 2654435761u ^ seed * 40503u;
mix ^= mix >> 15;
std::uint16_t value = static_cast<std::uint16_t>(mix >> 8);
if (format.isSigned && value == 0x8000u) value = 0x8001u;
std::memcpy(bytes.data() + i * 2, &value, 2);
}
return bytes;
}
// `image` with the w x h block at (dx, dy) replaced by the block of `source` at (sx, sy).
std::vector<std::uint8_t> WithBlock(std::vector<std::uint8_t> image, int width, const std::vector<std::uint8_t>& source,
int sourceWidth, int sx, int sy, int dx, int dy, int w, int h,
int texelBytes) {
for (int row = 0; row < h; ++row) {
std::memcpy(image.data() + (static_cast<std::size_t>(dy + row) * width + dx) * texelBytes,
source.data() + (static_cast<std::size_t>(sy + row) * sourceWidth + sx) * texelBytes,
static_cast<std::size_t>(w) * texelBytes);
}
return image;
}
class CopyImageStoreReadbackScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_wire = SplitRuntimeSkipReason().empty();
m_espryt = Gl().BackendName() == "DirectGLES";
if (const char* knob = std::getenv(kRefuseExtentKnob)) {
int width = 0, height = 0;
ASSERT_EQ(std::sscanf(knob, "%dx%d", &width, &height), 2)
<< kRefuseExtentKnob << "=" << knob << " is not <w>x<h>";
ASSERT_GE(width, 12) << "the copy windows below need at least a 12x6 level";
ASSERT_GE(height, 6) << "the copy windows below need at least a 12x6 level";
m_width = width;
m_height = height;
m_forced = true;
}
// 16-bit RGB rows are 6 bytes a texel: the default 4-byte alignment would pad every
// row of an odd-width level and shear both the upload and the read.
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
ASSERT_EQ(FirstGLError(), 0u);
}
void TearDown() override {
if (!Ready()) return;
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glBindTexture(GL_TEXTURE_2D, 0);
if (!m_textures.empty()) {
glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
}
if (m_renderbuffer != 0) glDeleteRenderbuffers(1, &m_renderbuffer);
if (m_fbo != 0) {
BindDefaultFramebuffer();
glDeleteFramebuffers(1, &m_fbo);
}
FirstGLError();
}
GLuint MakeTexture(const CopyFormat& format, const std::vector<std::uint8_t>& texels, bool immutable) {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
if (immutable) {
glTexStorage2D(GL_TEXTURE_2D, 1, format.internalFormat, m_width, m_height);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_width, m_height, format.format, format.type,
texels.data());
} else {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(format.internalFormat), m_width, m_height, 0,
format.format, format.type, texels.data());
}
glBindTexture(GL_TEXTURE_2D, 0);
return texture;
}
std::vector<std::uint8_t> ReadTexImage(GLuint texture, const CopyFormat& format) {
std::vector<std::uint8_t> bytes(
static_cast<std::size_t>(m_width) * m_height * static_cast<std::size_t>(TexelBytes(format)), 0xA5);
glBindTexture(GL_TEXTURE_2D, texture);
glGetTexImage(GL_TEXTURE_2D, 0, format.format, format.type, bytes.data());
glBindTexture(GL_TEXTURE_2D, 0);
return bytes;
}
// The first texel that differs names the defect (which half of the copy, which word);
// the rest would only bury it.
void ExpectImage(const std::vector<std::uint8_t>& got, const std::vector<std::uint8_t>& want,
const CopyFormat& format, const std::string& what) {
ASSERT_EQ(got.size(), want.size());
const int texelBytes = TexelBytes(format);
for (std::size_t at = 0; at < got.size(); ++at) {
if (got[at] == want[at]) continue;
const std::size_t texel = at / static_cast<std::size_t>(texelBytes);
const int x = static_cast<int>(texel % static_cast<std::size_t>(m_width));
const int y = static_cast<int>(texel / static_cast<std::size_t>(m_width));
std::string gotHex, wantHex;
char buffer[4];
for (int b = 0; b < texelBytes; ++b) {
std::snprintf(buffer, sizeof(buffer), "%02x", got[texel * texelBytes + b]);
gotHex += buffer;
std::snprintf(buffer, sizeof(buffer), "%02x", want[texel * texelBytes + b]);
wantHex += buffer;
}
ADD_FAILURE() << what << " (" << format.name << ", " << Gl().BackendName() << ", "
<< (m_wire ? "split" : "monolith") << (m_forced ? ", store read forced" : "")
<< "): texel (" << x << ", " << y << ") reads " << gotHex << ", want " << wantHex;
return;
}
}
// Under the knob the Espryt server must have answered from its store at least once;
// otherwise the entry proves nothing about that route.
void ExpectStoreReadNamedSince(const PipeStatsWindow::LogMark& mark) {
if (!m_forced || !m_espryt || !m_wire) return;
const std::string log = PipeStatsWindow::ReadServerLogSince(mark);
EXPECT_NE(log.find(kStoreReadMarker), std::string::npos)
<< kRefuseExtentKnob << " is set but the server log carries no '" << kStoreReadMarker
<< "' line: the knob did not reach the server, or no read took the store route";
}
// The copy window: 5x3 texels from (1, 1) of the source to (6, 2) of the destination,
// inside a 12x6 level and away from the partial upload's (0, 0) 3x2 corner.
static constexpr int kSrcX = 1, kSrcY = 1, kDstX = 6, kDstY = 2, kCopyW = 5, kCopyH = 3;
void CopyWindow(GLuint source, GLuint destination) {
glCopyImageSubData(source, GL_TEXTURE_2D, 0, kSrcX, kSrcY, 0, destination, GL_TEXTURE_2D, 0, kDstX,
kDstY, 0, kCopyW, kCopyH, 1);
}
bool m_wire = false;
bool m_espryt = false;
bool m_forced = false;
int m_width = kDefaultWidth;
int m_height = kDefaultHeight;
std::vector<GLuint> m_textures;
GLuint m_renderbuffer = 0;
GLuint m_fbo = 0;
};
// The control: an uploaded level reads back as uploaded.
TEST_F(CopyImageStoreReadbackScenario, AnUploadedLevelReadsBackExactly) {
if (!Ready()) return;
const auto mark = PipeStatsWindow::MarkLaneLog();
for (const CopyFormat& format : kFormats) {
const auto texels = MakeTexels(format, m_width, m_height, 11);
const GLuint texture = MakeTexture(format, texels, /*immutable=*/false);
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": setup raised a GL error";
ExpectImage(ReadTexImage(texture, format), texels, format, "the uploaded level");
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": glGetTexImage raised a GL error";
}
ExpectStoreReadNamedSince(mark);
}
// THE CASE THE PACKAGE IS ABOUT: the copy lands in the destination's GPU image only, and the
// read has to see it - whatever answers the read.
TEST_F(CopyImageStoreReadbackScenario, ACopiedWindowReadsBackOverTheUpload) {
if (!Ready()) return;
const auto mark = PipeStatsWindow::MarkLaneLog();
for (const CopyFormat& format : kFormats) {
const auto sourceTexels = MakeTexels(format, m_width, m_height, 23);
const auto destinationTexels = MakeTexels(format, m_width, m_height, 37);
const GLuint source = MakeTexture(format, sourceTexels, /*immutable=*/false);
const GLuint destination = MakeTexture(format, destinationTexels, /*immutable=*/false);
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": setup raised a GL error";
CopyWindow(source, destination);
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": glCopyImageSubData raised a GL error";
const auto want = WithBlock(destinationTexels, m_width, sourceTexels, m_width, kSrcX, kSrcY, kDstX,
kDstY, kCopyW, kCopyH, TexelBytes(format));
ExpectImage(ReadTexImage(destination, format), want, format, "the copy destination");
ExpectImage(ReadTexImage(source, format), sourceTexels, format, "the copy source");
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": glGetTexImage raised a GL error";
}
ExpectStoreReadNamedSince(mark);
}
// The same into immutable storage (glTexStorage2D, then the whole level uploaded).
TEST_F(CopyImageStoreReadbackScenario, ACopiedWindowReadsBackFromAnImmutableLevel) {
if (!Ready()) return;
const auto mark = PipeStatsWindow::MarkLaneLog();
for (const CopyFormat& format : kFormats) {
const auto sourceTexels = MakeTexels(format, m_width, m_height, 41);
const auto destinationTexels = MakeTexels(format, m_width, m_height, 43);
const GLuint source = MakeTexture(format, sourceTexels, /*immutable=*/true);
const GLuint destination = MakeTexture(format, destinationTexels, /*immutable=*/true);
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": setup raised a GL error";
CopyWindow(source, destination);
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": glCopyImageSubData raised a GL error";
const auto want = WithBlock(destinationTexels, m_width, sourceTexels, m_width, kSrcX, kSrcY, kDstX,
kDstY, kCopyW, kCopyH, TexelBytes(format));
ExpectImage(ReadTexImage(destination, format), want, format, "the immutable copy destination");
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": glGetTexImage raised a GL error";
}
ExpectStoreReadNamedSince(mark);
}
// A copy, then an ordinary partial upload into the same level elsewhere. The upload is the
// client's; the copied window is not, and a byte store that took the upload as "the level"
// would lose it again.
TEST_F(CopyImageStoreReadbackScenario, APartialUploadAfterTheCopyKeepsTheCopiedWindow) {
if (!Ready()) return;
const auto mark = PipeStatsWindow::MarkLaneLog();
for (const CopyFormat& format : kFormats) {
const auto sourceTexels = MakeTexels(format, m_width, m_height, 53);
const auto destinationTexels = MakeTexels(format, m_width, m_height, 59);
const auto patch = MakeTexels(format, 3, 2, 61);
const GLuint source = MakeTexture(format, sourceTexels, /*immutable=*/false);
const GLuint destination = MakeTexture(format, destinationTexels, /*immutable=*/false);
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": setup raised a GL error";
CopyWindow(source, destination);
glBindTexture(GL_TEXTURE_2D, destination);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 3, 2, format.format, format.type, patch.data());
glBindTexture(GL_TEXTURE_2D, 0);
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": the copy or the upload raised a GL error";
auto want = WithBlock(destinationTexels, m_width, sourceTexels, m_width, kSrcX, kSrcY, kDstX, kDstY,
kCopyW, kCopyH, TexelBytes(format));
want = WithBlock(want, m_width, patch, 3, 0, 0, 0, 0, 3, 2, TexelBytes(format));
ExpectImage(ReadTexImage(destination, format), want, format, "the copy destination after the upload");
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": glGetTexImage raised a GL error";
}
ExpectStoreReadNamedSince(mark);
}
// A RENDERBUFFER endpoint. The split arm resolves it by handle (DirectGLES.cpp,
// MakeGLESCopyImageEndpoint), which is what retired the "CopyImageSubData+RENDERBUFFER"
// latch; this is the case that says so on every arm. It has no store to answer from - a
// renderbuffer's texels were never uploaded - so the knob entries do not run it.
TEST_F(CopyImageStoreReadbackScenario, ARenderbufferSourceLandsInTheTexture) {
if (!Ready()) return;
const CopyFormat format{GL_RGBA16, GL_RGBA, GL_UNSIGNED_SHORT, 4, false, "rgba16"};
const auto destinationTexels = MakeTexels(format, m_width, m_height, 71);
const GLuint destination = MakeTexture(format, destinationTexels, /*immutable=*/false);
glGenRenderbuffers(1, &m_renderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA16, m_width, m_height);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
<< "the RGBA16 renderbuffer is not attachable";
// k / 65535 for four 16-bit words: exact through a float clear and a UNORM16 store.
const std::uint16_t words[4] = {0x1234, 0x5678, 0x9abc, 0xffff};
const GLfloat color[4] = {words[0] / 65535.0f, words[1] / 65535.0f, words[2] / 65535.0f,
words[3] / 65535.0f};
glDisable(GL_SCISSOR_TEST);
glClearBufferfv(GL_COLOR, 0, color);
BindDefaultFramebuffer();
ASSERT_EQ(FirstGLError(), 0u) << "setup raised a GL error";
glCopyImageSubData(m_renderbuffer, GL_RENDERBUFFER, 0, kSrcX, kSrcY, 0, destination, GL_TEXTURE_2D, 0,
kDstX, kDstY, 0, kCopyW, kCopyH, 1);
ASSERT_EQ(FirstGLError(), 0u) << "glCopyImageSubData from the renderbuffer raised a GL error";
std::vector<std::uint8_t> cleared(static_cast<std::size_t>(m_width) * m_height * 8);
for (std::size_t texel = 0; texel < cleared.size() / 8; ++texel) {
std::memcpy(cleared.data() + texel * 8, words, 8);
}
const auto want = WithBlock(destinationTexels, m_width, cleared, m_width, kSrcX, kSrcY, kDstX, kDstY,
kCopyW, kCopyH, 8);
ExpectImage(ReadTexImage(destination, format), want, format, "the texture a renderbuffer was copied into");
ASSERT_EQ(FirstGLError(), 0u) << "glGetTexImage raised a GL error";
}
} // namespace
} // namespace MGITest
@@ -350,6 +350,12 @@ namespace MobileGL::MG_Remote::Server {
shadow.Covered.clear();
}
#endif
if (shadow.HoldsFollowedCopy) {
// P8-E: a run landed whole over a followed copy (AdoptClientBoxes is the spelling
// that keeps it), so the bytes no longer hold what the driver does.
shadow.HoldsFollowedCopy = false;
m_driverWritten[key] = true;
}
shadow.GpuDirty = false;
return CopyRunInto(shadow, key, uploadTarget, level, imageOffset, bytes, byteSize);
}
@@ -367,6 +373,8 @@ namespace MobileGL::MG_Remote::Server {
LevelShadow& shadow = m_shadows[key].Levels[PackLevel(uploadTarget, level)];
RequireDeclaredLevel(shadow, key, uploadTarget, level, extent);
shadow.GpuDirty = false;
// P8-E: the caller states these bytes ARE the level, which a followed copy is part of.
shadow.HoldsFollowedCopy = false;
shadow.Bytes.clear();
shadow.Covered.clear();
return CopyRunInto(shadow, key, uploadTarget, level, 0, bytes, byteSize);
@@ -424,6 +432,7 @@ namespace MobileGL::MG_Remote::Server {
// the bytes it described.
shadow.Covered.clear();
shadow.GpuDirty = false;
shadow.HoldsFollowedCopy = false;
}
shadow.Extent = extent;
shadow.InternalFormat = internalFormat;
@@ -457,12 +466,155 @@ namespace MobileGL::MG_Remote::Server {
if (!m_any.load(std::memory_order_acquire)) return;
const std::lock_guard<std::mutex> lock(m_mutex);
m_shadows.erase(key);
m_driverWritten.erase(key);
}
void DropAll() {
if (!m_any.load(std::memory_order_acquire)) return;
const std::lock_guard<std::mutex> lock(m_mutex);
m_shadows.clear();
m_driverWritten.clear();
}
// ---- P8-E (docs/Disaggregated/notes/p8/E.md): WHEN THESE BYTES ARE THE LEVEL'S CONTENT ----
//
// The store holds what the client uploaded. It is also the only CPU copy of a level the
// server has, so a readback the driver refuses (a format the ES driver will not attach -
// RGB16 and 16-bit SNORM on Adreno) can be answered from it - but only while the driver
// holds nothing the store has not seen. Two facts decide that:
//
// * THE DRIVER-WRITTEN MARK, per texture and sticky: a framebuffer attachment, a writable
// image unit, a glCopyTexSubImage, a generated mip chain, a copy this store could not
// follow. The writers know the texture, not the level, and an attachment keeps writing
// for as long as it stays attached, so only the key's death (Drop / DropAll) clears it -
// a whole-resource respecify (ResetLevels) does not.
// * A COPY THE STORE FOLLOWED (FollowCopy): glCopyImageSubData between two levels whose
// bytes this store holds is a raw texel-block move, so the store makes the same move and
// the destination stays readable from here. Its level is flagged, because the client's
// next run for it is the CLIENT's shadow, which never saw the copy: AdoptClientBoxes
// lands only the texels that run says the client wrote.
void MarkDriverWritten(Uint64 key) {
if (!m_copies) return;
const std::lock_guard<std::mutex> lock(m_mutex);
m_driverWritten[key] = true;
m_any.store(true, std::memory_order_release);
}
Bool IsDriverWritten(Uint64 key) const {
if (!m_any.load(std::memory_order_acquire)) return false;
const std::lock_guard<std::mutex> lock(m_mutex);
return m_driverWritten.find(key) != m_driverWritten.end();
}
Bool LevelHoldsFollowedCopy(Uint64 key, Uint16 uploadTarget, Uint16 level) const {
if (!m_any.load(std::memory_order_acquire)) return false;
const std::lock_guard<std::mutex> lock(m_mutex);
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
return shadow != nullptr && shadow->HoldsFollowedCopy;
}
// The copy, in the level images' own texel grid (x, y, z = texel, row, slice/layer). True
// when the destination's bytes now hold it. False - and neither level touched - unless the
// source level is complete and its texture was never written by the driver, the destination
// level is complete (or the copy covers all of it), both levels' stored texels are one size,
// and the box lies inside both. The caller marks the destination driver-written on false.
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;
const std::lock_guard<std::mutex> lock(m_mutex);
if (m_driverWritten.find(srcKey) != m_driverWritten.end()) return false;
const LevelShadow* src = FindLevel(srcKey, srcTarget, srcLevel);
LevelShadow* dst = FindLevelMutable(dstKey, dstTarget, dstLevel);
if (src == nullptr || dst == nullptr || !LevelComplete(*src)) return false;
const SizeT texel = LevelTexelBytes(*src);
if (texel == 0 || !dst->Defined || dst->DeclaredByteBound == 0 || LevelTexelBytes(*dst) != texel) return false;
if (!BoxInside(srcOrigin, size, src->Extent) || !BoxInside(dstOrigin, size, dst->Extent)) return false;
const Bool wholeDestination = dstOrigin == IntVec3{0, 0, 0} && size == dst->Extent;
if (!LevelComplete(*dst) && !wholeDestination) return false;
// 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()));
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),
rowBytes);
}
}
if (!LevelComplete(*dst)) {
dst->Bytes.assign(static_cast<SizeT>(dst->DeclaredByteBound), Uint8{0});
dst->Covered.clear();
StagedShadowStore::CoverageAdd(dst->Covered, 0, static_cast<SizeT>(dst->DeclaredByteBound));
}
for (Int z = 0; z < size.z(); ++z) {
for (Int y = 0; y < size.y(); ++y) {
std::memcpy(dst->Bytes.data() + TexelOffset(dst->Extent, dstOrigin.x(), dstOrigin.y() + y,
dstOrigin.z() + z, texel),
rows.data() + (static_cast<SizeT>(z) * size.y() + y) * rowBytes, rowBytes);
}
}
dst->HoldsFollowedCopy = true;
m_any.store(true, std::memory_order_release);
return true;
}
// One box of texels, in the level image's grid, that a client record says it wrote.
struct ClientTexelBox {
Int32 X, Y, Z;
Uint32 W, H, D;
};
// A client run for a level that holds a followed copy. The run is the client's level shadow
// (or one slab of it) and is stale everywhere the client did not write, so only `boxes` land,
// each texel read from where it sits in the run (the run begins at `imageOffset` of the
// level image) - the rest of the level keeps the copy. False, touching nothing, when a box
// is outside the level or a row of it is outside the run: the caller then adopts the run as
// usual and marks the texture driver-written.
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;
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) {
return false;
}
const SizeT texel = LevelTexelBytes(*shadow);
if (texel == 0) return false;
const auto rowInRun = [&](const ClientTexelBox& box, Uint32 row, Uint32 slice, SizeT& offset) {
offset = TexelOffset(shadow->Extent, box.X, box.Y + static_cast<Int>(row), box.Z + static_cast<Int>(slice),
texel);
const Uint64 begin = static_cast<Uint64>(offset);
const Uint64 end = begin + static_cast<Uint64>(box.W) * texel;
return begin >= imageOffset && end <= imageOffset + byteSize;
};
for (SizeT i = 0; i < boxCount; ++i) {
const ClientTexelBox& box = boxes[i];
if (!BoxInside(IntVec3{box.X, box.Y, box.Z},
IntVec3{static_cast<Int>(box.W), static_cast<Int>(box.H), static_cast<Int>(box.D)},
shadow->Extent)) {
return false;
}
for (Uint32 slice = 0; slice < box.D; ++slice) {
for (Uint32 row = 0; row < box.H; ++row) {
SizeT offset = 0;
if (!rowInRun(box, row, slice, offset)) return false;
}
}
}
const auto* run = static_cast<const Uint8*>(bytes);
for (SizeT i = 0; i < boxCount; ++i) {
const ClientTexelBox& box = boxes[i];
for (Uint32 slice = 0; slice < box.D; ++slice) {
for (Uint32 row = 0; row < box.H; ++row) {
SizeT offset = 0;
rowInRun(box, row, slice, offset);
std::memcpy(shadow->Bytes.data() + offset, run + (offset - static_cast<SizeT>(imageOffset)),
static_cast<SizeT>(box.W) * texel);
}
}
}
return true;
}
// Fatal when a sync wants texels this store has no covered run for. This is THE data
@@ -589,6 +741,8 @@ namespace MobileGL::MG_Remote::Server {
Uint64 DeclaredByteBound = 0;
Bool Defined = false;
Bool GpuDirty = false;
// P8-E: FollowCopy wrote server-side texels into Bytes that the client's shadow lacks.
Bool HoldsFollowedCopy = false;
};
struct TextureShadow {
ska::flat_hash_map<Uint32, LevelShadow> Levels;
@@ -684,11 +838,51 @@ namespace MobileGL::MG_Remote::Server {
const auto levelIt = textureIt->second.Levels.find(PackLevel(uploadTarget, level));
return levelIt == textureIt->second.Levels.end() ? nullptr : &levelIt->second;
}
LevelShadow* FindLevelMutable(Uint64 key, Uint16 uploadTarget, Uint16 level) {
const auto textureIt = m_shadows.find(key);
if (textureIt == m_shadows.end()) return nullptr;
const auto levelIt = textureIt->second.Levels.find(PackLevel(uploadTarget, level));
return levelIt == textureIt->second.Levels.end() ? nullptr : &levelIt->second;
}
// P8-E helpers, with the lock held. A level is COMPLETE when every declared byte crossed
// (IsCovered's test); its texel size is the declared bound over the texel count, and a bound
// that does not divide evenly is a level this cannot address texel by texel.
static Bool LevelComplete(const LevelShadow& shadow) {
return shadow.Defined && shadow.DeclaredByteBound != 0 &&
shadow.Bytes.size() == shadow.DeclaredByteBound &&
StagedShadowStore::CoverageHas(shadow.Covered, 0, static_cast<SizeT>(shadow.DeclaredByteBound));
}
static SizeT LevelTexelBytes(const LevelShadow& shadow) {
if (shadow.Extent.x() <= 0 || shadow.Extent.y() <= 0 || shadow.Extent.z() <= 0) return 0;
const Uint64 texels = static_cast<Uint64>(shadow.Extent.x()) * static_cast<Uint64>(shadow.Extent.y()) *
static_cast<Uint64>(shadow.Extent.z());
if (shadow.DeclaredByteBound == 0 || shadow.DeclaredByteBound % texels != 0) return 0;
return static_cast<SizeT>(shadow.DeclaredByteBound / texels);
}
static Bool BoxInside(const IntVec3& origin, const IntVec3& size, const IntVec3& extent) {
for (Int axis = 0; axis < 3; ++axis) {
if (origin[axis] < 0 || size[axis] <= 0 ||
static_cast<Int64>(origin[axis]) + size[axis] > static_cast<Int64>(extent[axis])) {
return false;
}
}
return true;
}
static SizeT TexelOffset(const IntVec3& extent, Int x, Int y, Int z, SizeT texel) {
return ((static_cast<SizeT>(z) * static_cast<SizeT>(extent.y()) + static_cast<SizeT>(y)) *
static_cast<SizeT>(extent.x()) +
static_cast<SizeT>(x)) *
texel;
}
const Bool m_copies;
mutable std::mutex m_mutex;
StagedTextureDeviceLimits m_deviceLimits{};
ska::flat_hash_map<Uint64, TextureShadow> m_shadows;
// P8-E: MarkDriverWritten's keys (the value is always true). Apart from m_shadows on purpose,
// because ResetLevels erases a key's levels and must not forget this.
ska::flat_hash_map<Uint64, Bool> m_driverWritten;
// 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.
std::atomic<Bool> m_any{false};
@@ -302,6 +302,197 @@ TEST(StagedTextureStoreTest, TheGpuDirtyMarkIsTheServersOwnDirtyAnswer) {
EXPECT_EQ(monolithStore.TrackedResources(), 0u);
}
// ---- P8-E (docs/Disaggregated/notes/p8/E.md): the store follows glCopyImageSubData -----------
//
// Two 4x4 RGBA8 levels (four bytes a texel), each adopted whole with a distinct byte pattern so a
// mis-addressed row or a swapped endpoint shows as a wrong byte, not a coincidentally right one.
namespace {
const IntVec3 kFollowExtent{4, 4, 1};
Vector<Uint8> FollowPattern(Uint8 base) {
Vector<Uint8> bytes(64);
for (SizeT i = 0; i < bytes.size(); ++i) bytes[i] = static_cast<Uint8>(base + i);
return bytes;
}
void DefineAndAdopt(Server::StagedTextureStore& store, Uint64 key, const Vector<Uint8>& bytes) {
store.NoteLevelDefined(key, kTex2DTarget, 0, kFollowExtent);
store.Adopt(key, kTex2DTarget, 0, kFollowExtent, bytes.data(), bytes.size());
}
// The byte (x, y, channel) of a 4x4 RGBA8 level image.
SizeT At(Int x, Int y, Int c) { return static_cast<SizeT>((y * 4 + x) * 4 + c); }
} // namespace
// THE MOVE: the 2x3 window at (1, 1) of the source lands at (2, 0) of the destination, every other
// destination byte and every source byte stays, and only the destination is flagged as holding a
// copy the client never saw.
TEST(StagedTextureStoreTest, AFollowedCopyMovesTheWindowAndLeavesTheSourceAlone) {
Server::StagedTextureStore store(/*copies=*/true);
const Uint64 src = Server::StagedTextureStore::KeyForHandle(TestHandle(60, 1));
const Uint64 dst = Server::StagedTextureStore::KeyForHandle(TestHandle(61, 1));
const auto srcBytes = FollowPattern(0x00);
const auto dstBytes = FollowPattern(0x80);
DefineAndAdopt(store, src, srcBytes);
DefineAndAdopt(store, dst, dstBytes);
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_FALSE(store.LevelHoldsFollowedCopy(src, kTex2DTarget, 0));
const Uint8* got = store.RequireLevelBytes(dst, kTex2DTarget, 0, "unit");
const Uint8* source = store.RequireLevelBytes(src, kTex2DTarget, 0, "unit");
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)] : dstBytes[At(x, y, c)];
ASSERT_EQ(got[At(x, y, c)], want) << "destination texel (" << x << ", " << y << ")";
ASSERT_EQ(source[At(x, y, c)], srcBytes[At(x, y, c)]) << "the copy moved a SOURCE byte";
}
}
}
}
// 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.
TEST(StagedTextureStoreTest, ACopyTheStoreCannotVouchForIsNotFollowed) {
Server::StagedTextureStore store(/*copies=*/true);
const Uint64 src = Server::StagedTextureStore::KeyForHandle(TestHandle(62, 1));
const Uint64 dst = Server::StagedTextureStore::KeyForHandle(TestHandle(63, 1));
const Uint64 narrow = Server::StagedTextureStore::KeyForHandle(TestHandle(64, 1));
const Uint64 bare = Server::StagedTextureStore::KeyForHandle(TestHandle(65, 1));
const auto dstBytes = FollowPattern(0x80);
DefineAndAdopt(store, src, FollowPattern(0x00));
DefineAndAdopt(store, dst, dstBytes);
store.NoteLevelDefined(narrow, kTex2DTarget, 0, IntVec3{8, 8, 1},
static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D),
static_cast<Uint32>(TextureInternalFormat::R8)); // 64 bytes, one a texel
Vector<Uint8> narrowBytes(64, 0x11);
store.Adopt(narrow, kTex2DTarget, 0, IntVec3{8, 8, 1}, narrowBytes.data(), narrowBytes.size());
store.NoteLevelDefined(bare, kTex2DTarget, 0, kFollowExtent); // defined, no bytes crossed
const IntVec3 origin{0, 0, 0};
EXPECT_FALSE(store.FollowCopy(src, kTex2DTarget, 0, IntVec3{3, 0, 0}, dst, kTex2DTarget, 0, origin,
IntVec3{2, 1, 1}))
<< "the box runs off the source's right edge";
EXPECT_FALSE(store.FollowCopy(narrow, kTex2DTarget, 0, origin, dst, kTex2DTarget, 0, origin, IntVec3{1, 1, 1}))
<< "a one-byte texel moved into a four-byte one";
EXPECT_FALSE(store.FollowCopy(src, kTex2DTarget, 0, origin, bare, kTex2DTarget, 0, origin, IntVec3{2, 2, 1}))
<< "a partial copy into a level none of whose bytes crossed";
EXPECT_FALSE(store.FollowCopy(bare, kTex2DTarget, 0, origin, dst, kTex2DTarget, 0, origin, IntVec3{1, 1, 1}))
<< "a source none of whose bytes crossed";
store.MarkDriverWritten(src);
EXPECT_FALSE(store.FollowCopy(src, kTex2DTarget, 0, origin, dst, kTex2DTarget, 0, origin, IntVec3{1, 1, 1}))
<< "the driver wrote the source behind the store's back, so its bytes are not the texels copied";
EXPECT_FALSE(store.LevelHoldsFollowedCopy(dst, kTex2DTarget, 0));
const Uint8* got = store.RequireLevelBytes(dst, kTex2DTarget, 0, "unit");
for (SizeT i = 0; i < dstBytes.size(); ++i) ASSERT_EQ(got[i], dstBytes[i]) << "a refused copy moved byte " << i;
Server::StagedTextureStore monolithStore(/*copies=*/false);
EXPECT_FALSE(monolithStore.FollowCopy(src, kTex2DTarget, 0, origin, dst, kTex2DTarget, 0, origin,
IntVec3{1, 1, 1}));
}
// A copy that covers ALL of a destination level whose bytes never crossed (glTexStorage, no upload)
// makes that level's content known: the store completes it.
TEST(StagedTextureStoreTest, AWholeLevelCopyCompletesADestinationThatHadNoBytes) {
Server::StagedTextureStore store(/*copies=*/true);
const Uint64 src = Server::StagedTextureStore::KeyForHandle(TestHandle(66, 1));
const Uint64 dst = Server::StagedTextureStore::KeyForHandle(TestHandle(67, 1));
const auto srcBytes = FollowPattern(0x40);
DefineAndAdopt(store, src, srcBytes);
store.NoteLevelDefined(dst, kTex2DTarget, 0, kFollowExtent);
ASSERT_FALSE(store.IsCovered(dst, kTex2DTarget, 0));
ASSERT_TRUE(store.FollowCopy(src, kTex2DTarget, 0, IntVec3{0, 0, 0}, dst, kTex2DTarget, 0, IntVec3{0, 0, 0},
kFollowExtent));
ASSERT_TRUE(store.IsCovered(dst, kTex2DTarget, 0));
const Uint8* got = store.RequireLevelBytes(dst, kTex2DTarget, 0, "unit");
for (SizeT i = 0; i < srcBytes.size(); ++i) ASSERT_EQ(got[i], srcBytes[i]) << "byte " << i;
}
// The client's next run for a level holding a followed copy is the CLIENT's shadow, which never saw
// the copy: only the boxes it says it wrote land, the copied window survives, and a box whose rows
// are not in the run is refused with nothing written.
TEST(StagedTextureStoreTest, AClientRunOverAFollowedCopyLandsOnlyItsBoxes) {
Server::StagedTextureStore store(/*copies=*/true);
const Uint64 src = Server::StagedTextureStore::KeyForHandle(TestHandle(68, 1));
const Uint64 dst = Server::StagedTextureStore::KeyForHandle(TestHandle(69, 1));
const auto srcBytes = FollowPattern(0x00);
const auto dstBytes = FollowPattern(0x80);
DefineAndAdopt(store, src, srcBytes);
DefineAndAdopt(store, dst, dstBytes);
ASSERT_TRUE(store.FollowCopy(src, kTex2DTarget, 0, IntVec3{0, 0, 0}, dst, kTex2DTarget, 0, IntVec3{0, 0, 0},
IntVec3{2, 2, 1}));
// The client's whole-level shadow: its own old bytes everywhere, 0xEE in the one row it wrote.
Vector<Uint8> run = dstBytes;
for (Int x = 0; x < 4; ++x) {
for (Int c = 0; c < 4; ++c) run[At(x, 3, c)] = 0xEE;
}
const Server::StagedTextureStore::ClientTexelBox lastRow{0, 3, 0, 4, 1, 1};
ASSERT_TRUE(store.AdoptClientBoxes(dst, kTex2DTarget, 0, kFollowExtent, 0, run.data(), run.size(), &lastRow, 1));
const Uint8* got = store.RequireLevelBytes(dst, kTex2DTarget, 0, "unit");
for (Int y = 0; y < 4; ++y) {
for (Int x = 0; x < 4; ++x) {
for (Int c = 0; c < 4; ++c) {
const Uint8 want = y == 3 ? Uint8{0xEE} : (x < 2 && y < 2) ? srcBytes[At(x, y, c)] : dstBytes[At(x, y, c)];
ASSERT_EQ(got[At(x, y, c)], want) << "texel (" << x << ", " << y << ")";
}
}
}
EXPECT_TRUE(store.LevelHoldsFollowedCopy(dst, kTex2DTarget, 0));
EXPECT_FALSE(store.IsDriverWritten(dst));
// A slab run that holds rows 2..3 only (imageOffset 32): row 0 is not in it.
const Server::StagedTextureStore::ClientTexelBox firstRow{0, 0, 0, 4, 1, 1};
Vector<Uint8> slab(32, 0x33);
EXPECT_FALSE(store.AdoptClientBoxes(dst, kTex2DTarget, 0, kFollowExtent, 32, slab.data(), slab.size(), &firstRow, 1));
EXPECT_EQ(got[At(0, 0, 0)], srcBytes[At(0, 0, 0)]) << "a refused run wrote into the level";
}
// A run adopted WHOLE over a followed copy (any caller that does not take AdoptClientBoxes' route)
// puts the client's stale texels back over it - so the store stops standing for the texture.
TEST(StagedTextureStoreTest, AWholeRunOverAFollowedCopyMarksTheTextureDriverWritten) {
Server::StagedTextureStore store(/*copies=*/true);
const Uint64 src = Server::StagedTextureStore::KeyForHandle(TestHandle(70, 1));
const Uint64 dst = Server::StagedTextureStore::KeyForHandle(TestHandle(71, 1));
const auto dstBytes = FollowPattern(0x80);
DefineAndAdopt(store, src, FollowPattern(0x00));
DefineAndAdopt(store, dst, dstBytes);
ASSERT_TRUE(store.FollowCopy(src, kTex2DTarget, 0, IntVec3{0, 0, 0}, dst, kTex2DTarget, 0, IntVec3{0, 0, 0},
IntVec3{1, 1, 1}));
store.AdoptRun(dst, kTex2DTarget, 0, kFollowExtent, 0, dstBytes.data(), dstBytes.size());
EXPECT_TRUE(store.IsDriverWritten(dst));
EXPECT_FALSE(store.LevelHoldsFollowedCopy(dst, kTex2DTarget, 0));
EXPECT_FALSE(store.IsDriverWritten(src));
}
// The driver-written mark is per texture and sticky: a whole-resource respecify does not clear it
// (an attachment made before it keeps writing after it), only the key's death does. Inert on a
// monolith store.
TEST(StagedTextureStoreTest, TheDriverWrittenMarkOutlivesAResetAndDiesWithTheKey) {
Server::StagedTextureStore store(/*copies=*/true);
const Uint64 a = Server::StagedTextureStore::KeyForHandle(TestHandle(72, 1));
const Uint64 b = Server::StagedTextureStore::KeyForHandle(TestHandle(73, 1));
EXPECT_FALSE(store.IsDriverWritten(a));
store.MarkDriverWritten(a);
store.MarkDriverWritten(b);
EXPECT_TRUE(store.IsDriverWritten(a));
store.ResetLevels(a);
EXPECT_TRUE(store.IsDriverWritten(a)) << "a respecify forgot that the driver writes this texture";
store.Drop(a);
EXPECT_FALSE(store.IsDriverWritten(a)) << "a destroyed texture's mark must not answer for the slot's next owner";
EXPECT_TRUE(store.IsDriverWritten(b));
store.DropAll();
EXPECT_FALSE(store.IsDriverWritten(b));
Server::StagedTextureStore monolithStore(/*copies=*/false);
monolithStore.MarkDriverWritten(a);
EXPECT_FALSE(monolithStore.IsDriverWritten(a));
}
// The two key namespaces share one map, so their disjointness is a property to pin, not to
// assume: handle keys carry the top bit, twin addresses (user-space, aligned) never do.
TEST(StagedTextureStoreTest, HandleKeysAndTwinAddressKeysCannotCollide) {
+2 -2
View File
@@ -1,7 +1,7 @@
{
"abort_sites": 79,
"abort_sites": 78,
"abort_sites_by_file": {
"MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp": 15,
"MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp": 14,
"MobileGL/MG_Backend/DirectGLES/Managers.cpp": 12,
"MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp": 2,
"MobileGL/MG_Backend/DirectGLES/Utils.cpp": 1,