From 70fb6689ce413a08d71b9086b1630d98cf3891c5 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 20 Sep 2026 08:42:47 -0400 Subject: [PATCH] [Magma] key wire pipelines by exact render-pass compatibility identities --- .../DirectVulkan/Renderer/PipelineFactory.cpp | 21 ++++++- .../DirectVulkan/Renderer/PipelineFactory.h | 5 ++ .../DirectVulkan/Renderer/VulkanRenderer.cpp | 24 +++++++- .../DirectVulkan/Renderer/VulkanRenderer.h | 9 ++- .../DirectVulkan/Renderer/WireDraw.inc | 9 ++- .../Renderer/WireRenderPassCompatibility.h | 49 ++++++++++++++++ .../MG_Test/Pipeline/PipelineQuirkTest.cpp | 58 +++++++++++++++++++ 7 files changed, 169 insertions(+), 6 deletions(-) create mode 100644 MobileGL/MG_Backend/DirectVulkan/Renderer/WireRenderPassCompatibility.h diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp index 752cc4e5..b8a9a9f0 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp @@ -198,7 +198,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { XXHASH_VERIFY(XXH64_update(m_hashState, &payload.programHash, sizeof(payload.programHash))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.vertexInputHash, sizeof(payload.vertexInputHash))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.pipelineLayout, sizeof(payload.pipelineLayout))); - XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass))); +#if MOBILEGL_BUILD_DISAGGREGATED + if (payload.wireRenderPassCompatibilityId != 0) { + constexpr Uint64 wireDomain = 0x5749524552504b59ull; + XXHASH_VERIFY(XXH64_update(m_hashState, &wireDomain, sizeof(wireDomain))); + XXHASH_VERIFY(XXH64_update(m_hashState, &payload.wireRenderPassCompatibilityId, + sizeof(payload.wireRenderPassCompatibilityId))); + } else +#endif + { + XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass))); + } XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable))); @@ -275,7 +285,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { static_cast(payload.programHash)); return VK_NULL_HANDLE; } - m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, payload.renderPass, + m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, +#if MOBILEGL_BUILD_DISAGGREGATED + // Wire pipelines outlive their creator pass and + // may serve another compatible pass in flight. + // Never evict them by a recycled native handle. + payload.wireRenderPassCompatibilityId != 0 ? VK_NULL_HANDLE : +#endif + payload.renderPass, m_frameCounter}); return pipeline; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h index 318b9af7..79317b45 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h @@ -35,6 +35,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { HashType vertexInputHash = 0; VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; VkRenderPass renderPass = VK_NULL_HANDLE; +#if MOBILEGL_BUILD_DISAGGREGATED + // Nonzero is an exact, renderer-lifetime compatibility identity. + // renderPass remains the valid creation handle, not the cache key. + Uint64 wireRenderPassCompatibilityId = 0; +#endif Uint32 colorAttachmentCount = 1; VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; // glEnable(GL_SAMPLE_SHADING) + glMinSampleShading, which Vulkan bakes into the diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index f500c166..c4f0b137 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -5290,7 +5290,7 @@ void main() { VkPipeline VulkanRenderer::GetOrCreatePipelineWithInput(GLenum mode, const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, ProgramFactory::CompileOptionFlags transformFlags, const VertexInputStateFactory::BackendVertexInputState& vis, const RenderPassEntry& renderPassEntry, - Bool primitiveRestartEnable) { + Bool primitiveRestartEnable, Uint64 wireRenderPassCompatibilityId) { #endif Bool invertClockwise = transformFlags & ProgramFactory::CompileOptionBit::PositionYFlip; if (programObj.stages.empty()) { @@ -5313,7 +5313,11 @@ void main() { auto& vis = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao); #endif const Uint64 vertexLayoutHash = vis.layoutHash; - const Uint64 renderPassHash = renderPassEntry.hash; + const Uint64 renderPassHash = +#if MOBILEGL_BUILD_DISAGGREGATED + wireRenderPassCompatibilityId != 0 ? 0 : +#endif + renderPassEntry.hash; // The pipeline-relevant subset only: glViewport / glScissor / glBlendColor / glStencilMask // and friends are dynamic state or not pipeline state at all, and keying the memo on the // all-state counter made any of them evict a perfectly good VkPipeline. The memo compares @@ -5357,6 +5361,9 @@ void main() { if (entry.pipeline != VK_NULL_HANDLE && entry.mode == mode && entry.programHash == programObj.hash && entry.vertexInputHash == vertexLayoutHash && entry.renderPassHash == renderPassHash && +#if MOBILEGL_BUILD_DISAGGREGATED + entry.wireRenderPassCompatibilityId == wireRenderPassCompatibilityId && +#endif entry.pipelineStateHash == pipelineStateHash && #if MOBILEGL_PIPE_PUSH entry.renderStateCso == renderStateCso && @@ -5650,6 +5657,9 @@ void main() { .vertexInputHash = vertexLayoutHash, .pipelineLayout = programObj.pipelineLayout, .renderPass = renderPassEntry.renderPass, +#if MOBILEGL_BUILD_DISAGGREGATED + .wireRenderPassCompatibilityId = wireRenderPassCompatibilityId, +#endif .colorAttachmentCount = renderPassEntry.colorAttachmentCount, .rasterizationSamples = renderPassEntry.sampleCount, // ARB_sample_shading. Dropped on a device without sampleRateShading rather than @@ -5785,7 +5795,11 @@ void main() { const auto& drawFboBinding = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); #endif +#if MOBILEGL_BUILD_DISAGGREGATED + MOBILEGL_ASSERT(wireFbo != nullptr || drawFboBinding != nullptr, "GetOrCreatePipeline: draw framebuffer is null"); +#else MOBILEGL_ASSERT(drawFboBinding != nullptr, "GetOrCreatePipeline: draw framebuffer is null"); +#endif #if MOBILEGL_BUILD_DISAGGREGATED const Bool isDefaultDrawFbo = wireFbo ? wireFbo->IsDefault : drawFboBinding->IsDefaultFramebuffer(); Array wireDrawBuffers{}; @@ -6073,6 +6087,9 @@ void main() { entry.programHash = programObj.hash; entry.vertexInputHash = vertexLayoutHash; entry.renderPassHash = renderPassHash; +#if MOBILEGL_BUILD_DISAGGREGATED + entry.wireRenderPassCompatibilityId = wireRenderPassCompatibilityId; +#endif entry.pipelineStateHash = pipelineStateHash; #if MOBILEGL_PIPE_PUSH entry.renderStateCso = renderStateCso; @@ -6807,6 +6824,9 @@ void main() { if (entry.pipeline != VK_NULL_HANDLE && entry.mode == mode && entry.programHash == programObj.hash && entry.vertexInputHash == vaoLayoutHash && entry.renderPassHash == snap.renderPassHash && +#if MOBILEGL_BUILD_DISAGGREGATED + entry.wireRenderPassCompatibilityId == 0 && +#endif entry.pipelineStateHash == pipelineStateHash && #if MOBILEGL_PIPE_PUSH entry.renderStateCso == renderStateCso && diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 7f72925e..59a5be39 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -22,6 +22,9 @@ #include "VkSamplerManager.h" #include "VkTextureManager.h" #include "VkTimerQueryManager.h" +#if MOBILEGL_BUILD_DISAGGREGATED +#include "WireRenderPassCompatibility.h" +#endif #include "MG_Util/Math/VectorTypes.h" #include #include @@ -509,6 +512,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { void ClearAllWireDrawPassCaches(); Vector> m_wireDrawPassCaches; WireDrawPassKey m_wireDrawPassKey; + WireRenderPassCompatibilityTable m_wireRenderPassCompatibility; struct WireRetiredObjects { Uint64 submitIndex = 0; UniquePtr drawPass; @@ -527,7 +531,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkPipeline GetOrCreatePipelineWithInput(GLenum mode, const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, ProgramFactory::CompileOptionFlags transformFlags, const VertexInputStateFactory::BackendVertexInputState& vis, const RenderPassEntry& renderPassEntry, - Bool primitiveRestartEnable); + Bool primitiveRestartEnable, Uint64 wireRenderPassCompatibilityId = 0); void CopyWireFramebufferToTexture(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height); @@ -926,6 +930,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint64 programHash = 0; Uint64 vertexInputHash = 0; Uint64 renderPassHash = 0; +#if MOBILEGL_BUILD_DISAGGREGATED + Uint64 wireRenderPassCompatibilityId = 0; +#endif // The PRE-HANDLE arm's key component (P2 brief D12.1), and 0 in every entry the // handle arm mints. VALUE hash of the pipeline-relevant fixed-function state (see // ComputePipelineStateHash), not the monotonic pipeline-state version: the version diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc index 6b308c72..23c30a80 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -408,8 +408,15 @@ created.hash = created.compatibilityHash = XXH64(colors.data(), colors.size() * sizeof(colors[0]), passHash); } auto& pass = *m_wireDrawPass; + WireRenderPassCompatibilityKey pipelinePassKey{}; + for (const auto& attachment : attachments) + pipelinePassKey.attachmentFormatsAndSamples.push_back( + (static_cast(attachment.format) << 32) | static_cast(attachment.samples)); + for (const auto& color : colors) pipelinePassKey.colorReferences.push_back(color.attachment); + pipelinePassKey.depthReference = depth.attachment; + const Uint64 compatibilityId = m_wireRenderPassCompatibility.Intern(pipelinePassKey); const auto pipeline = GetOrCreatePipelineWithInput(mode, program, programObj, flags, input, pass, - ResolvePrimitiveRestartEnable(aspects, indices)); + ResolvePrimitiveRestartEnable(aspects, indices), compatibilityId); if (pipeline == VK_NULL_HANDLE) return false; for (Uint32 i = 0; i < vertexSlices.size(); ++i) vkCmdBindVertexBuffers(frame.commandBuffer, i, 1, &vertexSlices[i].buffer, &vertexSlices[i].offset); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireRenderPassCompatibility.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireRenderPassCompatibility.h new file mode 100644 index 00000000..645f0402 --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireRenderPassCompatibility.h @@ -0,0 +1,49 @@ +#pragma once +#include +#include "../VkIncludes.h" + +#if MOBILEGL_BUILD_DISAGGREGATED +namespace MobileGL::MG_Backend::DirectVulkan { + // The wire draw pass has one graphics subpass, no input attachments, + // multiview or extensions. Its Vulkan pipeline compatibility is formats, + // sample counts and the ordered attachment references, not native handles, + // image identity, dimensions, load/store operations or image layouts. + struct WireRenderPassCompatibilityKey { + Vector attachmentFormatsAndSamples; + Vector colorReferences; + Uint32 depthReference = VK_ATTACHMENT_UNUSED; + Bool operator==(const WireRenderPassCompatibilityKey&) const = default; + }; + + struct WireRenderPassCompatibilityHash { + SizeT operator()(const WireRenderPassCompatibilityKey& key) const { + SizeT hash = 0; + const auto mix = [&](Uint64 value) { + hash ^= static_cast(value) + 0x9e3779b97f4a7c15ull + (hash << 6) + (hash >> 2); + }; + mix(key.attachmentFormatsAndSamples.size()); + for (const auto value : key.attachmentFormatsAndSamples) mix(value); + mix(key.colorReferences.size()); + for (const auto value : key.colorReferences) mix(value); + mix(key.depthReference); + return hash; + } + }; + + class WireRenderPassCompatibilityTable { + public: + Uint64 Intern(const WireRenderPassCompatibilityKey& key) { + if (const auto found = m_ids.find(key); found != m_ids.end()) return found->second; + // No reset/eviction: an ID cannot be reused while a pipeline factory + // or a renderer memo can still hold its previous meaning. + MOBILEGL_ASSERT(m_nextId != 0, "wire render-pass compatibility ID exhausted"); + const Uint64 id = m_nextId++; + m_ids.emplace(key, id); + return id; + } + private: + UnorderedMap m_ids; + Uint64 m_nextId = 1; // zero selects the native-handle pipeline domain + }; +} +#endif diff --git a/MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp b/MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp index 8b5962bd..6b5ad74a 100644 --- a/MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp +++ b/MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp @@ -11,6 +11,9 @@ #include #include #include +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#endif using namespace MobileGL; using MobileGL::MG_Backend::DirectVulkan::PipelineFactory; @@ -457,3 +460,58 @@ TEST(ReflectedReadsInstanceIndexBuiltin, FalseForAnEmptyModule) { SpvReflectShaderModule emptyModule{}; EXPECT_FALSE(ProgramFactory::ReflectedReadsInstanceIndexBuiltin(emptyModule)); } + + +TEST(WirePipelineCompatibility, ExactCompatibilitySurvivesNativeObjectTurnover) { +#if MOBILEGL_BUILD_DISAGGREGATED + using namespace MobileGL::MG_Backend::DirectVulkan; + WireRenderPassCompatibilityTable table; + WireRenderPassCompatibilityKey a{{(Uint64(VK_FORMAT_R8G8B8A8_UNORM) << 32) | VK_SAMPLE_COUNT_1_BIT}, {0}, + VK_ATTACHMENT_UNUSED}; + const Uint64 first = table.Intern(a); + ASSERT_NE(first, 0u); + auto b = a; + b.attachmentFormatsAndSamples[0] = (Uint64(VK_FORMAT_R8G8B8A8_SRGB) << 32) | VK_SAMPLE_COUNT_1_BIT; + EXPECT_NE(table.Intern(b), first); + b = a; + b.attachmentFormatsAndSamples[0] = (Uint64(VK_FORMAT_R8G8B8A8_UNORM) << 32) | VK_SAMPLE_COUNT_4_BIT; + EXPECT_NE(table.Intern(b), first); + b = a; + b.colorReferences = {VK_ATTACHMENT_UNUSED, 0}; + EXPECT_NE(table.Intern(b), first); + b = a; + b.depthReference = 0; + EXPECT_NE(table.Intern(b), first); + // Native pass/image turnover and swapchain changes never reset this table. + EXPECT_EQ(table.Intern(a), first); +#else + GTEST_SKIP() << "wire pipeline compatibility requires disaggregated build"; +#endif +} + +TEST(WirePipelineCompatibility, HashUsesCompatibilityIdentityAndSeparatesNativeDomain) { +#if MOBILEGL_BUILD_DISAGGREGATED + using namespace MobileGL::MG_Backend::DirectVulkan; + VulkanRendererConfig config{}; + config.DisablePipelineCache = true; + // Hash-only unit: DisablePipelineCache prevents all device calls, and no + // pipeline is created. These tokens are never passed to a Vulkan driver. + PipelineFactory factory(reinterpret_cast(uintptr_t{1}), config); + PipelineFactory::PipelineCreatePayload payload{}; + payload.renderPass = (VkRenderPass)(uintptr_t{1}); + payload.wireRenderPassCompatibilityId = 1; + const auto a = factory.ComputeHash(payload); + payload.renderPass = (VkRenderPass)(uintptr_t{2}); + EXPECT_EQ(factory.ComputeHash(payload), a) << "compatible passes must reuse the pipeline across native handles"; + payload.wireRenderPassCompatibilityId = 2; + EXPECT_NE(factory.ComputeHash(payload), a) << "a recycled native handle must not alias incompatible passes"; + payload.renderPass = (VkRenderPass)(uintptr_t{1}); + payload.wireRenderPassCompatibilityId = 0; + EXPECT_NE(factory.ComputeHash(payload), a) << "native handles and wire identities are different domains"; + const auto native = factory.ComputeHash(payload); + payload.renderPass = (VkRenderPass)(uintptr_t{2}); + EXPECT_NE(factory.ComputeHash(payload), native); +#else + GTEST_SKIP() << "wire pipeline compatibility requires disaggregated build"; +#endif +}