diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 7cfecead..a05dcced 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -2393,6 +2393,22 @@ if (MOBILEGL_BUILD_DISAGGREGATED) TIMEOUT ${MGL_ITEST_TIMEOUT} ENVIRONMENT "${MGL_ITEST_MAGMA_WIRE_ENVIRONMENT}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p5f-fm-${fmSlot}.log") endforeach() + foreach(fmCase ClipDistanceScenario.AnEnabledClipDistanceRemovesTheNegativeHalf + ClipDistanceScenario.ADisabledClipDistanceRemovesNothing + ClipDistanceScenario.TheEnablesAreIndependentPerDistance + CopyImageLayeredScenario.ArrayToArrayCopiesEverySlice + CopyImageLayeredScenario.ArrayToArrayHonoursDifferentLayerOffsets + CopyImageLayeredScenario.VolumeToVolumeHonoursNonZeroZ + CopyImageLayeredScenario.VolumeToVolumeAtNonZeroMipLevel + CopyImageLayeredScenario.ArrayToVolumeCopiesEverySlice + CopyImageLayeredScenario.VolumeToArrayCopiesEverySlice) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectVulkan.Split.Fm." + TEST_FILTER "${fmCase}" + DISCOVERY_TIMEOUT 30 + PROPERTIES LABELS "integration-magma-split" TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_MAGMA_WIRE_ENVIRONMENT}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p5f-fm-${fmCase}.log") + endforeach() endif() # P5c ct (MG_Remote/CONTRACT-P5C.md ยง5): the two control records, split-only source for the diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index 6074bd61..a930a6a2 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -45,6 +45,7 @@ #include #include #include +#include #include #endif @@ -1012,6 +1013,59 @@ namespace MobileGL::MG_Pipe { return true; } +#if MOBILEGL_BUILD_DISAGGREGATED + // Texture records can have a server consumer without the buffer ops table (Magma). + // 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 MGPResourceRecord& stored, const MGPSubData& upload, + const void* bytes) { + 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::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, desc.Depth, upload.Level), + bytes, static_cast(upload.Blob.Size)); + } + + void DefineTextureWithoutBackendHook(const MGPResourceDesc& desc, const MGPRespecifiedLevel* level) { + if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return; + auto& store = MG_Remote::Server::ServerStagedTexture(); + const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(desc.Resource); + const auto define = [&](Uint16 target, Uint16 mip) { + store.NoteLevelDefined(key, target, mip, MG_Remote::Server::StagedTextureMipExtent( + desc.Target, desc.Width, desc.Height, desc.Depth, mip)); + }; + if (level) { + define(MGPipeSubDataUploadTargetOf(level->UploadTarget), level->Level); + return; + } + store.ResetLevels(key); + if (!desc.Immutable || !desc.Levels) return; + const auto defineChain = [&](TextureUploadTarget target) { + for (Uint32 mip = 0; mip < desc.Levels; ++mip) define(static_cast(target), static_cast(mip)); + }; + switch (static_cast(desc.Target)) { + case MGPipeResourceTarget::Tex1D: defineChain(TextureUploadTarget::Texture1D); break; + case MGPipeResourceTarget::Tex2D: defineChain(TextureUploadTarget::Texture2D); break; + case MGPipeResourceTarget::Tex3D: defineChain(TextureUploadTarget::Texture3D); break; + case MGPipeResourceTarget::Tex1DArray: defineChain(TextureUploadTarget::Texture1DArray); break; + case MGPipeResourceTarget::Tex2DArray: defineChain(TextureUploadTarget::Texture2DArray); break; + case MGPipeResourceTarget::TexCube: + for (Uint32 face = static_cast(TextureUploadTarget::CubeMapPositiveX); + face <= static_cast(TextureUploadTarget::CubeMapNegativeZ); ++face) + defineChain(static_cast(face)); + break; + case MGPipeResourceTarget::TexCubeArray: defineChain(TextureUploadTarget::CubeMapArray); break; + case MGPipeResourceTarget::Tex2DMS: defineChain(TextureUploadTarget::Texture2DMultisample); break; + case MGPipeResourceTarget::Tex2DMSArray: defineChain(TextureUploadTarget::Texture2DMultisampleArray); break; + case MGPipeResourceTarget::TexRect: defineChain(TextureUploadTarget::TextureRectangle); break; + case MGPipeResourceTarget::TexBuffer: defineChain(TextureUploadTarget::TextureBuffer); break; + default: break; + } + } +#endif + // The texture half of resource_subdata, and it DISPATCHES TO NOBODY at GL-call time: // a texture write marks a level dirty and Espryt uploads it at its own sync point, out // of the accumulated set below. So the whole of this function is the gate, the @@ -1068,6 +1122,8 @@ namespace MobileGL::MG_Pipe { // monolith, so the monolith shape keeps P5's pointer-dropping expression exactly. if (g_resourceOps != nullptr && g_resourceOps->TextureSubData != nullptr) { g_resourceOps->TextureSubData(record.Res, record, bytes, regions); + } else { + AdoptTextureWithoutBackendHook(*stored, record, bytes); } #endif return true; @@ -1413,6 +1469,12 @@ namespace MobileGL::MG_Pipe { } void MGPipeApplierReleaseObjectRecords() { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith && + (g_resourceOps == nullptr || g_resourceOps->TextureDestroy == nullptr)) { + MG_Remote::Server::ServerStagedTexture().DropAll(); + } +#endif // The served context is going away and this applier with it. Under split that is one // applier per served context; in the monolith there is one applier behind every // context, so nothing wires this - see PipeApply.h. The two serials advance here for @@ -1950,9 +2012,11 @@ namespace MobileGL::MG_Pipe { // system. Textures only: a buffer's storage is the ops table's own Respecify hook, and // a renderbuffer has no levels. if (desc.Target != kMGPipeResourceTargetBuffer && - desc.Target != static_cast(MGPipeResourceTarget::Renderbuffer) && !metadataOnly && - g_resourceOps != nullptr && g_resourceOps->TextureRespecify != nullptr) { - g_resourceOps->TextureRespecify(desc.Resource, desc, level); + desc.Target != static_cast(MGPipeResourceTarget::Renderbuffer) && !metadataOnly) { + if (g_resourceOps != nullptr && g_resourceOps->TextureRespecify != nullptr) + g_resourceOps->TextureRespecify(desc.Resource, desc, level); + else + DefineTextureWithoutBackendHook(desc, level); } #endif @@ -2160,9 +2224,12 @@ namespace MobileGL::MG_Pipe { // P5c (tx): with ONE exception - the staged-texture store is keyed by the handle, so // the death must reach it or a recycled slot's stale levels would answer for the // successor. This does not hand the texture to the buffer family's Destroy. - if (static_cast(handle.Kind) == MGPipeKind::Texture && - g_resourceOps != nullptr && g_resourceOps->TextureDestroy != nullptr) { - g_resourceOps->TextureDestroy(handle.Handle); + if (static_cast(handle.Kind) == MGPipeKind::Texture) { + if (g_resourceOps != nullptr && g_resourceOps->TextureDestroy != nullptr) + g_resourceOps->TextureDestroy(handle.Handle); + else if (MG_Config::Transport != MG_Config::TransportMode::Monolith) + MG_Remote::Server::ServerStagedTexture().Drop( + MG_Remote::Server::StagedTextureStore::KeyForHandle(handle.Handle)); } #endif if (static_cast(handle.Kind) != MGPipeKind::Buffer) return; diff --git a/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp b/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp index 2a7be263..27616eea 100644 --- a/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp +++ b/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp @@ -350,6 +350,74 @@ TEST(StagedTextureProductionTest, TextureSubDataThroughTheRealOpsTableCopiesAndS << "a destroyed texture's staged levels must not answer for the slot's next owner"; } +TEST(StagedTextureProductionTest, HooklessTextureConsumerOwnsBytesAndScopedStorageLifetime) { + // A record-only texture consumer has no texture callbacks in the buffer ops table. + // The empty fixture table admits records without installing any adoption callback. + 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(47, 2); + const auto key = Server::StagedTextureStore::KeyForHandle(res); + MG_Pipe::MGPResourceDesc desc{}; + desc.Resource = res; + desc.Target = static_cast(MG_Pipe::MGPipeResourceTarget::Tex2D); + ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceCreate(desc)); + desc.Width = 4; + desc.Height = 4; + desc.Depth = 1; + desc.Levels = 3; + MG_Pipe::MGPRespecifiedLevel level{}; + level.UploadTarget = MG_Pipe::MGPipePackSubDataTarget(desc.Target, kTex2DTarget); + level.Level = 0; + ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceRespecify(desc, nullptr, &level)); + EXPECT_EQ(store.LevelExtentOrUndefined(key, kTex2DTarget, 0), IntVec3(4, 4, 1)) + << "null-data glTexImage defines the level even though no upload follows"; + EXPECT_FALSE(store.IsCovered(key, kTex2DTarget, 0)); + + Vector src(64, 0xAB); + MG_Pipe::MGPSubData upload{}; + upload.Res = res; + upload.Target = level.UploadTarget; + upload.UnionBox = {0, 0, 0, 4, 4, 1}; + upload.Blob.Size = src.size(); + ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceSubData(upload, src.data(), nullptr)); + ASSERT_TRUE(store.IsCovered(key, kTex2DTarget, 0)); + 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. + level.Level = 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)); + desc.Width = 8; + desc.Height = 8; + desc.Immutable = 1; + ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceRespecify(desc, nullptr, nullptr)); + EXPECT_FALSE(store.IsCovered(key, kTex2DTarget, 0)); + EXPECT_EQ(store.LevelExtentOrUndefined(key, kTex2DTarget, 0), IntVec3(8, 8, 1)); + EXPECT_EQ(store.LevelExtentOrUndefined(key, kTex2DTarget, 2), IntVec3(2, 2, 1)); + MG_Pipe::MGPHandleOnly death{}; + death.Handle = res; + death.Kind = static_cast(MG_Pipe::MGPipeKind::Texture); + MG_Pipe::MGPipeApplyResourceDestroy(death); + EXPECT_FALSE(store.HasShadow(key)); + + // Context object release has the same ownership even without a backend hook. + desc.Resource = TestHandle(47, 3); + ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceCreate(desc)); + desc.Width = 16; + ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceRespecify(desc, nullptr, nullptr)); + const auto nextKey = Server::StagedTextureStore::KeyForHandle(desc.Resource); + ASSERT_TRUE(store.HasShadow(nextKey)); + MG_Pipe::MGPipeApplierReleaseObjectRecords(); + EXPECT_FALSE(store.HasShadow(nextKey)); +} + 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