diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index de44de12..86d86bbb 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -1083,6 +1083,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(), pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported(), m_vulkanCaps.SupportsImageCubeArray); + // GL_EXT_fragment_shading_rate rides on VK_KHR_fragment_shading_rate's pipeline rate, and + // its _primitive companion on the primitive rate; both only when the renderer enabled them. + // _attachment is not advertised: no framebuffer carries a rate attachment to this backend. + if (pVulkanRenderer != nullptr && pVulkanRenderer->GetFragmentShadingRateSupport().Pipeline) { + m_rendererInfo.RendererGLInfo.Extensions.push_back(E_GL_EXT_fragment_shading_rate); + if (pVulkanRenderer->GetFragmentShadingRateSupport().Primitive) { + m_rendererInfo.RendererGLInfo.Extensions.push_back(E_GL_EXT_fragment_shading_rate_primitive); + } + } } void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() { @@ -1467,5 +1476,30 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_dynamicParameters.GpuVendor = GpuVendorKind::Unknown; break; } + + // GL_EXT_fragment_shading_rate: what the renderer enabled of VK_KHR_fragment_shading_rate. + // The attachment rate stays off (see UpdateAdvertisedExtensions), and with it every limit + // that only describes the attachment. + { + const FragmentShadingRateSupport fsr = + pVulkanRenderer != nullptr ? pVulkanRenderer->GetFragmentShadingRateSupport() : FragmentShadingRateSupport{}; + m_dynamicParameters.SupportsFragmentShadingRate = fsr.Pipeline; + m_dynamicParameters.SupportsPrimitiveFragmentShadingRate = fsr.Pipeline && fsr.Primitive; + m_dynamicParameters.SupportsAttachmentFragmentShadingRate = false; + m_dynamicParameters.FragmentShadingRateNonTrivialCombiners = fsr.NonTrivialCombiners; + m_dynamicParameters.FragmentShadingRateWithShaderDepthStencilWrites = fsr.WithShaderDepthStencilWrites; + m_dynamicParameters.FragmentShadingRateWithSampleMask = fsr.WithSampleMask; + m_dynamicParameters.FragmentShadingRatePrimitiveWithMultiViewport = fsr.PrimitiveWithMultipleViewports; + m_dynamicParameters.FragmentShadingRateAttachmentWithDefaultFramebuffer = false; + for (SizeT i = 0; i < std::size(m_dynamicParameters.FragmentShadingRateMasks); ++i) { + m_dynamicParameters.FragmentShadingRateMasks[i] = fsr.Pipeline ? fsr.RateMasks[i] : 0; + } + m_dynamicParameters.MinFragmentShadingRateAttachmentTexelWidth = 0; + m_dynamicParameters.MinFragmentShadingRateAttachmentTexelHeight = 0; + m_dynamicParameters.MaxFragmentShadingRateAttachmentTexelWidth = 0; + m_dynamicParameters.MaxFragmentShadingRateAttachmentTexelHeight = 0; + m_dynamicParameters.MaxFragmentShadingRateAttachmentTexelAspectRatio = 0; + m_dynamicParameters.MaxFragmentShadingRateAttachmentLayers = 0; + } } } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp index 0e9aaae1..9488e751 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp @@ -382,6 +382,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { payload.colorAttachmentCount, payload.subpass); + // The fragment shading rate goes last so the common prefix is the eight-state list every + // pipeline shares; it is only present when the device enabled the pipeline rate. static constexpr VkDynamicState kDynamicStates[] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR, @@ -390,12 +392,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { VK_DYNAMIC_STATE_LINE_WIDTH, VK_DYNAMIC_STATE_STENCIL_COMPARE_MASK, VK_DYNAMIC_STATE_STENCIL_WRITE_MASK, - VK_DYNAMIC_STATE_STENCIL_REFERENCE + VK_DYNAMIC_STATE_STENCIL_REFERENCE, + VK_DYNAMIC_STATE_FRAGMENT_SHADING_RATE_KHR }; VkPipelineDynamicStateCreateInfo dynamicState{}; dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; - dynamicState.dynamicStateCount = static_cast(std::size(kDynamicStates)); + dynamicState.dynamicStateCount = + static_cast(std::size(kDynamicStates)) - (payload.fragmentShadingRateDynamic ? 0u : 1u); dynamicState.pDynamicStates = kDynamicStates; VkPipelineInputAssemblyStateCreateInfo ia{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h index 329640a6..03dd7b23 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h @@ -108,6 +108,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { // The fragment module writes gl_FragDepth (SPIR-V DepthReplacing); exempts the // pipeline from the blended depth-write quirk (see ShouldSuppressDepthWrite). Bool fragmentReplacesDepth = false; + // The device enabled VK_KHR_fragment_shading_rate's pipeline rate, so the pipeline takes + // VK_DYNAMIC_STATE_FRAGMENT_SHADING_RATE_KHR and every draw sets glShadingRateEXT's rate + // and combiner ops on the command buffer. NOT hashed: it is a device constant, the same + // for every pipeline this renderer builds. + Bool fragmentShadingRateDynamic = false; Array colorBlendAttachments{}; const Vector* stages = nullptr; // The tessellation control stage this renderer synthesized for a program that has diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 076345d3..d823c9c7 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -315,6 +315,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32 stencilBackWriteMask = 0; Uint32 stencilFrontReference = 0; Uint32 stencilBackReference = 0; + // glShadingRateEXT / glShadingRateCombinerOpsEXT, in RenderStateParameters' packed form + // (MGPackShadingRate). Only pipelines built with fragmentShadingRateDynamic carry the state. + Bool fragmentShadingRateValid = false; + Uint8 fragmentShadingRate = 0; + Uint8 fragmentShadingRateCombinerOp0 = 0; + Uint8 fragmentShadingRateCombinerOp1 = 0; // Gate over the whole per-draw dynamic-state tail (viewport, scissor, blend // constants, depth bias, line width, stencil) - see ApplyDynamicDrawStateTail. // Every GL input of that tail lives in RenderState's value-shadowed parameters: @@ -508,6 +514,34 @@ namespace MobileGL::MG_Backend::DirectVulkan { vkCmdSetScissor(commandBuffer, 0, 1, &scissor); } + // GL_EXT_fragment_shading_rate's pipeline rate and combiner ops for the draw about to be + // recorded. The packed rate is log2(width) << 2 | log2(height), which is exactly the extent + // VK_KHR_fragment_shading_rate takes, and the combiner op indices follow + // VkFragmentShadingRateCombinerOpKHR's order. Combiner op 1 combines with the attachment rate, + // which this backend does not enable, so it stays KEEP whatever the GL state holds. + static void ShadowedSetFragmentShadingRate(VkCommandBuffer commandBuffer, + PFN_vkCmdSetFragmentShadingRateKHR setFragmentShadingRate) { + const RenderStateParameters& p = MG_Pipe::gPipeInputs->GetRenderStateParameters(); + auto& shadow = *g_dynamicStateShadow; + if (shadow.fragmentShadingRateValid && shadow.fragmentShadingRate == p.ShadingRate && + shadow.fragmentShadingRateCombinerOp0 == p.ShadingRateCombinerOp0 && + shadow.fragmentShadingRateCombinerOp1 == p.ShadingRateCombinerOp1) { + return; + } + shadow.fragmentShadingRateValid = true; + shadow.fragmentShadingRate = p.ShadingRate; + shadow.fragmentShadingRateCombinerOp0 = p.ShadingRateCombinerOp0; + shadow.fragmentShadingRateCombinerOp1 = p.ShadingRateCombinerOp1; + const Uint8 rate = p.ShadingRate == kMGInvalidShadingRateEncoding ? 0 : p.ShadingRate; + const VkExtent2D fragmentSize{1u << ((rate >> 2) & 3u), 1u << (rate & 3u)}; + const VkFragmentShadingRateCombinerOpKHR combinerOps[2] = { + p.ShadingRateCombinerOp0 <= VK_FRAGMENT_SHADING_RATE_COMBINER_OP_MUL_KHR + ? static_cast(p.ShadingRateCombinerOp0) + : VK_FRAGMENT_SHADING_RATE_COMBINER_OP_KEEP_KHR, + VK_FRAGMENT_SHADING_RATE_COMBINER_OP_KEEP_KHR}; + setFragmentShadingRate(commandBuffer, &fragmentSize, combinerOps); + } + // One viewport of the ARB_viewport_array state, mapped into Vulkan's frame. Split out of // ApplyGLViewportState so the multi-viewport path derives index i through EXACTLY the same // arithmetic as index 0 - the default-framebuffer Y-flip and pre-transform rotation @@ -2494,6 +2528,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { s_vkCmdWireDrawIndirectCount = nullptr; s_vkCmdDrawMultiEXT = nullptr; s_vkCmdDrawMultiIndexedEXT = nullptr; + s_vkCmdSetFragmentShadingRateKHR = nullptr; + m_fragmentShadingRate = {}; if (m_instance != VK_NULL_HANDLE && m_surface != VK_NULL_HANDLE) { vkDestroySurfaceKHR(m_instance, m_surface, nullptr); @@ -3146,6 +3182,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { .backStencilDepthFailOp = MG_Util::ConvertStencilOperationToVkEnum(backStencil.PassDepthFailOp), .backStencilCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(backStencil.Func), .fragmentReplacesDepth = programObj.fragmentReplacesDepth, + .fragmentShadingRateDynamic = m_fragmentShadingRate.Pipeline, .stages = &programObj.stages, .vertexInputState = pipelineVertexInputState, .stageSpirvDigests = &programObj.stageSpirvDigests @@ -8063,6 +8100,84 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } + // VK_KHR_fragment_shading_rate, for GL_EXT_fragment_shading_rate. pipelineFragmentShadingRate + // is the floor (glShadingRateEXT, read back through gl_ShadingRateEXT); the primitive rate + // (gl_PrimitiveShadingRateEXT, combiner op 0) rides along when the device has it. The + // attachment rate is left off: no GL framebuffer carries a rate attachment to this backend + // yet, and enabling the feature would only change what the frontend may promise. + // The extension builds on VK_KHR_create_renderpass2, which this 1.1 device needs by name. + m_fragmentShadingRate = {}; + VkPhysicalDeviceFragmentShadingRateFeaturesKHR fragmentShadingRateFeatures{ + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADING_RATE_FEATURES_KHR}; + if (IsExtensionSupported(availableExtensions, VK_KHR_FRAGMENT_SHADING_RATE_EXTENSION_NAME) && + (m_physicalDevice.properties.apiVersion >= VK_API_VERSION_1_2 || + IsExtensionSupported(availableExtensions, VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME)) && + getPhysicalDeviceFeatures2 != nullptr && getPhysicalDeviceProperties2 != nullptr) { + VkPhysicalDeviceFeatures2 featureQuery{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2}; + featureQuery.pNext = &fragmentShadingRateFeatures; + getPhysicalDeviceFeatures2(m_physicalDevice.handle, &featureQuery); + auto getFragmentShadingRates = reinterpret_cast( + vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceFragmentShadingRatesKHR")); + if (fragmentShadingRateFeatures.pipelineFragmentShadingRate == VK_TRUE && + getFragmentShadingRates != nullptr) { + EnableOptionalDeviceExtension(availableExtensions, enabledDeviceExtensions, + VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME); + EnableOptionalDeviceExtension(availableExtensions, enabledDeviceExtensions, + VK_KHR_FRAGMENT_SHADING_RATE_EXTENSION_NAME); + fragmentShadingRateFeatures.attachmentFragmentShadingRate = VK_FALSE; + fragmentShadingRateFeatures.pNext = const_cast(deviceCreateInfo.pNext); + deviceCreateInfo.pNext = &fragmentShadingRateFeatures; + + VkPhysicalDeviceFragmentShadingRatePropertiesKHR fragmentShadingRateProperties{ + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADING_RATE_PROPERTIES_KHR}; + VkPhysicalDeviceProperties2 propertyQuery{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2}; + propertyQuery.pNext = &fragmentShadingRateProperties; + getPhysicalDeviceProperties2(m_physicalDevice.handle, &propertyQuery); + + auto& support = m_fragmentShadingRate; + support.Pipeline = true; + support.Primitive = fragmentShadingRateFeatures.primitiveFragmentShadingRate == VK_TRUE; + support.NonTrivialCombiners = fragmentShadingRateProperties.fragmentShadingRateNonTrivialCombinerOps; + support.WithShaderDepthStencilWrites = + fragmentShadingRateProperties.fragmentShadingRateWithShaderDepthStencilWrites; + support.WithSampleMask = fragmentShadingRateProperties.fragmentShadingRateWithSampleMask; + support.PrimitiveWithMultipleViewports = + fragmentShadingRateProperties.primitiveFragmentShadingRateWithMultipleViewports; + support.LayeredAttachments = fragmentShadingRateProperties.layeredShadingRateAttachments; + support.MinAttachmentTexelWidth = + fragmentShadingRateProperties.minFragmentShadingRateAttachmentTexelSize.width; + support.MinAttachmentTexelHeight = + fragmentShadingRateProperties.minFragmentShadingRateAttachmentTexelSize.height; + support.MaxAttachmentTexelWidth = + fragmentShadingRateProperties.maxFragmentShadingRateAttachmentTexelSize.width; + support.MaxAttachmentTexelHeight = + fragmentShadingRateProperties.maxFragmentShadingRateAttachmentTexelSize.height; + support.MaxAttachmentTexelAspectRatio = + fragmentShadingRateProperties.maxFragmentShadingRateAttachmentTexelSizeAspectRatio; + + Uint32 rateCount = 0; + getFragmentShadingRates(m_physicalDevice.handle, &rateCount, nullptr); + Vector rates( + rateCount, VkPhysicalDeviceFragmentShadingRateKHR{ + VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADING_RATE_KHR}); + getFragmentShadingRates(m_physicalDevice.handle, &rateCount, rates.data()); + const auto log2Of = [](Uint32 size) -> Int { return size == 1 ? 0 : size == 2 ? 1 : size == 4 ? 2 : -1; }; + for (Uint32 r = 0; r < rateCount; ++r) { + const Int w = log2Of(rates[r].fragmentSize.width); + const Int h = log2Of(rates[r].fragmentSize.height); + if (w < 0 || h < 0) continue; // larger than GL's 4x4 can name + const Uint16 bit = static_cast(1u << ((w << 2) | h)); + for (Uint32 i = 0; i < std::size(support.RateMasks); ++i) { + if ((rates[r].sampleCounts & (1u << i)) != 0) support.RateMasks[i] |= bit; + } + } + MGLOG_I("%s: primitive=%d nonTrivialCombiners=%d " + "rates(1x)=0x%04x rates(4x)=0x%04x", + VK_KHR_FRAGMENT_SHADING_RATE_EXTENSION_NAME, support.Primitive, support.NonTrivialCombiners, + support.RateMasks[0], support.RateMasks[2]); + } + } + // Codex closeout finding 2 (cf-magma): GL 4.6 core 8.26 makes an access through an // invalid image unit load zero and DISCARD stores and atomics. A null storage-image // descriptor is that rule exactly, so the wire arm binds one for such a unit @@ -8197,6 +8312,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } + s_vkCmdSetFragmentShadingRateKHR = nullptr; + if (m_fragmentShadingRate.Pipeline) { + s_vkCmdSetFragmentShadingRateKHR = reinterpret_cast( + vkGetDeviceProcAddr(m_device, "vkCmdSetFragmentShadingRateKHR")); + if (s_vkCmdSetFragmentShadingRateKHR == nullptr) { + MGLOG_W("VK_KHR_fragment_shading_rate enabled but vkCmdSetFragmentShadingRateKHR is missing, " + "will continue without fragment shading rates!"); + m_fragmentShadingRate = {}; + } + } + // Resolve the multi-draw dispatch tiers once: device support clamped by the // MOBILEGL_MAGMA_MULTIDRAW_MODE preference. Requesting an unavailable tier is // never an error - the dispatch falls down the chain ext -> indirect -> unroll. diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 7f511234..679a3092 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -130,6 +130,28 @@ namespace MobileGL::MG_Backend::DirectVulkan { } }; + // VK_KHR_fragment_shading_rate as this renderer enabled it, which is everything + // GL_EXT_fragment_shading_rate needs to know about the device. The three feature flags are the + // ENABLED ones; the rest are the device's properties and are meaningful only under them. + struct FragmentShadingRateSupport { + Bool Pipeline = false; + Bool Primitive = false; + Bool Attachment = false; + Bool NonTrivialCombiners = false; + Bool WithShaderDepthStencilWrites = false; + Bool WithSampleMask = false; + Bool PrimitiveWithMultipleViewports = false; + Bool LayeredAttachments = false; + // Per sample count 1, 2, 4 .. 64: bit (log2(w) << 2 | log2(h)) per rate the device lists + // for that count (vkGetPhysicalDeviceFragmentShadingRatesKHR). + Uint16 RateMasks[7] = {}; + Uint32 MinAttachmentTexelWidth = 0; + Uint32 MinAttachmentTexelHeight = 0; + Uint32 MaxAttachmentTexelWidth = 0; + Uint32 MaxAttachmentTexelHeight = 0; + Uint32 MaxAttachmentTexelAspectRatio = 0; + }; + class VulkanRenderer : public IBufferCopyCommandProvider, public FrameContext::IRecordingObserver, public ProgramFactory::IEvictionObserver { @@ -391,6 +413,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is // honored rather than accepted-and-ignored. Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; } + const FragmentShadingRateSupport& GetFragmentShadingRateSupport() const { return m_fragmentShadingRate; } // ARB_base_instance extends indirect command records with a non-zero firstInstance and // requires gl_InstanceID to remain zero-based. Vulkan needs both features to honor that // complete contract: one legalizes the command word, the other enables the shader rebase. @@ -1192,6 +1215,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { // (and its multiDraw feature) is enabled; null otherwise. PFN_vkCmdDrawMultiEXT s_vkCmdDrawMultiEXT = nullptr; PFN_vkCmdDrawMultiIndexedEXT s_vkCmdDrawMultiIndexedEXT = nullptr; + // VK_KHR_fragment_shading_rate, which backs GL_EXT_fragment_shading_rate: what was enabled + // at device creation and what the device answers for it (see GetFragmentShadingRateSupport). + // Null / all-false on a device without pipelineFragmentShadingRate. + PFN_vkCmdSetFragmentShadingRateKHR s_vkCmdSetFragmentShadingRateKHR = nullptr; + FragmentShadingRateSupport m_fragmentShadingRate{}; // VK_EXT_transform_feedback (GL transform feedback capture) Bool m_transformFeedbackFeatureEnabled = false; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc index ef67c75e..efdcf9ab 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -903,6 +903,9 @@ } ApplyDynamicDrawStateTail(frame, pass.extent, fbo->IsDefault, ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin)); + if (m_fragmentShadingRate.Pipeline) { + ShadowedSetFragmentShadingRate(frame.commandBuffer, s_vkCmdSetFragmentShadingRateKHR); + } // Storage images this draw writes are what the next sampled-image barrier must cover. if (programObj.hasStorageImages) BumpWireImageWriteEpoch(); if (MG_Util::PipeStats::Enabled()) MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::Draws, 1);