From 5922e54b4792a51916dae740fb6ca3d9a31445a8 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 20 Sep 2026 12:21:36 -0400 Subject: [PATCH] [Magma] provide native placeholders for unbound wire image descriptors --- .../DirectVulkan/Renderer/UniformManager.cpp | 15 +- .../DirectVulkan/Renderer/UniformManager.h | 19 ++ .../Renderer/WirePlaceholderImages.inc | 197 ++++++++++++++++++ 3 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 MobileGL/MG_Backend/DirectVulkan/Renderer/WirePlaceholderImages.inc diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index 31870835..771c351b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -118,9 +118,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (unit < 0 || static_cast(unit) >= (storage ? MG_Pipe::kMGPipeMaxImageUnits : MG_Pipe::kMGPipeMaxTextureUnits)) return false; const auto handle = storage ? state.BoundShaderImages[unit].Res : state.BoundSamplerViews[unit].Texture; - // The old placeholder constructor enters the guarded client allocator. Keep that - // P7 boundary explicit instead of constructing a frontend texture on the server. - if (MG_Pipe::MGPipeHandleIsNull(handle)) WireDescriptorFatal("unbound-image-placeholder@P7"); + if (MG_Pipe::MGPipeHandleIsNull(handle)) + return ResolveWirePlaceholderImage(commandBuffer, program, programObj, binding, storage, out); if (handle.Slot >= state.TextureResources.size()) WireDescriptorFatal("image-record"); const auto& record = state.TextureResources[handle.Slot]; if (!record.Live || record.Gen != handle.Gen) WireDescriptorFatal("image-record-generation"); @@ -402,6 +401,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } +#include "WirePlaceholderImages.inc" + static Bool FindFramebufferAttachmentForTexture(const MG_State::GLState::FramebufferObject& framebuffer, const MG_State::GLState::ITextureObject& texture, FramebufferAttachmentType& outAttachment, Int& outLevel) { @@ -598,6 +599,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { frame.peakAllocatedSetsThisFrame = 0; } m_frames.clear(); +#if MOBILEGL_BUILD_DISAGGREGATED + for (auto& entry : m_wirePlaceholderImages) DestroyWirePlaceholderImage(entry.second); + m_wirePlaceholderImages.clear(); +#endif m_bufferManager = nullptr; m_programFactory = nullptr; @@ -2825,7 +2830,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { (storage ? MG_Pipe::kMGPipeMaxImageUnits : MG_Pipe::kMGPipeMaxTextureUnits)) return false; const auto handle = storage ? state.BoundShaderImages[unit].Res : state.BoundSamplerViews[unit].Texture; - if (MG_Pipe::MGPipeHandleIsNull(handle)) WireDescriptorFatal("unbound-image-placeholder@P7"); + // Null is the wire's unbound/incomplete binding. Its native + // placeholder needs no resource upload or frontend allocation. + if (MG_Pipe::MGPipeHandleIsNull(handle)) continue; if (!m_textureManager->SyncTextureResourceByHandle(handle, false, storage)) return false; } } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h index 1f172fe6..06e0c6f6 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h @@ -188,6 +188,25 @@ namespace MobileGL::MG_Backend::DirectVulkan { struct WireImageViewKeyHash { SizeT operator()(const WireImageViewKey& key) const; }; + + struct WirePlaceholderImage { + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + VkSampler sampler = VK_NULL_HANDLE; + VkRenderPass clearPass = VK_NULL_HANDLE; + VkFramebuffer clearFramebuffer = VK_NULL_HANDLE; + VkFormat format = VK_FORMAT_UNDEFINED; + VkImageAspectFlags aspect = VK_IMAGE_ASPECT_COLOR_BIT; + VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT; + VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED; + Uint32 layers = 1; + }; + mutable UnorderedMap m_wirePlaceholderImages; + Bool ResolveWirePlaceholderImage(VkCommandBuffer commandBuffer, const MagmaProgramSource& program, + const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Bool storage, + VkDescriptorImageInfo& out) const; + void DestroyWirePlaceholderImage(WirePlaceholderImage& image) const; #endif struct FrameResources { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WirePlaceholderImages.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WirePlaceholderImages.inc new file mode 100644 index 00000000..65122edf --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WirePlaceholderImages.inc @@ -0,0 +1,197 @@ +// UniformManager-owned native descriptors for wire bindings with no image. +// No GL object is constructed or inserted into the client's resource namespace. +#if MOBILEGL_BUILD_DISAGGREGATED + void UniformManager::DestroyWirePlaceholderImage(WirePlaceholderImage& image) const { + if (m_device != VK_NULL_HANDLE) { + if (image.clearFramebuffer) vkDestroyFramebuffer(m_device, image.clearFramebuffer, nullptr); + if (image.clearPass) vkDestroyRenderPass(m_device, image.clearPass, nullptr); + if (image.sampler) vkDestroySampler(m_device, image.sampler, nullptr); + if (image.view) vkDestroyImageView(m_device, image.view, nullptr); + if (image.image) vkDestroyImage(m_device, image.image, nullptr); + if (image.memory) vkFreeMemory(m_device, image.memory, nullptr); + } + image = {}; + } + + Bool UniformManager::ResolveWirePlaceholderImage(VkCommandBuffer commands, const MagmaProgramSource& program, + const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Bool storage, + VkDescriptorImageInfo& out) const { + if (binding >= programObj.samplerTextureTargetByBinding.size() || + binding >= programObj.samplerNumericDomainByBinding.size()) return false; + const auto target = programObj.samplerTextureTargetByBinding[binding]; + const auto domain = programObj.samplerNumericDomainByBinding[binding]; + const Bool multisample = target == TextureTarget::Texture2DMultisample || + target == TextureTarget::Texture2DMultisampleArray; + Bool shadow = false; + if (!storage) { + const Int location = programObj.samplerUniformLocationByBinding[binding]; + switch (program.GetUniformType(static_cast(location))) { + case GL_SAMPLER_1D_SHADOW: case GL_SAMPLER_2D_SHADOW: case GL_SAMPLER_CUBE_SHADOW: + case GL_SAMPLER_1D_ARRAY_SHADOW: case GL_SAMPLER_2D_ARRAY_SHADOW: + case GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW: case GL_SAMPLER_2D_RECT_SHADOW: shadow = true; break; + default: break; + } + } + VkFormat format = VK_FORMAT_UNDEFINED; + if (storage) { + if (binding >= programObj.storageImageFormatByBinding.size()) return false; + format = programObj.storageImageFormatByBinding[binding]; + if (format == VK_FORMAT_UNDEFINED) format = PlaceholderFormatForNumericDomain(domain); + } else if (shadow) format = VK_FORMAT_D16_UNORM; + else if (domain == SamplerNumericDomain::Float) format = VK_FORMAT_R8G8B8A8_UNORM; + else if (domain == SamplerNumericDomain::SignedInteger) format = VK_FORMAT_R8G8B8A8_SINT; + else if (domain == SamplerNumericDomain::UnsignedInteger) format = VK_FORMAT_R8G8B8A8_UINT; + if (format == VK_FORMAT_UNDEFINED) return false; + VkImageType imageType = VK_IMAGE_TYPE_2D; + VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D; + VkImageCreateFlags flags = 0; + Uint32 layers = 1; + switch (target) { + case TextureTarget::Texture1D: imageType = VK_IMAGE_TYPE_1D; viewType = VK_IMAGE_VIEW_TYPE_1D; break; + case TextureTarget::Texture1DArray: imageType = VK_IMAGE_TYPE_1D; viewType = VK_IMAGE_VIEW_TYPE_1D_ARRAY; break; + case TextureTarget::Texture2D: case TextureTarget::TextureRectangle: + case TextureTarget::Texture2DMultisample: break; + case TextureTarget::Texture2DArray: case TextureTarget::Texture2DMultisampleArray: + viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY; break; + case TextureTarget::Texture3D: imageType = VK_IMAGE_TYPE_3D; viewType = VK_IMAGE_VIEW_TYPE_3D; break; + case TextureTarget::TextureCubeMap: case TextureTarget::TextureCubeMapArray: + flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; layers = 6; + viewType = target == TextureTarget::TextureCubeMap ? VK_IMAGE_VIEW_TYPE_CUBE : VK_IMAGE_VIEW_TYPE_CUBE_ARRAY; + break; + default: return false; + } + const Uint64 key = static_cast(format) | (static_cast(target) << 32) | + (static_cast(storage) << 48) | (static_cast(shadow) << 49); + auto found = m_wirePlaceholderImages.find(key); + if (found == m_wirePlaceholderImages.end()) { + WirePlaceholderImage image{}; + image.format = format; image.layers = layers; + image.aspect = shadow ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT; + const auto fail = [&] { + MGLOG_E_ONCE("Magma wire placeholder allocation failed: target=%d format=%d storage=%d multisample=%d", + static_cast(target), static_cast(format), static_cast(storage), static_cast(multisample)); + DestroyWirePlaceholderImage(image); + return false; + }; + const VkImageUsageFlags usage = (storage ? VK_IMAGE_USAGE_STORAGE_BIT : VK_IMAGE_USAGE_SAMPLED_BIT) | + (multisample ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : VK_IMAGE_USAGE_TRANSFER_DST_BIT); + VkImageFormatProperties properties{}; + if (vkGetPhysicalDeviceImageFormatProperties(m_physicalDevice, format, imageType, + VK_IMAGE_TILING_OPTIMAL, usage, flags, &properties) != VK_SUCCESS) return fail(); + if (multisample) { + // MS=1 in SPIR-V cannot name a single-sampled image, even when + // the shader's runtime branch does not fetch that descriptor. + image.samples = VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM; + for (const auto sample : {VK_SAMPLE_COUNT_2_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT, + VK_SAMPLE_COUNT_16_BIT, VK_SAMPLE_COUNT_32_BIT, VK_SAMPLE_COUNT_64_BIT}) { + if (properties.sampleCounts & sample) { image.samples = sample; break; } + } + if (image.samples == VK_SAMPLE_COUNT_FLAG_BITS_MAX_ENUM) return fail(); + } + VkImageCreateInfo create{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + create.flags = flags; create.imageType = imageType; create.format = format; + create.extent = {1, 1, 1}; create.mipLevels = 1; create.arrayLayers = layers; + create.samples = image.samples; create.tiling = VK_IMAGE_TILING_OPTIMAL; + create.usage = usage; create.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + if (vkCreateImage(m_device, &create, nullptr, &image.image) != VK_SUCCESS) return fail(); + VkMemoryRequirements requirements{}; + vkGetImageMemoryRequirements(m_device, image.image, &requirements); + VkPhysicalDeviceMemoryProperties memoryProperties{}; + vkGetPhysicalDeviceMemoryProperties(m_physicalDevice, &memoryProperties); + Uint32 memoryType = ~0u; + for (Uint32 i = 0; i < memoryProperties.memoryTypeCount; ++i) { + if (!(requirements.memoryTypeBits & (1u << i))) continue; + if (memoryType == ~0u) memoryType = i; + if (memoryProperties.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) { + memoryType = i; break; + } + } + if (memoryType == ~0u) return fail(); + VkMemoryAllocateInfo allocate{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocate.allocationSize = requirements.size; allocate.memoryTypeIndex = memoryType; + if (vkAllocateMemory(m_device, &allocate, nullptr, &image.memory) != VK_SUCCESS || + vkBindImageMemory(m_device, image.image, image.memory, 0) != VK_SUCCESS) return fail(); + VkImageViewCreateInfo view{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + view.image = image.image; view.viewType = viewType; view.format = format; + view.subresourceRange = {image.aspect, 0, 1, 0, layers}; + if (vkCreateImageView(m_device, &view, nullptr, &image.view) != VK_SUCCESS) return fail(); + if (!storage) { + VkSamplerCreateInfo sampler{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; + sampler.magFilter = sampler.minFilter = VK_FILTER_NEAREST; + sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + sampler.addressModeU = sampler.addressModeV = sampler.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sampler.compareEnable = shadow; sampler.compareOp = VK_COMPARE_OP_LESS_OR_EQUAL; + sampler.maxLod = 0.0f; + if (vkCreateSampler(m_device, &sampler, nullptr, &image.sampler) != VK_SUCCESS) return fail(); + } + if (multisample) { + VkAttachmentDescription attachment{}; + attachment.format = format; attachment.samples = image.samples; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; attachment.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + VkAttachmentReference color{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &color; + VkRenderPassCreateInfo pass{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + pass.attachmentCount = 1; pass.pAttachments = &attachment; + pass.subpassCount = 1; pass.pSubpasses = &subpass; + VkSubpassDependency dependencies[2]{}; + dependencies[0] = {VK_SUBPASS_EXTERNAL, 0, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, 0}; + dependencies[1] = {0, VK_SUBPASS_EXTERNAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, 0}; + pass.dependencyCount = 2; pass.pDependencies = dependencies; + if (vkCreateRenderPass(m_device, &pass, nullptr, &image.clearPass) != VK_SUCCESS) return fail(); + VkFramebufferCreateInfo framebuffer{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebuffer.renderPass = image.clearPass; framebuffer.attachmentCount = 1; + framebuffer.pAttachments = &image.view; framebuffer.width = framebuffer.height = 1; framebuffer.layers = layers; + if (vkCreateFramebuffer(m_device, &framebuffer, nullptr, &image.clearFramebuffer) != VK_SUCCESS) return fail(); + } + found = m_wirePlaceholderImages.emplace(key, image).first; + } + auto& image = found->second; + // Sampled incomplete textures remain immutable opaque black. A storage + // placeholder is reset before each use, so a prior discarded store cannot + // leak into an unrelated invocation's unbound image loads. + if (storage || image.layout == VK_IMAGE_LAYOUT_UNDEFINED) { + VkClearValue clear{}; + if (!storage) { + if (domain == SamplerNumericDomain::SignedInteger) clear.color.int32[3] = 1; + else if (domain == SamplerNumericDomain::UnsignedInteger) clear.color.uint32[3] = 1; + else clear.color.float32[3] = 1.0f; + } + if (multisample) { + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = image.clearPass; begin.framebuffer = image.clearFramebuffer; + begin.renderArea.extent = {1, 1}; begin.clearValueCount = 1; begin.pClearValues = &clear; + vkCmdBeginRenderPass(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + vkCmdEndRenderPass(commands); + image.layout = VK_IMAGE_LAYOUT_GENERAL; + } else { + if (!VkTextureManager::TransitionImageLayout(commands, image.image, image.layout, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_MEMORY_READ_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT, image.aspect)) return false; + const VkImageSubresourceRange range{image.aspect, 0, 1, 0, image.layers}; + if (shadow) { + const VkClearDepthStencilValue depth{0.0f, 0}; + vkCmdClearDepthStencilImage(commands, image.image, image.layout, &depth, 1, &range); + } else vkCmdClearColorImage(commands, image.image, image.layout, &clear.color, 1, &range); + if (!VkTextureManager::TransitionImageLayout(commands, image.image, image.layout, + VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, + image.aspect)) return false; + } + VkMemoryBarrier visible{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + visible.srcAccessMask = multisample ? VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT : VK_ACCESS_TRANSFER_WRITE_BIT; + visible.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; + vkCmdPipelineBarrier(commands, multisample ? VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT : VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &visible, 0, nullptr, 0, nullptr); + } + out = {image.sampler, image.view, VK_IMAGE_LAYOUT_GENERAL}; + return true; + } +#endif