diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index dc32cf44..e0e3eea1 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -3446,6 +3446,7 @@ void main() { ClearAllWireDrawPassCaches(); DestroyWireColorBlitResources(); DestroyWireDepthMipmapResources(); + DestroyWireMultisampleResolveResources(); #endif OnSubmitsCompletedUpTo(m_submitCounter); DestroySubmitFencePool(); @@ -7913,6 +7914,9 @@ void main() { // AFTER WireColorBlit.inc: the depth mip reuses that file's window-Y convention and is // written to be read beside it. #include "WireDepthMipmap.inc" + // AFTER WireDepthMipmap.inc for the same reason: the multisample depth/stencil resolve is + // that file's pass with the box filter taken out and a sample index put in. + #include "WireMultisampleResolve.inc" #include "WireDraw.inc" void VulkanRenderer::Clear(GLbitfield mask) { @@ -14777,6 +14781,16 @@ void main() { m_wireDepthResolveModes = resolveProperties.supportedDepthResolveModes; m_wireStencilResolveModes = resolveProperties.supportedStencilResolveModes; } + // P7 wave 2-B2, CONTRACT-P7 §3.2 (`multisample-blit-aspect`): the shader resolve's + // stencil half needs SPV_EXT_shader_stencil_export, because a fragment shader cannot + // write the stencil aspect without it. Enabled on the same terms as the resolve + // extensions above - wire arms only - and the arm declines stencil where it is absent + // rather than producing an undefined aspect. + m_wireShaderStencilExport = MG_Config::Transport != MG_Config::TransportMode::Monolith && + IsExtensionSupported(availableExtensions, VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME); + if (m_wireShaderStencilExport) + EnableOptionalDeviceExtension(availableExtensions, enabledDeviceExtensions, + VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME); #endif if ((descriptorIndexingCore || descriptorIndexingExtension) && getPhysicalDeviceFeatures2 != nullptr && getPhysicalDeviceProperties2 != nullptr) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 46412be5..f91845fb 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -467,6 +467,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkImageLayout* trackedLayout = nullptr; VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT; VkImageAspectFlags aspect = 0; + // Zero for the default framebuffer, which no arm that reads this ever reaches. + VkImageUsageFlags usageFlags = 0; Uint32 level = 0, layer = 0, layers = 1, levels = 1; Bool isDefault = false; // Set when isDefault: the swapchain image index this role resolved to. @@ -491,6 +493,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { PFN_vkCreateRenderPass2 m_wireCreateRenderPass2 = nullptr; VkResolveModeFlags m_wireDepthResolveModes = 0; VkResolveModeFlags m_wireStencilResolveModes = 0; + // P7 wave 2-B2, CONTRACT-P7 §3.2 (`multisample-blit-aspect`): the SECOND arm of the + // multisample depth/stencil resolve, for the device that does not carry + // VK_KHR_depth_stencil_resolve. One baked pass per aspect, sample zero, no frontend + // object - see WireMultisampleResolve.inc. `resolved` must be a single-sample image of + // the source's format and extent; the rect is expressed by the scissor. + Bool ResolveWireDepthStencilWithShader(WireImage source, WireImage resolved, + VkImageAspectFlags aspect, GLint sx0, GLint sy0, + Uint32 width, Uint32 height); + struct WireMultisampleResolveResources; + WireMultisampleResolveResources* m_wireMultisampleResolveResources = nullptr; + void DestroyWireMultisampleResolveResources(); + Bool m_wireShaderStencilExport = false; Bool BlitWireColorToDefault(WireImage source, WireImage destination, GLint sx0, GLint sy0, GLint sx1, GLint sy1, GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLenum filter); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc index 1ef02132..1d96e605 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc @@ -101,6 +101,21 @@ return forced; } + // AND THE SAME PROBLEM AGAIN FOR THE SHADER MULTISAMPLE RESOLVE (P7 wave 2-B2). lavapipe + // carries VK_KHR_create_renderpass2 AND VK_KHR_depth_stencil_resolve, so the extension arm + // is taken for every depth/stencil resolve this host can run and the shader arm - the one + // that exists for the device WITHOUT them, which is where the session used to die at + // `Magma:multisample-depth-resolve-capability` - would never execute under any lane. The + // knob drops the extension arm. It does not pretend the extension is missing anywhere else: + // nothing but the arm choice reads it. + static Bool MagmaWireForcedShaderDepthResolve() { + static const Bool forced = [] { + const char* value = std::getenv("MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE"); + return value && *value && value[0] != '0'; + }(); + return forced; + } + VulkanRenderer::WireImage VulkanRenderer::ResolveWireImage( const MG_Pipe::MGPFramebufferState& fbo, const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect, Bool isWriteTarget) { @@ -145,6 +160,11 @@ out.layout = resource->layout; out.trackedLayout = &resource->layout; out.samples = resource->sampleCount; + // P7 wave 2-B2: the shader multisample resolve SAMPLES this image, which is a question + // about the image and not about the format - VkTextureManager asks for SAMPLED on every + // attachment it creates, and a device that declined it would make the descriptor write + // undefined rather than fail. Carried here so the arm can decline instead. + out.usageFlags = resource->usageFlags; out.level = level; out.levels = resource->mipLevels; out.layer = layer; @@ -428,10 +448,25 @@ else if (source.format == VK_FORMAT_D16_UNORM_S8_UINT || source.format == VK_FORMAT_D24_UNORM_S8_UINT || source.format == VK_FORMAT_D32_SFLOAT_S8_UINT) formatAspects |= VK_IMAGE_ASPECT_STENCIL_BIT; - if (!m_wireCreateRenderPass2 || - ((formatAspects & VK_IMAGE_ASPECT_DEPTH_BIT) && !(m_wireDepthResolveModes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT)) || - ((formatAspects & VK_IMAGE_ASPECT_STENCIL_BIT) && !(m_wireStencilResolveModes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT))) - MagmaWireFatal("multisample-depth-resolve-capability"); + // P7 wave 2-B2, CONTRACT-P7 §3.2 (`multisample-blit-aspect`): TWO ARMS, and which one + // runs is a fact about the DEVICE, not about the shape. VK_KHR_depth_stencil_resolve is + // the direct route and is taken wherever it exists; where it does not - which is where + // the session used to end at `Magma:multisample-depth-resolve-capability`, on the first + // glBlitFramebuffer(GL_DEPTH_BUFFER_BIT) out of a multisampled read framebuffer - a + // baked pass fetches sample ZERO and writes gl_FragDepth (or, with + // VK_EXT_shader_stencil_export, gl_FragStencilRefARB). Sample zero in both arms, because + // VK_RESOLVE_MODE_SAMPLE_ZERO_BIT is what the subpass below asks for. + // + // THE TWO CAPABILITY TESTS ARE NOT THE SAME TEST, and the difference is structural: the + // subpass resolves every aspect the FORMAT has, so it needs a mode for each of them, + // while the shader pass writes only the one aspect this blit asked for and leaves the + // rest of the scratch undefined - which is safe because the copy below reads exactly + // that aspect. + const Bool extensionResolve = !MagmaWireForcedShaderDepthResolve() && m_wireCreateRenderPass2 && + (!(formatAspects & VK_IMAGE_ASPECT_DEPTH_BIT) || + (m_wireDepthResolveModes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT)) && + (!(formatAspects & VK_IMAGE_ASPECT_STENCIL_BIT) || + (m_wireStencilResolveModes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT)); // vkCmdResolveImage is color-only. A depth/stencil resolve subpass must // write at the source coordinates and may require both aspects to use @@ -461,10 +496,14 @@ resolved.extent = scratch->extent; resolved.aspect = formatAspects; resolved.trackedLayout = &scratch->layout; + + VkImageView views[2]{}; + VkRenderPass pass = VK_NULL_HANDLE; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + if (extensionResolve) { TransitionWireImage(source, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); TransitionWireImage(resolved, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); - VkImageView views[2]{}; VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = source.image; viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; @@ -503,7 +542,6 @@ passInfo.pAttachments = attachments; passInfo.subpassCount = 1; passInfo.pSubpasses = &subpass; - VkRenderPass pass = VK_NULL_HANDLE; VK_VERIFY(m_wireCreateRenderPass2(m_device, &passInfo, nullptr, &pass)); VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; framebufferInfo.renderPass = pass; @@ -512,15 +550,38 @@ framebufferInfo.width = source.extent.width; framebufferInfo.height = source.extent.height; framebufferInfo.layers = 1; - VkFramebuffer framebuffer = VK_NULL_HANDLE; VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &framebuffer)); VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; begin.renderPass = pass; begin.framebuffer = framebuffer; begin.renderArea = {{sx0, sy0}, {static_cast(width), static_cast(height)}}; + vkCmdBeginRenderPass(m_frameContext.GetCurrent().commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE); + vkCmdEndRenderPass(m_frameContext.GetCurrent().commandBuffer); + } else if (!ResolveWireDepthStencilWithShader(source, resolved, source.aspect, sx0, sy0, + static_cast(width), static_cast(height))) { + // NEITHER ARM, and rule I (a) says what that is: this device has no + // VK_KHR_depth_stencil_resolve and the pass cannot stand in for it either - the + // commonest reason being a stencil-only resolve without VK_EXT_shader_stencil_export, + // where a fragment shader cannot put a value in the stencil aspect at all. A decline + // leaves the destination as the application last wrote it, which is what + // glBlitFramebuffer would do if the driver refused the resolve. + MGLOG_E_ONCE("DirectVulkan::ResolveWireDepthStencil: resolving aspect 0x%x of a %u-sample " + "format %d needs either VK_KHR_depth_stencil_resolve (renderpass2=%d, " + "depth modes 0x%x, stencil modes 0x%x) or a shader pass " + "(stencil export=%d, source usage 0x%x); declining the blit", + static_cast(source.aspect), static_cast(source.samples), + static_cast(source.format), m_wireCreateRenderPass2 ? 1 : 0, + static_cast(m_wireDepthResolveModes), + static_cast(m_wireStencilResolveModes), + m_wireShaderStencilExport ? 1 : 0, static_cast(source.usageFlags)); + RecordTextureCopyError("ResolveWireDepthStencil", ErrorCode::InvalidOperation, + "This device cannot resolve that multisample depth/stencil aspect."); + WireRetiredObjects declined{}; + declined.textures.push_back(Move(scratch)); + RetireWireObjects(Move(declined)); + return; + } auto& frame = m_frameContext.GetCurrent(); - vkCmdBeginRenderPass(frame.commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE); - vkCmdEndRenderPass(frame.commandBuffer); // Depth/stencil resolve writes execute in COLOR_ATTACHMENT_OUTPUT, // unlike ordinary depth attachment writes. Include both when exposing // the resolved image to a transfer; layout-based inference only covers @@ -579,9 +640,11 @@ MagmaWireFatal("multisample-depth-resolve-conversion"); } WireRetiredObjects retired{}; - retired.framebuffers.push_back(framebuffer); - retired.renderPasses.push_back(pass); - retired.imageViews = {views[0], views[1]}; + // The shader arm owns and retires its own pass, framebuffer and views, so these four + // are null on that path - pushed only when the extension arm made them. + if (framebuffer != VK_NULL_HANDLE) retired.framebuffers.push_back(framebuffer); + if (pass != VK_NULL_HANDLE) retired.renderPasses.push_back(pass); + if (views[0] != VK_NULL_HANDLE) retired.imageViews = {views[0], views[1]}; retired.textures.push_back(Move(scratch)); if (aspectBuffer) retired.buffers.push_back(Move(aspectBuffer)); RetireWireObjects(Move(retired)); @@ -883,7 +946,35 @@ VkImageCopy copy{region.srcSubresource, region.srcOffsets[0], region.dstSubresource, region.dstOffsets[0], {static_cast(width), static_cast(height), 1}}; vkCmdCopyImage(command, source.image, srcLayout, destination.image, dstLayout, 1, ©); - } else MagmaWireFatal("multisample-blit-aspect@P7"); + } else { + // `multisample-blit-aspect@P7` RETIRES. The half with an arm is + // ResolveWireDepthStencil's: a multisample depth or stencil SOURCE now + // reaches a single-sample destination on every device, through + // VK_KHR_depth_stencil_resolve where it exists and through the baked pass in + // WireMultisampleResolve.inc where it does not. This is the half with none - + // the sample counts differ and the DESTINATION is the multisampled side, so + // it is either one sample fanned out to N or N reduced to M. + // + // Vulkan has no command for either: vkCmdResolveImage only ever writes a + // single-sample destination, vkCmdBlitImage refuses both sides, and + // vkCmdCopyImage requires the counts to match. Reaching it through a draw + // would need a pipeline with the destination's sample count and a + // multisample source view, i.e. a second copy of the resolve machinery + // pointed the other way, for a shape GL 4.6 core 18.3.1 calls + // INVALID_OPERATION whenever the two rectangles are not identical and + // describes as sample replication when they are. Rule I (a): name both + // counts, record the error, leave the destination alone. + MGLOG_E_ONCE("DirectVulkan::BlitWireFramebuffers: aspect 0x%x cannot go from a " + "%u-sample source to a %u-sample destination - a resolve only ever " + "writes one sample and a copy needs the counts to match; declining " + "the blit", + static_cast(aspect), static_cast(source.samples), + static_cast(destination.samples)); + RecordTextureCopyError("BlitWireFramebuffers", ErrorCode::InvalidOperation, + "glBlitFramebuffer cannot change the sample count of an " + "attachment in that direction."); + return; + } } else { vkCmdBlitImage(command,source.image,srcLayout,destination.image,dstLayout,1,®ion, filter == GL_LINEAR ? VK_FILTER_LINEAR : VK_FILTER_NEAREST); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleDepthResolve.frag b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleDepthResolve.frag new file mode 100644 index 00000000..f508f5bb --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleDepthResolve.frag @@ -0,0 +1,16 @@ +#version 450 +// SAMPLE ZERO, and it is not an approximation: VK_RESOLVE_MODE_SAMPLE_ZERO_BIT is the mode the +// VK_KHR_depth_stencil_resolve arm asks for, and GL 4.6 core 18.3.1 says a depth resolve picks +// a value "in an implementation-dependent manner where the result will be between the minimum +// and maximum depth values in the pixel" - sample 0 is one of the samples, so it qualifies, and +// taking the same sample as the extension arm is what makes the two arms interchangeable on one +// device. +// +// AVERAGING WOULD NOT BE: a mean of four depths is a surface that exists nowhere, and GL's +// bound is on the per-pixel min/max, which a mean satisfies but a shadow comparison against it +// does not survive. +layout(set = 0, binding = 0) uniform sampler2DMS sourceImage; + +void main() { + gl_FragDepth = texelFetch(sourceImage, ivec2(gl_FragCoord.xy), 0).r; +} diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolve.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolve.inc new file mode 100644 index 00000000..b822e0f4 --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolve.inc @@ -0,0 +1,319 @@ +// Included by VulkanRenderer.cpp inside the backend namespace, after WireDepthMipmap.inc. +// The native multisample depth/stencil resolve owns no frontend object and never invokes the +// GL shader compiler. +// +// WHY THIS FILE EXISTS. vkCmdResolveImage is COLOUR ONLY. The one way Vulkan resolves a depth +// or stencil aspect without a shader is a render pass with a depth/stencil resolve attachment, +// which needs VK_KHR_create_renderpass2 and VK_KHR_depth_stencil_resolve - and until this +// package a device without either killed the session at +// `Magma:multisample-depth-resolve-capability` the first time an application resolved a +// multisample depth buffer, which is what glBlitFramebuffer(GL_DEPTH_BUFFER_BIT) from a +// multisampled read framebuffer IS. CONTRACT-P7 §3.2's `multisample-blit-aspect` row names both +// arms; this is the second one. +// +// SAMPLE ZERO IN BOTH ARMS. The extension arm asks for VK_RESOLVE_MODE_SAMPLE_ZERO_BIT, so the +// shader arm fetches sample 0 - not an average. Two arms that answer differently on two devices +// would make "the wire arm is observably the monolith's" untestable, and GL 4.6 core 18.3.1 +// requires a SELECTED sample for stencil anyway. +#if MOBILEGL_BUILD_DISAGGREGATED +#include "WireMultisampleResolveSpirv.h" + + struct VulkanRenderer::WireMultisampleResolveResources { + struct Target { + VkFormat format = VK_FORMAT_UNDEFINED; + VkImageAspectFlags aspect = 0; // the ONE aspect this pipeline writes + VkRenderPass renderPass = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; + }; + VkShaderModule vertex = VK_NULL_HANDLE; + VkShaderModule depthFragment = VK_NULL_HANDLE, stencilFragment = VK_NULL_HANDLE; + VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkSampler nearest = VK_NULL_HANDLE; + Vector targets; + }; + + void VulkanRenderer::DestroyWireMultisampleResolveResources() { + if (!m_wireMultisampleResolveResources) return; + // Shutdown calls this after vkDeviceWaitIdle and after all per-submit descriptor + // pools/framebuffers/views have been collected. + auto& cache = *m_wireMultisampleResolveResources; + for (const auto& target : cache.targets) { + vkDestroyPipeline(m_device, target.pipeline, nullptr); + vkDestroyRenderPass(m_device, target.renderPass, nullptr); + } + vkDestroySampler(m_device, cache.nearest, nullptr); + vkDestroyPipelineLayout(m_device, cache.pipelineLayout, nullptr); + vkDestroyDescriptorSetLayout(m_device, cache.descriptorLayout, nullptr); + vkDestroyShaderModule(m_device, cache.vertex, nullptr); + vkDestroyShaderModule(m_device, cache.depthFragment, nullptr); + vkDestroyShaderModule(m_device, cache.stencilFragment, nullptr); + delete m_wireMultisampleResolveResources; + m_wireMultisampleResolveResources = nullptr; + } + + // ONE ASPECT, source -> resolved, both 2D images of the same format and extent, the source + // multisampled and the destination not. Returns false for every shape it cannot express, + // which the caller turns into a decline; it never ends the session. + Bool VulkanRenderer::ResolveWireDepthStencilWithShader(WireImage source, WireImage resolved, + VkImageAspectFlags aspect, GLint sx0, GLint sy0, Uint32 width, Uint32 height) { + if (!source.image || !resolved.image || !width || !height) return false; + if (aspect != VK_IMAGE_ASPECT_DEPTH_BIT && aspect != VK_IMAGE_ASPECT_STENCIL_BIT) return false; + // A fragment shader cannot write the stencil aspect at all without + // VK_EXT_shader_stencil_export. §3.2: decline stencil-only rather than produce a level + // whose stencil is whatever the DONT_CARE left there. + if (aspect == VK_IMAGE_ASPECT_STENCIL_BIT && !m_wireShaderStencilExport) return false; + if (source.samples == VK_SAMPLE_COUNT_1_BIT) return false; + if (source.format != resolved.format) return false; + // SAMPLED is asked for by every attachment VkTextureManager creates; a device that + // declined it would make the descriptor write below undefined, not failed. + if (!(source.usageFlags & VK_IMAGE_USAGE_SAMPLED_BIT)) return false; + VkFormatProperties properties{}; + vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, source.format, &properties); + const VkFormatFeatureFlags required = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | + VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT; + if ((properties.optimalTilingFeatures & required) != required) return false; + + VkImageAspectFlags formatAspects = VK_IMAGE_ASPECT_DEPTH_BIT; + if (source.format == VK_FORMAT_S8_UINT) formatAspects = VK_IMAGE_ASPECT_STENCIL_BIT; + else if (source.format == VK_FORMAT_D16_UNORM_S8_UINT || source.format == VK_FORMAT_D24_UNORM_S8_UINT || + source.format == VK_FORMAT_D32_SFLOAT_S8_UINT) + formatAspects |= VK_IMAGE_ASPECT_STENCIL_BIT; + if (!(formatAspects & aspect)) return false; + + if (!m_wireMultisampleResolveResources) { + m_wireMultisampleResolveResources = new WireMultisampleResolveResources; + auto& cache = *m_wireMultisampleResolveResources; + const auto shader = [&](const Uint32* code, SizeT bytes, VkShaderModule& module) { + VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + info.codeSize = bytes; + info.pCode = code; + VK_VERIFY(vkCreateShaderModule(m_device, &info, nullptr, &module)); + }; + shader(kWireMultisampleResolveVertexSpirv, sizeof(kWireMultisampleResolveVertexSpirv), cache.vertex); + shader(kWireMultisampleDepthResolveFragmentSpirv, + sizeof(kWireMultisampleDepthResolveFragmentSpirv), cache.depthFragment); + // THE STENCIL MODULE IS ONLY CREATED WHERE IT CAN RUN: its SPIR-V declares the + // StencilExportEXT capability, and handing a module with an unsupported capability + // to vkCreateShaderModule is undefined behaviour - not an error this could catch. + if (m_wireShaderStencilExport) + shader(kWireMultisampleStencilResolveFragmentSpirv, + sizeof(kWireMultisampleStencilResolveFragmentSpirv), cache.stencilFragment); + const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + 1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}; + VkDescriptorSetLayoutCreateInfo layout{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + layout.bindingCount = 1; + layout.pBindings = &binding; + VK_VERIFY(vkCreateDescriptorSetLayout(m_device, &layout, nullptr, &cache.descriptorLayout)); + // NO PUSH CONSTANT RANGE AT ALL: the pass reads its coordinate from gl_FragCoord and + // its rectangle from the scissor, so neither stage takes an input. + VkPipelineLayoutCreateInfo pipelineLayout{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + pipelineLayout.setLayoutCount = 1; + pipelineLayout.pSetLayouts = &cache.descriptorLayout; + VK_VERIFY(vkCreatePipelineLayout(m_device, &pipelineLayout, nullptr, &cache.pipelineLayout)); + // NEAREST: texelFetch ignores the sampler's filter, and most devices do not + // advertise linear filtering for a depth format anyway. The sampler exists only + // because GLSL's sampler2DMS is a COMBINED image sampler. + VkSamplerCreateInfo sampler{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; + sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + sampler.addressModeU = sampler.addressModeV = sampler.addressModeW = + VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sampler.magFilter = sampler.minFilter = VK_FILTER_NEAREST; + sampler.maxLod = 0.0f; + VK_VERIFY(vkCreateSampler(m_device, &sampler, nullptr, &cache.nearest)); + } + auto& cache = *m_wireMultisampleResolveResources; + const VkShaderModule fragment = aspect == VK_IMAGE_ASPECT_STENCIL_BIT ? cache.stencilFragment + : cache.depthFragment; + if (fragment == VK_NULL_HANDLE) return false; + + WireMultisampleResolveResources::Target* target = nullptr; + for (auto& entry : cache.targets) + if (entry.format == resolved.format && entry.aspect == aspect) { target = &entry; break; } + if (!target) { + cache.targets.emplace_back(); + target = &cache.targets.back(); + target->format = resolved.format; + target->aspect = aspect; + VkAttachmentDescription attachment{}; + attachment.format = resolved.format; + attachment.samples = VK_SAMPLE_COUNT_1_BIT; + // DONT_CARE on BOTH aspects: the pass writes every texel of the scissored rect for + // the aspect it owns, and the caller copies only that aspect out. The other aspect + // of a packed format is left undefined on purpose - it is never read, and LOADing + // it would order this pass behind a write nobody is waiting for. + attachment.loadOp = attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.storeOp = attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.initialLayout = attachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + const VkAttachmentReference depthStencil{0, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.pDepthStencilAttachment = &depthStencil; + VkRenderPassCreateInfo pass{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + pass.attachmentCount = 1; + pass.pAttachments = &attachment; + pass.subpassCount = 1; + pass.pSubpasses = &subpass; + VK_VERIFY(vkCreateRenderPass(m_device, &pass, nullptr, &target->renderPass)); + VkPipelineShaderStageCreateInfo stages[2]{}; + for (auto& stage : stages) { + stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + stage.pName = "main"; + } + stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; + stages[0].module = cache.vertex; + stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; + stages[1].module = fragment; + const VkPipelineVertexInputStateCreateInfo vertex{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + VkPipelineViewportStateCreateInfo viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewport.viewportCount = viewport.scissorCount = 1; + VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + raster.polygonMode = VK_POLYGON_MODE_FILL; + raster.cullMode = VK_CULL_MODE_NONE; + raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + raster.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineDepthStencilStateCreateInfo depthStencilState{ + VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; + depthStencilState.minDepthBounds = 0.0f; + depthStencilState.maxDepthBounds = 1.0f; + if (aspect == VK_IMAGE_ASPECT_DEPTH_BIT) { + // ALWAYS + write: gl_FragDepth is the answer, and a comparison against whatever + // DONT_CARE left behind would discard some of it. + depthStencilState.depthTestEnable = VK_TRUE; + depthStencilState.depthWriteEnable = VK_TRUE; + depthStencilState.depthCompareOp = VK_COMPARE_OP_ALWAYS; + } else { + // REPLACE on every outcome, compare ALWAYS, full write mask: that is what makes + // the shader-exported reference the value that lands. The static `reference` is + // overridden by gl_FragStencilRefARB, which is the whole point of the extension. + depthStencilState.stencilTestEnable = VK_TRUE; + VkStencilOpState op{}; + op.failOp = op.passOp = op.depthFailOp = VK_STENCIL_OP_REPLACE; + op.compareOp = VK_COMPARE_OP_ALWAYS; + op.compareMask = op.writeMask = 0xFFu; + op.reference = 0; + depthStencilState.front = depthStencilState.back = op; + } + // No colour attachment in this render pass, so no attachment states either. + VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + const VkDynamicState dynamicStates[]{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; + dynamic.dynamicStateCount = 2; + dynamic.pDynamicStates = dynamicStates; + VkGraphicsPipelineCreateInfo pipeline{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + pipeline.stageCount = 2; + pipeline.pStages = stages; + pipeline.pVertexInputState = &vertex; + pipeline.pInputAssemblyState = &assembly; + pipeline.pViewportState = &viewport; + pipeline.pRasterizationState = &raster; + pipeline.pMultisampleState = &samples; + pipeline.pDepthStencilState = &depthStencilState; + pipeline.pColorBlendState = &blend; + pipeline.pDynamicState = &dynamic; + pipeline.layout = cache.pipelineLayout; + pipeline.renderPass = target->renderPass; + if (vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &pipeline, nullptr, + &target->pipeline) != VK_SUCCESS) { + vkDestroyRenderPass(m_device, target->renderPass, nullptr); + cache.targets.pop_back(); + return false; + } + } + + m_textureManager->FlushPendingUploads(); + TransitionWireImage(source, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + TransitionWireImage(resolved, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); + + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = source.image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = source.format; + // ONE ASPECT on the sampled view: a depth/stencil image can only be SAMPLED through a + // view that names exactly one of its aspects, which is also what makes the stencil half + // a usampler2DMS and the depth half a sampler2DMS. + viewInfo.subresourceRange = {aspect, source.level, 1, source.layer, 1}; + VkImageView sourceView = VK_NULL_HANDLE; + VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &sourceView)); + viewInfo.image = resolved.image; + // BOTH aspects on the attachment view: a depth/stencil ATTACHMENT names every aspect + // its format has, whichever one the pipeline writes. + viewInfo.subresourceRange = {formatAspects, 0, 1, 0, 1}; + VkImageView targetView = VK_NULL_HANDLE; + VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &targetView)); + + VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebufferInfo.renderPass = target->renderPass; + framebufferInfo.attachmentCount = 1; + framebufferInfo.pAttachments = &targetView; + framebufferInfo.width = resolved.extent.width; + framebufferInfo.height = resolved.extent.height; + framebufferInfo.layers = 1; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &framebuffer)); + + // Each in-flight pass owns its descriptor set, for the colour blit's reason: updating a + // cached set would rewrite the source view of an older queued pass. + VkDescriptorPool descriptorPool = VK_NULL_HANDLE; + VkDescriptorSet descriptorSet = VK_NULL_HANDLE; + const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1}; + VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + pool.maxSets = 1; + pool.poolSizeCount = 1; + pool.pPoolSizes = &poolSize; + VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &descriptorPool)); + VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + alloc.descriptorPool = descriptorPool; + alloc.descriptorSetCount = 1; + alloc.pSetLayouts = &cache.descriptorLayout; + VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet)); + const VkDescriptorImageInfo sampled{cache.nearest, sourceView, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = descriptorSet; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &sampled; + vkUpdateDescriptorSets(m_device, 1, &write, 0, nullptr); + + auto& frame = m_frameContext.GetCurrent(); + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = target->renderPass; + begin.framebuffer = framebuffer; + // The RECTANGLE is the render area, exactly as the extension arm's renderArea is - the + // scratch outside it is never read. + begin.renderArea = {{sx0, sy0}, {width, height}}; + vkCmdBeginRenderPass(frame.commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE); + vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, target->pipeline); + // THE VIEWPORT IS THE WHOLE ATTACHMENT AND THE SCISSOR IS THE RECT, and the two are not + // interchangeable here: gl_FragCoord is what the fragment shader fetches by, so the + // clip-space-to-attachment mapping has to be the identity for every rectangle. A + // viewport shrunk onto the rect would slide the whole image instead of moving a window + // over it. + const VkViewport viewport{0.0f, 0.0f, static_cast(resolved.extent.width), + static_cast(resolved.extent.height), 0.0f, 1.0f}; + const VkRect2D scissor{{sx0, sy0}, {width, height}}; + vkCmdSetViewport(frame.commandBuffer, 0, 1, &viewport); + vkCmdSetScissor(frame.commandBuffer, 0, 1, &scissor); + vkCmdBindDescriptorSets(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, + cache.pipelineLayout, 0, 1, &descriptorSet, 0, nullptr); + vkCmdDraw(frame.commandBuffer, 3, 1, 0, 0); + vkCmdEndRenderPass(frame.commandBuffer); + ResetDynamicStateShadow(); + InvalidatePipelineMemo(); + // This native pass binds its own set/layout without going through the application's + // descriptor binder. Its next fast path must rebind. + if (m_uniformManager) m_uniformManager->OnCommandBufferBoundary(); + WireRetiredObjects retired{}; + retired.framebuffers.push_back(framebuffer); + retired.imageViews = {sourceView, targetView}; + retired.descriptorPools.push_back(descriptorPool); + RetireWireObjects(Move(retired)); + return true; + } +#endif diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolve.vert b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolve.vert new file mode 100644 index 00000000..00075b34 --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolve.vert @@ -0,0 +1,18 @@ +#version 450 +// Native Vulkan multisample depth/stencil resolve: no GL program, registry entry, or runtime +// transpilation. +// +// THIS STAGE CARRIES NOTHING, and that is the whole design. The scratch this pass writes has +// the SAME extent and the SAME coordinate space as the multisample source it reads - both are +// native images, neither is flipped against the other - so the fragment stage can take its +// coordinate straight out of gl_FragCoord and needs no varying, no push constant and no rect. +// The requested rectangle is expressed by the scissor instead, which is also what the +// VK_KHR_depth_stencil_resolve arm next door expresses with its renderArea. +// +// An oversized triangle rather than WireDepthMipmap.vert's quad: there is no destination +// rectangle to clip to here, only the scissor, and a triangle rasterizes the same area with +// one fewer primitive and no shared edge. +void main() { + const vec2 corners[3] = vec2[](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + gl_Position = vec4(corners[gl_VertexIndex], 0.0, 1.0); +} diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolveSpirv.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolveSpirv.h new file mode 100644 index 00000000..9b3b27bc --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolveSpirv.h @@ -0,0 +1,103 @@ +// Generated from WireMultisampleResolve.vert / WireMultisampleDepthResolve.frag / +// WireMultisampleStencilResolve.frag by scripts/bake_internal_shaders.py, which runs MG_Test's +// BakedInternalShadersTest with the IN-TREE glslang and splices the words it emits into the +// arrays below. Do not edit the words by hand: MOBILEGL_BAKED_INTERNAL_SHADERS (the same test, +// run as a gate) recompiles every source in its table and reds on the first word that +// disagrees. +#pragma once + +static constexpr Uint32 kWireMultisampleResolveVertexSpirv[] = { + 0x07230203u, 0x00010000u, 0x0008000bu, 0x00000028u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu, + 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, + 0x0007000fu, 0x00000000u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x0000001au, 0x00030003u, + 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00060005u, 0x0000000bu, + 0x505f6c67u, 0x65567265u, 0x78657472u, 0x00000000u, 0x00060006u, 0x0000000bu, 0x00000000u, 0x505f6c67u, + 0x7469736fu, 0x006e6f69u, 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u, 0x746e696fu, 0x657a6953u, + 0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, 0x4470696cu, 0x61747369u, 0x0065636eu, + 0x00070006u, 0x0000000bu, 0x00000003u, 0x435f6c67u, 0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u, + 0x0000000du, 0x00000000u, 0x00060005u, 0x0000001au, 0x565f6c67u, 0x65747265u, 0x646e4978u, 0x00007865u, + 0x00050005u, 0x0000001du, 0x65646e69u, 0x6c626178u, 0x00000065u, 0x00030047u, 0x0000000bu, 0x00000002u, + 0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu, 0x00000000u, 0x00050048u, 0x0000000bu, 0x00000001u, + 0x0000000bu, 0x00000001u, 0x00050048u, 0x0000000bu, 0x00000002u, 0x0000000bu, 0x00000003u, 0x00050048u, + 0x0000000bu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x0000001au, 0x0000000bu, 0x0000002au, + 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, + 0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u, 0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u, + 0x0004002bu, 0x00000008u, 0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, 0x00000009u, + 0x0006001eu, 0x0000000bu, 0x00000007u, 0x00000006u, 0x0000000au, 0x0000000au, 0x00040020u, 0x0000000cu, + 0x00000003u, 0x0000000bu, 0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu, + 0x00000020u, 0x00000001u, 0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040017u, 0x00000010u, + 0x00000006u, 0x00000002u, 0x0004002bu, 0x00000008u, 0x00000011u, 0x00000003u, 0x0004001cu, 0x00000012u, + 0x00000010u, 0x00000011u, 0x0004002bu, 0x00000006u, 0x00000013u, 0xbf800000u, 0x0005002cu, 0x00000010u, + 0x00000014u, 0x00000013u, 0x00000013u, 0x0004002bu, 0x00000006u, 0x00000015u, 0x40400000u, 0x0005002cu, + 0x00000010u, 0x00000016u, 0x00000015u, 0x00000013u, 0x0005002cu, 0x00000010u, 0x00000017u, 0x00000013u, + 0x00000015u, 0x0006002cu, 0x00000012u, 0x00000018u, 0x00000014u, 0x00000016u, 0x00000017u, 0x00040020u, + 0x00000019u, 0x00000001u, 0x0000000eu, 0x0004003bu, 0x00000019u, 0x0000001au, 0x00000001u, 0x00040020u, + 0x0000001cu, 0x00000007u, 0x00000012u, 0x00040020u, 0x0000001eu, 0x00000007u, 0x00000010u, 0x0004002bu, + 0x00000006u, 0x00000021u, 0x00000000u, 0x0004002bu, 0x00000006u, 0x00000022u, 0x3f800000u, 0x00040020u, + 0x00000026u, 0x00000003u, 0x00000007u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, + 0x000200f8u, 0x00000005u, 0x0004003bu, 0x0000001cu, 0x0000001du, 0x00000007u, 0x0004003du, 0x0000000eu, + 0x0000001bu, 0x0000001au, 0x0003003eu, 0x0000001du, 0x00000018u, 0x00050041u, 0x0000001eu, 0x0000001fu, + 0x0000001du, 0x0000001bu, 0x0004003du, 0x00000010u, 0x00000020u, 0x0000001fu, 0x00050051u, 0x00000006u, + 0x00000023u, 0x00000020u, 0x00000000u, 0x00050051u, 0x00000006u, 0x00000024u, 0x00000020u, 0x00000001u, + 0x00070050u, 0x00000007u, 0x00000025u, 0x00000023u, 0x00000024u, 0x00000021u, 0x00000022u, 0x00050041u, + 0x00000026u, 0x00000027u, 0x0000000du, 0x0000000fu, 0x0003003eu, 0x00000027u, 0x00000025u, 0x000100fdu, + 0x00010038u, +}; + +static constexpr Uint32 kWireMultisampleDepthResolveFragmentSpirv[] = { + 0x07230203u, 0x00010000u, 0x0008000bu, 0x0000001du, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu, + 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, + 0x0007000fu, 0x00000004u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00000008u, 0x00000010u, 0x00030010u, + 0x00000004u, 0x00000007u, 0x00030010u, 0x00000004u, 0x0000000cu, 0x00030003u, 0x00000002u, 0x000001c2u, + 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00060005u, 0x00000008u, 0x465f6c67u, 0x44676172u, + 0x68747065u, 0x00000000u, 0x00050005u, 0x0000000cu, 0x72756f73u, 0x6d496563u, 0x00656761u, 0x00060005u, + 0x00000010u, 0x465f6c67u, 0x43676172u, 0x64726f6fu, 0x00000000u, 0x00040047u, 0x00000008u, 0x0000000bu, + 0x00000016u, 0x00040047u, 0x0000000cu, 0x00000021u, 0x00000000u, 0x00040047u, 0x0000000cu, 0x00000022u, + 0x00000000u, 0x00040047u, 0x00000010u, 0x0000000bu, 0x0000000fu, 0x00020013u, 0x00000002u, 0x00030021u, + 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040020u, 0x00000007u, 0x00000003u, + 0x00000006u, 0x0004003bu, 0x00000007u, 0x00000008u, 0x00000003u, 0x00090019u, 0x00000009u, 0x00000006u, + 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x0003001bu, 0x0000000au, + 0x00000009u, 0x00040020u, 0x0000000bu, 0x00000000u, 0x0000000au, 0x0004003bu, 0x0000000bu, 0x0000000cu, + 0x00000000u, 0x00040017u, 0x0000000eu, 0x00000006u, 0x00000004u, 0x00040020u, 0x0000000fu, 0x00000001u, + 0x0000000eu, 0x0004003bu, 0x0000000fu, 0x00000010u, 0x00000001u, 0x00040017u, 0x00000011u, 0x00000006u, + 0x00000002u, 0x00040015u, 0x00000014u, 0x00000020u, 0x00000001u, 0x00040017u, 0x00000015u, 0x00000014u, + 0x00000002u, 0x0004002bu, 0x00000014u, 0x00000017u, 0x00000000u, 0x00040015u, 0x0000001au, 0x00000020u, + 0x00000000u, 0x0004002bu, 0x0000001au, 0x0000001bu, 0x00000000u, 0x00050036u, 0x00000002u, 0x00000004u, + 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x0000000au, 0x0000000du, 0x0000000cu, + 0x0004003du, 0x0000000eu, 0x00000012u, 0x00000010u, 0x0007004fu, 0x00000011u, 0x00000013u, 0x00000012u, + 0x00000012u, 0x00000000u, 0x00000001u, 0x0004006eu, 0x00000015u, 0x00000016u, 0x00000013u, 0x00040064u, + 0x00000009u, 0x00000018u, 0x0000000du, 0x0007005fu, 0x0000000eu, 0x00000019u, 0x00000018u, 0x00000016u, + 0x00000040u, 0x00000017u, 0x00050051u, 0x00000006u, 0x0000001cu, 0x00000019u, 0x00000000u, 0x0003003eu, + 0x00000008u, 0x0000001cu, 0x000100fdu, 0x00010038u, +}; + +static constexpr Uint32 kWireMultisampleStencilResolveFragmentSpirv[] = { + 0x07230203u, 0x00010000u, 0x0008000bu, 0x0000001fu, 0x00000000u, 0x00020011u, 0x00000001u, 0x00020011u, + 0x00001395u, 0x0009000au, 0x5f565053u, 0x5f545845u, 0x64616873u, 0x735f7265u, 0x636e6574u, 0x655f6c69u, + 0x726f7078u, 0x00000074u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, + 0x0003000eu, 0x00000000u, 0x00000001u, 0x0007000fu, 0x00000004u, 0x00000004u, 0x6e69616du, 0x00000000u, + 0x00000008u, 0x00000012u, 0x00030010u, 0x00000004u, 0x00000007u, 0x00030010u, 0x00000004u, 0x000013a3u, + 0x00030003u, 0x00000002u, 0x000001c2u, 0x00090004u, 0x415f4c47u, 0x735f4252u, 0x65646168u, 0x74735f72u, + 0x69636e65u, 0x78655f6cu, 0x74726f70u, 0x00000000u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, + 0x00080005u, 0x00000008u, 0x465f6c67u, 0x53676172u, 0x636e6574u, 0x65526c69u, 0x42524166u, 0x00000000u, + 0x00050005u, 0x0000000du, 0x72756f73u, 0x6d496563u, 0x00656761u, 0x00060005u, 0x00000012u, 0x465f6c67u, + 0x43676172u, 0x64726f6fu, 0x00000000u, 0x00040047u, 0x00000008u, 0x0000000bu, 0x00001396u, 0x00040047u, + 0x0000000du, 0x00000021u, 0x00000000u, 0x00040047u, 0x0000000du, 0x00000022u, 0x00000000u, 0x00040047u, + 0x00000012u, 0x0000000bu, 0x0000000fu, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, + 0x00040015u, 0x00000006u, 0x00000020u, 0x00000001u, 0x00040020u, 0x00000007u, 0x00000003u, 0x00000006u, + 0x0004003bu, 0x00000007u, 0x00000008u, 0x00000003u, 0x00040015u, 0x00000009u, 0x00000020u, 0x00000000u, + 0x00090019u, 0x0000000au, 0x00000009u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, + 0x00000000u, 0x0003001bu, 0x0000000bu, 0x0000000au, 0x00040020u, 0x0000000cu, 0x00000000u, 0x0000000bu, + 0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000000u, 0x00030016u, 0x0000000fu, 0x00000020u, 0x00040017u, + 0x00000010u, 0x0000000fu, 0x00000004u, 0x00040020u, 0x00000011u, 0x00000001u, 0x00000010u, 0x0004003bu, + 0x00000011u, 0x00000012u, 0x00000001u, 0x00040017u, 0x00000013u, 0x0000000fu, 0x00000002u, 0x00040017u, + 0x00000016u, 0x00000006u, 0x00000002u, 0x0004002bu, 0x00000006u, 0x00000018u, 0x00000000u, 0x00040017u, + 0x0000001au, 0x00000009u, 0x00000004u, 0x0004002bu, 0x00000009u, 0x0000001cu, 0x00000000u, 0x00050036u, + 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x0000000bu, + 0x0000000eu, 0x0000000du, 0x0004003du, 0x00000010u, 0x00000014u, 0x00000012u, 0x0007004fu, 0x00000013u, + 0x00000015u, 0x00000014u, 0x00000014u, 0x00000000u, 0x00000001u, 0x0004006eu, 0x00000016u, 0x00000017u, + 0x00000015u, 0x00040064u, 0x0000000au, 0x00000019u, 0x0000000eu, 0x0007005fu, 0x0000001au, 0x0000001bu, + 0x00000019u, 0x00000017u, 0x00000040u, 0x00000018u, 0x00050051u, 0x00000009u, 0x0000001du, 0x0000001bu, + 0x00000000u, 0x0004007cu, 0x00000006u, 0x0000001eu, 0x0000001du, 0x0003003eu, 0x00000008u, 0x0000001eu, + 0x000100fdu, 0x00010038u, +}; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleStencilResolve.frag b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleStencilResolve.frag new file mode 100644 index 00000000..4d566a0c --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleStencilResolve.frag @@ -0,0 +1,19 @@ +#version 450 +// The stencil half, and it needs an extension to exist at all: a fragment shader cannot write +// the stencil aspect without VK_EXT_shader_stencil_export (SPV_EXT_shader_stencil_export, which +// this `#extension` line is how glslang is asked for). CONTRACT-P7 §3.2 says so and says what +// happens otherwise - a stencil-only resolve DECLINES on a device without it, because there is +// no second way to put a value in the stencil aspect from a pass. +// +// usampler2DMS and not sampler2DMS: the stencil aspect of a depth/stencil image is an unsigned +// integer, and a float view of it would round the index. +// +// Sample zero, for WireMultisampleDepthResolve.frag's reason, and GL is stricter here: 18.3.1 +// says integer and stencil resolves SELECT one sample rather than combining them, so sample 0 +// is the answer and not merely an acceptable one. +#extension GL_ARB_shader_stencil_export : require +layout(set = 0, binding = 0) uniform usampler2DMS sourceImage; + +void main() { + gl_FragStencilRefARB = int(texelFetch(sourceImage, ivec2(gl_FragCoord.xy), 0).r); +} diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index ba3c3158..b17e7579 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -3091,7 +3091,8 @@ if (MOBILEGL_BUILD_DISAGGREGATED) # NO KNOB: every driver that can host a 4x multisample RGBA8 renderbuffer reaches both # entries with the plain Fm environment, and one that cannot says so as a SKIP. foreach(multisampleBlitCase MultisampleColorBlitScalesAndFlipsThroughTheResolveScratch - MultisampleBlitOntoAMultisampleDestinationDeclinesAndKeepsTheSession) + MultisampleBlitOntoAMultisampleDestinationDeclinesAndKeepsTheSession + MultisampleBlitFromASingleSampleSourceDeclinesAndKeepsTheSession) gtest_discover_tests(MobileGLIntegrationTest TEST_PREFIX "DirectVulkan.${mglItestArm}.MsBlit." TEST_FILTER "F1WireScenario.${multisampleBlitCase}" @@ -3100,6 +3101,35 @@ if (MOBILEGL_BUILD_DISAGGREGATED) ${mglItestArmProperties} ENVIRONMENT "${mglItestMagmaWireEnvironment}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p7-msblit${mglItestArmLogTag}-${multisampleBlitCase}.log") endforeach() + # ---- P7 wave 2-B2: `multisample-blit-aspect@P7`'s retirement, BOTH ARMS --------------- + # + # THE CASE IS PROMOTED, NOT WRITTEN. DepthStencilReadbackMatrixScenario already owns the + # observable - clear a 4x multisample DEPTH24_STENCIL8 renderbuffer, resolve it into a + # single-sampled framebuffer with glBlitFramebuffer, read both aspects back, and check + # that a depth-only resolve left the destination's stencil alone and vice versa - and it + # already ran on these three arms in the INFORMATIONAL tier (`…Full.`). What it did not + # have was a gated entry, which is what CONTRACT-P7 §3.2's aspect row needs. + # + # TWO ENTRIES OVER ONE CASE, and the second is the only way its arm is reachable here: + # lavapipe carries VK_KHR_create_renderpass2 AND VK_KHR_depth_stencil_resolve, so the + # extension arm answers every resolve on this host and the shader pass - which exists FOR + # the device that has neither, where the session used to end at + # `Magma:multisample-depth-resolve-capability` - would never execute. + # MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE drops the extension arm, and the two entries + # then assert the same depth values and the same stencil indices, which is the whole + # claim: sample zero either way. + # + # ITS OWN ENTRIES RATHER THAN A LANE-WIDE FLIP, the shader-mip block's reason: the knob + # changes how EVERY depth/stencil resolve in the process is performed. + foreach(shaderDepthResolve 0 1) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectVulkan.${mglItestArm}.MsResolve${shaderDepthResolve}." + TEST_FILTER "DepthStencilReadbackMatrixScenario.AResolvedMultisampleDepthReadsBackFromTheDestination" + DISCOVERY_TIMEOUT 30 + PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT} + ${mglItestArmProperties} + ENVIRONMENT "${mglItestMagmaWireEnvironment}\;MGITEST_MAGMA_FORCE_SHADER_DEPTH_RESOLVE=${shaderDepthResolve}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p7-msresolve${mglItestArmLogTag}-shader${shaderDepthResolve}.log") + endforeach() foreach(inPlaceCase CopyImageInPlaceAcrossLevelsAndLayers CopyImageInPlaceOverlapDeclinesAndLeavesTheLevelAlone) gtest_discover_tests(MobileGLIntegrationTest diff --git a/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp index 07c05fe9..25e6e051 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp @@ -1837,6 +1837,84 @@ TEST_F(F1WireScenario, MultisampleBlitOntoAMultisampleDestinationDeclinesAndKeep Gl().EndFrame(); } +// P7 wave 2-B2, CONTRACT-P7 §3.2: the surviving half of `multisample-blit-aspect@P7`. +// +// The half WITH an arm is ResolveWireDepthStencil's: a multisample depth or stencil source now +// reaches a single-sample destination on every device - through VK_KHR_depth_stencil_resolve +// where it exists, through WireMultisampleResolve.inc's baked pass where it does not - and the +// two MsResolve entries on each arm are its reading. This is the half with none. +// +// ONE SAMPLE INTO FOUR, with rectangles that MATCH so the shape decline next door is not what +// fires. Vulkan has no command for it: vkCmdResolveImage only ever writes a single-sample +// destination, vkCmdBlitImage refuses a multisampled side outright, and vkCmdCopyImage needs +// the two counts to be equal. GL 4.6 core 18.3.1 calls this sample replication and the wire arm +// declines it by name rather than ending the session over it. +// +// Red once (executed, reverted): restore MagmaWireFatal("multisample-blit-aspect@P7") and this +// case dies as Fatal{UnmigratedVerb, "Magma:multisample-blit-aspect@P7"}. +TEST_F(F1WireScenario, MultisampleBlitFromASingleSampleSourceDeclinesAndKeepsTheSession) { + if (!Ready()) return; + GLuint msFbo = 0, msRenderbuffer = 0; + glGenFramebuffers(1, &msFbo); + glBindFramebuffer(GL_FRAMEBUFFER, msFbo); + glGenRenderbuffers(1, &msRenderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, msRenderbuffer); + glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_RGBA8, 4, 2); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msRenderbuffer); + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GLenum(GL_FRAMEBUFFER_COMPLETE)) { + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glDeleteFramebuffers(1, &msFbo); + glDeleteRenderbuffers(1, &msRenderbuffer); + GTEST_SKIP() << "this driver cannot host a 4x multisample RGBA8 renderbuffer"; + } + while (glGetError() != GLenum(GL_NO_ERROR)) {} + glDisable(GL_SCISSOR_TEST); + glClearColor(1, 0, 1, 1); // magenta: the colour the declined blit must leave behind + glClear(GL_COLOR_BUFFER_BIT); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.MsAspectDecline.setup"; + + constexpr GLfloat kYellow[4]{1, 1, 0, 1}; + GLuint srcFbo = 0, srcTexture = 0; + MakeSingleSampleTarget(4, 2, kYellow, srcFbo, srcTexture); + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE)); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.MsAspectDecline.source"; + + glBindFramebuffer(GL_READ_FRAMEBUFFER, srcFbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, msFbo); + const auto before = PeekSplitRuntime().emitSeq; + glBlitFramebuffer(0, 0, 4, 2, 0, 0, 4, 2, GL_COLOR_BUFFER_BIT, GL_NEAREST); + EXPECT_GT(PeekSplitRuntime().emitSeq, before) << "F1.MsAspectDecline.wire"; + glFinish(); // the decline's error rides a later reply - see the shape case next door + EXPECT_EQ(FirstGLError(), GLenum(GL_INVALID_OPERATION)) + << "F1.MsAspectDecline: the decline must record the error GL names for the shape"; + + constexpr GLfloat kBlue[4]{0, 0, 1, 1}; + GLuint readFbo = 0, readTexture = 0; + MakeSingleSampleTarget(4, 2, kBlue, readFbo, readTexture); + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE)); + glBindFramebuffer(GL_READ_FRAMEBUFFER, msFbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, readFbo); + glBlitFramebuffer(0, 0, 4, 2, 0, 0, 4, 2, GL_COLOR_BUFFER_BIT, GL_NEAREST); + EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.MsAspectDecline.resolve"; + glBindFramebuffer(GL_READ_FRAMEBUFFER, readFbo); + constexpr std::array kMagenta{255, 0, 255, 255}; + for (int y = 0; y < 2; ++y) + for (int x = 0; x < 4; ++x) + EXPECT_EQ(ReadOnePixel(x, y), kMagenta) + << "F1.MsAspectDecline: the declined blit wrote (" << x << "," << y << ")"; + EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.MsAspectDecline: the session survived"; + + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glDeleteFramebuffers(1, &readFbo); + glDeleteTextures(1, &readTexture); + glDeleteFramebuffers(1, &srcFbo); + glDeleteTextures(1, &srcTexture); + glDeleteFramebuffers(1, &msFbo); + glDeleteRenderbuffers(1, &msRenderbuffer); + glBindTexture(GL_TEXTURE_2D, texture); + Gl().EndFrame(); +} + namespace { // One 8x8x2 RGBA8 array texture with two levels, filled so that EVERY subresource this pair // of cases touches is distinguishable from every other one. A flat fill would let a copy that diff --git a/MobileGL/MG_Test/SelfTest/BakedInternalShadersTest.cpp b/MobileGL/MG_Test/SelfTest/BakedInternalShadersTest.cpp index 469a39db..8803e5c8 100644 --- a/MobileGL/MG_Test/SelfTest/BakedInternalShadersTest.cpp +++ b/MobileGL/MG_Test/SelfTest/BakedInternalShadersTest.cpp @@ -59,6 +59,7 @@ namespace { using Uint32 = std::uint32_t; #include #include +#include struct BakedShader { const char* name; // ctest entry name, and the bake file's stem @@ -103,6 +104,28 @@ const BakedShader kBakedShaders[] = { "kWireDepthMipmapFragmentSpirv", EShLangFragment, glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0, kWireDepthMipmapFragmentSpirv), + // P7 wave 2-B2: the multisample depth/stencil resolve's own pass. THREE ROWS FOR TWO + // PIPELINES - one vertex stage shared by both, and a fragment stage per aspect, because a + // fragment shader cannot write the stencil aspect without VK_EXT_shader_stencil_export and + // a device without that extension must still be able to compile the depth half. + MGL_BAKED_ROW("WireMultisampleResolveVertex", + "MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolve.vert", + "MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolveSpirv.h", + "kWireMultisampleResolveVertexSpirv", EShLangVertex, + glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0, + kWireMultisampleResolveVertexSpirv), + MGL_BAKED_ROW("WireMultisampleDepthResolveFragment", + "MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleDepthResolve.frag", + "MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolveSpirv.h", + "kWireMultisampleDepthResolveFragmentSpirv", EShLangFragment, + glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0, + kWireMultisampleDepthResolveFragmentSpirv), + MGL_BAKED_ROW("WireMultisampleStencilResolveFragment", + "MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleStencilResolve.frag", + "MobileGL/MG_Backend/DirectVulkan/Renderer/WireMultisampleResolveSpirv.h", + "kWireMultisampleStencilResolveFragmentSpirv", EShLangFragment, + glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0, + kWireMultisampleStencilResolveFragmentSpirv), // The two driver self-test probes. They are not Magma's, and they are in this table for // the reason the contract names them: they are baked, so they can be stale, and one of // them was.