mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-13 04:53:23 +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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user