diff --git a/src/SharpEmu.Libs/Gpu/Vulkan/VulkanPushDescriptorPolicy.cs b/src/SharpEmu.Libs/Gpu/Vulkan/VulkanPushDescriptorPolicy.cs new file mode 100644 index 0000000..0cec065 --- /dev/null +++ b/src/SharpEmu.Libs/Gpu/Vulkan/VulkanPushDescriptorPolicy.cs @@ -0,0 +1,51 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Text; +using Silk.NET.Vulkan; + +namespace SharpEmu.Libs.Gpu.Vulkan; + +// How many descriptors a set may push. MoltenVK reports a zero OpArrayLength for storage buffers +// bound through push descriptors, which turns every bounds-checked load into zero and drops every +// store, so portability (MoltenVK) devices push none. The portability-subset extension is not +// enumerated for every device, and macOS only ever has MoltenVK, so macOS pushes none outright. +// Every device the emulator or its tools create takes the limit from here. +public static unsafe class VulkanPushDescriptorPolicy +{ + private const string PortabilitySubsetExtensionName = "VK_KHR_portability_subset"; + + public static uint UsableCount(Vk vk, PhysicalDevice physical, uint reported) => + OperatingSystem.IsMacOS() || HasDeviceExtension(vk, physical, PortabilitySubsetExtensionName) ? 0 : reported; + + private static bool HasDeviceExtension(Vk vk, PhysicalDevice physical, string name) + { + uint count = 0; + if (vk.EnumerateDeviceExtensionProperties(physical, (byte*)null, &count, null) != Result.Success || count == 0) + { + return false; + } + + var properties = new ExtensionProperties[count]; + fixed (ExtensionProperties* pointer = properties) + { + if (vk.EnumerateDeviceExtensionProperties(physical, (byte*)null, &count, pointer) != Result.Success) + { + return false; + } + + var expected = Encoding.UTF8.GetBytes(name); + for (var index = 0; index < count; index++) + { + var actual = new ReadOnlySpan(pointer[index].ExtensionName, 256); + var length = actual.IndexOf((byte)0); + if (actual[..(length < 0 ? actual.Length : length)].SequenceEqual(expected)) + { + return true; + } + } + } + + return false; + } +} diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs index f5c9524..08d2bd4 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs @@ -594,7 +594,7 @@ internal static unsafe partial class VulkanVideoPresenter SetNativeSubgroupCapabilities(subgroup.SubgroupSize, subgroup.SupportedStages); // MoltenVK reports a zero OpArrayLength for storage buffers bound through push // descriptors, which turns every bounds-checked load into zero and drops every store. - _maxPushDescriptors = IsDeviceExtensionAvailable(PortabilitySubsetExtensionName) ? 0 : pushDescriptorProperties.MaxPushDescriptors; + _maxPushDescriptors = Gpu.Vulkan.VulkanPushDescriptorPolicy.UsableCount(_vk, _physicalDevice, pushDescriptorProperties.MaxPushDescriptors); _noAttachmentSampleCounts = properties.Limits.FramebufferNoAttachmentsSampleCounts; _maxComputeWorkGroupCountX = properties.Limits.MaxComputeWorkGroupCount[0]; _maxComputeWorkGroupCountY = properties.Limits.MaxComputeWorkGroupCount[1]; diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs index ead42a4..6d937c2 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/HeadlessVulkan.cs @@ -49,7 +49,7 @@ internal sealed unsafe class HeadlessVulkan : IDisposable "VK_KHR_push_descriptor", ]; - // The push descriptor limit of the device; the render host pushes sets that fit it. + // The push descriptor limit the render host may use on this device (none on MoltenVK). public uint MaxPushDescriptors { get @@ -57,7 +57,7 @@ internal sealed unsafe class HeadlessVulkan : IDisposable var pushDescriptors = new PhysicalDevicePushDescriptorPropertiesKHR { SType = StructureType.PhysicalDevicePushDescriptorPropertiesKhr }; var properties = new PhysicalDeviceProperties2 { SType = StructureType.PhysicalDeviceProperties2, PNext = &pushDescriptors }; Vk.GetPhysicalDeviceProperties2(Physical, &properties); - return pushDescriptors.MaxPushDescriptors; + return SharpEmu.Libs.Gpu.Vulkan.VulkanPushDescriptorPolicy.UsableCount(Vk, Physical, pushDescriptors.MaxPushDescriptors); } }