mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Merge] (P8): integrate package E - a texture readback the Espryt driver refuses is answered from the split server's staged store, which now follows glCopyImageSubData (FollowCopy, client-box merge, sticky driver-written mark, named decline texture-readback-store-declined); CopyImageStoreReadbackScenario on both backends x every arm plus the extent-scoped StoreRead knob; the dead CopyImageSubData+RENDERBUFFER latch goes (census 79 -> 78)
This commit is contained in:
@@ -13402,25 +13402,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;
|
||||
@@ -13541,19 +13530,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
|
||||
@@ -13640,9 +13624,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) ||
|
||||
@@ -13650,6 +13636,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.
|
||||
@@ -13723,6 +13729,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
|
||||
|
||||
@@ -3189,6 +3189,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.
|
||||
@@ -10963,7 +10988,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;
|
||||
@@ -10973,6 +10998,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
|
||||
|
||||
@@ -2345,7 +2345,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);
|
||||
|
||||
@@ -140,6 +140,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
|
||||
@@ -3248,6 +3249,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) {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
# P8-E:CopyImageSubData 由 server 的 staged store 跟随;死闩清理
|
||||
|
||||
> 2026-09-29,分支 `p8/e`(基线 `8d3e2317`)。简报 `~/w7/notes/p8/BRIEF-E.md`;证据 `~/w7/notes/p8/e-evidence/`。只在 Espryt;Magma 无代码改动,作对照。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
- 审计的推断「split 下 copy 后 `glGetTexImage` 读到陈旧影子」不成立:split 的 `glGetTexImage` 不进 `DirectGLES::GetTexImage`,也不读任何影子。
|
||||
- split 路径:client `EmitGetTexImage`(`MG_Remote/Client/TextureReadbackEmit.inc:158`)→ server `OnGetTextureImage`(`MG_Remote/Server/PipeApplier.cpp:443`)→ `ReadTextureImageWire`(`DirectGLES/WireTextureReadback.inc:422`)→ `ReadTextureLevelTight`(`DirectGLES/WireTextureReadback.inc:217`),只读 GPU(R32UI 字腿或 FBO)。
|
||||
- 真实缺口:驱动不能 attach 的格式(Adreno:RGB16、16 位 SNORM)FBO 不完整(`DirectGLES/WireTextureReadback.inc:269`)→ `kStatusError`(`PipeApplier.cpp:462`)→ client `Fatal{ReplyError, "ReadPixels"}`(`EmitTables.cpp:903`)。与有没有 copy 无关,只上传过的层也结束会话。
|
||||
- 主机 llvmpipe 能 attach 这些格式:不加旋钮时 split / spawn / tcp 与 monolith 全绿,所以要旋钮才能在主机上走到这条路(§4 R1)。
|
||||
- 修法全在 server,不搬到 client:驱动拒绝时由 staged store 回答(store 路由),store 跟随 copy 做同一次 texel 块搬移(`FollowCopy`)。选「正确字节」,理由见 §2。
|
||||
- monolith 同一缺陷(影子不随 copy 更新)记给 dev(§6)。
|
||||
|
||||
## 2. 设计
|
||||
|
||||
| 部件 | 位置 | 内容 |
|
||||
|---|---|---|
|
||||
| store 路由 | `DirectGLES/WireTextureReadback.inc:352` | 驱动拒绝回读时,store 仅在「整层覆盖、尺寸一致、纹理从未被驱动写过」时回答;转换同 `GetTexImageViaShadowConversion`(打包格式按字原样,其余解成宽 RGBA 逐行转) |
|
||||
| 具名拒绝 / 武装标记 | `DirectGLES/WireTextureReadback.inc:411`,`:416` | 拒绝:`MGLOG_E_ONCE` `texture-readback-store-declined`,回复 error,client Fatal。回答:`MGLOG_W_ONCE` `texture-readback-from-store` |
|
||||
| 驱动写标记 | `MG_Remote/Server/StagedTextureStore.h:479-514` | 按纹理、粘性;只有 `Drop` / `DropAll` 清,`ResetLevels` 不清(附件在重定义后照样写) |
|
||||
| 标记来源 | `DirectGLES/Managers.cpp:10901` | `NoteDriverSideTextureWriteByHandle` 同时标 store:附件、可写 image、`CopyTexSubImage`、生成 mip 都经过它 |
|
||||
| store 跟随 copy | `StagedTextureStore.h:519`,`DirectGLES/Managers.cpp:10924`,调用点 `DirectGLES/DirectGLES.cpp:13347-13365` | 两端同一内部格式(`DirectGLES/Managers.cpp:10946`)、单 upload target(非 cube / 1D array)、源整层覆盖且未被驱动写、目标整层覆盖或 copy 覆盖整层 → store 做同一搬移;否则给目标标驱动写 |
|
||||
| copy 失败 | `DirectGLES/DirectGLES.cpp:13440` | 驱动拒绝 copy 时 store 已搬,补标驱动写 |
|
||||
| 跟随后的 client 上传 | `DirectGLES/Managers.cpp:3193`,`StagedTextureStore.h:573` | client run 是 client 影子(没见过 copy),只落 record 声明写过的盒(regions,无 region 时 union box);放不下则照常收养并标驱动写;`AdoptRun` 兜底同样标(`StagedTextureStore.h:353`) |
|
||||
| 测试旋钮 | `DirectGLES/WireTextureReadback.inc:328` | `MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT=<w>x<h>`,server 读;该尺寸颜色层的 FBO 腿视为驱动拒绝,R32UI 字腿不动 |
|
||||
|
||||
不选「标 GPU 脏」:
|
||||
|
||||
- Espryt 的 `GpuDirty` 语义是「需要从 store 重传」(`DirectGLES/Managers.cpp:8460`),标了会把 store 旧字节传回去盖掉 GPU 上的 copy。
|
||||
- 跟随后其他整层读 store 的路径也拿到对的字节:重铸回读被拒时的 W2-b 重放(`DirectGLES/Managers.cpp:6888`)、整层重传。
|
||||
- 跟随不了的形状(跨格式、renderbuffer 源、被驱动写过的源、cube / 1D array、视图)降为具名拒绝,不给旧字节。
|
||||
|
||||
旋钮按尺寸定界:
|
||||
|
||||
- tcp 的 server 是整条 lane 共用的 `TcpServer.Start` fixture,旋钮只能放进它的 ENVIRONMENT(`MG_IntegrationTest/CMakeLists.txt:3245`)。
|
||||
- 场景只有自身环境里有旋钮时才建 13x7 的纹理,否则 12x6(`CopyImageStoreReadbackScenario.cpp:66`),所以 lane 级旋钮只碰 StoreRead 条目。
|
||||
- 实测:tcp 上无旋钮条目的 server 日志没有 `texture-readback-from-store`,StoreRead 条目有。
|
||||
|
||||
## 3. 场景与登记
|
||||
|
||||
`CopyImageStoreReadbackScenario`(`MG_IntegrationTest/Scenarios/CopyImageStoreReadbackScenario.cpp:251-337`);格式 RGB16、RGBA16_SNORM、RGB16_SNORM、RGBA16,RGB9_E5 为正对照(R32UI 字腿)。
|
||||
|
||||
| 例 | 内容 |
|
||||
|---|---|
|
||||
| `AnUploadedLevelReadsBackExactly` | 上传后读回(对照) |
|
||||
| `ACopiedWindowReadsBackOverTheUpload` | 可变纹理,5x3 窗口 copy 后读目标与源 |
|
||||
| `ACopiedWindowReadsBackFromAnImmutableLevel` | 同上,`glTexStorage2D` |
|
||||
| `APartialUploadAfterTheCopyKeepsTheCopiedWindow` | copy 后在同层别处 `glTexSubImage2D` |
|
||||
| `ARenderbufferSourceLandsInTheTexture` | RGBA16 renderbuffer → 纹理(handle 臂,证明 `+RENDERBUFFER` 闩已死) |
|
||||
|
||||
| 条目 | 臂 | 登记 |
|
||||
|---|---|---|
|
||||
| `DirectGLES.*` / `DirectVulkan.*` | monolith | 整体发现 |
|
||||
| `DirectGLES.{Split,Spawn,Tcp}.*` | 三臂,无旋钮 | `mgl_itest_register_split_arms`(`MG_IntegrationTest/CMakeLists.txt:3229`) |
|
||||
| `DirectGLES.{Split,Spawn,Tcp}.StoreRead.*` | 三臂,旋钮 13x7,前 4 例 | `MG_IntegrationTest/CMakeLists.txt:3247-3273`,带 `integration-gpu` |
|
||||
| `DirectVulkan.{Split,Spawn,Tcp}.CopyStore.*` | Magma 门控三臂 | `MG_IntegrationTest/CMakeLists.txt:3281`;另有 tier 2 信息层 |
|
||||
|
||||
- 三臂对称,`spawn_lane_parity.py` 不需例外行(`StoreRead.` 尾不被 Espryt 的 `CASE` 解析,`scripts/ci/spawn_lane_parity.py:39`)。
|
||||
- store 单元用例 6 个:`MG_Test/Wire/StagedTextureStoreTest.cpp:330-475`。
|
||||
|
||||
## 4. red-once
|
||||
|
||||
| # | 变异 | 载体 | 红 |
|
||||
|---|---|---|---|
|
||||
| R1 | 基线代码 + 旋钮(无 store 路由) | StoreRead 12 条 | 12/12 `Fatal{ReplyError, "ReadPixels"}`,只上传的对照也死 |
|
||||
| M0 | server 跳过 copy | 无旋钮 Espryt 三臂 15 条 | 12 红(4 例 × 3 臂),读到 copy 前字节;上传对照绿 |
|
||||
| M1 | store 既不跟随也不标记 | StoreRead | 9/12 红,读到 copy 前字节(简报的「去掉即陈旧字节」) |
|
||||
| M2 | 不跟随但标记 | StoreRead | 9/12 红,`Fatal{ReplyError}`(具名拒绝) |
|
||||
| M3 | 关掉盒合并与 `AdoptRun` 兜底 | StoreRead 部分上传例 + 单元 | 3 条读到旧 copy 窗口 + `AWholeRunOverAFollowedCopyMarksTheTextureDriverWritten` 红 |
|
||||
| M4 | store 路由忽略驱动写标记 | renderbuffer 例 + 旋钮 12x6(手动) | 旧字节;未变异时是 `texture-readback-store-declined` |
|
||||
| U1 | `FollowCopy` 信任被驱动写过的源 | `ACopyTheStoreCannotVouchForIsNotFollowed` | 红 |
|
||||
| U2 | `ResetLevels` 清驱动写标记 | `TheDriverWrittenMarkOutlivesAResetAndDiesWithTheKey` | 红 |
|
||||
| U3 | `AdoptClientBoxes` 落整条 run | `AClientRunOverAFollowedCopyLandsOnlyItsBoxes` | 红 |
|
||||
| MV | Magma 跳过 copy | Magma 35 条 | 28 红(4 例 × 7 条目),上传对照绿 |
|
||||
| MONO | monolith `GetTexImage` 强制走影子(临时探针,不提交) | `DirectGLES.CopyImageStoreReadbackScenario.*` | 4 个 copy 例 × 4 个 16 位格式读到 copy 前字节;RGB9_E5 与上传对照绿 |
|
||||
|
||||
每次变异后按字节复制还原、`touch`、`cmp` 一致;日志在证据目录(`redonce-*.log`、`red1-head-knob-all.log`、`monolith-probe-shadow.log`)。
|
||||
|
||||
## 5. 死闩清理(无行为变化)
|
||||
|
||||
- `CopyImageSubData+RENDERBUFFER`:原在 `SyncRenderbufferObjectToBackend` 内,条件是 transport≠monolith;唯一调用点只在 monolith 臂(split 臂按句柄处理,`DirectGLES/DirectGLES.cpp:13134-13155`),删去并改写注释(`DirectGLES/DirectGLES.cpp:13110-13117`)。
|
||||
- 镜像函数里过时的「client 侧镜像留给 P8」「由两个 Fatal 兜底」注释改写(`DirectGLES/DirectGLES.cpp:13239-13252`)。
|
||||
- Fatal 普查:abort 点 79 → 78(`DirectGLES.cpp` 15 → 14),族 45 不变;基线用 `fatal_census.py --write-baseline` 重写。
|
||||
|
||||
## 6. monolith 缺陷(ID-P8-3,记给 dev,不在此修)
|
||||
|
||||
- `MirrorCopyImageIntoDestinationShadow` 只对 `HasRedundantPackedEncoding`(仅 RGB9_E5)调用(`DirectGLES/DirectGLES.cpp:13450`);其他格式 copy 后 CPU 影子不变。
|
||||
- 驱动 attach 不了目标格式时 `GetTexImage` 退到影子(`DirectGLES/DirectGLES.cpp:15748`)→ 返回 copy 前字节。设备(Adreno:RGB16 / 16 位 SNORM / 无 norm16 时的 RGBA16)可达。
|
||||
- red-once 见 §4 MONO;修法建议:镜像对所有非压缩同格式 copy 都做,或像 split 一样按纹理记「驱动写过」后拒绝。
|
||||
|
||||
## 7. 未做 / 发现未修
|
||||
|
||||
- `copy-image-shadow-mirror` 的 `MGPipeUnmigratedEmulation` 调用(`DirectGLES/DirectGLES.cpp:13261`)在 transport 下不可达(`:13253` 先返回),未删:名单在 `PipeCatalogueTest.cpp:1439`,归 B / F 统一改。
|
||||
- `MGPipeTypes.h:1535` 的 P5b 历史注释仍提 `+RENDERBUFFER` 拒绝;是 wire 头注释,未动。
|
||||
- 跨格式 copy 不跟随(store 存规范影子,16 位打包、RGB10 / RGB12 的规范表示不是 GL 位),降为具名拒绝。
|
||||
- 具名拒绝仍是会话 Fatal(client 的 `RequireReadbackReplyComplete`);改成 `GL_INVALID_OPERATION` 需要协议变更,不在本包。
|
||||
- 深度格式经 `BlitDepthTexture2D` 的 copy 由 store 按原样搬移;若该仿真非逐位,store 与 GPU 可能有精度差,只影响驱动拒绝深度回读的情形。
|
||||
- 未上设备验证(简报:只有 S 包用手机)。
|
||||
|
||||
- tcp 的 lane 级旋钮今后会碰到任何读回 13x7x1 颜色层的 tcp 条目;现有场景无 13x7 纹理(grep),现有 357 个 tcp 条目在它下面全过。
|
||||
- 设备上走 store 路由时,`ReadTextureLevelTight` 先打一行 `MGLOG_E_ONCE` `framebuffer status=`(`DirectGLES/WireTextureReadback.inc:269`),随后才是成功的 `texture-readback-from-store`;日志里会有一行 ERROR 在正常路径上。
|
||||
|
||||
## 8. 门
|
||||
|
||||
`gate.sh ~/w7/p8-e p8e`,头 `00d6c248`,dirty 0(`~/w7/notes/p8/e-evidence/gate-p8e.log`)。
|
||||
|
||||
| 项 | 读数 |
|
||||
|---|---|
|
||||
| G1 | `added=0 removed=0`;`.text` a5ba13 与基线相同 |
|
||||
| Fatal 普查 | 78 abort 点 / 20 文件 / 45 族 / 0 未标记(79 → 78,§5) |
|
||||
| link ratchet | 172 不变 |
|
||||
| parity errors | 0 |
|
||||
| protocol pin / wire-declines | revision 5 / 53 行 53 处 0 未记 |
|
||||
| hygiene | 12/12 OK |
|
||||
| unit | 2720 / 2720 |
|
||||
| integration-split / spawn / tcp | 436 / 355 / 357,全过 |
|
||||
| magma split / spawn / tcp / full-split | 256 / 233 / 202 / 641,全过 |
|
||||
| integration-gpu[monolith] / [inproc] | 2725 / 2725 |
|
||||
| retrace(`-L retrace`,236) | 230 过;6 红 = 已知 `iterationrp-in-world` llvmpipe JIT 崩溃(monolith 两后端 + split / spawn 两后端);split 76、spawn 76 条全过(`retrace-p8e.log`) |
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user