[Feat] (Magma): pipeline and primitive fragment shading rate via VK_KHR_fragment_shading_rate

This commit is contained in:
2026-10-12 08:03:31 -04:00
parent db23b9d551
commit c881bc2396
6 changed files with 202 additions and 2 deletions
@@ -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
@@ -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<uint32_t>(std::size(kDynamicStates));
dynamicState.dynamicStateCount =
static_cast<uint32_t>(std::size(kDynamicStates)) - (payload.fragmentShadingRateDynamic ? 0u : 1u);
dynamicState.pDynamicStates = kDynamicStates;
VkPipelineInputAssemblyStateCreateInfo ia{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
@@ -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<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
// The tessellation control stage this renderer synthesized for a program that has
@@ -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<VkFragmentShadingRateCombinerOpKHR>(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<PFN_vkGetPhysicalDeviceFragmentShadingRatesKHR>(
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<void*>(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<VkPhysicalDeviceFragmentShadingRateKHR> 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<Uint16>(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<PFN_vkCmdSetFragmentShadingRateKHR>(
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.
@@ -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;
@@ -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);