[Magma] key wire pipelines by exact render-pass compatibility identities

This commit is contained in:
2026-09-20 08:42:47 -04:00
parent 0ece468e14
commit 70fb6689ce
7 changed files with 169 additions and 6 deletions
@@ -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<unsigned long long>(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;
}
@@ -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
@@ -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<FramebufferAttachmentType, MG_Pipe::kMGPipeMaxColorAttachments> 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 &&
@@ -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 <Includes.h>
#include <MG_Backend/BackendObject.h>
@@ -509,6 +512,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void ClearAllWireDrawPassCaches();
Vector<Vector<WireDrawPassCacheEntry>> m_wireDrawPassCaches;
WireDrawPassKey m_wireDrawPassKey;
WireRenderPassCompatibilityTable m_wireRenderPassCompatibility;
struct WireRetiredObjects {
Uint64 submitIndex = 0;
UniquePtr<RenderPassEntry> 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
@@ -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<Uint64>(attachment.format) << 32) | static_cast<Uint32>(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);
@@ -0,0 +1,49 @@
#pragma once
#include <Includes.h>
#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<Uint64> attachmentFormatsAndSamples;
Vector<Uint32> 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<SizeT>(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<WireRenderPassCompatibilityKey, Uint64, WireRenderPassCompatibilityHash> m_ids;
Uint64 m_nextId = 1; // zero selects the native-handle pipeline domain
};
}
#endif
@@ -11,6 +11,9 @@
#include <Config.h>
#include <MG_Backend/DirectVulkan/Renderer/PipelineFactory.h>
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Backend/DirectVulkan/Renderer/WireRenderPassCompatibility.h>
#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<VkDevice>(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
}