From caaedd7abe85e963af5ae38991b7b9dcdb1c2dad Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 23 Sep 2026 11:09:26 -0400 Subject: [PATCH] [MG_Remote, MG_Pipe, MG_Backend, MG_Test] (P7): repair F2 ID-49, byteless-level and R8 lane regressions --- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 8 +- .../DirectVulkan/Renderer/WireFramebuffer.inc | 6 +- MobileGL/MG_Remote/Server/PipeApplier.cpp | 20 ++++- .../MG_Remote/Server/StagedTextureStore.h | 37 +++++++-- MobileGL/MG_Test/SanityTest.cpp | 48 ++++++++++++ MobileGL/MG_Test/Wire/ServerLoopTest.cpp | 14 +++- .../MG_Test/Wire/StagedTextureStoreTest.cpp | 76 ++++++++++++++++++- 7 files changed, 197 insertions(+), 12 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 682f1d57..adcd622f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -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(uploadTarget), static_cast(level), - ComputeMipTexelSize(storageBaseTexelSize, level)); + ComputeMipTexelSize(storageBaseTexelSize, level), + static_cast(MG_Pipe::MGPipeResourceTarget::Tex2D), + static_cast(TextureInternalFormat::Unknown)); store.MarkLevelGpuDirty(key, static_cast(uploadTarget), static_cast(level), true); } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc index 423257ab..ebdc7645 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc @@ -1474,7 +1474,11 @@ for (Uint32 level=base+1;level(upload),static_cast(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(upload),static_cast(level),extent, + record.Desc.Target,record.Desc.InternalFormat); store.MarkLevelGpuDirty(key,static_cast(upload),static_cast(level),true); } } diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index b9f86563..896c1997 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -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(std::numeric_limits::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(info.DstSize), + static_cast(tight), info.Box.W, info.Box.H, + static_cast(bpp)); + } const MG_Backend::GlobalBackendFunctionsTable* table = Table("read_pixels"); if (table == nullptr || table->GL.ReadPixels == nullptr) { diff --git a/MobileGL/MG_Remote/Server/StagedTextureStore.h b/MobileGL/MG_Remote/Server/StagedTextureStore.h index 47bd983e..8b136596 100644 --- a/MobileGL/MG_Remote/Server/StagedTextureStore.h +++ b/MobileGL/MG_Remote/Server/StagedTextureStore.h @@ -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(MG_Pipe::MGPipeResourceTarget::Tex2D), Uint32 internalFormat = static_cast(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(internalFormat)) == 0 || !validExtent || - (supportsSubData && !emptyExtent && !StagedTextureDeclaredLevelByteBound( + const Bool sizedFormat = MG_Util::GetSizedInternalFormatSizeInBytes( + static_cast(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(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(key), uploadTarget, level, static_cast(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(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\"} - " diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index b2028965..e840edf1 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -47,6 +47,9 @@ // a split-armed case has to arm that half too - registering an op table is the SERVER's arming. #include #include +// PH-2 (F2): the handle-keyed tables' backstop refusals die through MG_Pipe's session-fail seam. +#include +#include #endif #include #include @@ -3519,6 +3522,19 @@ namespace { using FakeShaderCsoSlotTable = MobileGL::MG_Backend::DirectGLES:: BackendSlotTable; +#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); } diff --git a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp index 5ae94eaf..ab1a8de1 100644 --- a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp @@ -735,7 +735,9 @@ TEST(ServerLoopTest, TheAuditPoisonFillsExactlyTheStagedRunAfterTheApplierReturn create.Resource.Slot = 61; create.Resource.Gen = 1; create.Target = static_cast(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(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(MG_Pipe::MGPipeResourceTarget::Tex2D), 0u), + 0, 4, 4, 1); + ASSERT_TRUE(fixture.EmitAndWait(MG_Pipe::MGPWireOp::ResourceRespecify, &respecify, sizeof(respecify))); Vector texels(4 * 4 * 4, 0x5A); MG_Pipe::MGPSubData upload{}; diff --git a/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp b/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp index 0feacd79..9206363d 100644 --- a/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp +++ b/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp @@ -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(MG_Pipe::MGPipeResourceTarget::Tex2D), + static_cast(TextureInternalFormat::R8)); + ASSERT_EQ(store.LevelDeclaredByteBound(key, kTex2DTarget, 0), 16u) << "4x4 texels of one byte"; + Vector 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(MG_Pipe::MGPipeResourceTarget::Tex2D); + store.NoteLevelDefined(key, kTex2DTarget, 0, IntVec3{1, 1, 1}, tex2D, + static_cast(TextureInternalFormat::Unknown)); + store.NoteLevelDefined(key, kTex2DTarget, 1, IntVec3{0, 0, 1}, tex2D, + static_cast(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 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(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(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 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(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(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(MG_Pipe::MGPipeResourceTarget::Tex2D); + desc.InternalFormat = static_cast(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(MG_Pipe::MGPipeResourceTarget::Tex2D); + desc.InternalFormat = static_cast(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 upper(32, 0x77); Vector lower(32, 0x33);