mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[MGPipe, MG_Remote] cut a texture level's staged run at the stage chunk
ONE RECORD'S BLOB IS STAGED WHOLE IN SEG_STAGE, a linear arena, so a texture
level larger than the segment's chunk budget was Fatal{RingOverrun, "SEG_STAGE"}
at the encoder rather than a split - and the CI traces hit it: the texture half of
resource_subdata stages the whole level shadow regardless of how little of it
changed, and a 512x128x33 GL_RGBA32F level is 34,603,008 bytes while a 192-cube
GL_RGBA16 level is 56,623,104, both against the default 32 MiB segment. Three
retrace cases aborted there: iterationt-in-world and iterationt-nodsa-in-world in
both backends, and iterationrp-in-world in DirectVulkan.
THE CLIENT now cuts such a level into WHOLE-WIDTH SLABS, one resource_subdata
each (TextureEmit.h's MGPipeForEachTextureSlab, driven at MGPipeStageChunkBytes -
the same cap PipeFill.cpp's buffer walks are cut at). A piece is a set of
consecutive rows of the level shadow, so its box names both its texels and its
byte run; whole slices are the coarsest cut, a slice bigger than the cap falls
back to whole rows and a row bigger than the cap to whole texels inside one row.
Every piece is emitted, dirty or not: the server assembles one level image out of
the runs, so a piece left out would leave a hole no texel read can answer. Each
piece carries at least one region (its clipped dirty rects, or its own box when
none reaches it) because RegionCount == 0 remains the whole-level spelling, and
each region's SrcOffset is rebased onto its own piece's run - MGPipeTypes.h has
always said SrcOffset is "into the blob" - while the strides stay the level's. A
level that FITS keeps the single whole-level record, field for field what it has
always been, which is what every existing gate is written against.
THE SERVER now assembles levels from those runs. StagedTextureStore::AdoptRun
places one run at an image offset, its coverage set is exactly the runs it has
adopted (StagedShadowStore's CoverageAdd, the buffer half's helpers), and only a
FULLY covered level satisfies IsCovered / RequireLevelBytes - a level with one
piece missing is as unreadable as one that never arrived, and it is the same
Fatal{StageSnapshotTooNarrow}. The whole-level spelling survives as Adopt, its
offset-0 replacement, and the two are told apart by the run's own LENGTH: the
whole-level run is the level's complete shadow whatever its box says, a piece's
run is exactly its box's byte extent. Both adoption call sites -
Ops_H_TextureSubData and PipeApply.cpp's hookless fallback, which is what Magma
takes - read the offset that way.
THE PROTOCOL IS UNCHANGED: no field moved, no caps bit was added. The one
compatibility statement is that a split level now crosses as several records of
one apply batch, so an OLD server (which replaces the level on every record) would
keep only the last piece - inproc runs both roles out of one library, so the
constraint is noted here rather than armed with a gate.
Tests. TextureEmit.ALevelTooLargeForTheStageChunkIsCutIntoSlabs drives the split
at MGPipeStageChunkBytesFor's clamp with both trace shapes (5 and 7 slabs), the
row-granularity and texel-granularity fallbacks, the three shapes that refuse to
cut, the region clipping/rebasing, and the piece no rect reaches.
StagedTextureStoreTest gains the multi-run assembly, the gap that must not read as
coverage, the partial-level Fatal by name, the run-offset discriminator, and two
production cases - the applier's hookless arm and DirectGLES's real TextureSubData
hook - that would put both halves at the front of the image if the offset were
dropped.
This commit is contained in:
@@ -2713,20 +2713,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const void* bytes, const MG_Pipe::MGPSubRegion* regions) {
|
||||
// The region set is the upload planner's shape and stays in the applier's
|
||||
// pending set; the store's coverage is the staged run itself
|
||||
// (StagedTextureStore.h's coverage ruling).
|
||||
(void)regions;
|
||||
// (StagedTextureStore.h's coverage ruling). It is ALSO what places that run in
|
||||
// the level image when the record carries a piece of the level rather than the
|
||||
// whole of it (fix A2), which is why it is read here now.
|
||||
auto& store = MG_Remote::Server::ServerStagedTexture();
|
||||
if (!store.CopiesIntoServerStorage()) return;
|
||||
// The applier's gate has already faulted every shape that reaches here without
|
||||
// bytes, and under split the codec declared the run's length (Blob.Size) -
|
||||
// TextureEmit.h:1285's "the bytes this record declares ARE the level shadow".
|
||||
// TextureEmit.h's "the bytes this record declares ARE the level shadow".
|
||||
if (bytes == nullptr || record.Blob.Size == 0) return;
|
||||
const auto* stored = PipeTextureRecordForHandle(res);
|
||||
if (stored == nullptr) return;
|
||||
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(res);
|
||||
const Uint16 uploadTarget = MG_Pipe::MGPipeSubDataUploadTargetOf(record.Target);
|
||||
const IntVec3 extent = MG_Remote::Server::StagedTextureUploadExtent(stored->Desc, record);
|
||||
store.Adopt(MG_Remote::Server::StagedTextureStore::KeyForHandle(res),
|
||||
MG_Pipe::MGPipeSubDataUploadTargetOf(record.Target), record.Level, extent,
|
||||
bytes, static_cast<SizeT>(record.Blob.Size));
|
||||
const SizeT runBytes = static_cast<SizeT>(record.Blob.Size);
|
||||
// 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.
|
||||
if (record.RegionCount == 0) {
|
||||
store.Adopt(key, uploadTarget, record.Level, extent, bytes, runBytes);
|
||||
return;
|
||||
}
|
||||
store.AdoptRun(key, uploadTarget, record.Level, extent,
|
||||
MG_Remote::Server::StagedTextureRunImageOffset(record, regions), bytes,
|
||||
runBytes);
|
||||
}
|
||||
|
||||
void Ops_H_TextureRespecify(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc,
|
||||
@@ -8419,6 +8430,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// derived pointer, which is what the legacy arm sends - and one named
|
||||
// line says so. Gated on subRectEligible because bpp is 0 otherwise
|
||||
// and the regions are not read at all.
|
||||
//
|
||||
// FIX A2 IS THE SECOND LEGITIMATE DISAGREEMENT, and the fallback is
|
||||
// exactly right for it: a level too large to stage whole crosses as
|
||||
// several slab records (TextureEmit.h's MGPipeForEachTextureSlab) whose
|
||||
// regions carry offsets into THEIR OWN run rather than into the level
|
||||
// shadow. Every piece of the level is merged into this one entry, the
|
||||
// entry's rects are then read out of the SERVER'S WHOLE LEVEL IMAGE -
|
||||
// which is what the derived pointer names - and those piece-relative
|
||||
// offsets are precisely the ones that must not be used here.
|
||||
Bool pendingOffsetsAgree = true;
|
||||
if (pendingUpload != nullptr && subRectEligible) {
|
||||
for (const auto& region : pendingUpload->Regions) {
|
||||
@@ -8430,7 +8450,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_E_ONCE("MGPipe: texture %u level %u carries a region whose SrcOffset "
|
||||
"%llu is not its own origin (%d,%d,%d) at the level's pitch "
|
||||
"(%llu expected) - the carried offsets are dropped and every "
|
||||
"rect is read from the shadow instead",
|
||||
"rect is read from the shadow instead. Expected once for a "
|
||||
"level cut into stage-chunk pieces, whose regions index their "
|
||||
"own run; otherwise this names a record whose regions and "
|
||||
"origins disagree",
|
||||
MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject), static_cast<Uint>(level),
|
||||
static_cast<unsigned long long>(region.SrcOffset), region.X,
|
||||
region.Y, region.Z,
|
||||
|
||||
@@ -67,6 +67,13 @@
|
||||
|
||||
#include <Config.h>
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// MGPipeStageChunkBytes: the cap one texture level's staged run is cut at (see
|
||||
// MGPipeTextureStageChunkBytes below). Behind the build option for ResourceTracker.h's reason -
|
||||
// nothing under MG_Remote may be reachable from a pull build.
|
||||
#include <MG_Remote/Client/GpuWritePending.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
@@ -400,6 +407,170 @@ namespace MobileGL::MG_Pipe {
|
||||
return region;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// THE STAGE-CHUNK SLAB SPLIT (fix A2)
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// ONE RECORD'S BLOB IS STAGED WHOLE IN SEG_STAGE, a linear arena, so a level shadow larger
|
||||
// than that arena is Fatal{RingOverrun, "SEG_STAGE"} at the encoder rather than a split
|
||||
// (PipeWireCodec.cpp:856-864) - the same wall PipeFill.cpp's content walks are cut at. It is
|
||||
// not hypothetical: measured on the CI traces, a 512x128x33 GL_RGBA32F level is 34,603,008
|
||||
// bytes and a 192-cube GL_RGBA16 level is 56,623,104, both against the default 32 MiB
|
||||
// segment, so those glTexImage3D calls aborted their replays at the emitter.
|
||||
//
|
||||
// THE SPLIT IS INTO WHOLE-WIDTH SLABS. A piece is a set of CONSECUTIVE ROWS OF THE LEVEL
|
||||
// SHADOW - {x0, y0, z0, w, dy, dz} in level coordinates - because that is the one shape whose
|
||||
// box names both its texels and its byte run: the run begins at
|
||||
// z0 * sliceStride + y0 * rowStride + x0 * bpp and is exactly the box's own byte extent. So
|
||||
// the run's placement is derivable from the strides and box the record carries, and the far
|
||||
// side never has to know the level's format or pixel size. Whole slices are the coarsest cut
|
||||
// and the two CI blobs take it; a slice bigger than the cap falls back to whole rows, and a
|
||||
// ROW bigger than the cap to whole texels inside one row - still one box exactly matching its
|
||||
// own run, which is the property everything downstream depends on.
|
||||
//
|
||||
// THE PIECES ARE CONTIGUOUS AND ASCENDING, and their union is the level exactly once: the
|
||||
// server assembles one level image out of them (StagedTextureStore::AdoptRun), so a gap would
|
||||
// lose texels and an overlap would place them twice.
|
||||
struct MGPipeTextureSlab {
|
||||
MGPBox Box{}; // level coordinates
|
||||
Uint64 RunOffset = 0; // the run's first byte, as an offset into the level shadow
|
||||
Uint64 RunBytes = 0; // the run's length, which is exactly the box's own byte extent
|
||||
};
|
||||
|
||||
// THE WALK. `chunkBytes` is how many bytes one record may stage - MGPipeTextureStageChunkBytes
|
||||
// below, 0 meaning "do not cut". False is the answer when the level cannot be tiled at this
|
||||
// cap (a pitch that does not describe the level's bytes exactly, or a cap too small for one
|
||||
// texel), and NOTHING has been handed out when it returns false, so the caller keeps the
|
||||
// whole-level record it would have emitted anyway.
|
||||
template <class Fn>
|
||||
inline Bool MGPipeForEachTextureSlab(const IntVec3& levelSize, const MGPipeLevelPitch& pitch,
|
||||
Uint64 levelBytes, SizeT chunkBytes, Fn&& body) {
|
||||
const Uint64 cap = static_cast<Uint64>(chunkBytes);
|
||||
const Uint64 width = static_cast<Uint64>(std::max<Int>(levelSize.x(), 0));
|
||||
const Uint64 height = static_cast<Uint64>(std::max<Int>(levelSize.y(), 0));
|
||||
const Uint64 depth = static_cast<Uint64>(std::max<Int>(levelSize.z(), 1));
|
||||
const Uint64 texelBytes = pitch.BytesPerTexel;
|
||||
const Uint64 rowStride = pitch.RowStride;
|
||||
const Uint64 sliceStride = pitch.SliceStride;
|
||||
if (cap == 0 || width == 0 || height == 0 || texelBytes == 0) return false;
|
||||
// The pitch has to TILE THE LEVEL EXACTLY, because every piece's box is its run's extent:
|
||||
// a level whose byte size is not sliceStride * depth is the one shape that would leave a
|
||||
// tail no box describes, and it stays on the whole-level record.
|
||||
if (sliceStride * depth != levelBytes) return false;
|
||||
|
||||
const auto emit = [&](Uint64 x0, Uint64 y0, Uint64 z0, Uint64 w, Uint64 h, Uint64 d,
|
||||
Uint64 runOffset, Uint64 runBytes) {
|
||||
MGPipeTextureSlab slab{};
|
||||
slab.Box = MGPBox{static_cast<Int32>(x0), static_cast<Int32>(y0), static_cast<Int32>(z0),
|
||||
static_cast<Uint32>(w), static_cast<Uint32>(h),
|
||||
static_cast<Uint32>(d)};
|
||||
slab.RunOffset = runOffset;
|
||||
slab.RunBytes = runBytes;
|
||||
body(slab);
|
||||
};
|
||||
|
||||
if (sliceStride <= cap) {
|
||||
const Uint64 slabDepth = std::max<Uint64>(cap / sliceStride, 1);
|
||||
for (Uint64 z = 0; z < depth; z += slabDepth) {
|
||||
const Uint64 d = std::min(slabDepth, depth - z);
|
||||
emit(0, 0, z, width, height, d, z * sliceStride, d * sliceStride);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (rowStride <= cap) {
|
||||
// One slice is bigger than the cap, so the pieces are runs of whole rows and none
|
||||
// spans two slices - a piece that did would have a box this arithmetic cannot name.
|
||||
const Uint64 slabRows = std::max<Uint64>(cap / rowStride, 1);
|
||||
for (Uint64 z = 0; z < depth; ++z) {
|
||||
for (Uint64 y = 0; y < height; y += slabRows) {
|
||||
const Uint64 h = std::min(slabRows, height - y);
|
||||
emit(0, y, z, width, h, 1, (z * height + y) * rowStride, h * rowStride);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const Uint64 slabTexels = cap / texelBytes;
|
||||
if (slabTexels == 0) return false;
|
||||
for (Uint64 z = 0; z < depth; ++z) {
|
||||
for (Uint64 y = 0; y < height; ++y) {
|
||||
for (Uint64 x = 0; x < width; x += slabTexels) {
|
||||
const Uint64 w = std::min(slabTexels, width - x);
|
||||
emit(x, y, z, w, 1, 1, (z * height + y) * rowStride + x * texelBytes,
|
||||
w * texelBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ONE CLIPPED REGION OF A PIECE, with its SrcOffset rebased onto the piece's own run.
|
||||
// MGPipeTypes.h says SrcOffset is "into the blob", and under this split the blob IS the
|
||||
// piece's run rather than the level shadow; the two coincide only for the whole-level record,
|
||||
// where box and piece are the same box and this leaves the field exactly as
|
||||
// MGPipeBuildSubRegion computes it. THE STRIDES STAY THE LEVEL'S: a clipped rect's rows are no
|
||||
// more contiguous inside the piece than inside the level, and a 0 would be read as "tightly
|
||||
// packed" - the whole-level spelling - which would repack the wrong bytes.
|
||||
inline MGPSubRegion MGPipeBuildPieceRegion(const MGPBox& box, const MGPBox& piece,
|
||||
const MGPipeLevelPitch& pitch) {
|
||||
MGPSubRegion region{};
|
||||
region.X = box.X;
|
||||
region.Y = box.Y;
|
||||
region.Z = box.Z;
|
||||
region.W = box.W;
|
||||
region.H = box.H;
|
||||
region.D = box.D;
|
||||
region.SrcOffset = static_cast<Uint64>(box.Z - piece.Z) * pitch.SliceStride +
|
||||
static_cast<Uint64>(box.Y - piece.Y) * pitch.RowStride +
|
||||
static_cast<Uint64>(box.X - piece.X) * pitch.BytesPerTexel;
|
||||
region.SrcRowStride = pitch.RowStride;
|
||||
region.SrcSliceStride = pitch.SliceStride;
|
||||
return region;
|
||||
}
|
||||
|
||||
inline MGPBox MGPipeIntersectBoxes(const MGPBox& a, const MGPBox& b) {
|
||||
const Int64 x0 = std::max<Int64>(a.X, b.X);
|
||||
const Int64 y0 = std::max<Int64>(a.Y, b.Y);
|
||||
const Int64 z0 = std::max<Int64>(a.Z, b.Z);
|
||||
const Int64 x1 = std::min<Int64>(static_cast<Int64>(a.X) + a.W, static_cast<Int64>(b.X) + b.W);
|
||||
const Int64 y1 = std::min<Int64>(static_cast<Int64>(a.Y) + a.H, static_cast<Int64>(b.Y) + b.H);
|
||||
const Int64 z1 = std::min<Int64>(static_cast<Int64>(a.Z) + a.D, static_cast<Int64>(b.Z) + b.D);
|
||||
if (x1 <= x0 || y1 <= y0 || z1 <= z0) return MGPBox{};
|
||||
return MGPBox{static_cast<Int32>(x0), static_cast<Int32>(y0), static_cast<Int32>(z0),
|
||||
static_cast<Uint32>(x1 - x0), static_cast<Uint32>(y1 - y0),
|
||||
static_cast<Uint32>(z1 - z0)};
|
||||
}
|
||||
|
||||
// THE REGIONS ONE PIECE CARRIES: every dirty rect clipped to the piece's box, each with its
|
||||
// own run-relative offset. A piece the dirty set does not reach still carries ONE region - its
|
||||
// own box - and that is not padding: RegionCount == 0 is the whole-level spelling (a bare box
|
||||
// the far side reads as tightly packed), and the piece's bytes have to cross whether or not
|
||||
// any of them changed, because the server's level image is assembled from these runs and a
|
||||
// hole in it is a texel read nothing on that side can answer.
|
||||
inline SizeT MGPipeBuildSlabRegions(const MGPBox* rects, SizeT rectCount,
|
||||
const MGPipeTextureSlab& slab, const MGPipeLevelPitch& pitch,
|
||||
MGPSubRegion* out, SizeT maxOut) {
|
||||
if (out == nullptr || maxOut == 0) return 0;
|
||||
SizeT count = 0;
|
||||
for (SizeT i = 0; i < rectCount && count < maxOut; ++i) {
|
||||
const MGPBox clipped = MGPipeIntersectBoxes(rects[i], slab.Box);
|
||||
if (clipped.W == 0 || clipped.H == 0 || clipped.D == 0) continue;
|
||||
out[count++] = MGPipeBuildPieceRegion(clipped, slab.Box, pitch);
|
||||
}
|
||||
if (count == 0) out[count++] = MGPipeBuildPieceRegion(slab.Box, slab.Box, pitch);
|
||||
return count;
|
||||
}
|
||||
|
||||
// The cap one level's staged run is cut at, or 0 for "keep the whole level in one record":
|
||||
// monolith, the server role's own uploads and a process with no session - every unit gate -
|
||||
// all answer 0, which is what keeps those lanes byte for byte what they were.
|
||||
inline SizeT MGPipeTextureStageChunkBytes() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
return MG_Remote::Client::MGPipeStageChunkBytes();
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The emitter: handles, the inverse, the sticky mask, the drain list
|
||||
// ---------------------------------------------------------------------------------
|
||||
@@ -984,6 +1155,10 @@ namespace MobileGL::MG_Pipe {
|
||||
const MGPTextureParams& LastParams() const { return m_lastParams; }
|
||||
const MGPSubData& LastSubData() const { return m_lastSubData; }
|
||||
const Vector<MGPSubRegion>& LastRegions() const { return m_regions; }
|
||||
// HOW MANY resource_subdata records the last level's drain put on the wire: 1 for the
|
||||
// whole-level shape, N for a stage-chunk split (fix A2). LastSubData() and LastRegions()
|
||||
// are the LAST piece's, and that is the whole-level record in the 1 case.
|
||||
Uint64 SubDataPieceCount() const { return m_subDataPieces; }
|
||||
Uint64 CreateCount() const { return m_creates; }
|
||||
Uint64 RespecifyCount() const { return m_respecifies; }
|
||||
Uint64 ParamCount() const { return m_paramSets; }
|
||||
@@ -1205,7 +1380,15 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
}
|
||||
|
||||
// True when the level's record went out and its flags may be cleared.
|
||||
// True when the level's record(s) went out and its flags may be cleared.
|
||||
//
|
||||
// ONE LEVEL MAY TAKE MORE THAN ONE RECORD (fix A2). A level shadow larger than the
|
||||
// staging segment's chunk budget is cut into whole-width slabs, each staged as its own
|
||||
// resource_subdata (MGPipeForEachTextureSlab above; the buffer half's walks are cut the
|
||||
// same way). Every piece is emitted even when nothing in it changed, because the
|
||||
// server's level image is assembled out of these runs; and the level's dirty flag is
|
||||
// cleared only when EVERY piece was accepted - a refusal anywhere leaves the whole level
|
||||
// dirty and on the drain list, which is the safe direction and self-heals.
|
||||
Bool EmitOneLevel(const DrainEntry& pending, Uint64& bytes) {
|
||||
ITextureObject* texture = ResolveTexture(pending.Handle);
|
||||
if (texture == nullptr) return true; // the object is gone; nothing is owed
|
||||
@@ -1231,69 +1414,122 @@ namespace MobileGL::MG_Pipe {
|
||||
// beats many small ones. The invariant that makes the server's choice safe is that
|
||||
// the two describe the SAME texels: every rect lies inside the box, and their union
|
||||
// is the box.
|
||||
//
|
||||
// READ AS BOXES, ONCE, because both shapes below want them in that form: the
|
||||
// whole-level record converts each one straight back through MGPipeBuildSubRegion,
|
||||
// and a piece clips each one to its own slab.
|
||||
MG_State::GLState::MipmapDirtyRegion rects[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
|
||||
const SizeT rectCount = mipmap->GetStorageDirtyRects(
|
||||
uploadTarget, level, rects, MG_State::GLState::MipmapStorage::kMaxDirtyRects);
|
||||
m_regions.clear();
|
||||
m_regions.reserve(rectCount);
|
||||
for (SizeT i = 0; i < rectCount; ++i) {
|
||||
m_regions.push_back(
|
||||
MGPipeBuildSubRegion(MGPipeBoxOfDirtyRegion(rects[i]), levelSize, pitch));
|
||||
}
|
||||
MGPBox rectBoxes[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
|
||||
for (SizeT i = 0; i < rectCount; ++i) rectBoxes[i] = MGPipeBoxOfDirtyRegion(rects[i]);
|
||||
|
||||
m_lastSubData = MGPSubData{};
|
||||
m_lastSubData.Res = pending.Handle;
|
||||
// The contract's packer takes two Uint32s (c0c keeps MGPipeTypes.h backend-neutral),
|
||||
// so the frontend enumeration is widened here rather than there.
|
||||
m_lastSubData.Target = MGPipePackSubDataTarget(
|
||||
static_cast<Uint32>(MGPipeResourceTargetForTextureTarget(texture->GetTarget())),
|
||||
static_cast<Uint32>(uploadTarget));
|
||||
m_lastSubData.Level = static_cast<Uint16>(level);
|
||||
m_lastSubData.LevelWidth = static_cast<Uint32>(levelSize.x());
|
||||
m_lastSubData.LevelHeight = static_cast<Uint32>(levelSize.y());
|
||||
m_lastSubData.LevelDepth = static_cast<Uint32>(levelSize.z());
|
||||
// ALWAYS 1 ON THE CLIENT SIDE. The conversion fallbacks (the packed-norm, widened
|
||||
// and fallback upload preparers) are the server's and run there, so the bytes this
|
||||
// record declares ARE the level shadow. The server clears the flag internally when
|
||||
// it converts, which is the `uploadData == mipData` pointer comparison turned into
|
||||
// a carried fact.
|
||||
m_lastSubData.SourceIsVerbatimLevelShadow = 1;
|
||||
m_lastSubData.UnionBox = unionBox;
|
||||
m_lastSubData.RegionCount = static_cast<Uint32>(m_regions.size());
|
||||
// "THIS RECORD DOES NOT DECLARE ITS BLOB", which is what a monolith emission is:
|
||||
// Seg is kMGHostSpanSegNone, Offset IS the address of the level shadow base, and a
|
||||
// zero Size means the destination box is what bounds the write. A non-zero Size
|
||||
// that did not match the record's own byte count would be Fatal{ProtocolCorruption}
|
||||
// on the far side, and a texture record's byte count is the server's to compute
|
||||
// once it has picked box-or-rects.
|
||||
m_lastSubData.Blob.Seg = kMGHostSpanSegNone;
|
||||
m_lastSubData.Blob.Offset = static_cast<Uint64>(reinterpret_cast<std::uintptr_t>(shadow));
|
||||
m_lastSubData.Blob.Size = 0;
|
||||
Bool dispatched = false;
|
||||
Bool accepted = true;
|
||||
m_subDataPieces = 0;
|
||||
|
||||
const auto fillRecord = [&](const MGPBox& box, Uint64 runAddress) {
|
||||
m_lastSubData = MGPSubData{};
|
||||
m_lastSubData.Res = pending.Handle;
|
||||
// The contract's packer takes two Uint32s (c0c keeps MGPipeTypes.h backend-neutral),
|
||||
// so the frontend enumeration is widened here rather than there.
|
||||
m_lastSubData.Target = MGPipePackSubDataTarget(
|
||||
static_cast<Uint32>(MGPipeResourceTargetForTextureTarget(texture->GetTarget())),
|
||||
static_cast<Uint32>(uploadTarget));
|
||||
m_lastSubData.Level = static_cast<Uint16>(level);
|
||||
m_lastSubData.LevelWidth = static_cast<Uint32>(levelSize.x());
|
||||
m_lastSubData.LevelHeight = static_cast<Uint32>(levelSize.y());
|
||||
m_lastSubData.LevelDepth = static_cast<Uint32>(levelSize.z());
|
||||
// ALWAYS 1 ON THE CLIENT SIDE. The conversion fallbacks (the packed-norm, widened
|
||||
// and fallback upload preparers) are the server's and run there, so the bytes this
|
||||
// record declares ARE the level shadow. The server clears the flag internally when
|
||||
// it converts, which is the `uploadData == mipData` pointer comparison turned into
|
||||
// a carried fact.
|
||||
m_lastSubData.SourceIsVerbatimLevelShadow = 1;
|
||||
m_lastSubData.UnionBox = box;
|
||||
m_lastSubData.RegionCount = static_cast<Uint32>(m_regions.size());
|
||||
// "THIS RECORD DOES NOT DECLARE ITS BLOB", which is what a monolith emission is:
|
||||
// Seg is kMGHostSpanSegNone, Offset IS the address of this record's run in the
|
||||
// level shadow, and a zero Size means the destination box is what bounds the
|
||||
// write. A non-zero Size that did not match the record's own byte count would be
|
||||
// Fatal{ProtocolCorruption} on the far side, and a texture record's byte count is
|
||||
// the server's to compute once it has picked box-or-rects. Under split the encoder
|
||||
// fills Seg/Size from the staged run it just cut.
|
||||
m_lastSubData.Blob.Seg = kMGHostSpanSegNone;
|
||||
m_lastSubData.Blob.Offset = runAddress;
|
||||
m_lastSubData.Blob.Size = 0;
|
||||
};
|
||||
|
||||
// THE REGION LIST IS THE CALL'S VARIABLE TAIL AND IT IS HANDED OVER (M1). v1 built
|
||||
// m_regions, wrote its size into RegionCount and passed nothing, so on this base -
|
||||
// where the applier's tail exists - every scattered upload would have declared N
|
||||
// regions and supplied none (the applier faults on exactly that). A null tail is
|
||||
// correct ONLY for the whole-level shape, where RegionCount is 0.
|
||||
Bool accepted = false;
|
||||
Bool dispatched = false;
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
|
||||
dispatched = true;
|
||||
// `levelBytes` closes CONTRACT-P5 table 1 row 7's open half. The record still
|
||||
// declares Blob.Size 0 on the monolith arm - where the applier reads the
|
||||
// companion pointer and the destination box bounds the write - but under split
|
||||
// the staged run needs a length, and the comment above already says what it
|
||||
// is: "the bytes this record declares ARE the level shadow". The regions' own
|
||||
// SrcOffsets index into exactly that run.
|
||||
accepted = MGPipeRouteResourceSubData(m_lastSubData, shadow,
|
||||
static_cast<Uint64>(levelBytes),
|
||||
m_regions.empty() ? nullptr : m_regions.data());
|
||||
const auto emitRecord = [&](const void* run, Uint64 runBytes) {
|
||||
Bool pieceAccepted = true;
|
||||
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
|
||||
dispatched = true;
|
||||
// `runBytes` closes CONTRACT-P5 table 1 row 7's open half. The record still
|
||||
// declares Blob.Size 0 on the monolith arm - where the applier reads the
|
||||
// companion pointer and the destination box bounds the write - but under split
|
||||
// the staged run needs a length, and the comment above already says what it
|
||||
// is: "the bytes this record declares ARE the level shadow". For a piece that
|
||||
// is the piece's own run of it, and the regions' SrcOffsets index into exactly
|
||||
// that run.
|
||||
pieceAccepted = MGPipeRouteResourceSubData(
|
||||
m_lastSubData, run, runBytes,
|
||||
m_regions.empty() ? nullptr : m_regions.data());
|
||||
} else {
|
||||
(void)run;
|
||||
(void)runBytes;
|
||||
}
|
||||
++m_subDatas;
|
||||
++m_subDataPieces;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ClientTextureUploadEmissions, 1);
|
||||
}
|
||||
bytes += sizeof(MGPSubData) + m_regions.size() * sizeof(MGPSubRegion);
|
||||
return pieceAccepted;
|
||||
};
|
||||
|
||||
// WHICH SHAPE THIS LEVEL TAKES. The split is taken only when the level's whole run
|
||||
// does not fit one record's chunk budget; a level that fits keeps the single
|
||||
// whole-level record, field for field what this emitter has always sent and what
|
||||
// every existing expectation is written against.
|
||||
const SizeT chunkBytes = MGPipeTextureStageChunkBytes();
|
||||
Bool split = false;
|
||||
if (chunkBytes != 0 && static_cast<Uint64>(levelBytes) > static_cast<Uint64>(chunkBytes)) {
|
||||
split = MGPipeForEachTextureSlab(
|
||||
levelSize, pitch, levelBytes, chunkBytes, [&](const MGPipeTextureSlab& slab) {
|
||||
// One slot per rect plus one for a piece no rect reaches, which is the
|
||||
// most MGPipeBuildSlabRegions can write.
|
||||
m_regions.assign(std::max<SizeT>(rectCount, 1), MGPSubRegion{});
|
||||
const SizeT pieceRegions = MGPipeBuildSlabRegions(
|
||||
rectBoxes, rectCount, slab, pitch, m_regions.data(), m_regions.size());
|
||||
m_regions.resize(pieceRegions);
|
||||
fillRecord(slab.Box,
|
||||
reinterpret_cast<Uint64>(reinterpret_cast<std::uintptr_t>(shadow)) +
|
||||
slab.RunOffset);
|
||||
const Bool pieceAccepted = emitRecord(
|
||||
static_cast<const Uint8*>(shadow) + slab.RunOffset, slab.RunBytes);
|
||||
accepted = accepted && pieceAccepted;
|
||||
});
|
||||
}
|
||||
++m_subDatas;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ClientTextureUploadEmissions, 1);
|
||||
if (!split) {
|
||||
// THE WHOLE-LEVEL RECORD: one run, the level shadow itself, beginning at the
|
||||
// level's first byte - which is why Blob.Offset is the shadow base and why the
|
||||
// box may be the dirty union box without describing the run. The far side learned
|
||||
// to tell the two shapes apart from the run's own length
|
||||
// (StagedTextureStore::AdoptRunImageOffset).
|
||||
m_regions.clear();
|
||||
m_regions.reserve(rectCount);
|
||||
for (SizeT i = 0; i < rectCount; ++i) {
|
||||
m_regions.push_back(MGPipeBuildSubRegion(rectBoxes[i], levelSize, pitch));
|
||||
}
|
||||
fillRecord(unionBox, static_cast<Uint64>(reinterpret_cast<std::uintptr_t>(shadow)));
|
||||
accepted = emitRecord(shadow, static_cast<Uint64>(levelBytes));
|
||||
}
|
||||
bytes += sizeof(MGPSubData) + m_regions.size() * sizeof(MGPSubRegion);
|
||||
|
||||
// THE CLIENT CLEARS ITS OWN FLAG ONLY FOR A LEVEL THE APPLIER ACCEPTED (D-D5 step 1
|
||||
// read literally; ID-18 M3). v1 cleared on DISPATCH - and, with the wired constant
|
||||
// still 0, even on a call the `if constexpr` had discarded - so any refusal left the
|
||||
@@ -1307,7 +1543,10 @@ namespace MobileGL::MG_Pipe {
|
||||
// direction and self-heals: the ordinary cause is a record the applier does not
|
||||
// hold, and the next respecify's self-healing create gives it one. It is LOUD
|
||||
// because a permanently refused level would otherwise re-emit once per verb for
|
||||
// ever with nothing to show for it.
|
||||
// ever with nothing to show for it. WITH PIECES IN FLIGHT it is the same rule one
|
||||
// level up: every piece is still emitted, so a refusal leaves the server with a
|
||||
// partial image and the WHOLE level dirty, and the next drain re-sends every piece -
|
||||
// an adoption of the same bytes twice is idempotent.
|
||||
if (dispatched && !accepted) {
|
||||
++m_refusedSubDatas;
|
||||
MGLOG_E_ONCE("MGPipe: resource_subdata for texture {slot=%u, gen=%u} level %u was refused; "
|
||||
@@ -1333,6 +1572,8 @@ namespace MobileGL::MG_Pipe {
|
||||
Uint64 m_respecifies = 0;
|
||||
Uint64 m_paramSets = 0;
|
||||
Uint64 m_subDatas = 0;
|
||||
// What the LAST EmitOneLevel put on the wire: 1 unless a level's run was cut into slabs.
|
||||
Uint64 m_subDataPieces = 0;
|
||||
Uint64 m_refusedSubDatas = 0;
|
||||
Uint64 m_refusedParamSets = 0;
|
||||
Uint64 m_samplerCsoPayloadBytes = 0;
|
||||
|
||||
@@ -1018,14 +1018,25 @@ namespace MobileGL::MG_Pipe {
|
||||
// Their staged bytes still belong to the server before SEG_STAGE retires. Keep the
|
||||
// existing backend hooks authoritative when present, and use this fallback otherwise.
|
||||
void AdoptTextureWithoutBackendHook(const MGPipeResourceRecord& stored, const MGPSubData& upload,
|
||||
const void* bytes) {
|
||||
const void* bytes, const MGPSubRegion* regions) {
|
||||
if (MG_Config::Transport == MG_Config::TransportMode::Monolith || !bytes || upload.Blob.Size == 0) return;
|
||||
auto& store = MG_Remote::Server::ServerStagedTexture();
|
||||
const auto& desc = stored.Desc;
|
||||
store.Adopt(MG_Remote::Server::StagedTextureStore::KeyForHandle(upload.Res),
|
||||
MGPipeSubDataUploadTargetOf(upload.Target), upload.Level,
|
||||
MG_Remote::Server::StagedTextureUploadExtent(desc, upload),
|
||||
bytes, static_cast<SizeT>(upload.Blob.Size));
|
||||
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(upload.Res);
|
||||
const Uint16 target = MGPipeSubDataUploadTargetOf(upload.Target);
|
||||
const IntVec3 extent = MG_Remote::Server::StagedTextureUploadExtent(desc, upload);
|
||||
// RegionCount == 0 IS the whole-level spelling - "the run is the level shadow" - so it
|
||||
// is adopted with the spelling that replaces the level; every other record names a RUN
|
||||
// of the level by its own box (fix A2: a level too large to stage whole crosses as
|
||||
// slabs, one record each, and the first of them is at the level's first byte).
|
||||
if (upload.RegionCount == 0) {
|
||||
store.Adopt(key, target, upload.Level, extent, bytes,
|
||||
static_cast<SizeT>(upload.Blob.Size));
|
||||
return;
|
||||
}
|
||||
store.AdoptRun(key, target, upload.Level, extent,
|
||||
MG_Remote::Server::StagedTextureRunImageOffset(upload, regions), bytes,
|
||||
static_cast<SizeT>(upload.Blob.Size));
|
||||
}
|
||||
|
||||
void DefineTextureWithoutBackendHook(const MGPResourceDesc& desc, const MGPRespecifiedLevel* level) {
|
||||
@@ -1120,10 +1131,13 @@ namespace MobileGL::MG_Pipe {
|
||||
// deliberately holds no byte pointer). The hook copies the run into the server's
|
||||
// staged-texture store keyed by this record's own handle; it is a no-op in
|
||||
// monolith, so the monolith shape keeps P5's pointer-dropping expression exactly.
|
||||
// The REGIONS travel with it because a record may carry a RUN of its level rather
|
||||
// than the whole of it (fix A2): they are what places that run in the level image,
|
||||
// and the applier's pending set - which already has them - is not the store's.
|
||||
if (g_resourceOps != nullptr && g_resourceOps->TextureSubData != nullptr) {
|
||||
g_resourceOps->TextureSubData(record.Res, record, bytes, regions);
|
||||
} else {
|
||||
AdoptTextureWithoutBackendHook(*stored, record, bytes);
|
||||
AdoptTextureWithoutBackendHook(*stored, record, bytes, regions);
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
|
||||
@@ -41,20 +41,27 @@
|
||||
// (ResetLevels), resource_destroy drops the key (Ops_H_TextureDestroy), context death drops
|
||||
// all (OnBackendContextDestroyed, beside MGL_SERVER_STAGED_DROP_ALL).
|
||||
//
|
||||
// COVERAGE IS EXACTLY THE STAGED RUN, NEVER WIDENED AND NEVER NARROWER. The texture half of
|
||||
// resource_subdata ALWAYS stages the whole level shadow (TextureEmit.h:1285: "the bytes this
|
||||
// record declares ARE the level shadow", Blob.Size = the level's byte count), so one Adopt
|
||||
// covers its level whole. The record's region set is deliberately NOT the coverage unit: it
|
||||
// declares which texels CHANGED (the upload planner's shape, which the applier's pending set
|
||||
// already carries), while the full-level upload paths - every conversion fallback, and the
|
||||
// immutable/mutable regeneration arms - read the whole level including texels no region
|
||||
// named. Those texels crossed in the staged run; treating them as uncovered would Fatal a
|
||||
// legal glTexStorage-then-small-glTexSubImage sequence whose first (and only) record names a
|
||||
// small box, and moving them into the store is not the buffer half's silent-zero case
|
||||
// because they are the client's own shadow bytes, delivered and declared. A sync that asks
|
||||
// for a level this store has no covered run for is Fatal{StageSnapshotTooNarrow} - the same
|
||||
// words as the buffer half, for the same reason: the bytes have never existed on this side,
|
||||
// and inventing them is silent data loss, not a missing optimisation.
|
||||
// COVERAGE IS EXACTLY THE STAGED RUNS, NEVER WIDENED AND NEVER NARROWER. One resource_subdata
|
||||
// stages one RUN of the level shadow - the whole level for a level that fits one record's chunk
|
||||
// budget (TextureEmit.h: "the bytes this record declares ARE the level shadow", Blob.Size = the
|
||||
// level's byte count), a whole-width slab of it for one that does not
|
||||
// (TextureEmit.h's MGPipeForEachTextureSlab) - and what this store covers is the union of the
|
||||
// runs it has adopted, in the level's own byte coordinates. The record's region set is
|
||||
// deliberately NOT the coverage unit: it declares which texels CHANGED (the upload planner's
|
||||
// shape, which the applier's pending set already carries), while the full-level upload paths -
|
||||
// every conversion fallback, and the immutable/mutable regeneration arms - read the whole level
|
||||
// including texels no region named. Those texels crossed in the staged run; treating them as
|
||||
// uncovered would Fatal a legal glTexStorage-then-small-glTexSubImage sequence whose first (and
|
||||
// only) record names a small box, and moving them into the store is not the buffer half's
|
||||
// silent-zero case because they are the client's own shadow bytes, delivered and declared.
|
||||
//
|
||||
// SO A FULL-LEVEL READ ASKS FOR THE WHOLE LEVEL TO BE COVERED, which is where the texture half
|
||||
// parts company with the buffer half's one-range shape: the runs of a split level arrive as
|
||||
// several records of ONE apply batch - the same verb, so all of them land before the barrier
|
||||
// that lets the sync read - the set is complete exactly when every piece has landed, and a level
|
||||
// with a hole in it is Fatal{StageSnapshotTooNarrow}: the same words as the buffer half, for the
|
||||
// same reason, because those bytes have never existed on this side and inventing them is silent
|
||||
// data loss rather than a missing optimisation.
|
||||
//
|
||||
// DEFINED-NESS IS TRACKED, NOT DERIVED ALONE. §1's per-level extent derivation (max(1,
|
||||
// base >> level), layer axes fixed) computes the extent of a level that EXISTS; it cannot
|
||||
@@ -87,6 +94,9 @@
|
||||
#include <Includes.h>
|
||||
|
||||
#include <MG_Pipe/MGPipeTypes.h>
|
||||
// StagedShadowStore::CoverageAdd / CoverageHas: ONE spelling of the covered set, so the texture
|
||||
// half's runs and the buffer half's ranges cannot drift apart on what "covered" means.
|
||||
#include <MG_Remote/Server/StagedShadow.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
#include <MG_Util/Math/VectorTypes.h>
|
||||
|
||||
@@ -139,6 +149,45 @@ namespace MobileGL::MG_Remote::Server {
|
||||
return StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, desc.Depth, upload.Level);
|
||||
}
|
||||
|
||||
// WHERE A RECORD'S STAGED RUN BEGINS IN THE SERVER-OWNED LEVEL IMAGE. The record itself says
|
||||
// so, and the two shapes that reach AdoptRun are told apart by the run's own LENGTH - the one
|
||||
// field of MGPSubData that differs between them:
|
||||
//
|
||||
// * THE WHOLE-LEVEL RUN, which is what resource_subdata's texture half has always staged:
|
||||
// TextureEmit.h's "the bytes this record declares ARE the level shadow", so Blob.Size is
|
||||
// the level's byte count and the run begins at the level's FIRST byte whatever the record's
|
||||
// box says - the box describes the dirty texels, not the run. Answer: 0. That is also the
|
||||
// answer for the whole-level spelling that carries no regions at all (RegionCount == 0,
|
||||
// strides 0 = tightly packed).
|
||||
//
|
||||
// * A SLAB of a level too large to stage whole (TextureEmit.h's MGPipeForEachTextureSlab),
|
||||
// whose run is exactly the byte extent of its own box and therefore begins at that box's
|
||||
// first byte: z0 * sliceStride + y0 * rowStride + x0 * bpp, every one of them carried in
|
||||
// the record's own regions. So the box names the placement, and the far side still needs no
|
||||
// knowledge of the level's format: the texel's byte size falls out of the carried row
|
||||
// stride and the carried level width - the two numbers the region and the record both
|
||||
// state.
|
||||
//
|
||||
// A run SHORTER than its box's byte extent is neither shape and answers 0 - not silently
|
||||
// placed: no emitter produces one, and the whole-level reading is the only one that cannot
|
||||
// lose texels the record did carry.
|
||||
inline Uint64 StagedTextureRunImageOffset(const MG_Pipe::MGPSubData& upload,
|
||||
const MG_Pipe::MGPSubRegion* regions) {
|
||||
const MG_Pipe::MGPBox& box = upload.UnionBox;
|
||||
if (regions == nullptr || upload.RegionCount == 0) return 0;
|
||||
const Uint64 rowStride = regions[0].SrcRowStride;
|
||||
const Uint64 sliceStride = regions[0].SrcSliceStride;
|
||||
if (rowStride == 0 || sliceStride == 0 || upload.LevelWidth == 0) return 0;
|
||||
const Uint64 texelBytes = rowStride / upload.LevelWidth;
|
||||
if (texelBytes == 0 || box.W == 0 || box.H == 0 || box.D == 0) return 0;
|
||||
const Uint64 boxBytes = static_cast<Uint64>(box.D - 1) * sliceStride +
|
||||
static_cast<Uint64>(box.H - 1) * rowStride +
|
||||
static_cast<Uint64>(box.W) * texelBytes;
|
||||
if (upload.Blob.Size != boxBytes) return 0;
|
||||
return static_cast<Uint64>(box.Z) * sliceStride + static_cast<Uint64>(box.Y) * rowStride +
|
||||
static_cast<Uint64>(box.X) * texelBytes;
|
||||
}
|
||||
|
||||
class StagedTextureStore {
|
||||
public:
|
||||
// `copies` is "this process is really split". False reproduces the monolith expression
|
||||
@@ -163,29 +212,46 @@ namespace MobileGL::MG_Remote::Server {
|
||||
return static_cast<Uint64>(reinterpret_cast<std::uintptr_t>(twin));
|
||||
}
|
||||
|
||||
// THE ADOPTION. Copies the record's staged run - the whole level shadow, byteSize =
|
||||
// MGPSubData::Blob.Size under split - into server-owned storage and marks the level
|
||||
// covered whole (see the header comment for why the run, not the region set, is the
|
||||
// coverage). REPLACES the level's entry: the run is the level's complete current
|
||||
// content, so nothing of a previous run survives, and a fresh adoption is by
|
||||
// definition not GPU-dirty. Returns the server-owned base; under monolith it does
|
||||
// nothing and returns nullptr, and the caller (Ops_H_TextureSubData) has already
|
||||
// returned before reaching here.
|
||||
// THE ADOPTION OF ONE RUN OF A LEVEL. `imageOffset` is where the run's first byte belongs
|
||||
// in the server-owned level image and `byteSize` (MGPSubData::Blob.Size under split) is
|
||||
// its length; the covered set grows by exactly [imageOffset, imageOffset + byteSize), so
|
||||
// coverage stays EXACTLY the staged runs and a hole between them is what RequireLevelBytes
|
||||
// refuses on. The caller gets the offset from the record itself
|
||||
// (StagedTextureRunImageOffset below). Runs may arrive in any order: coverage is a set,
|
||||
// and the level's extent comes from the record either way.
|
||||
//
|
||||
// THE RETURNED BASE IS ONLY VALID UNTIL THE NEXT RUN OF THE SAME LEVEL: growing the image
|
||||
// reallocates it, which is StagedShadowStore::Adopt's note one level down. Nothing needs
|
||||
// the base while a level is still being assembled - the readers run at the barrier, after
|
||||
// the last piece - so callers here ignore it.
|
||||
const Uint8* AdoptRun(Uint64 key, Uint16 uploadTarget, Uint16 level, const IntVec3& extent,
|
||||
Uint64 imageOffset, const void* bytes, SizeT byteSize) {
|
||||
if (!m_copies) return nullptr;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
LevelShadow& shadow = m_shadows[key].Levels[PackLevel(uploadTarget, level)];
|
||||
shadow.Extent = extent;
|
||||
shadow.Defined = true;
|
||||
shadow.GpuDirty = false;
|
||||
return CopyRunInto(shadow, imageOffset, bytes, byteSize);
|
||||
}
|
||||
|
||||
// THE WHOLE-LEVEL SPELLING: the run is the level's COMPLETE current content, beginning at
|
||||
// the level's first byte, so nothing of a previous run survives it - which is what makes a
|
||||
// re-adoption the right answer for the image-promotion readback and for every
|
||||
// monolith-shaped caller. AdoptRun(offset = 0) with the entry replaced rather than grown
|
||||
// into; under monolith both do nothing and return nullptr, and the caller
|
||||
// (Ops_H_TextureSubData) has already returned before reaching either.
|
||||
const Uint8* Adopt(Uint64 key, Uint16 uploadTarget, Uint16 level, const IntVec3& extent,
|
||||
const void* bytes, SizeT byteSize) {
|
||||
if (!m_copies) return nullptr;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
LevelShadow& shadow = m_shadows[key].Levels[PackLevel(uploadTarget, level)];
|
||||
shadow.Extent = extent;
|
||||
shadow.Bytes.clear();
|
||||
if (bytes != nullptr && byteSize != 0) {
|
||||
const auto* raw = static_cast<const Uint8*>(bytes);
|
||||
shadow.Bytes.assign(raw, raw + byteSize);
|
||||
}
|
||||
shadow.Defined = true;
|
||||
shadow.GpuDirty = false;
|
||||
m_any.store(true, std::memory_order_release);
|
||||
return shadow.Bytes.empty() ? nullptr : shadow.Bytes.data();
|
||||
shadow.Bytes.clear();
|
||||
shadow.Covered.clear();
|
||||
return CopyRunInto(shadow, 0, bytes, byteSize);
|
||||
}
|
||||
|
||||
// A storage-defining respecify named this level: it EXISTS from here on, at this
|
||||
@@ -200,6 +266,9 @@ namespace MobileGL::MG_Remote::Server {
|
||||
LevelShadow& shadow = m_shadows[key].Levels[PackLevel(uploadTarget, level)];
|
||||
if (!shadow.Defined || shadow.Extent != extent) {
|
||||
shadow.Bytes.clear();
|
||||
// The covered set is a statement about THIS coordinate system, so it goes with
|
||||
// the bytes it described.
|
||||
shadow.Covered.clear();
|
||||
shadow.GpuDirty = false;
|
||||
}
|
||||
shadow.Extent = extent;
|
||||
@@ -242,24 +311,31 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// Fatal when a sync wants texels this store has no covered run for. This is THE data
|
||||
// -correctness refusal of the texture half: a pending upload with no adoption behind
|
||||
// it, a GPU-generated level (no bytes by construction), a level the client never
|
||||
// defined, and a monolith-arm misuse all land here, because in every one of them the
|
||||
// bytes have never existed on this side and re-reading the client's shadow for them
|
||||
// is the cross-role access tx exists to end.
|
||||
// defined, a level whose pieces did not all arrive, and a monolith-arm misuse all land
|
||||
// here, because in every one of them the bytes have never existed on this side and
|
||||
// re-reading the client's shadow for them is the cross-role access tx exists to end.
|
||||
//
|
||||
// THE QUESTION IS "IS THE WHOLE LEVEL COVERED", not "are there any bytes": a split
|
||||
// level's pieces arrive as separate records of the same apply batch, and this caller
|
||||
// reads the level image WHOLE (LevelByteSize / the sync's texel base), so a level with
|
||||
// one piece missing is as unreadable as one with none.
|
||||
const Uint8* RequireLevelBytes(Uint64 key, Uint16 uploadTarget, Uint16 level, const char* site) const {
|
||||
if (!m_copies) return nullptr;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
|
||||
if (shadow != nullptr && shadow->Defined && !shadow->Bytes.empty()) {
|
||||
if (shadow != nullptr && shadow->Defined && !shadow->Bytes.empty() &&
|
||||
StagedShadowStore::CoverageHas(shadow->Covered, 0, shadow->Bytes.size())) {
|
||||
return shadow->Bytes.data();
|
||||
}
|
||||
MGLOG_F("MGPipe: Fatal{StageSnapshotTooNarrow, \"%s\"} - the texture sync wants the "
|
||||
"bytes of (uploadTarget=%u, level=%u) and the server's staged shadow has no "
|
||||
"covered run for it. Under split the authoritative shadow is SERVER-OWNED "
|
||||
"(rule C) and resource_subdata is the only way bytes reach it, so these "
|
||||
"texels have never existed on this side: the record that should have "
|
||||
"carried them is missing, or the level's texels were generated on the GPU "
|
||||
"and no byte answer exists at all. Re-reading the client's shadow would be "
|
||||
"the cross-role access this store exists to end",
|
||||
"COMPLETE covered run for it. Under split the authoritative shadow is "
|
||||
"SERVER-OWNED (rule C) and resource_subdata is the only way bytes reach it, "
|
||||
"so these texels have never existed on this side: the record that should "
|
||||
"have carried them is missing, a piece of a level cut into stage-chunk runs "
|
||||
"has not arrived, or the level's texels were generated on the GPU and no "
|
||||
"byte answer exists at all. Re-reading the client's shadow would be the "
|
||||
"cross-role access this store exists to end",
|
||||
site, static_cast<Uint32>(uploadTarget), static_cast<Uint32>(level));
|
||||
std::abort();
|
||||
}
|
||||
@@ -269,7 +345,20 @@ namespace MobileGL::MG_Remote::Server {
|
||||
Bool IsCovered(Uint64 key, Uint16 uploadTarget, Uint16 level) const {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
|
||||
return shadow != nullptr && shadow->Defined && !shadow->Bytes.empty();
|
||||
if (shadow == nullptr || !shadow->Defined || shadow->Bytes.empty()) return false;
|
||||
// THE WHOLE LEVEL, not merely "some bytes": a level assembled out of stage-chunk
|
||||
// pieces is covered only once every piece has landed, and both readers of this
|
||||
// answer then read the image whole - the image-promotion merge and Magma's
|
||||
// unbacked-level readback.
|
||||
return StagedShadowStore::CoverageHas(shadow->Covered, 0, shadow->Bytes.size());
|
||||
}
|
||||
// How many runs this level's coverage is made of: 1 for a whole-level adoption, one per
|
||||
// piece for a level cut into stage-chunk slabs. Diagnostics for the unit cases, so a
|
||||
// check can assert WHAT happened rather than that nothing blew up.
|
||||
SizeT LevelCoveredRunCount(Uint64 key, Uint16 uploadTarget, Uint16 level) const {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const LevelShadow* shadow = FindLevel(key, uploadTarget, level);
|
||||
return shadow == nullptr ? 0 : shadow->Covered.size();
|
||||
}
|
||||
Bool IsLevelDefined(Uint64 key, Uint16 uploadTarget, Uint16 level) const {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
@@ -314,7 +403,6 @@ namespace MobileGL::MG_Remote::Server {
|
||||
return it == m_shadows.end() ? 0 : it->second.Levels.size();
|
||||
}
|
||||
|
||||
private:
|
||||
static Uint32 PackLevel(Uint16 uploadTarget, Uint16 level) {
|
||||
return (static_cast<Uint32>(uploadTarget) << 16) | static_cast<Uint32>(level);
|
||||
}
|
||||
@@ -322,8 +410,14 @@ namespace MobileGL::MG_Remote::Server {
|
||||
struct LevelShadow {
|
||||
IntVec3 Extent{0, 0, 0};
|
||||
// The level's whole staged run. EMPTY for a defined-but-byteless level (null-data
|
||||
// definition, GPU-generated) - emptiness is the coverage answer, not an error.
|
||||
// definition, GPU-generated) - emptiness is the coverage answer, not an error. For a
|
||||
// level adopted in pieces it is the assembled image, grown to the high-water mark of
|
||||
// the runs.
|
||||
Vector<Uint8> Bytes;
|
||||
// Sorted, disjoint, in the level image's byte coordinates, and EXACTLY the runs
|
||||
// adopted for this level: a gap in it is a piece that never arrived, which is the
|
||||
// Fatal below (StagedShadowStore's coverage rules, same helpers).
|
||||
Vector<Range1D> Covered;
|
||||
Bool Defined = false;
|
||||
Bool GpuDirty = false;
|
||||
};
|
||||
@@ -331,6 +425,24 @@ namespace MobileGL::MG_Remote::Server {
|
||||
ska::flat_hash_map<Uint32, LevelShadow> Levels;
|
||||
};
|
||||
|
||||
// The copy both adoption spellings share, with the lock already held: grow the image to
|
||||
// hold the run, place it, and widen the covered set by exactly its range. An empty run
|
||||
// (a level defined with no bytes, or a null pointer) covers nothing - emptiness is the
|
||||
// coverage answer for such a level, not an error.
|
||||
const Uint8* CopyRunInto(LevelShadow& shadow, Uint64 imageOffset, const void* bytes,
|
||||
SizeT byteSize) {
|
||||
if (bytes != nullptr && byteSize != 0) {
|
||||
const auto* raw = static_cast<const Uint8*>(bytes);
|
||||
const SizeT needed = static_cast<SizeT>(imageOffset) + byteSize;
|
||||
if (shadow.Bytes.size() < needed) shadow.Bytes.resize(needed, 0);
|
||||
std::memcpy(shadow.Bytes.data() + imageOffset, raw, byteSize);
|
||||
StagedShadowStore::CoverageAdd(shadow.Covered, static_cast<SizeT>(imageOffset),
|
||||
needed);
|
||||
}
|
||||
m_any.store(true, std::memory_order_release);
|
||||
return shadow.Bytes.empty() ? nullptr : shadow.Bytes.data();
|
||||
}
|
||||
|
||||
const LevelShadow* FindLevel(Uint64 key, Uint16 uploadTarget, Uint16 level) const {
|
||||
const auto textureIt = m_shadows.find(key);
|
||||
if (textureIt == m_shadows.end()) return nullptr;
|
||||
|
||||
@@ -74,6 +74,13 @@
|
||||
#include <MG_State/GLState/TextureState/TextureObjectView.h>
|
||||
|
||||
#include <algorithm>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// MGPipeStageChunkBytesFor: the cap the slab split is driven at. A unit process has no
|
||||
// session, so the live answer (MGPipeTextureStageChunkBytes) is 0 - "keep the whole level in
|
||||
// one record" - and the clamp is what a case can drive the cut at, exactly as
|
||||
// ResourceEmitTest does for the buffer half's walk.
|
||||
#include <MG_Remote/Client/GpuWritePending.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
using namespace MobileGL;
|
||||
@@ -206,6 +213,7 @@ TEST(TextureEmit, TheEmitterIsOneNeverDestroyedProcessSingleton) {
|
||||
X(TextureEmit, AScatteredUploadCarriesTheLevelShadowsStridesAndNotZero) \
|
||||
X(TextureEmit, AWholeLevelUploadCarriesZeroStrides) \
|
||||
X(TextureEmit, MoreThanKMaxDirtyRectsCollapsesToTheBoxWithRegionCountZero) \
|
||||
X(TextureEmit, ALevelTooLargeForTheStageChunkIsCutIntoSlabs) \
|
||||
X(TextureEmit, AnUploadThroughAViewKeysOnTheStorageOwner) \
|
||||
X(TextureEmit, EveryTexturesParamsNameItsBuiltinSamplerCso) \
|
||||
X(TextureEmit, TwoTexturesWithIdenticalSamplingShareOneBuiltinCso) \
|
||||
@@ -549,6 +557,9 @@ TEST(TextureEmit, AWholeLevelUploadCarriesZeroStrides) {
|
||||
EXPECT_EQ(record.Blob.Seg, kMGHostSpanSegNone);
|
||||
EXPECT_EQ(record.Blob.Size, 0u) << "a monolith record does not declare its blob";
|
||||
EXPECT_NE(record.Blob.Offset, 0u) << "Blob.Offset is the level shadow's address in monolith";
|
||||
EXPECT_EQ(Textures().SubDataPieceCount(), 1u)
|
||||
<< "a level that fits one record is exactly one piece (fix A2's counter), so the "
|
||||
"whole-level shape stays what it always was";
|
||||
// The upload target rides in the record's Target byte beside the resource target, which is
|
||||
// the only place a cube face could ever be carried.
|
||||
EXPECT_EQ(MGPipeSubDataResourceTargetOf(record.Target),
|
||||
@@ -586,6 +597,220 @@ TEST(TextureEmit, MoreThanKMaxDirtyRectsCollapsesToTheBoxWithRegionCountZero) {
|
||||
EXPECT_EQ(Textures().LastRegions().size(), oracleCount);
|
||||
}
|
||||
|
||||
// ==================== fix A2: the stage-chunk slab split ====================
|
||||
//
|
||||
// ONE RECORD'S BLOB IS STAGED WHOLE IN SEG_STAGE, a linear arena, so a level shadow larger than
|
||||
// the segment's chunk budget is Fatal{RingOverrun, "SEG_STAGE"} at the encoder rather than a
|
||||
// split - and it is not hypothetical: measured on the CI traces, a 512x128x33 GL_RGBA32F level
|
||||
// is 34,603,008 bytes and a 192-cube GL_RGBA16 level 56,623,104, both against the default 32 MiB
|
||||
// segment. EmitOneLevel cuts such a level into slabs; THIS case drives the arithmetic of the cut
|
||||
// at the cap MGPipeStageChunkBytesFor really returns, because a unit process has no ClientSession
|
||||
// and MGPipeTextureStageChunkBytes() therefore answers 0.
|
||||
//
|
||||
// THREE PROPERTIES, AND EACH IS LOAD-BEARING:
|
||||
// * the pieces are CONTIGUOUS AND ASCENDING and their union is the level EXACTLY ONCE - the
|
||||
// server assembles one level image out of the runs (StagedTextureStore::AdoptRun), so a gap
|
||||
// loses texels and an overlap places them twice;
|
||||
// * EACH PIECE'S RUN IS EXACTLY ITS OWN BOX'S BYTE EXTENT - that is what lets the far side place
|
||||
// a run from the box and the carried strides alone, with no knowledge of the format;
|
||||
// * EACH PIECE CARRIES AT LEAST ONE REGION, with the LEVEL's pitches - RegionCount == 0 is the
|
||||
// whole-level spelling, so a piece with an empty tail would be read as one.
|
||||
TEST(TextureEmit, ALevelTooLargeForTheStageChunkIsCutIntoSlabs) {
|
||||
#if !MOBILEGL_PIPE_PUSH
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no emitter in this build";
|
||||
#elif !MOBILEGL_BUILD_DISAGGREGATED
|
||||
GTEST_SKIP() << "there is no stage segment to size a content chunk against without the "
|
||||
"transport built in";
|
||||
#else
|
||||
EXPECT_EQ(MGPipeTextureStageChunkBytes(), 0u)
|
||||
<< "a unit process has no ClientSession, so the drain must keep the whole-level record";
|
||||
// SessionRings.h:89's default segment, and the cap is a quarter of it - the number the
|
||||
// emitter really passes to the walk.
|
||||
const SizeT cap = MG_Remote::Client::MGPipeStageChunkBytesFor(32ull * 1024ull * 1024ull);
|
||||
ASSERT_EQ(cap, static_cast<SizeT>(8ull * 1024ull * 1024ull));
|
||||
|
||||
// THE CUT ITSELF, with the two properties that hold for every arm: the pieces tile the level
|
||||
// exactly once, in order, each within the cap and each exactly its own box's byte extent.
|
||||
const auto cut = [&](const IntVec3& levelSize, Uint64 levelBytes, SizeT chunk) {
|
||||
std::vector<MGPipeTextureSlab> slabs;
|
||||
const MGPipeLevelPitch pitch =
|
||||
MGPipeLevelPitchOf(levelSize, static_cast<SizeT>(levelBytes));
|
||||
EXPECT_TRUE(MGPipeForEachTextureSlab(levelSize, pitch, levelBytes, chunk,
|
||||
[&](const MGPipeTextureSlab& slab) {
|
||||
slabs.push_back(slab);
|
||||
}));
|
||||
EXPECT_GE(slabs.size(), 2u) << "a level of " << levelBytes << " bytes was not cut at a "
|
||||
<< chunk << " byte cap";
|
||||
Uint64 expectedOffset = 0;
|
||||
Uint64 covered = 0;
|
||||
for (const MGPipeTextureSlab& slab : slabs) {
|
||||
EXPECT_GT(slab.RunBytes, 0u);
|
||||
EXPECT_LE(slab.RunBytes, static_cast<Uint64>(chunk)) << "a piece is bigger than the cap";
|
||||
EXPECT_EQ(slab.RunOffset, expectedOffset)
|
||||
<< "the pieces are not contiguous and ascending";
|
||||
expectedOffset += slab.RunBytes;
|
||||
covered += slab.RunBytes;
|
||||
const Uint64 boxBytes = static_cast<Uint64>(slab.Box.D - 1) * pitch.SliceStride +
|
||||
static_cast<Uint64>(slab.Box.H - 1) * pitch.RowStride +
|
||||
static_cast<Uint64>(slab.Box.W) * pitch.BytesPerTexel;
|
||||
EXPECT_EQ(boxBytes, slab.RunBytes)
|
||||
<< "the box does not describe the run, so the far side cannot place it";
|
||||
}
|
||||
EXPECT_EQ(covered, levelBytes) << "the pieces do not cover the level exactly once";
|
||||
return slabs;
|
||||
};
|
||||
|
||||
// (1) THE TRACE'S FIRST SHAPE: 512x128x33 RGBA32F, 34,603,008 bytes. Its slice pitch is
|
||||
// 1 MiB, so the coarsest arm applies and the cut is by whole slices.
|
||||
const IntVec3 volume{512, 128, 33};
|
||||
const Uint64 volumeBytes = 512ull * 128ull * 33ull * 16ull;
|
||||
const std::vector<MGPipeTextureSlab> volumeSlabs = cut(volume, volumeBytes, cap);
|
||||
ASSERT_EQ(volumeSlabs.size(), 5u);
|
||||
for (const MGPipeTextureSlab& slab : volumeSlabs) {
|
||||
EXPECT_EQ(slab.Box.X, 0);
|
||||
EXPECT_EQ(slab.Box.Y, 0);
|
||||
EXPECT_EQ(slab.Box.W, 512u) << "a slab is not the level's full width";
|
||||
EXPECT_EQ(slab.Box.H, 128u);
|
||||
EXPECT_EQ(slab.Box.D, 8u);
|
||||
}
|
||||
EXPECT_EQ(volumeSlabs.back().Box.D, 1u) << "the remainder slice is its own piece";
|
||||
|
||||
// (2) THE TRACE'S SECOND SHAPE: 192-cubed RGBA16, 56,623,104 bytes, cut the same way.
|
||||
const IntVec3 cube{192, 192, 192};
|
||||
const Uint64 cubeBytes = 192ull * 192ull * 192ull * 8ull;
|
||||
const std::vector<MGPipeTextureSlab> cubeSlabs = cut(cube, cubeBytes, cap);
|
||||
ASSERT_EQ(cubeSlabs.size(), 7u);
|
||||
EXPECT_EQ(cubeSlabs.back().Box.D, 24u);
|
||||
|
||||
// (3) A SLICE BIGGER THAN THE CAP: a 2D level degenerates to whole ROWS, and every piece is
|
||||
// still the level's FULL WIDTH - the property that makes its box name its run.
|
||||
const IntVec3 sheet{256, 256, 1};
|
||||
const Uint64 sheetBytes = 256ull * 256ull * 4ull;
|
||||
const SizeT sheetCap = MG_Remote::Client::MGPipeStageChunkBytesFor(256ull * 1024ull);
|
||||
ASSERT_EQ(sheetCap, static_cast<SizeT>(64ull * 1024ull));
|
||||
const std::vector<MGPipeTextureSlab> sheetSlabs = cut(sheet, sheetBytes, sheetCap);
|
||||
ASSERT_EQ(sheetSlabs.size(), 4u) << "64 of 256 rows per piece";
|
||||
for (const MGPipeTextureSlab& slab : sheetSlabs) {
|
||||
EXPECT_EQ(slab.Box.X, 0);
|
||||
EXPECT_EQ(slab.Box.W, 256u);
|
||||
EXPECT_EQ(slab.Box.H, 64u);
|
||||
EXPECT_EQ(slab.Box.D, 1u);
|
||||
EXPECT_EQ(slab.RunBytes, 64ull * 1024ull);
|
||||
}
|
||||
EXPECT_EQ(sheetSlabs[1].Box.Y, 64);
|
||||
EXPECT_EQ(sheetSlabs[1].RunOffset, 64ull * 1024ull);
|
||||
|
||||
// (4) THE LAST RESORT: a ROW wider than the cap leaves runs of whole texels inside one row.
|
||||
// The full-width property is the one that goes, and nothing downstream depends on it - the
|
||||
// box still names the run exactly.
|
||||
const IntVec3 line{64, 4, 1};
|
||||
const Uint64 lineBytes = 64ull * 4ull * 4ull;
|
||||
const std::vector<MGPipeTextureSlab> lineSlabs = cut(line, lineBytes, static_cast<SizeT>(64));
|
||||
ASSERT_EQ(lineSlabs.size(), 16u) << "16 texels per piece over 4 rows of 64";
|
||||
for (const MGPipeTextureSlab& slab : lineSlabs) {
|
||||
EXPECT_EQ(slab.Box.W, 16u);
|
||||
EXPECT_EQ(slab.Box.H, 1u);
|
||||
EXPECT_EQ(slab.Box.D, 1u);
|
||||
EXPECT_EQ(slab.RunBytes, 64ull);
|
||||
}
|
||||
|
||||
// (5) AND THE SHAPES THAT REFUSE TO CUT, which keep the whole-level record they had before the
|
||||
// split existed: no cap at all, a pitch that does not tile the level's bytes exactly, and a cap
|
||||
// below one texel.
|
||||
std::vector<MGPipeTextureSlab> refusals;
|
||||
const auto refuses = [&](const IntVec3& levelSize, const MGPipeLevelPitch& pitch,
|
||||
Uint64 levelBytes, SizeT chunk) {
|
||||
return !MGPipeForEachTextureSlab(levelSize, pitch, levelBytes, chunk,
|
||||
[&](const MGPipeTextureSlab& slab) {
|
||||
refusals.push_back(slab);
|
||||
});
|
||||
};
|
||||
const MGPipeLevelPitch volumePitch =
|
||||
MGPipeLevelPitchOf(volume, static_cast<SizeT>(volumeBytes));
|
||||
const MGPipeLevelPitch linePitch = MGPipeLevelPitchOf(line, static_cast<SizeT>(lineBytes));
|
||||
EXPECT_TRUE(refuses(volume, volumePitch, volumeBytes, 0));
|
||||
EXPECT_TRUE(refuses(volume, volumePitch, volumeBytes + 1, cap))
|
||||
<< "a level whose bytes the pitch does not tile has a tail no box describes";
|
||||
EXPECT_TRUE(refuses(line, linePitch, lineBytes, 1)) << "a cap below one texel";
|
||||
EXPECT_TRUE(refusals.empty()) << "a refused walk handed out pieces";
|
||||
|
||||
// (6) THE REGIONS OF A PIECE: every dirty rect CLIPPED to the slab, its SrcOffset REBASED onto
|
||||
// the piece's own run (MGPipeTypes.h: SrcOffset is "into the blob") and the LEVEL's pitches
|
||||
// kept. A slab the dirty set does not reach still carries its own box as ONE region.
|
||||
const MGPBox dirty[5] = {
|
||||
{0, 0, 0, 512, 128, 1}, // the whole first slice, inside slab 0
|
||||
{0, 0, 4, 64, 64, 1}, // a small rect in the middle of slab 0
|
||||
{10, 20, 8, 64, 64, 2}, // a rect spanning the first two slices of slab 1
|
||||
{460, 100, 7, 64, 64, 3}, // clipped on X, Y and the slab-0/slab-1 boundary
|
||||
{0, 0, 32, 512, 128, 1}, // the last slice, inside slab 4
|
||||
};
|
||||
SizeT regionCounts[5] = {0, 0, 0, 0, 0};
|
||||
for (SizeT i = 0; i < volumeSlabs.size(); ++i) {
|
||||
const MGPipeTextureSlab& slab = volumeSlabs[i];
|
||||
MGPSubRegion regions[5]{};
|
||||
const SizeT count = MGPipeBuildSlabRegions(dirty, 5, slab, volumePitch, regions, 5);
|
||||
regionCounts[i] = count;
|
||||
ASSERT_GE(count, 1u) << "a piece with no dirty rect must still carry one region";
|
||||
for (SizeT r = 0; r < count; ++r) {
|
||||
const MGPSubRegion& region = regions[r];
|
||||
EXPECT_EQ(region.SrcRowStride, volumePitch.RowStride);
|
||||
EXPECT_EQ(region.SrcSliceStride, volumePitch.SliceStride);
|
||||
EXPECT_EQ(region.SrcOffset,
|
||||
static_cast<Uint64>(region.Z - slab.Box.Z) * volumePitch.SliceStride +
|
||||
static_cast<Uint64>(region.Y - slab.Box.Y) * volumePitch.RowStride +
|
||||
static_cast<Uint64>(region.X - slab.Box.X) * volumePitch.BytesPerTexel)
|
||||
<< "SrcOffset is not relative to the piece's own run";
|
||||
EXPECT_GE(region.X, slab.Box.X);
|
||||
EXPECT_GE(region.Y, slab.Box.Y);
|
||||
EXPECT_GE(region.Z, slab.Box.Z);
|
||||
EXPECT_LE(region.X + static_cast<Int32>(region.W),
|
||||
slab.Box.X + static_cast<Int32>(slab.Box.W))
|
||||
<< "a clipped rect reaches outside the piece's box";
|
||||
EXPECT_LE(region.Y + static_cast<Int32>(region.H),
|
||||
slab.Box.Y + static_cast<Int32>(slab.Box.H));
|
||||
EXPECT_LE(region.Z + static_cast<Int32>(region.D),
|
||||
slab.Box.Z + static_cast<Int32>(slab.Box.D));
|
||||
}
|
||||
}
|
||||
// Slab 0 sees three rects (one of them clipped at its own z end), slab 1 two, slabs 2 and 3
|
||||
// none at all - so their single region IS their box - and slab 4 the last slice.
|
||||
EXPECT_EQ(regionCounts[0], 3u);
|
||||
EXPECT_EQ(regionCounts[1], 2u);
|
||||
EXPECT_EQ(regionCounts[2], 1u);
|
||||
EXPECT_EQ(regionCounts[3], 1u);
|
||||
EXPECT_EQ(regionCounts[4], 1u);
|
||||
const MGPipeTextureSlab& unreached = volumeSlabs[2];
|
||||
MGPSubRegion invented[1]{};
|
||||
ASSERT_EQ(MGPipeBuildSlabRegions(dirty, 5, unreached, volumePitch, invented, 1), 1u);
|
||||
EXPECT_EQ(invented[0].Z, unreached.Box.Z);
|
||||
EXPECT_EQ(invented[0].W, unreached.Box.W);
|
||||
EXPECT_EQ(invented[0].H, unreached.Box.H);
|
||||
EXPECT_EQ(invented[0].D, unreached.Box.D);
|
||||
EXPECT_EQ(invented[0].SrcOffset, 0u) << "the piece's own box starts at its own first byte";
|
||||
|
||||
// The fourth rect reaches past the level's width (460 + 64 > 512), and its Y and Z runs
|
||||
// cross the slab's own end: the clip is what the emitted region shows, and its SrcOffset is
|
||||
// the CLIPPED origin's offset into the piece.
|
||||
bool sawClippedRect = false;
|
||||
{
|
||||
MGPSubRegion regions[5]{};
|
||||
ASSERT_EQ(MGPipeBuildSlabRegions(dirty, 5, volumeSlabs[0], volumePitch, regions, 5), 3u);
|
||||
for (SizeT i = 0; i < 3; ++i) {
|
||||
if (regions[i].X != 460 || regions[i].W != 52u) continue;
|
||||
sawClippedRect = true;
|
||||
EXPECT_EQ(regions[i].Y, 100);
|
||||
EXPECT_EQ(regions[i].H, 28u);
|
||||
EXPECT_EQ(regions[i].Z, 7);
|
||||
EXPECT_EQ(regions[i].D, 1u);
|
||||
EXPECT_EQ(regions[i].SrcOffset,
|
||||
static_cast<Uint64>(7) * volumePitch.SliceStride +
|
||||
static_cast<Uint64>(100) * volumePitch.RowStride +
|
||||
static_cast<Uint64>(460) * volumePitch.BytesPerTexel);
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(sawClippedRect) << "a rect wider than the level was not clipped to the piece";
|
||||
#endif
|
||||
}
|
||||
// ============================ D-D4 ============================
|
||||
TEST(TextureEmit, AnUploadThroughAViewKeysOnTheStorageOwner) {
|
||||
TextureScope scope;
|
||||
|
||||
@@ -228,6 +228,125 @@ TEST(StagedTextureStoreTest, TheMipExtentDerivationKeepsArrayLayersFixed) {
|
||||
IntVec3(1, 1, 1)) << "shrinking clamps at 1, never 0";
|
||||
}
|
||||
|
||||
// ==================== fix A2: one level, several staged runs ====================
|
||||
//
|
||||
// ONE RECORD'S BLOB IS STAGED WHOLE IN SEG_STAGE, so a level shadow larger than the segment's
|
||||
// chunk budget is cut by the CLIENT into whole-width slabs, one resource_subdata each
|
||||
// (TextureEmit.h's MGPipeForEachTextureSlab). The server's half of that is here: place each run
|
||||
// where its record says and answer "covered" only once the level is whole. All the runs of one
|
||||
// level arrive in ONE apply batch (one verb), so nothing reads between them in production; what a
|
||||
// case can assert is what the readers would see at each instant, and that is what these do.
|
||||
TEST(StagedTextureStoreTest, StageChunkRunsAssembleTheLevelImageAndAGapIsNotCovered) {
|
||||
Server::StagedTextureStore store(/*copies=*/true);
|
||||
Server::StagedTextureStore monolithStore(/*copies=*/false);
|
||||
const Uint64 key = Server::StagedTextureStore::KeyForHandle(TestHandle(11, 1));
|
||||
// A 4x4 level of four bytes per texel - 64 bytes - cut into two 32-byte runs.
|
||||
const IntVec3 extent{4, 4, 1};
|
||||
Vector<Uint8> lower(32, 0x33);
|
||||
Vector<Uint8> upper(32, 0x77);
|
||||
|
||||
// THE SECOND RUN FIRST, deliberately: coverage is a SET and the placement comes from the
|
||||
// record, so the order the pieces land in cannot matter.
|
||||
EXPECT_EQ(monolithStore.AdoptRun(key, kTex2DTarget, 0, extent, 32, upper.data(), upper.size()),
|
||||
nullptr);
|
||||
EXPECT_EQ(monolithStore.TrackedResources(), 0u);
|
||||
store.AdoptRun(key, kTex2DTarget, 0, extent, 32, upper.data(), upper.size());
|
||||
EXPECT_TRUE(store.IsLevelDefined(key, kTex2DTarget, 0));
|
||||
EXPECT_EQ(store.LevelByteSize(key, kTex2DTarget, 0), 64u) << "the image grew to the run's end";
|
||||
EXPECT_FALSE(store.IsCovered(key, kTex2DTarget, 0))
|
||||
<< "half a level read as covered, so a sync would take zeroes for the missing half";
|
||||
EXPECT_EQ(store.LevelCoveredRunCount(key, kTex2DTarget, 0), 1u);
|
||||
|
||||
store.AdoptRun(key, kTex2DTarget, 0, extent, 0, lower.data(), lower.size());
|
||||
EXPECT_TRUE(store.IsCovered(key, kTex2DTarget, 0));
|
||||
EXPECT_EQ(store.LevelCoveredRunCount(key, kTex2DTarget, 0), 1u)
|
||||
<< "two adjacent runs must merge into one covered range";
|
||||
const Uint8* base = store.RequireLevelBytes(key, kTex2DTarget, 0, "unit_stage_chunk_runs");
|
||||
ASSERT_NE(base, nullptr);
|
||||
for (SizeT i = 0; i < 32; ++i) EXPECT_EQ(base[i], 0x33) << "byte " << i << " of the lower run";
|
||||
for (SizeT i = 32; i < 64; ++i) EXPECT_EQ(base[i], 0x77) << "byte " << i << " of the upper run";
|
||||
|
||||
// w1's retired-stage poison, at the store's own scope: the COPY is what answers the readers.
|
||||
std::fill(lower.begin(), lower.end(), Uint8{0xDD});
|
||||
std::fill(upper.begin(), upper.end(), Uint8{0xDD});
|
||||
EXPECT_EQ(base[0], 0x33);
|
||||
EXPECT_EQ(base[63], 0x77);
|
||||
|
||||
// AND A GAP IS NOT COVERAGE: it is the missing-piece case, which is the one thing the covered
|
||||
// set exists to make visible instead of papered over.
|
||||
const Uint64 holed = Server::StagedTextureStore::KeyForHandle(TestHandle(12, 1));
|
||||
Vector<Uint8> part(16, 0x11);
|
||||
store.AdoptRun(holed, kTex2DTarget, 0, extent, 0, part.data(), part.size());
|
||||
store.AdoptRun(holed, kTex2DTarget, 0, extent, 32, part.data(), part.size());
|
||||
EXPECT_EQ(store.LevelCoveredRunCount(holed, kTex2DTarget, 0), 2u)
|
||||
<< "two runs with a 16-byte gap merged into one";
|
||||
EXPECT_FALSE(store.IsCovered(holed, kTex2DTarget, 0)) << "a span across the gap reads covered";
|
||||
// Filling the gap merges all three into one run: adjacency, not proximity, is the rule.
|
||||
store.AdoptRun(holed, kTex2DTarget, 0, extent, 16, part.data(), part.size());
|
||||
EXPECT_EQ(store.LevelCoveredRunCount(holed, kTex2DTarget, 0), 1u);
|
||||
EXPECT_TRUE(store.IsCovered(holed, kTex2DTarget, 0));
|
||||
|
||||
// An extent move redefines the coordinate system, so the assembled runs go with it - the same
|
||||
// rule the whole-level adoption has always been held to.
|
||||
store.NoteLevelDefined(key, kTex2DTarget, 0, IntVec3{8, 8, 1});
|
||||
EXPECT_FALSE(store.IsCovered(key, kTex2DTarget, 0));
|
||||
EXPECT_EQ(store.LevelCoveredRunCount(key, kTex2DTarget, 0), 0u);
|
||||
EXPECT_EQ(store.LevelByteSize(key, kTex2DTarget, 0), 0u);
|
||||
}
|
||||
|
||||
// WHICH OF THE TWO STAGED-RUN SHAPES A RECORD CARRIES, read off the run's own LENGTH because the
|
||||
// wire layout is frozen and carries no field for it. The whole-level run (the shape this half has
|
||||
// always staged) is the level's complete shadow and begins at the level's first byte; a slab's run
|
||||
// is exactly the byte extent of its own box, so the box names where it goes. Getting this wrong
|
||||
// places a whole level's bytes at a dirty box's offset, or a slab's bytes at the level's base.
|
||||
TEST(StagedTextureStoreTest, TheRunImageOffsetIsReadOffTheRecordsOwnRunAndBox) {
|
||||
// THE TRACE'S SLAB: 512x128 RGBA32F, slices 8..16 of a 33-slice level.
|
||||
constexpr Uint64 kSliceStride = 512ull * 128ull * 16ull;
|
||||
constexpr Uint64 kLevelBytes = kSliceStride * 33ull;
|
||||
MG_Pipe::MGPSubData slab{};
|
||||
slab.LevelWidth = 512;
|
||||
slab.LevelHeight = 128;
|
||||
slab.LevelDepth = 33;
|
||||
slab.UnionBox = MG_Pipe::MGPBox{0, 0, 8, 512, 128, 8};
|
||||
slab.RegionCount = 1;
|
||||
slab.Blob.Size = 8ull * kSliceStride;
|
||||
MG_Pipe::MGPSubRegion region{};
|
||||
region.X = 0;
|
||||
region.Y = 0;
|
||||
region.Z = 8;
|
||||
region.W = 512;
|
||||
region.H = 128;
|
||||
region.D = 8;
|
||||
region.SrcRowStride = 512u * 16u;
|
||||
region.SrcSliceStride = static_cast<Uint32>(kSliceStride);
|
||||
EXPECT_EQ(Server::StagedTextureRunImageOffset(slab, ®ion), 8ull * kSliceStride);
|
||||
|
||||
// THE SAME FIELDS WITH A WHOLE-LEVEL RUN - only the length differs - is the whole-level shape,
|
||||
// whose run begins at the level's first byte however the box describes the dirty texels.
|
||||
MG_Pipe::MGPSubData whole = slab;
|
||||
whole.UnionBox = MG_Pipe::MGPBox{16, 32, 0, 64, 32, 1};
|
||||
whole.Blob.Size = kLevelBytes;
|
||||
EXPECT_EQ(Server::StagedTextureRunImageOffset(whole, ®ion), 0u);
|
||||
|
||||
// A whole-level BOX over a whole-level RUN is the same answer by the other route: the box
|
||||
// starts at the level's first byte, so "derived from the box" and "offset 0" agree.
|
||||
whole.UnionBox = MG_Pipe::MGPBox{0, 0, 0, 512, 128, 33};
|
||||
EXPECT_EQ(Server::StagedTextureRunImageOffset(whole, ®ion), 0u);
|
||||
|
||||
// THE NO-REGION SPELLING is the whole-level one by construction: RegionCount == 0 means the
|
||||
// box is the whole story and the strides are 0 = tightly packed.
|
||||
EXPECT_EQ(Server::StagedTextureRunImageOffset(slab, nullptr), 0u);
|
||||
slab.RegionCount = 0;
|
||||
EXPECT_EQ(Server::StagedTextureRunImageOffset(slab, ®ion), 0u);
|
||||
slab.RegionCount = 1;
|
||||
|
||||
// A RUN SHORTER THAN ITS BOX'S EXTENT is neither shape. No emitter produces one, and the
|
||||
// whole-level reading is the only one that cannot lose texels the record did carry.
|
||||
MG_Pipe::MGPSubData shortRun = slab;
|
||||
shortRun.Blob.Size = 8ull * kSliceStride - 16ull;
|
||||
EXPECT_EQ(Server::StagedTextureRunImageOffset(shortRun, ®ion), 0u);
|
||||
}
|
||||
|
||||
// The Fatal, and it asserts ITS OWN failure string rather than "the process died" -
|
||||
// ServerLoopTest.cpp:702-704's reason: a death test that only checks for a crash goes green
|
||||
// on any other abort in the same body. ONE death per case: the forked children of two
|
||||
@@ -272,6 +391,32 @@ TEST(StagedTextureStoreTest, AGpuGeneratedLevelHasNoBytesAndItsTexelReadIsFatalB
|
||||
}
|
||||
#endif
|
||||
|
||||
// The same refusal for a level that arrived IN PIECES (fix A2): the sync reads the level image
|
||||
// whole, so a level with one piece missing is as unreadable as one that never arrived - and it is
|
||||
// the same named Fatal. ONE DEATH PER CASE, ServerLoopTest's ruling above.
|
||||
#if !defined(_WIN32)
|
||||
TEST(StagedTextureStoreTest, ALevelWithAPieceMissingIsRefusedAsAWholeLevelReadByName) {
|
||||
Server::StagedTextureStore store(/*copies=*/true);
|
||||
const Uint64 key = Server::StagedTextureStore::KeyForHandle(TestHandle(13, 1));
|
||||
Vector<Uint8> half(32, 0x55);
|
||||
Vector<Uint8> whole(64, 0x22);
|
||||
store.AdoptRun(key, kTex2DTarget, 0, IntVec3{4, 4, 1}, 0, half.data(), half.size());
|
||||
ASSERT_FALSE(store.IsCovered(key, kTex2DTarget, 0));
|
||||
ASSERT_EQ(store.LevelCoveredRunCount(key, kTex2DTarget, 0), 1u);
|
||||
|
||||
// A DIFFERENT level of the same store, adopted whole, reads fine - asserted first so that the
|
||||
// death below cannot be a function that aborts on everything.
|
||||
store.AdoptRun(key, kTex2DTarget, 1, IntVec3{4, 4, 1}, 0, whole.data(), whole.size());
|
||||
ASSERT_NE(store.RequireLevelBytes(key, kTex2DTarget, 1, "unit"), nullptr);
|
||||
|
||||
EXPECT_EXIT(store.RequireLevelBytes(key, kTex2DTarget, 0, "unit_missing_piece"),
|
||||
::testing::KilledBySignal(SIGABRT), ".*");
|
||||
const std::string log = ReadLog();
|
||||
EXPECT_NE(log.find("Fatal{StageSnapshotTooNarrow, \"unit_missing_piece\"}"), std::string::npos)
|
||||
<< "the abort happened but not for this rule's reason; the log says: " << log;
|
||||
}
|
||||
#endif
|
||||
|
||||
// =====================================================================================
|
||||
// The production wiring - E-P5c gate #2 at unit scope
|
||||
// =====================================================================================
|
||||
@@ -418,6 +563,150 @@ TEST(StagedTextureProductionTest, HooklessTextureConsumerOwnsBytesAndScopedStora
|
||||
EXPECT_FALSE(store.HasShadow(nextKey));
|
||||
}
|
||||
|
||||
// FIX A2's PRODUCTION WIRING, hookless arm: the two runs of ONE level cross the applier as two
|
||||
// records and reach the store through PipeApply.cpp's AdoptTextureWithoutBackendHook, which has no
|
||||
// backend hook to lean on. Each must land where its OWN box and carried strides say - the record is
|
||||
// the only place the placement exists. The second run is sent first so that a store that ignored
|
||||
// the box and grew the image from the front cannot pass.
|
||||
TEST(StagedTextureProductionTest, StageChunkRunsThroughTheHooklessApplierLandWhereTheirBoxesSay) {
|
||||
// A record-only texture consumer has no texture callbacks in the buffer ops table, which is
|
||||
// exactly the arm that takes the fallback rather than Ops_H_TextureSubData.
|
||||
const MG_Pipe::MGPipeResourceOps emptyOps{};
|
||||
struct RestoreOps {
|
||||
const MG_Pipe::MGPipeResourceOps* Previous = MG_Pipe::MGPipeGetResourceOps();
|
||||
~RestoreOps() { MG_Pipe::MGPipeSetResourceOps(Previous); }
|
||||
} restore;
|
||||
MG_Pipe::MGPipeSetResourceOps(&emptyOps);
|
||||
|
||||
auto& store = Server::ServerStagedTexture();
|
||||
const auto res = TestHandle(45, 1);
|
||||
const auto key = Server::StagedTextureStore::KeyForHandle(res);
|
||||
MG_Pipe::MGPResourceDesc desc{};
|
||||
desc.Resource = res;
|
||||
desc.Target = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D);
|
||||
desc.Width = 4;
|
||||
desc.Height = 4;
|
||||
desc.Depth = 1;
|
||||
desc.Levels = 1;
|
||||
ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceCreate(desc));
|
||||
|
||||
// A 4x4 RGBA8 level: 64 bytes, four bytes per texel, a 16-byte row pitch. The two runs are its
|
||||
// upper and lower halves, 32 bytes each.
|
||||
Vector<Uint8> upper(32, 0x77);
|
||||
Vector<Uint8> lower(32, 0x33);
|
||||
MG_Pipe::MGPSubData upperRecord{};
|
||||
upperRecord.Res = res;
|
||||
upperRecord.Target = MG_Pipe::MGPipePackSubDataTarget(
|
||||
static_cast<Uint32>(MG_Pipe::MGPipeResourceTarget::Tex2D),
|
||||
static_cast<Uint32>(TextureUploadTarget::Texture2D));
|
||||
upperRecord.LevelWidth = 4;
|
||||
upperRecord.LevelHeight = 4;
|
||||
upperRecord.LevelDepth = 1;
|
||||
upperRecord.UnionBox = MG_Pipe::MGPBox{0, 2, 0, 4, 2, 1};
|
||||
upperRecord.RegionCount = 1;
|
||||
upperRecord.Blob.Size = upper.size();
|
||||
MG_Pipe::MGPSubRegion upperRegion{};
|
||||
upperRegion.Y = 2;
|
||||
upperRegion.W = 4;
|
||||
upperRegion.H = 2;
|
||||
upperRegion.D = 1;
|
||||
upperRegion.SrcOffset = 0;
|
||||
upperRegion.SrcRowStride = 16;
|
||||
upperRegion.SrcSliceStride = 64;
|
||||
ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceSubData(upperRecord, upper.data(), &upperRegion));
|
||||
EXPECT_EQ(store.LevelByteSize(key, kTex2DTarget, 0), 64u)
|
||||
<< "the image did not grow to the second half's end";
|
||||
EXPECT_EQ(store.LevelCoveredRunCount(key, kTex2DTarget, 0), 1u);
|
||||
EXPECT_FALSE(store.IsCovered(key, kTex2DTarget, 0)) << "only half the level has arrived";
|
||||
|
||||
MG_Pipe::MGPSubData lowerRecord = upperRecord;
|
||||
lowerRecord.UnionBox = MG_Pipe::MGPBox{0, 0, 0, 4, 2, 1};
|
||||
MG_Pipe::MGPSubRegion lowerRegion = upperRegion;
|
||||
lowerRegion.Y = 0;
|
||||
lowerRegion.SrcOffset = 0;
|
||||
ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceSubData(lowerRecord, lower.data(), &lowerRegion));
|
||||
EXPECT_TRUE(store.IsCovered(key, kTex2DTarget, 0));
|
||||
EXPECT_EQ(store.LevelCoveredRunCount(key, kTex2DTarget, 0), 1u)
|
||||
<< "the two halves did not merge into one covered range";
|
||||
const Uint8* base = store.RequireLevelBytes(key, kTex2DTarget, 0, "unit_production_runs");
|
||||
ASSERT_NE(base, nullptr);
|
||||
for (SizeT i = 0; i < 32; ++i) EXPECT_EQ(base[i], 0x33) << "byte " << i << " of the lower run";
|
||||
for (SizeT i = 32; i < 64; ++i) EXPECT_EQ(base[i], 0x77) << "byte " << i << " of the upper run";
|
||||
|
||||
MG_Pipe::MGPHandleOnly death{};
|
||||
death.Handle = res;
|
||||
death.Kind = static_cast<Uint32>(MG_Pipe::MGPipeKind::Texture);
|
||||
MG_Pipe::MGPipeApplyResourceDestroy(death);
|
||||
EXPECT_FALSE(store.HasShadow(key));
|
||||
}
|
||||
|
||||
// THE SAME, through the REAL texture hook DirectGLES registers (Ops_H_TextureSubData), which is the
|
||||
// arm the shipped backend takes. Reverting the offset - passing 0, or adopting every run as if it
|
||||
// were the whole level - puts both halves at the front of the image and turns the byte checks below
|
||||
// red.
|
||||
TEST(StagedTextureProductionTest, StageChunkRunsThroughTheRealTextureHookLandWhereTheirBoxesSay) {
|
||||
MG_Backend::DirectGLES::BufferImpl::RegisterBufferBackendOps();
|
||||
const MG_Pipe::MGPipeResourceOps* ops = MG_Pipe::MGPipeGetResourceOps();
|
||||
ASSERT_NE(ops, nullptr);
|
||||
ASSERT_NE(ops->TextureSubData, nullptr);
|
||||
|
||||
auto& store = Server::ServerStagedTexture();
|
||||
const auto res = TestHandle(46, 1);
|
||||
const auto key = Server::StagedTextureStore::KeyForHandle(res);
|
||||
MG_Pipe::MGPResourceDesc desc{};
|
||||
desc.Resource = res;
|
||||
desc.Target = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D);
|
||||
desc.Width = 4;
|
||||
desc.Height = 4;
|
||||
desc.Depth = 1;
|
||||
desc.Levels = 1;
|
||||
ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceCreate(desc));
|
||||
|
||||
Vector<Uint8> upper(32, 0x77);
|
||||
Vector<Uint8> lower(32, 0x33);
|
||||
MG_Pipe::MGPSubData upperRecord{};
|
||||
upperRecord.Res = res;
|
||||
upperRecord.Target = MG_Pipe::MGPipePackSubDataTarget(
|
||||
static_cast<Uint32>(MG_Pipe::MGPipeResourceTarget::Tex2D),
|
||||
static_cast<Uint32>(TextureUploadTarget::Texture2D));
|
||||
upperRecord.LevelWidth = 4;
|
||||
upperRecord.LevelHeight = 4;
|
||||
upperRecord.LevelDepth = 1;
|
||||
upperRecord.UnionBox = MG_Pipe::MGPBox{0, 2, 0, 4, 2, 1};
|
||||
upperRecord.RegionCount = 1;
|
||||
upperRecord.Blob.Size = upper.size();
|
||||
MG_Pipe::MGPSubRegion upperRegion{};
|
||||
upperRegion.Y = 2;
|
||||
upperRegion.W = 4;
|
||||
upperRegion.H = 2;
|
||||
upperRegion.D = 1;
|
||||
upperRegion.SrcRowStride = 16;
|
||||
upperRegion.SrcSliceStride = 64;
|
||||
// THE HOOK, not MGPipeApplyResourceSubData: the applier's own dispatch is what routes a
|
||||
// texture record into Ops_H_TextureSubData, and that is the call site under test.
|
||||
ops->TextureSubData(res, upperRecord, upper.data(), &upperRegion);
|
||||
EXPECT_EQ(store.LevelByteSize(key, kTex2DTarget, 0), 64u);
|
||||
EXPECT_EQ(store.LevelCoveredRunCount(key, kTex2DTarget, 0), 1u);
|
||||
EXPECT_FALSE(store.IsCovered(key, kTex2DTarget, 0));
|
||||
|
||||
MG_Pipe::MGPSubData lowerRecord = upperRecord;
|
||||
lowerRecord.UnionBox = MG_Pipe::MGPBox{0, 0, 0, 4, 2, 1};
|
||||
MG_Pipe::MGPSubRegion lowerRegion = upperRegion;
|
||||
lowerRegion.Y = 0;
|
||||
ops->TextureSubData(res, lowerRecord, lower.data(), &lowerRegion);
|
||||
EXPECT_TRUE(store.IsCovered(key, kTex2DTarget, 0));
|
||||
const Uint8* base = store.RequireLevelBytes(key, kTex2DTarget, 0, "unit_hook_runs");
|
||||
ASSERT_NE(base, nullptr);
|
||||
for (SizeT i = 0; i < 32; ++i) EXPECT_EQ(base[i], 0x33) << "byte " << i << " of the lower run";
|
||||
for (SizeT i = 32; i < 64; ++i) EXPECT_EQ(base[i], 0x77) << "byte " << i << " of the upper run";
|
||||
|
||||
MG_Pipe::MGPHandleOnly death{};
|
||||
death.Handle = res;
|
||||
death.Kind = static_cast<Uint32>(MG_Pipe::MGPipeKind::Texture);
|
||||
MG_Pipe::MGPipeApplyResourceDestroy(death);
|
||||
EXPECT_FALSE(store.HasShadow(key));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first
|
||||
// write, and caches the FILE*. The name carries this process's pid, because
|
||||
|
||||
Reference in New Issue
Block a user