diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h new file mode 100644 index 00000000..95ee39c7 --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h @@ -0,0 +1,63 @@ +// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#pragma once +#include + +#include +#if MOBILEGL_PIPE_PUSH +// kMGPipeSubsystem* - the runtime bitmask's named bits. Push-only, so the pull build's +// include graph is unchanged. +#include +#endif + +#include + +// Magma's arm selector for the P2 Track H / render-state re-keys (P2 brief D14). +// +// Two switches decide which arm a re-keyed site runs, and they are NOT the same switch: +// +// MOBILEGL_PIPE_PUSH (compile) - is the pushed state there to be keyed on at all +// Features.PipePush (runtime bitmask) - is THIS subsystem migrated in THIS run +// MOBILEGL_PIPE_LEGACY_MEMOS (compile) - is the pre-handle arm compiled beside it +// Features.PipeLegacyMemos (runtime) - may the pre-handle arm be ENTERED in this run +// +// ARCHITECTURE.md 9.6's point: once a handle wave lands, a clear MOBILEGL_PIPE_PUSH bit is +// only a valid A/B while the legacy arm is still compiled, because with the bit clear the +// backend would otherwise still run the re-keyed code. So a clear bit selects the legacy +// arm, and a run that has explicitly disabled the legacy arm may not fall into it. +// +// The whole header is inert in a pull build: MOBILEGL_PIPE_PUSH is 0 there, every helper +// below is behind it, and the pull build's translation units are byte-identical (G1). +namespace MobileGL::MG_Backend::DirectVulkan { + +#if MOBILEGL_PIPE_PUSH + // Is `subsystemBit` (MG_Pipe/MGPipe.h's kMGPipeSubsystem*) migrated in this run? + inline Bool MagmaPipeSubsystemOn(Uint64 subsystemBit) { + return (MG_Config::Features.PipePush & subsystemBit) != 0; + } + + // The legacy arm is about to be entered. Features.PipeLegacyMemos=0 is the operator + // asserting "the pre-handle arm is never entered in this run", which is the lever + // HandleRecycleScenario.Handles pulls (P2 brief D18): entering it anyway would make + // that arm green for the wrong reason, so it is Fatal rather than a fallback. + inline void MagmaPipeRequireLegacyArm(const char* site) { +#if MOBILEGL_PIPE_LEGACY_MEMOS + if (MG_Config::Features.PipeLegacyMemos) return; + MGLOG_F("MGPipe: Fatal{PipeLegacyMemosDisabled} %s wanted the pre-handle arm but " + "MOBILEGL_PIPE_LEGACY_MEMOS=0 forbids entering it", + site); +#else + MGLOG_F("MGPipe: Fatal{PipeLegacyMemosDisabled} %s wanted the pre-handle arm but this " + "build did not compile one (cmake -DMOBILEGL_PIPE_LEGACY_MEMOS=OFF)", + site); +#endif + std::abort(); + } +#endif // MOBILEGL_PIPE_PUSH +} // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 88869629..12a4fadd 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -4818,6 +4818,7 @@ void main() { return MGB_CTX->GetRenderStateParameters().SampleMaskValue; } +#if MOBILEGL_PIPE_LEGACY_MEMOS Uint64 VulkanRenderer::ComputePipelineStateHash(Uint32 colorAttachmentCount, VkSampleCountFlagBits rasterizationSamples) const { // One bulk fetch instead of ~17 per-field accessor calls into MG_State: every @@ -4908,6 +4909,8 @@ void main() { } return hash; } +#endif // MOBILEGL_PIPE_LEGACY_MEMOS + // A program that runs a geometry shader AND captures transform feedback. Both halves are // link-time properties, so this is safe to fold into a pipeline keyed on the program hash. @@ -4983,23 +4986,49 @@ void main() { // per-draw state flips (GL_BLEND toggles) would otherwise miss entries the memo holds. // The version only guards recomputing the hash - unchanged version, unchanged bytes. const Uint renderStateVersion = MGB_CTX->GetPipelineStateVersion(); - if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion || - m_pipelineStateHashColorCount != renderPassEntry.colorAttachmentCount || - m_pipelineStateHashSampleCount != renderPassEntry.sampleCount) { - m_pipelineStateHash = - ComputePipelineStateHash(renderPassEntry.colorAttachmentCount, renderPassEntry.sampleCount); - m_pipelineStateHashVersion = renderStateVersion; - m_pipelineStateHashColorCount = renderPassEntry.colorAttachmentCount; - m_pipelineStateHashSampleCount = renderPassEntry.sampleCount; - m_pipelineStateHashValid = true; +#if MOBILEGL_PIPE_PUSH + // P2 D12.1. Non-null means the client's render-state CSO handle is this draw's state + // key and the hash below is not computed at all; null means the pre-handle arm. The + // two arms' entries can never match each other: the handle arm stores hash 0 and a + // real handle, the legacy arm a real hash and the null handle, and the probe compares + // both components. + const MG_Pipe::MGPipeHandle renderStateCso = ResolveBoundRenderStateCso(); +#endif +#if MOBILEGL_PIPE_LEGACY_MEMOS +#if MOBILEGL_PIPE_PUSH + if (MG_Pipe::MGPipeHandleIsNull(renderStateCso)) +#endif + { + if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion || + m_pipelineStateHashColorCount != renderPassEntry.colorAttachmentCount || + m_pipelineStateHashSampleCount != renderPassEntry.sampleCount) { + m_pipelineStateHash = + ComputePipelineStateHash(renderPassEntry.colorAttachmentCount, renderPassEntry.sampleCount); + m_pipelineStateHashVersion = renderStateVersion; + m_pipelineStateHashColorCount = renderPassEntry.colorAttachmentCount; + m_pipelineStateHashSampleCount = renderPassEntry.sampleCount; + m_pipelineStateHashValid = true; + } } - const Uint64 pipelineStateHash = m_pipelineStateHash; +#endif + const Uint64 pipelineStateHash = +#if MOBILEGL_PIPE_PUSH + !MG_Pipe::MGPipeHandleIsNull(renderStateCso) ? 0 : +#endif +#if MOBILEGL_PIPE_LEGACY_MEMOS + m_pipelineStateHash; +#else + 0; +#endif for (Uint32 i = 0; i < m_pipelineMemoCount; ++i) { const PipelineMemoEntry& entry = m_pipelineMemo[i]; if (entry.pipeline != VK_NULL_HANDLE && entry.mode == mode && entry.programHash == programObj.hash && entry.vertexInputHash == vertexLayoutHash && entry.renderPassHash == renderPassHash && entry.pipelineStateHash == pipelineStateHash && +#if MOBILEGL_PIPE_PUSH + entry.renderStateCso == renderStateCso && +#endif entry.primitiveRestartEnable == primitiveRestartEnable && entry.transformFlags == transformFlags) { if (MG_Util::PipeStats::Enabled()) { @@ -5668,6 +5697,9 @@ void main() { entry.vertexInputHash = vertexLayoutHash; entry.renderPassHash = renderPassHash; entry.pipelineStateHash = pipelineStateHash; +#if MOBILEGL_PIPE_PUSH + entry.renderStateCso = renderStateCso; +#endif entry.primitiveRestartEnable = primitiveRestartEnable; entry.transformFlags = transformFlags; entry.pipeline = pipeline; @@ -6332,16 +6364,38 @@ void main() { // what lets a per-draw GL_BLEND toggle alternate between two memo entries // instead of missing forever on a monotonic version. A miss falls through // to the full lookup. - if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion || - m_pipelineStateHashColorCount != snap.renderPassColorCount || - m_pipelineStateHashSampleCount != snap.renderPassSampleCount) { - m_pipelineStateHash = - ComputePipelineStateHash(snap.renderPassColorCount, snap.renderPassSampleCount); - m_pipelineStateHashVersion = renderStateVersion; - m_pipelineStateHashColorCount = snap.renderPassColorCount; - m_pipelineStateHashSampleCount = snap.renderPassSampleCount; - m_pipelineStateHashValid = true; +#if MOBILEGL_PIPE_PUSH + // Same arm selector as GetOrCreatePipeline's probe (P2 D12.1); this site is the + // fast path's copy of it, and the two must key identically or the fast path would + // hand back a pipeline the full path would not have matched. + const MG_Pipe::MGPipeHandle renderStateCso = ResolveBoundRenderStateCso(); +#endif +#if MOBILEGL_PIPE_LEGACY_MEMOS +#if MOBILEGL_PIPE_PUSH + if (MG_Pipe::MGPipeHandleIsNull(renderStateCso)) +#endif + { + if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion || + m_pipelineStateHashColorCount != snap.renderPassColorCount || + m_pipelineStateHashSampleCount != snap.renderPassSampleCount) { + m_pipelineStateHash = + ComputePipelineStateHash(snap.renderPassColorCount, snap.renderPassSampleCount); + m_pipelineStateHashVersion = renderStateVersion; + m_pipelineStateHashColorCount = snap.renderPassColorCount; + m_pipelineStateHashSampleCount = snap.renderPassSampleCount; + m_pipelineStateHashValid = true; + } } +#endif + const Uint64 pipelineStateHash = +#if MOBILEGL_PIPE_PUSH + !MG_Pipe::MGPipeHandleIsNull(renderStateCso) ? 0 : +#endif +#if MOBILEGL_PIPE_LEGACY_MEMOS + m_pipelineStateHash; +#else + 0; +#endif const auto memoTransformFlags = ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags); for (Uint32 i = 0; i < m_pipelineMemoCount; ++i) { @@ -6349,7 +6403,10 @@ void main() { if (entry.pipeline != VK_NULL_HANDLE && entry.mode == mode && entry.programHash == programObj.hash && entry.vertexInputHash == vaoLayoutHash && entry.renderPassHash == snap.renderPassHash && - entry.pipelineStateHash == m_pipelineStateHash && + entry.pipelineStateHash == pipelineStateHash && +#if MOBILEGL_PIPE_PUSH + entry.renderStateCso == renderStateCso && +#endif entry.primitiveRestartEnable == drawPrimitiveRestartEnable && entry.transformFlags == memoTransformFlags) { pipeline = entry.pipeline; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index f8498a3a..46a8a162 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -9,6 +9,7 @@ #pragma once #include "Config.h" #include "FrameContext.h" +#include "MagmaPipeArms.h" #include "PipelineFactory.h" #include "ProgramFactory.h" #include "SwapchainObject.h" @@ -24,7 +25,13 @@ #include "MG_Util/Math/VectorTypes.h" #include #include +#include #include +#if MOBILEGL_PIPE_PUSH +// The applier's CSO store: MGPipeApplier().BoundRenderStateCso is what the pipeline memo +// keys on after P2 (D12.1). Push-only, so the pull build's include graph is unchanged. +#include +#endif #include #include "../VkIncludes.h" @@ -820,12 +827,31 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint64 programHash = 0; Uint64 vertexInputHash = 0; Uint64 renderPassHash = 0; - // VALUE hash of the pipeline-relevant fixed-function state (see - // ComputePipelineStateHash), not the monotonic pipeline-state version: - // the version never repeats, so a per-draw GL_BLEND toggle would miss - // all entries forever even though the state alternates between two - // values the memo already holds. + // 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 + // never repeats, so a per-draw GL_BLEND toggle would miss all entries forever even + // though the state alternates between two values the memo already holds. Uint64 pipelineStateHash = 0; +#if MOBILEGL_PIPE_PUSH + // The HANDLE arm's key component, and the whole of D12.1: the CLIENT already + // hashed the pipeline subset of RenderStateParameters and minted a content- + // addressed CSO for it (MG_Pipe/MGPipeRenderStateSpans.h, MG_Impl/Pipe/CsoCache), + // so re-hashing the same 396 bytes here was work the boundary had already done. + // Two draws share a CSO handle exactly when their pipeline bytes are equal, and + // the client's subset is a strict SUPERSET of what ComputePipelineStateHash read, + // so the handle discriminates at least as finely as the hash it replaces. + // + // renderPassHash STAYS beside it and is what keeps this key complete: the CSO + // carries GL state only, while colorAttachmentCount and the rasterization sample + // count - which ComputePipelineStateHash folded in through its signature and + // through ResolveEffectiveSampleMask - are render-pass facts that the render-pass + // hash already separates. + // + // Null in an entry minted by the legacy arm, so entries of the two arms can never + // match each other: the compare below tests BOTH components. + MG_Pipe::MGPipeHandle renderStateCso = MG_Pipe::kMGPipeNullHandle; +#endif ProgramFactory::CompileOptionFlags transformFlags = {}; // Baked into the pipeline (PipelineFactory::ComputeHash mixes it), and NOT derivable // from anything else in this key: it depends on whether the draw is indexed and on the @@ -839,19 +865,66 @@ namespace MobileGL::MG_Backend::DirectVulkan { PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize]; Uint32 m_pipelineMemoCount = 0; Uint32 m_pipelineMemoNext = 0; - // Hash of every fixed-function GL state the pipeline payload reads that the - // memo key's other fields (mode / program / vertex input / render pass / - // transform flags) do not already pin down. Equal hash under an equal rest - // of key => byte-identical PipelineCreatePayload. Cached per pipeline-state + +#if MOBILEGL_PIPE_PUSH + // P2 D12.1's arm selector, and the whole of the pipeline memo's re-key. Returns the + // render-state CSO this draw is keyed on, or the null handle when the pre-handle arm + // is the one that runs. + // + // Under the handle arm the memo's state key IS this handle. The client hashed those + // 396 pipeline bytes when it minted the CSO (MGPipeComputePipelineSubsetHash), so + // recomputing an overlapping hash here was work the boundary had already done; the + // client's pipeline subset is a strict SUPERSET of what ComputePipelineStateHash read, + // so the handle discriminates at least as finely as the hash it replaces. What the + // handle does NOT carry is the render-pass side - colorAttachmentCount and the + // rasterization sample count, which ComputePipelineStateHash folded in through its + // signature and through ResolveEffectiveSampleMask - and that is exactly why + // entry.renderPassHash stays in the key beside it. + // + // The arm is live only when the render-state subsystem is migrated in this run AND the + // client has actually bound a CSO. The second half is not belt and braces: a tree whose + // tracker does not emit create/bind_render_state yet has no handle to key on, and + // keying every draw on the null handle would alias every render state onto one entry. + // + // Push-only by construction: the pull build does not compile this function at all, so + // its two callers are statement-for-statement what they were (G1). + MG_Pipe::MGPipeHandle ResolveBoundRenderStateCso() const { + if (!MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemRenderState)) { + MagmaPipeRequireLegacyArm("GetOrCreatePipeline"); + return MG_Pipe::kMGPipeNullHandle; + } + const MG_Pipe::MGPipeHandle boundCso = MG_Pipe::MGPipeApplier().BoundRenderStateCso; + if (MG_Pipe::MGPipeHandleIsNull(boundCso)) { + MGLOG_D_ONCE("MGPipe: kMGPipeSubsystemRenderState is on but no render-state CSO is " + "bound; the pipeline memo falls back to the pre-handle state hash"); + MagmaPipeRequireLegacyArm("GetOrCreatePipeline"); + } + return boundCso; + } +#endif +#if MOBILEGL_PIPE_LEGACY_MEMOS + // THE PRE-HANDLE ARM (P2 brief D12.1 / D14). Hash of every fixed-function GL state the + // pipeline payload reads that the memo key's other fields (mode / program / vertex + // input / render pass / transform flags) do not already pin down. Equal hash under an + // equal rest of key => byte-identical PipelineCreatePayload. Cached per pipeline-state // version: the version is monotonic and bumps on every pipeline-state // change, so an unchanged (version, colorAttachmentCount) proves the state // bytes are unchanged and the hash can be reused without re-reading them. + // + // The handle arm computes none of this: the client hashed the same bytes when it + // minted the CSO, so all five cached-hash members below exist only to avoid a + // re-hash the handle arm never performs. Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount, VkSampleCountFlagBits rasterizationSamples) const; +#endif // The effective GL_SAMPLE_MASK word for a draw at this rasterization sample count; see // the definition for the GL-vs-Vulkan rule it reconciles. Shared by the pipeline payload - // and the pipeline-state memo word so the two cannot disagree. + // and the pipeline-state memo word so the two cannot disagree. NOT part of the legacy + // arm: it is a PAYLOAD computation that depends on rasterizationSamples, so it survives + // the re-key and keeps reading Multisample / SampleMask / SampleMaskValue out of the + // working block. Uint32 ResolveEffectiveSampleMask(VkSampleCountFlagBits rasterizationSamples) const; +#if MOBILEGL_PIPE_LEGACY_MEMOS Uint m_pipelineStateHashVersion = 0; Uint32 m_pipelineStateHashColorCount = 0; // The sample count the cached hash was computed at. A pipeline-state input now depends on @@ -860,6 +933,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkSampleCountFlagBits m_pipelineStateHashSampleCount = VK_SAMPLE_COUNT_1_BIT; Uint64 m_pipelineStateHash = 0; Bool m_pipelineStateHashValid = false; +#endif // GetShaderTransformFlags memo. NOT pure in the pre-transform alone: the // function also reads whether the bound DRAW framebuffer is the default one // (only the default framebuffer gets the Y-flip and rotation bits - an FBO @@ -877,11 +951,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Drops every memoized pipeline handle. Required at command-buffer // boundaries and whenever any pipeline may have been destroyed. Also drops // the cached pipeline-state hash: the same boundaries can retire the GL - // context whose monotonic version the cache is keyed on. + // context whose monotonic version the cache is keyed on. The handle arm has no + // such cache to drop - a CSO handle is not derived from a monotonic version. void InvalidatePipelineMemo() { m_pipelineMemoCount = 0; m_pipelineMemoNext = 0; +#if MOBILEGL_PIPE_LEGACY_MEMOS m_pipelineStateHashValid = false; +#endif } UnorderedMap m_computePipelines; UniquePtr m_programFactory;