[MG_Remote, MG_Pipe, MG_Backend, MG_Test] (P7): repair F2 ID-49, byteless-level and R8 lane regressions

This commit is contained in:
2026-09-23 13:44:37 -04:00
parent 10c5168be2
commit caaedd7abe
7 changed files with 197 additions and 12 deletions
@@ -1694,8 +1694,14 @@ void main() {
for (Uint32 level = baseMipLevel + 1; level < requiredMipLevelCount; ++level) {
// A level the shadow already tracks (an adopted base chain) keeps its bytes;
// the generation made the GPU newer than either, which the mark says.
// PH-4: a twin-address key never receives a staged run (no Adopt names one),
// so these generated levels are declared BYTELESS - an Unknown format gives a
// zero byte bound and skips the Tex2D device-limit rule the defaults would
// apply to a 2D-array or 3D level's depth.
store.NoteLevelDefined(key, static_cast<Uint16>(uploadTarget), static_cast<Uint16>(level),
ComputeMipTexelSize(storageBaseTexelSize, level));
ComputeMipTexelSize(storageBaseTexelSize, level),
static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D),
static_cast<Uint32>(TextureInternalFormat::Unknown));
store.MarkLevelGpuDirty(key, static_cast<Uint16>(uploadTarget), static_cast<Uint16>(level),
true);
}
@@ -1474,7 +1474,11 @@
for (Uint32 level=base+1;level<base+count;++level) {
const IntVec3 extent = MG_Remote::Server::StagedTextureMipExtent(
record.Desc.Target, record.Desc.Width, record.Desc.Height, record.Desc.Depth, level);
store.NoteLevelDefined(key,static_cast<Uint16>(upload),static_cast<Uint16>(level),extent);
// PH-4: the record's own target and format, not NoteLevelDefined's Tex2D/RGBA8
// defaults - a 2D-array or 3D level has depth > 1, which the Tex2D device-limit rule
// refuses by name (TextureViewScenario.GenerateMipmapThroughAView on all Magma arms).
store.NoteLevelDefined(key,static_cast<Uint16>(upload),static_cast<Uint16>(level),extent,
record.Desc.Target,record.Desc.InternalFormat);
store.MarkLevelGpuDirty(key,static_cast<Uint16>(upload),static_cast<Uint16>(level),true);
}
}
+19 -1
View File
@@ -429,10 +429,28 @@ namespace MobileGL::MG_Remote::Server {
const Uint64 pixels = width * height;
if (pixels > kUint64Max / bpp) return rejectReadback();
const Uint64 tight = pixels * bpp;
if (tight != info.DstSize || tight > m_maxReplyBytes ||
// PH-3's bound is the SERVER's answer size: w*h*bpp against its own maxReplyBytes,
// checked before the scratch grows. It is not a rule about DstSize.
if (tight > m_maxReplyBytes ||
tight > static_cast<Uint64>(std::numeric_limits<SizeT>::max())) {
return rejectReadback();
}
// ID-49: THE REPLY CROSSES TIGHT, WHATEVER DstSize THE CLIENT SENT. The server reads with
// neutral pack state into a w*h*bpp extent that IS the reply payload and the client
// scatters it per its own GL_PACK_* state, so a DstSize that disagrees with the tight
// extent (a client that sized its destination with pack padding) is logged and the tight
// extent this side owns is read and posted - never the client's number, so a wrong
// DstSize cannot make this a short read into uninitialised scratch. F2's first PH-3 draft
// refused the mismatch instead, which broke the ID-49 control
// (ServerLoopEglTest.AReadPixelsReplyIsTheTightExtentWhateverDstSizeTheClientSent); the
// bound above is what PH-3 needs and it does not depend on DstSize.
if (tight != info.DstSize) {
MGLOG_E_ONCE("MG_Remote server: read_pixels DstSize %llu != tight w*h*bpp %llu "
"(%ux%u, bpp %llu); reading the tight extent (ID-49)",
static_cast<unsigned long long>(info.DstSize),
static_cast<unsigned long long>(tight), info.Box.W, info.Box.H,
static_cast<unsigned long long>(bpp));
}
const MG_Backend::GlobalBackendFunctionsTable* table = Table("read_pixels");
if (table == nullptr || table->GL.ReadPixels == nullptr) {
+30 -7
View File
@@ -385,6 +385,16 @@ namespace MobileGL::MG_Remote::Server {
// a resource_subdata against it is RequireDeclaredLevel's refusal rather than a write
// into storage that has no texels. Only a NEGATIVE component (a 32-bit carrier word past
// INT_MAX) is malformed.
//
// SO IS A LEVEL WHOSE FORMAT HAS NO UNCOMPRESSED SIZE HERE (F2 lane repair). A declared
// TextureInternalFormat::Unknown level - the TextureView, TextureViewAlias and
// TextureUploadShape split lanes all declare one, a 1x1x1 level of an Unknown-format
// texture, before any upload - is legal and byteless in exactly the empty level's sense:
// declared with a zero byte bound, so any run into it is refused by name
// (StagedTextureStore.LevelBound) and the declaration itself is not a session Fatal. What
// stays a named refusal here is a declaration this server cannot bound: a negative
// component, or a known-format level past the server's own device limits (or whose byte
// count overflows).
void NoteLevelDefined(Uint64 key, Uint16 uploadTarget, Uint16 level, const IntVec3& extent,
Uint8 resourceTarget = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D),
Uint32 internalFormat = static_cast<Uint32>(TextureInternalFormat::RGBA8)) {
@@ -394,9 +404,10 @@ namespace MobileGL::MG_Remote::Server {
const Bool supportsSubData = StagedTextureTargetSupportsSubData(resourceTarget);
const Bool validExtent = extent.x() >= 0 && extent.y() >= 0 && extent.z() >= 0;
const Bool emptyExtent = validExtent && (extent.x() == 0 || extent.y() == 0 || extent.z() == 0);
if (MG_Util::GetSizedInternalFormatSizeInBytes(
static_cast<TextureInternalFormat>(internalFormat)) == 0 || !validExtent ||
(supportsSubData && !emptyExtent && !StagedTextureDeclaredLevelByteBound(
const Bool sizedFormat = MG_Util::GetSizedInternalFormatSizeInBytes(
static_cast<TextureInternalFormat>(internalFormat)) != 0;
if (!validExtent ||
(supportsSubData && sizedFormat && !emptyExtent && !StagedTextureDeclaredLevelByteBound(
resourceTarget, internalFormat, extent, m_deviceLimits, &byteBound))) {
SessionFail(MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.NoteLevelDefined\"} - "
@@ -589,22 +600,34 @@ namespace MobileGL::MG_Remote::Server {
// coverage answer for such a level, not an error.
void RequireDeclaredLevel(LevelShadow& shadow, Uint64 key, Uint16 uploadTarget,
Uint16 level, const IntVec3& extent) {
if (!shadow.Defined || shadow.InternalFormat == 0) {
// `Defined` alone, which only NoteLevelDefined sets. The first draft also refused
// `InternalFormat == 0`, but 0 is TextureInternalFormat::R8, so every R8 upload read
// as "arrived before the level was declared".
if (!shadow.Defined) {
SessionFail(MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.LevelExtent\"} - "
"resource_subdata arrived before the server declared texture level "
"(key=%llu, uploadTarget=%u, level=%u)",
static_cast<unsigned long long>(key), uploadTarget, level);
}
if (!StagedTextureTargetSupportsSubData(shadow.ResourceTarget) ||
shadow.DeclaredByteBound == 0) {
if (!StagedTextureTargetSupportsSubData(shadow.ResourceTarget)) {
SessionFail(MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.Target\"} - "
"resource_subdata is not legal for the server-declared texture target "
"or empty level (key=%llu, uploadTarget=%u, level=%u, target=%u)",
"(key=%llu, uploadTarget=%u, level=%u, target=%u)",
static_cast<unsigned long long>(key), uploadTarget, level,
static_cast<Uint32>(shadow.ResourceTarget));
}
if (shadow.DeclaredByteBound == 0) {
SessionFail(MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.LevelBound\"} - "
"resource_subdata into a byteless declared level: its extent is empty or "
"its format has no uncompressed size on this server "
"(key=%llu, uploadTarget=%u, level=%u, extent=%d,%d,%d, internalFormat=%u)",
static_cast<unsigned long long>(key), uploadTarget, level,
shadow.Extent.x(), shadow.Extent.y(), shadow.Extent.z(),
shadow.InternalFormat);
}
if (shadow.Extent == extent) return;
SessionFail(MGFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.LevelExtent\"} - "
+48
View File
@@ -47,6 +47,9 @@
// a split-armed case has to arm that half too - registering an op table is the SERVER's arming.
#include <MG_Remote/CapsCodec.h>
#include <MG_Remote/Client/CapsMirror.h>
// PH-2 (F2): the handle-keyed tables' backstop refusals die through MG_Pipe's session-fail seam.
#include <MG_Pipe/PipeSessionFail.h>
#include <cstdio>
#endif
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Types.h>
@@ -3519,6 +3522,19 @@ namespace {
using FakeShaderCsoSlotTable = MobileGL::MG_Backend::DirectGLES::
BackendSlotTable<FakeStateObject, FakeBackendObject, MobileGL::MG_Pipe::MGPipeKind::ShaderCso>;
#if MOBILEGL_BUILD_DISAGGREGATED
// PH-2 (F2, ID-P7-54): on a disaggregated build the handle overload's backward-generation and
// past-the-bound refusals are NAMED session faults through MG_Pipe's seam, not a quiet null
// twin - the handle arrived in a peer's payload. The seam's no-hook default writes the line to
// the log FILE only, so a death child installs this hook first to put the same line where the
// death matcher reads it; the seam then logs and aborts exactly as it would have.
void EchoPipeSessionFailToStderr(MobileGL::MG_Pipe::MGPipeFatalFamily, const char* line) {
std::fputs(line, stderr);
std::fputc('\n', stderr);
std::fflush(stderr);
}
#endif
// A log file path no other process and no other case can be writing to: the pid keeps two
// SanityTest processes on one host apart, the counter keeps two cases in one process apart.
std::filesystem::path UniqueScratchLogPath(const char* stem) {
@@ -4333,10 +4349,21 @@ TEST(DirectGLESSlotTable, EverySwitchedOverKindResolvesItsTwinThroughTheHandleAr
// adoption the live twin at `second` is gone. The first is kept because a non-null there
// would mean the table handed back the incumbent's own twin under the dead handle, which
// is a third outcome and a worse one.
#if MOBILEGL_BUILD_DISAGGREGATED
// PH-2: the refusal is a named session fault here, so it is asserted in a death child;
// the live twin below is then the parent's, untouched by construction AND by assertion.
EXPECT_DEATH(
{
MG_Pipe::MGPipeInstallSessionFailHook(&EchoPipeSessionFailToStderr);
(void)table.GetOrCreate(first);
},
"BackendSlotTable.Generation");
#else
auto& stale = table.GetOrCreate(first);
EXPECT_EQ(stale, nullptr)
<< "SamplerViewCso: a stale handle was answered with the LIVE twin at its slot (this "
"assertion cannot tell a refusal from an adoption - the next one does)";
#endif
EXPECT_NE(table.FindByHandle(second), nullptr)
<< "SamplerViewCso: the live twin was destroyed by a handle from its slot's past - "
"the backward generation was ADOPTED rather than refused";
@@ -5024,10 +5051,22 @@ TEST(DirectGLESSlotTable, AGenerationBehindTheLiveTwinIsRefusedRatherThanAdopted
incumbent->marker = 0xC0FFEE;
const FakeBackendObject* const raw = incumbent.get();
#if MOBILEGL_BUILD_DISAGGREGATED
// PH-2 (F2): the backwards generation is a NAMED session fault on this build - the handle is a
// peer's payload - so the refusal is asserted in a death child and the incumbent below is the
// parent's, which the refused call never touched.
EXPECT_DEATH(
{
MobileGL::MG_Pipe::MGPipeInstallSessionFailHook(&EchoPipeSessionFailToStderr);
(void)table.GetOrCreate(stale);
},
"BackendSlotTable.Generation");
#else
auto& answer = table.GetOrCreate(stale);
EXPECT_EQ(answer, nullptr)
<< "a backwards generation was handed a twin rather than refused; FindByHandle refuses "
"the same input, so the two entry points disagreed";
#endif
auto* const still = table.FindByHandle(current);
ASSERT_NE(still, nullptr) << "the live twin's entry was retired by a handle from its past";
@@ -5043,8 +5082,17 @@ TEST(DirectGLESSlotTable, AGenerationBehindTheLiveTwinIsRefusedRatherThanAdopted
EXPECT_EQ(table.FindByHandle(current), nullptr) << "the predecessor's handle still resolves";
// And a slot past the table's bound is refused rather than resized to.
#if MOBILEGL_BUILD_DISAGGREGATED
EXPECT_DEATH(
{
MobileGL::MG_Pipe::MGPipeInstallSessionFailHook(&EchoPipeSessionFailToStderr);
(void)table.GetOrCreate(MG_Pipe::MGPipeHandle{FakeSlotTable::kMaxHandleSlot, 1u});
},
"BackendSlotTable.HandleSlot");
#else
auto& absurd = table.GetOrCreate(MG_Pipe::MGPipeHandle{FakeSlotTable::kMaxHandleSlot, 1u});
EXPECT_EQ(absurd, nullptr) << "an unbounded client slot decided a vector resize";
#endif
EXPECT_EQ(table.FindByHandle(MG_Pipe::MGPipeHandle{FakeSlotTable::kMaxHandleSlot, 1u}), nullptr);
}
+13 -1
View File
@@ -735,7 +735,9 @@ TEST(ServerLoopTest, TheAuditPoisonFillsExactlyTheStagedRunAfterTheApplierReturn
create.Resource.Slot = 61;
create.Resource.Gen = 1;
create.Target = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D);
create.InternalFormat = 1;
// RGBA8, because the 64 staged bytes below are 4x4 texels of four bytes: PH-4 bounds the run
// by the declared level's w*h*d*bpp, and the old `1` (R8Snorm, one byte) bounded it at 16.
create.InternalFormat = static_cast<Uint32>(TextureInternalFormat::RGBA8);
create.Width = 4;
create.Height = 4;
create.Depth = 1;
@@ -743,6 +745,16 @@ TEST(ServerLoopTest, TheAuditPoisonFillsExactlyTheStagedRunAfterTheApplierReturn
create.Levels = 1;
create.Samples = 1;
ASSERT_TRUE(fixture.EmitAndWait(MG_Pipe::MGPWireOp::ResourceCreate, &create, sizeof(create)));
// PH-4: a level exists for resource_subdata only once a respecify declared it (the emitter's
// glTexImage*D always does). Declare level 0 at upload target 0 - the target the upload below
// packs - at the 4x4x1 extent the upload's descriptor-derived extent will name.
MG_Pipe::MGPResourceDesc respecify = create;
respecify.HasDefinedContent = 1;
MG_Pipe::MGPipeSetRespecifiedLevel(
respecify,
MG_Pipe::MGPipePackSubDataTarget(static_cast<Uint32>(MG_Pipe::MGPipeResourceTarget::Tex2D), 0u),
0, 4, 4, 1);
ASSERT_TRUE(fixture.EmitAndWait(MG_Pipe::MGPWireOp::ResourceRespecify, &respecify, sizeof(respecify)));
Vector<Uint8> texels(4 * 4 * 4, 0x5A);
MG_Pipe::MGPSubData upload{};
@@ -75,6 +75,17 @@ namespace {
return handle;
}
// PH-4: resource_subdata lands only in a level a respecify DECLARED - the emitter's
// glTexImage*D always sends one before the bytes. The production cases below used to adopt
// into a level nothing had declared, which PH-4 refuses by name
// (Fatal{ProtocolCorruption, "StagedTextureStore.LevelExtent"}), so each now declares level 0
// of the Texture2D upload target at its 4x4x1 extent through the real applier entry point.
void DeclareTex2DLevel0(const MG_Pipe::MGPResourceDesc& desc, Uint32 width, Uint32 height) {
const MG_Pipe::MGPRespecifiedLevel level = MG_Pipe::MGPipeMakeRespecifiedLevel(
MG_Pipe::MGPipePackSubDataTarget(desc.Target, kTex2DTarget), 0, width, height, 1);
ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceRespecify(desc, nullptr, &level));
}
} // namespace
// =====================================================================================
@@ -556,6 +567,54 @@ TEST(StagedTextureStoreTest, ALevelWithAPieceMissingIsRefusedAsAWholeLevelReadBy
}
#endif
// F2 LANE REPAIR, R8: TextureInternalFormat::R8 is enumerator 0, and PH-4's first draft read
// `InternalFormat == 0` as "never declared" - so an R8 level a respecify HAD declared refused its
// own upload as `StagedTextureStore.LevelExtent`. Red-once: put the `== 0` test back and the run
// below dies by that name.
TEST(StagedTextureStoreTest, AnR8LevelIsDeclaredAndTakesItsUpload) {
Server::StagedTextureStore store(/*copies=*/true);
const Uint64 key = Server::StagedTextureStore::KeyForHandle(TestHandle(15, 1));
store.NoteLevelDefined(key, kTex2DTarget, 0, IntVec3{4, 4, 1},
static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D),
static_cast<Uint32>(TextureInternalFormat::R8));
ASSERT_EQ(store.LevelDeclaredByteBound(key, kTex2DTarget, 0), 16u) << "4x4 texels of one byte";
Vector<Uint8> texels(16, 0x3C);
store.AdoptRun(key, kTex2DTarget, 0, IntVec3{4, 4, 1}, 0, texels.data(), texels.size());
EXPECT_TRUE(store.IsCovered(key, kTex2DTarget, 0));
EXPECT_EQ(store.RequireLevelBytes(key, kTex2DTarget, 0, "unit_r8")[15], 0x3C);
}
// F2 LANE REPAIR, BYTELESS DECLARATIONS: a level whose format has no uncompressed size here
// (TextureInternalFormat::Unknown - what the TextureView / TextureViewAlias / TextureUploadShape
// split lanes declare) and an EMPTY level (glTexImage*D with a zero extent -
// UnboundImageDescriptorScenario's incomplete default texture) are both legal DECLARATIONS: no
// Fatal at the respecify, the level exists, and its byte bound is 0. A staged run into such a
// level is then refused by its own name. Red-once: drop the LevelBound refusal and the same run
// dies as `StagedTextureStore.CopyRunInto` instead (end 4 > bound 0), which the name check
// below reads as red. ONE DEATH PER CASE, ServerLoopTest's ruling.
#if !defined(_WIN32)
TEST(StagedTextureStoreTest, ABytelessDeclaredLevelIsLegalAndRefusesARunByName) {
Server::StagedTextureStore store(/*copies=*/true);
const Uint64 key = Server::StagedTextureStore::KeyForHandle(TestHandle(16, 1));
const auto tex2D = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D);
store.NoteLevelDefined(key, kTex2DTarget, 0, IntVec3{1, 1, 1}, tex2D,
static_cast<Uint32>(TextureInternalFormat::Unknown));
store.NoteLevelDefined(key, kTex2DTarget, 1, IntVec3{0, 0, 1}, tex2D,
static_cast<Uint32>(TextureInternalFormat::RGBA8));
ASSERT_TRUE(store.IsLevelDefined(key, kTex2DTarget, 0));
ASSERT_TRUE(store.IsLevelDefined(key, kTex2DTarget, 1));
EXPECT_EQ(store.LevelDeclaredByteBound(key, kTex2DTarget, 0), 0u);
EXPECT_EQ(store.LevelDeclaredByteBound(key, kTex2DTarget, 1), 0u);
Vector<Uint8> texel(4, 0x5A);
EXPECT_EXIT(store.AdoptRun(key, kTex2DTarget, 0, IntVec3{1, 1, 1}, 0, texel.data(), texel.size()),
::testing::KilledBySignal(SIGABRT), ".*");
const std::string log = ReadLog();
EXPECT_NE(log.find("Fatal{ProtocolCorruption, \"StagedTextureStore.LevelBound\"}"), std::string::npos)
<< "the run died, but not as the byteless-level refusal; the log says: " << log;
}
#endif
// =====================================================================================
// The production wiring - E-P5c gate #2 at unit scope
// =====================================================================================
@@ -582,11 +641,15 @@ TEST(StagedTextureProductionTest, TextureSubDataThroughTheRealOpsTableCopiesAndS
MG_Pipe::MGPResourceDesc desc{};
desc.Resource = res;
desc.Target = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D);
// RGBA8: the 64 bytes below are 4x4 texels of four bytes, and PH-4 bounds the run by the
// declared level's w*h*d*bpp (a zero-initialised format is R8, one byte a texel).
desc.InternalFormat = static_cast<Uint32>(TextureInternalFormat::RGBA8);
desc.Width = 4;
desc.Height = 4;
desc.Depth = 1;
desc.Levels = 1;
ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceCreate(desc));
DeclareTex2DLevel0(desc, 4, 4);
Vector<Uint8> src(64, 0xAB); // 4x4 texels, 4 bytes each
MG_Pipe::MGPSubData rec{};
@@ -649,6 +712,8 @@ TEST(StagedTextureProductionTest, HooklessTextureConsumerOwnsBytesAndScopedStora
MG_Pipe::MGPResourceDesc desc{};
desc.Resource = res;
desc.Target = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D);
// RGBA8 for the 64-byte 4x4 upload below (PH-4's w*h*d*bpp bound; zero would be R8).
desc.InternalFormat = static_cast<Uint32>(TextureInternalFormat::RGBA8);
ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceCreate(desc));
desc.Width = 4;
desc.Height = 4;
@@ -672,8 +737,13 @@ TEST(StagedTextureProductionTest, HooklessTextureConsumerOwnsBytesAndScopedStora
std::fill(src.begin(), src.end(), Uint8{0xDD});
EXPECT_EQ(store.RequireLevelBytes(key, kTex2DTarget, 0, "hookless_source_poison")[0], 0xAB);
// Defining another mip must retain bytes already accepted for the first mip.
// Defining another mip must retain bytes already accepted for the first mip. PH-4: the scope
// carries that mip's EXACT extent (TextureEmit.h reads it off the frontend's level), so level 1
// of a 4x4 base crosses as 2x2x1 rather than inheriting level 0's numbers.
level.Level = 1;
level.Width = 2;
level.Height = 2;
level.Depth = 1;
ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceRespecify(desc, nullptr, &level));
EXPECT_EQ(store.LevelExtentOrUndefined(key, kTex2DTarget, 1), IntVec3(2, 2, 1));
EXPECT_TRUE(store.IsCovered(key, kTex2DTarget, 0));
@@ -722,11 +792,13 @@ TEST(StagedTextureProductionTest, StageChunkRunsThroughTheHooklessApplierLandWhe
MG_Pipe::MGPResourceDesc desc{};
desc.Resource = res;
desc.Target = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D);
desc.InternalFormat = static_cast<Uint32>(TextureInternalFormat::RGBA8); // PH-4 bound: 4x4x4
desc.Width = 4;
desc.Height = 4;
desc.Depth = 1;
desc.Levels = 1;
ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceCreate(desc));
DeclareTex2DLevel0(desc, 4, 4);
// 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.
@@ -794,11 +866,13 @@ TEST(StagedTextureProductionTest, StageChunkRunsThroughTheRealTextureHookLandWhe
MG_Pipe::MGPResourceDesc desc{};
desc.Resource = res;
desc.Target = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D);
desc.InternalFormat = static_cast<Uint32>(TextureInternalFormat::RGBA8); // PH-4 bound: 4x4x4
desc.Width = 4;
desc.Height = 4;
desc.Depth = 1;
desc.Levels = 1;
ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceCreate(desc));
DeclareTex2DLevel0(desc, 4, 4);
Vector<Uint8> upper(32, 0x77);
Vector<Uint8> lower(32, 0x33);