diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 0c980db3a9..28e4e295ad 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -125,6 +125,25 @@ foreach(agcPresentationStage IN ITEMS Vert Frag) ) list(APPEND agcPresentationShaderHeaders ${agcPresentationShaderHeader}) endforeach() +foreach(agcSampleOrderVariant IN ITEMS "R8;comp;-DTEXEL_FORMAT=r8ui" "R16;comp;-DTEXEL_FORMAT=r16ui" "R32;comp;-DTEXEL_FORMAT=r32ui" "RG32;comp;-DTEXEL_FORMAT=rg32ui" "RGBA32;comp;-DTEXEL_FORMAT=rgba32ui" "Vertex;vert;-DPLAIN=1" "Depth;frag;-DPLAIN=1" "StencilExport;frag;-DSTENCIL_EXPORT=1" "StencilBits;frag;-DSTENCIL_BITS=1") + list(GET agcSampleOrderVariant 0 agcSampleOrderName) + list(GET agcSampleOrderVariant 1 agcSampleOrderExtension) + list(GET agcSampleOrderVariant 2 agcSampleOrderDefine) + string(TOUPPER "${agcSampleOrderName}" agcSampleOrderSymbol) + set(agcSampleOrderShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/SampleOrder.${agcSampleOrderExtension}) + set(agcSampleOrderShaderSpv ${agcTextureDetileGeneratedDir}/SampleOrder${agcSampleOrderName}.spv) + set(agcSampleOrderShaderHeader ${agcTextureDetileGeneratedDir}/SampleOrder${agcSampleOrderName}_spv.h) + add_custom_command( + OUTPUT "${agcSampleOrderShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 ${agcSampleOrderDefine} -o "${agcSampleOrderShaderSpv}" "${agcSampleOrderShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcSampleOrderShaderSpv} -DOUTPUT=${agcSampleOrderShaderHeader} + -DSYMBOL=SAMPLE_ORDER_${agcSampleOrderSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcSampleOrderShaderSource}" glslang-standalone + VERBATIM + ) + list(APPEND agcSampleOrderShaderHeaders ${agcSampleOrderShaderHeader}) +endforeach() set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp @@ -224,8 +243,10 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/TextureDetiler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Texture.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PassHazards.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/SampleOrder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/UnitShadow.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Sampler.cpp + ${agcSampleOrderShaderHeaders} ${agcTextureDetileShaderHeader} ${agcCmaskClearShaderHeader} ${agcMeshArgumentsShaderHeader} @@ -1436,6 +1457,27 @@ foreach(stage IN ITEMS vert frag) endforeach() target_include_directories(agc_driver_provoking_vertex_tests PRIVATE ${agcTestShaderGeneratedRoot}) +foreach(agcSampleOrderTestVariant IN ITEMS "R8;SampleOrderTexels.comp;-DTEXEL_FORMAT=r8ui" "R16;SampleOrderTexels.comp;-DTEXEL_FORMAT=r16ui" "R32;SampleOrderTexels.comp;-DTEXEL_FORMAT=r32ui" "RG32;SampleOrderTexels.comp;-DTEXEL_FORMAT=rg32ui" "RGBA32;SampleOrderTexels.comp;-DTEXEL_FORMAT=rgba32ui" "FillVertex;SampleOrderFill.vert;-DPLAIN=1" "FillDepth;SampleOrderFill.frag;-DPLAIN=1" "FillStencil;SampleOrderFill.frag;-DSTENCIL=1" "ReadDepth;SampleOrderRead.comp;-DPLAIN=1" "ReadStencil;SampleOrderRead.comp;-DSTENCIL=1" "ReadColor;SampleOrderRead.comp;-DCOLOR=1") + list(GET agcSampleOrderTestVariant 0 agcSampleOrderTestName) + list(GET agcSampleOrderTestVariant 1 agcSampleOrderTestFile) + list(GET agcSampleOrderTestVariant 2 agcSampleOrderTestDefine) + string(TOUPPER "${agcSampleOrderTestName}" agcSampleOrderTestSymbol) + set(agcSampleOrderTestSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/${agcSampleOrderTestFile}) + set(agcSampleOrderTestSpv ${agcTestShaderGeneratedRoot}/SampleOrderTest${agcSampleOrderTestName}.spv) + set(agcSampleOrderTestHeader ${agcTestShaderGeneratedRoot}/SampleOrderTest${agcSampleOrderTestName}_spv.h) + add_custom_command( + OUTPUT "${agcSampleOrderTestHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}" + COMMAND "$" -V --target-env vulkan1.1 ${agcSampleOrderTestDefine} -o "${agcSampleOrderTestSpv}" "${agcSampleOrderTestSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcSampleOrderTestSpv} -DOUTPUT=${agcSampleOrderTestHeader} + -DSYMBOL=SAMPLE_ORDER_TEST_${agcSampleOrderTestSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcSampleOrderTestSource}" glslang-standalone + VERBATIM + ) + target_sources(agc_driver_sample_order_tests PRIVATE ${agcSampleOrderTestHeader}) +endforeach() +target_include_directories(agc_driver_sample_order_tests PRIVATE ${agcTestShaderGeneratedRoot}) + target_include_directories(agc_driver_prepared_exports_tests PRIVATE $) target_include_directories(agc_driver_buffer_descriptor_runtime_tests PRIVATE $) @@ -1526,6 +1568,12 @@ if(BUILD_TESTING) endif() add_test(NAME agc_sparse_buffer_holes COMMAND agc_driver_graphics_tests --sparse-buffer-holes) set_tests_properties(agc_driver_native_multisample_depth PROPERTIES TIMEOUT 30) + set_tests_properties(agc_driver_sample_order PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_sample_order_grid_reference COMMAND agc_driver_sample_order_tests --grid-reference) + set_tests_properties(agc_driver_sample_order_grid_reference PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_sample_order_grid_reference PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_native_depth_retirement COMMAND agc_driver_native_multisample_depth_tests --retirement) set_tests_properties(agc_driver_native_depth_retirement PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_driver_native_depth_clears COMMAND agc_driver_native_multisample_depth_tests --clears) @@ -1926,10 +1974,13 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_covered_centroid_state PROPERTIES TIMEOUT 30) add_test(NAME agc_driver_sample_location_grid_fallback COMMAND agc_driver_graphics_tests --sample-location-grid-fallback) set_tests_properties(agc_driver_sample_location_grid_fallback PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_sample_order_planning COMMAND agc_driver_graphics_tests --sample-order) + set_tests_properties(agc_driver_sample_order_planning PROPERTIES TIMEOUT 30) if(WIN32) set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") set_tests_properties(agc_driver_covered_centroid_state PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") set_tests_properties(agc_driver_sample_location_grid_fallback PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_sample_order_planning PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests) set_tests_properties(agc_compute_pipeline_capture PROPERTIES TIMEOUT 30) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 87800734a3..643d35b536 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -44,6 +44,7 @@ public: std::string DriverInfo() const; bool CalibrateClock(double& gpuNs, std::chrono::steady_clock::time_point& cpu) const; ShaderRecompiler::SpirvTarget Target() const; + Graphics::SampleOrder HostSampleOrder(const Graphics::State& graphics) const; std::optional GeometryLimits() const; const std::uint32_t* WorkGroupCountLimit() const; std::uint32_t VendorId() const; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index 9079eb1c16..322bc4be2c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -130,7 +130,11 @@ std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Su product->pixel.reversedBlend = product->state.reversedBlend; product->pixel.alphaToCoverage = product->state.alphaToCoverage; product->pixel.quadPixelMask = Graphics::QuadPixelMask(product->state); - product->pixel.quadSampleMask = Graphics::QuadSampleMasks(product->state); + if (product->state.sampleLocationGrid) { + const auto localDevice = device.Load(); + if (localDevice != nullptr) product->pixel.sampleOrder = localDevice->HostSampleOrder(product->state); + } + product->pixel.quadSampleMask = Graphics::QuadSampleMasks(product->state, product->pixel.sampleOrder); if (product->pixel.stencilRefExportEnable && Graphics::StencilReferenceUnused(product->state)) { const auto localDevice = device.Load(); const auto capabilities = localDevice == nullptr ? std::vector{} : localDevice->Target().supportedCapabilities; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 8308914ff2..5425368ed8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -76,6 +76,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptrcapabilities.push_back(spv::CapabilityStencilExportEXT); state->spirvExtensions.push_back("SPV_EXT_shader_stencil_export"); + state->shaderStencilExport = true; } if (state->shaderClock) { deviceExtensions.push_back(VK_KHR_SHADER_CLOCK_EXTENSION_NAME); @@ -1538,6 +1543,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->occlusionQueryPrecise = enabled.occlusionQueryPrecise == VK_TRUE; // Recompiled storage-image access declares no format (the guest descriptor decides it). enabled.shaderStorageImageMultisample = available.shaderStorageImageMultisample; + state->storageImageMultisample = enabled.shaderStorageImageMultisample == VK_TRUE; if (enabled.shaderStorageImageMultisample) state->capabilities.push_back(spv::CapabilityStorageImageMultisample); state->capabilities.push_back(spv::CapabilityImageMSArray); enabled.shaderStorageImageWriteWithoutFormat = available.shaderStorageImageWriteWithoutFormat; @@ -3375,6 +3381,11 @@ ShaderRecompiler::SpirvTarget VulkanDevice::Target() const { return state->shaderProfile->Target(); } +Graphics::SampleOrder VulkanDevice::HostSampleOrder(const Graphics::State& graphics) const { + if (!graphics.sampleLocationGrid) return {}; + return Graphics::HostSampleLocationsFor(graphicsContext(), graphics).order; +} + ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { const auto& limits = state->properties.limits; ShaderRecompiler::SpirvTarget target{VK_API_VERSION_1_1, state->meshShader ? 0x00010400u : 0x00010300u, state->subgroup.subgroupSize, ShaderRecompiler::BdaAbi::Version, state->capabilities, state->spirvExtensions, false, {limits.maxComputeWorkGroupSize[0], limits.maxComputeWorkGroupSize[1], limits.maxComputeWorkGroupSize[2]}, limits.maxComputeWorkGroupInvocations, limits.maxComputeSharedMemorySize, {}, {}}; @@ -3521,7 +3532,9 @@ Graphics::Context VulkanDevice::buildContext() const { context.srgbDecodeFormats = state->srgbDecodeFormats; context.sampleLocationSampleCounts = state->sampleLocationSampleCounts; context.sampleLocationGridSampleCounts = state->sampleLocationGridSampleCounts; - context.memoryProperties2 = state->memoryBudget ? state->InstanceFunction("vkGetPhysicalDeviceMemoryProperties2") : nullptr; + context.storageImageMultisample = state->storageImageMultisample; + context.shaderStencilExport = state->shaderStencilExport; + context.memoryProperties2 =state->memoryBudget ? state->InstanceFunction("vkGetPhysicalDeviceMemoryProperties2") : nullptr; context.singlePassStorage = state->storageImageWithoutFormat; return context; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index cba5321c23..1daafaa07b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -231,6 +231,8 @@ struct Context { bool singlePassStorage = false; VkSampleCountFlags sampleLocationSampleCounts = 0; VkSampleCountFlags sampleLocationGridSampleCounts = 0; + bool storageImageMultisample = false; + bool shaderStencilExport = false; PipelineCacheSynchronization* pipelineCacheSynchronization = nullptr; PipelineCacheSynchronization::PipelineUseLock LockPipelineCacheForUse() const { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 2f54bee302..70fd7c4e20 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DEPTHSURFACE_HPP #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include #include @@ -15,6 +16,7 @@ class Texture; class StorageTexture; VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); +SampleOrderTarget DepthSurfaceOrderTarget(const Context& context, const DepthTarget& target); bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples); std::uint64_t DepthSurfacesSerial(); std::uint64_t DepthSliceBytes(VkExtent2D extent, std::uint32_t bytesPerTexel); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index 237fd10ad0..01d624a85d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include #include @@ -52,6 +53,7 @@ std::uint64_t MultisampledColorBytes(const ColorTarget& target); bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear); bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear); VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target); +SampleOrderTarget MultisampleTargetOrderTarget(const Context& context, const ColorTarget& target); std::shared_ptr MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components); void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget& target); void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 684787b95e..a95d8bfb33 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -58,6 +58,8 @@ public: void ContinueBackFaces(VkCommandBuffer commands, const State& state) const; bool SplitsSecondChannel() const { return secondChannel != VK_NULL_HANDLE; } void ContinueSecondChannel(VkCommandBuffer commands) const; + const SampleOrder& HostSampleOrder() const { return sampleOrder; } + bool ReordersSamples() const { return Graphics::ReordersSamples(sampleOrder); } void PushConstants(VkCommandBuffer commands, VkShaderStageFlags stages, std::span bytes) const; // Forgets the Vulkan objects without destroying them: for entries of a device that is already gone. void Abandon() noexcept; @@ -92,6 +94,7 @@ private: std::vector colorSlots; bool sampleLocationsEnabled = false; VkExtent2D sampleLocationGrid{1, 1}; + SampleOrder sampleOrder{}; std::vector framebuffers; }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp new file mode 100644 index 0000000000..faf785dfc1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp @@ -0,0 +1,62 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SAMPLEORDER_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SAMPLEORDER_HPP + +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +struct HostSampleLocations { + VkExtent2D grid{1, 1}; + SampleOrder order{}; +}; + +enum class SampleOrderMethod : std::uint8_t { + Storage, + Depth, + DepthStencilExport, + DepthStencilBits +}; + +struct SampleOrderStep { + std::uint32_t attachment = 0; + SampleOrderMethod method = SampleOrderMethod::Storage; + bool toHost = false; + bool toGuest = false; +}; + +struct SampleOrderPass { + SampleOrder order{}; + VkExtent2D extent{}; + std::array locations{}; + bool located = false; +}; + +struct SampleOrderTarget { + VkImage image = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + VkFormat format = VK_FORMAT_UNDEFINED; + VkExtent2D extent{}; + std::uint32_t samples = 1; + SampleOrderMethod method = SampleOrderMethod::Storage; + std::shared_ptr keep; +}; + +HostSampleLocations HostSampleLocationsFor(const Context& context, const State& state); +bool MayReorderSamples(const Context& context, std::uint32_t samples); +SampleOrderMethod DepthSampleOrderMethod(const Context& context, const DepthTarget& depth); +VkFormat SampleOrderStorageFormat(const Context& context, VkFormat format, std::uint32_t elementBytes, std::uint32_t samples); +std::string SampleOrderRefusal(const Context& context, const State& state); +std::vector PlanSampleOrder(const Context& context, const State& state, const SampleOrder& order, VkAttachmentLoadOp load = VK_ATTACHMENT_LOAD_OP_LOAD, VkAttachmentStoreOp store = VK_ATTACHMENT_STORE_OP_STORE); +SampleOrderPass SampleOrderPassFor(const Context& context, const State& state, const SampleOrder& order); +void RecordSampleOrder(const Context& context, VkCommandBuffer commands, const SampleOrderPass& pass, std::span targets, bool toHost, std::vector>& kept); +void ClearSampleOrders(VkDevice device); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index b9844764d0..e019c692be 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -151,6 +151,14 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = State DecodeState(const QueueState& queue, bool vertexWorkgroup = false); std::array SampleLocations(const State& state); VkExtent2D SampleLocationGrid(const State& state); +using SampleOrder = std::array; +inline constexpr std::uint32_t KeptSampleOrder = 0x76543210u; +std::optional SameSetSampleOrder(const State& state); +bool ReordersSamples(const SampleOrder& order); +std::uint32_t InvertSampleOrder(std::uint32_t order); +std::uint32_t HostSampleBits(std::uint32_t guestBits, std::uint32_t order); +bool SampleMaskFollowsOrder(const State& state, const SampleOrder& order); +std::array QuadSampleMasks(const State& state, const SampleOrder& order); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index bc6efaf720..2757a4335d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -62,7 +62,7 @@ public: // compute pass writing it and the next pass sampling it share one image and copy nothing. // CanCopyFrom says whether the two descriptors address the same surface compatibly. Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components); - Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, std::uint32_t baseLayer = 0, std::shared_ptr owner = {}); + Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, std::uint32_t baseLayer = 0, std::shared_ptr owner = {}, VkImageUsageFlags usage = 0); struct DepthCopyTag {}; Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag); static VkFormat DepthCopyFormat(VkFormat colorFormat); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.comp new file mode 100644 index 0000000000..04ce1d7895 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.comp @@ -0,0 +1,21 @@ +#version 450 + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +layout(binding = 0, TEXEL_FORMAT) uniform restrict uimage2DMS samples; + +layout(push_constant) uniform Order { + uvec4 map; + uvec2 extent; + uint count; +} order; + +void main() { + const uvec2 at = gl_GlobalInvocationID.xy; + if (at.x >= order.extent.x || at.y >= order.extent.y) return; + const uint map = order.map[(at.x & 1u) | ((at.y & 1u) << 1u)]; + if (map == 0x76543210u) return; + uvec4 held[8]; + for (uint index = 0u; index < order.count; ++index) held[index] = imageLoad(samples, ivec2(at), int(index)); + for (uint index = 0u; index < order.count; ++index) imageStore(samples, ivec2(at), int(index), held[(map >> (index * 4u)) & 7u]); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.frag b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.frag new file mode 100644 index 0000000000..dfe0d74126 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.frag @@ -0,0 +1,28 @@ +#version 450 +#extension GL_EXT_samplerless_texture_functions : require +#ifdef STENCIL_EXPORT +#extension GL_ARB_shader_stencil_export : require +#endif + +layout(binding = 0) uniform texture2DMS depthSource; +layout(binding = 1) uniform utexture2DMS stencilSource; + +layout(push_constant) uniform Order { + uvec4 map; + uint bit; +} order; + +void main() { + const ivec2 at = ivec2(gl_FragCoord.xy); + const uint map = order.map[(at.x & 1) | ((at.y & 1) << 1)]; + if (map == 0x76543210u) discard; + const int source = int((map >> (uint(gl_SampleID) * 4u)) & 7u); +#ifdef STENCIL_BITS + if (((texelFetch(stencilSource, at, source).r >> order.bit) & 1u) == 0u) discard; +#else + gl_FragDepth = texelFetch(depthSource, at, source).r; +#ifdef STENCIL_EXPORT + gl_FragStencilRefARB = int(texelFetch(stencilSource, at, source).r); +#endif +#endif +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.vert b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.vert new file mode 100644 index 0000000000..05606ecec4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/SampleOrder.vert @@ -0,0 +1,6 @@ +#version 450 + +void main() { + const vec2 corner = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2)); + gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 59f21fea59..c3190e5391 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -1138,6 +1138,17 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) return surface->view; } +SampleOrderTarget DepthSurfaceOrderTarget(const Context& context, const DepthTarget& target) { + std::lock_guard gpu(GuestMemory::GpuMutex()); + std::unique_lock lock(surfacesMutex()); + for (const auto& candidate : surfaces()) { + if (!candidate->active->load(std::memory_order_acquire) || candidate->context.device != context.device || !sameSurface(candidate->target, target)) continue; + Require(candidate->sampleable, "the multisampled depth surface whose samples are put in the guest's order cannot be sampled"); + return {candidate->image, candidate->view, candidate->hostFormat, target.extent, target.samples, DepthSampleOrderMethod(context, target), candidate->owned}; + } + throw std::runtime_error("AGC graphics: the multisampled depth surface whose samples are put in the guest's order is not resident"); +} + std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads) { static constexpr std::array htileMaskKernel{0xd7460000u, 0x04010c06u, 0x34000084u, 0xdc388000u, 0x04020000u, 0xbf8c3f70u, 0xd7710008u, 0x00120805u, 0xd7710009u, 0x00120a05u, 0xd771000au, 0x00120c05u, 0xd771000bu, 0x00120e05u, 0xdc788000u, 0x00020800u, 0xbf810000u}; if (userData.size() < 6 || numThreads[0] != 64 || numThreads[1] != 1 || numThreads[2] != 1) return 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 7fb4593c6e..94f67e1170 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -8,6 +8,7 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" @@ -1560,6 +1561,8 @@ struct RecordedDraw { const std::array* pushBytes = nullptr; VkShaderStageFlags pushStages = 0; std::vector>> stencilReplay; + std::vector reordered; + SampleOrderPass reorderPass; }; void recordStencilReplay(const Context& context, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, const DrawInputs& inputs, std::span>> replay, std::span shaders, const ShaderResources& resources, const std::array* bytes = nullptr, VkShaderStageFlags stages = 0) { @@ -1801,6 +1804,11 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam } // Earlier recorded work (dispatches, the previous draw) wrote the images in the general // layout, which a lean draw renders in: no transitions. + if (!record.reordered.empty()) { + std::vector> kept; + RecordSampleOrder(context, commands, record.reorderPass, record.reordered, true, kept); + for (auto& object : kept) recorder->Keep(std::move(object)); + } APS5_LOG_OUT_DEBUG("Beginning pipeline renderExtent=%ux%u", state.renderExtent.width, state.renderExtent.height); record.pipeline->Begin(commands, *record.framebuffer, state.renderExtent, state); } @@ -1834,12 +1842,16 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam auto checkRecords = indirectRecordCheck(record.indirect); APS5_LOG_CHARS_OUT_DEBUG("Draw recorded"); std::function storeProxies; - if (!continued && !record.proxies.empty()) { - storeProxies = [proxies = record.proxies](VkCommandBuffer passCommands) { + if (!continued && (!record.proxies.empty() || !record.reordered.empty())) { + storeProxies = [proxies = record.proxies, reordered = record.reordered, reorderPass = record.reorderPass, context, recorder](VkCommandBuffer passCommands) { for (const auto& proxy : proxies) { proxy->RecordAttachmentProxyStore(passCommands, VK_IMAGE_LAYOUT_GENERAL); Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); } + if (reordered.empty()) return; + std::vector> kept; + RecordSampleOrder(context, passCommands, reorderPass, reordered, false, kept); + for (auto& object : kept) recorder->Keep(std::move(object)); }; } recorder->LeaveRenderPassOpen(passKey, drawTiming, fenced, passAccess, std::move(storeProxies)); @@ -2242,6 +2254,16 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& persistent.push_back(binding.multisampled ? 1u : 0u); } auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent, state.renderLayers, persistent); + std::vector reordered; + SampleOrderPass reorderPass; + if (pipeline->ReordersSamples()) { + reorderPass = SampleOrderPassFor(context, pipelineState, pipeline->HostSampleOrder()); + for (const auto& step : PlanSampleOrder(context, state, pipeline->HostSampleOrder())) { + if (step.attachment < targets.size()) reordered.push_back(MultisampleTargetOrderTarget(context, targets[step.attachment].color)); + else reordered.push_back(DepthSurfaceOrderTarget(context, *state.depth)); + Require(reordered.back().method == step.method, "a reordered attachment's surface disagrees with the planned way of putting its samples in order"); + } + } timer.phase(PhasePipeline); APS5_LOG_CHARS_OUT_DEBUG("Pipeline created"); if (lean) { @@ -2262,12 +2284,14 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& record.completion = completion; record.waited = outcome.waited; record.stencilReplay = stencilReplay; + record.reordered = reordered; + record.reorderPass = reorderPass; recordDraw(context, state, draw, shaders, inputs, record, outcome, timer, ownWaitedMs); // The recipe for the caller's draw-cache entry (design_cpu_final M8, rule R3 for draws): // only a template the resource cache serves under this content key (reusable: no lease, // no copied writes, every direct region import- or mirror-served) of a direct draw, so a // hit's proof is the template's ProveCurrent and nothing needs completion work. - if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && resolved.moved.empty() && !multisampled) { + if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && resolved.moved.empty() && !multisampled && !pipeline->ReordersSamples()) { auto recipe = std::make_shared(); recipe->device = context.device; recipe->templateRef = resources; @@ -2382,6 +2406,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& context.Resolved(&DeviceFunctions::cmdCopyBufferToImage, "vkCmdCopyBufferToImage")(commands, linear, binding.target->Image(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); imageBarrier(context, commands, binding.target->Image(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); } + std::vector> reorderKept; + RecordSampleOrder(context, commands, reorderPass, reordered, true, reorderKept); APS5_LOG_OUT_DEBUG("Beginning pipeline renderExtent=%ux%u", state.renderExtent.width, state.renderExtent.height); pipeline->Begin(commands, *framebuffer, state.renderExtent, state); APS5_LOG_CHARS_OUT_DEBUG("Pipeline Begin OK"); @@ -2408,6 +2434,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& APS5_LOG_CHARS_OUT_DEBUG("Draw recorded"); if (recorded) recorder->EndPassSamples(); EndRenderPass(context, commands); + RecordSampleOrder(context, commands, reorderPass, reordered, false, reorderKept); + if (recorded) { + for (auto& object : reorderKept) recorder->Keep(std::move(object)); + } APS5_LOG_CHARS_OUT_DEBUG("Render pass ended"); for (auto& binding : targets) { if (binding.proxied) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 15da911cfc..b5c69b1b65 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -59,13 +60,17 @@ public: context.formatProperties(context.physical, target.format, &properties); Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0, "multisampled render-target format cannot be a color attachment"); VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + storageFormat = MayReorderSamples(context, target.samples) ? SampleOrderStorageFormat(context, target.format, target.elementBytes, target.samples) : VK_FORMAT_UNDEFINED; + const VkImageCreateFlags flags = storageFormat != VK_FORMAT_UNDEFINED ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT : 0u; + if (storageFormat != VK_FORMAT_UNDEFINED) usage |= VK_IMAGE_USAGE_STORAGE_BIT; VkImageFormatProperties supported{}; - sampleable = (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0 && context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage | VK_IMAGE_USAGE_SAMPLED_BIT, 0, &supported) == VK_SUCCESS && (supported.sampleCounts & samples) != 0; + sampleable = (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0 && context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage | VK_IMAGE_USAGE_SAMPLED_BIT, flags, &supported) == VK_SUCCESS && (supported.sampleCounts & samples) != 0; if (sampleable) usage |= VK_IMAGE_USAGE_SAMPLED_BIT; - Check(context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &supported), "vkGetPhysicalDeviceImageFormatProperties multisampled target"); + Check(context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, flags, &supported), "vkGetPhysicalDeviceImageFormatProperties multisampled target"); Require((supported.sampleCounts & samples) != 0 && target.extent.width <= supported.maxExtent.width && target.extent.height <= supported.maxExtent.height, "multisampled render target exceeds device image limits"); try { VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.flags = flags; info.imageType = VK_IMAGE_TYPE_2D; info.format = target.format; info.extent = {target.extent.width, target.extent.height, 1}; @@ -89,7 +94,11 @@ public: viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; viewInfo.format = target.format; viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + viewUsage.usage = usage & ~VK_IMAGE_USAGE_STORAGE_BIT; + if (storageFormat != VK_FORMAT_UNDEFINED) viewInfo.pNext = &viewUsage; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView multisampled target"); + viewInfo.pNext = nullptr; recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; @@ -126,6 +135,21 @@ public: }); } + VkImageView StorageView() { + Require(storageFormat != VK_FORMAT_UNDEFINED, "the multisampled color target has no storage view to put its samples in the guest's order"); + if (storageView != VK_NULL_HANDLE) return storageView; + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = storageFormat; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + viewUsage.usage = VK_IMAGE_USAGE_STORAGE_BIT; + viewInfo.pNext = &viewUsage; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &storageView), "vkCreateImageView multisampled target storage"); + return storageView; + } + std::shared_ptr Sampled(VkComponentMapping components) { Require(sampleable, "the device cannot sample the multisampled color target's format at its sample count"); Require(cmask.Refusal().empty(), cmask.Refusal()); @@ -138,7 +162,7 @@ public: }); const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a)}; if (const auto found = textures.find(key); found != textures.end()) return found->second; - auto texture = std::make_shared(context, image, target.format, VK_IMAGE_ASPECT_COLOR_BIT, components); + auto texture = std::make_shared(context, image, target.format, VK_IMAGE_ASPECT_COLOR_BIT, components, VK_IMAGE_VIEW_TYPE_2D, 0, std::shared_ptr{}, storageFormat != VK_FORMAT_UNDEFINED ? VK_IMAGE_USAGE_SAMPLED_BIT : VkImageUsageFlags{0}); textures.emplace(key, texture); return texture; } @@ -182,6 +206,8 @@ public: VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; + VkFormat storageFormat = VK_FORMAT_UNDEFINED; + VkImageView storageView = VK_NULL_HANDLE; VkRenderPass resolvePass = VK_NULL_HANDLE; MultisampledCmask cmask; MultisampledColorFill colorFill; @@ -193,6 +219,7 @@ private: void release() noexcept { textures.clear(); if (resolvePass) context.Function("vkDestroyRenderPass")(context.device, resolvePass, nullptr); + if (storageView) context.Function("vkDestroyImageView")(context.device, storageView, nullptr); if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); @@ -465,6 +492,12 @@ VkImageView MultisampleTargetView(const Context& context, const ColorTarget& tar return acquire(context, target).view; } +SampleOrderTarget MultisampleTargetOrderTarget(const Context& context, const ColorTarget& target) { + std::lock_guard lock(targetsMutex()); + auto& found = acquire(context, target); + return {found.image, found.StorageView(), found.storageFormat, target.extent, target.samples, SampleOrderMethod::Storage, nullptr}; +} + std::shared_ptr MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components) { std::lock_guard lock(targetsMutex()); const auto format = ResolveTextureFormat(resource.format); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index aa927cc6c5..ba1123ca19 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include #include @@ -160,10 +161,11 @@ bool ShadersWriteMemory(std::span shaders) { void ValidateSampleMask(const Context& context, const State& state, std::span shaders) { if (!PartialSampleMask(state)) return; + const bool uniform = UniformSampleMask(state) && !SampleMaskFollowsOrder(state, HostSampleLocationsFor(context, state).order); for (const auto& shader : shaders) { if (shader.stage != ShaderRecompiler::ShaderStage::Fragment) continue; Require(shader.program != nullptr, "missing compiled fragment shader"); - if (UniformSampleMask(state)) { + if (uniform) { Require(context.earlyFragmentSampleMaskTestBeforeSampleCounting || !DeclaresEarlyFragmentTests(*shader.program), "a sample mask with early fragment tests needs VkPhysicalDeviceMaintenance5Properties::earlyFragmentSampleMaskTestBeforeSampleCounting, which the device lacks"); continue; } @@ -181,13 +183,6 @@ VkShaderStageFlags pushStagesOf(const Context& context) { return PreRasterizationPushStages(context.meshShader, context.tessellationShader, context.geometryShader); } -VkExtent2D hostSampleLocationGrid(const Context& context, const State& state) { - if (!state.sampleLocationGrid || (context.sampleLocationGridSampleCounts & state.samples) != 0) return SampleLocationGrid(state); - static std::atomic reported{0}; - if ((reported.fetch_or(state.samples, std::memory_order_relaxed) & state.samples) == 0) std::fprintf(stderr, "[gpu] no 2x2 sample location grid at sample count %u on this device: sample locations varying between pixels take pixel (0,0)'s in every pixel\n", state.samples); - return {1, 1}; -} - template void appendKey(std::vector& key, const TValue& value) { static_assert(std::is_trivially_copyable_v); @@ -265,6 +260,12 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& if (state.depth) appendKey(keys.renderPass, state.depth->format); appendKey(keys.fragmentShader, HostSampleMask(state)); appendKey(keys.fragmentOutput, HostSampleMask(state)); + for (auto* key : {&keys.fragmentShader, &keys.fragmentOutput}) { + appendKey(*key, context.sampleLocationSampleCounts); + appendKey(*key, context.sampleLocationGridSampleCounts); + appendKey(*key, context.storageImageMultisample); + appendKey(*key, context.shaderStencilExport); + } if (!resources.split) { appendKey(keys.layout, pushStagesOf(context)); for (const auto word : resources.mainKey) appendKey(keys.layout, word); @@ -297,7 +298,9 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias), dynamicRendering(context.graphicsPipelineLibrary) { PerformanceTimer timing("Vulkan.GraphicsPipeline"); sampleLocationsEnabled = ValidateSampleLocations(context, state); - sampleLocationGrid = hostSampleLocationGrid(context, state); + const auto host = HostSampleLocationsFor(context, state); + sampleLocationGrid = host.grid; + sampleOrder = host.order; // A cached pipeline may outlive its device's teardown (see ClearCachedPipelines); it must not keep // the buffer pool, which is reset with the device, alive past it. this->context.bufferPool.reset(); @@ -486,7 +489,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput depthStencil.front = state.stencilFront; depthStencil.back = state.stencilBack; const VkSampleMask sampleMask = HostSampleMask(state); - if (PartialSampleMask(state) && UniformSampleMask(state)) samples.pSampleMask = &sampleMask; + if (PartialSampleMask(state) && UniformSampleMask(state) && !SampleMaskFollowsOrder(state, sampleOrder)) samples.pSampleMask = &sampleMask; VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; const bool alternateAlpha = SplitsAlternateAlpha(state); std::vector firstChannels(state.blends.begin(), state.blends.end()); @@ -894,6 +897,10 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, tessellation.breakAtInstance); } append(key, HostSampleMask(state)); + append(key, context.sampleLocationSampleCounts); + append(key, context.sampleLocationGridSampleCounts); + append(key, context.storageImageMultisample); + append(key, context.shaderStencilExport); } std::uint64_t hashKey(const std::vector& key) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/SampleOrder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/SampleOrder.cpp new file mode 100644 index 0000000000..df10e12c25 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/SampleOrder.cpp @@ -0,0 +1,639 @@ +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderDepth_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderR16_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderR32_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderR8_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderRG32_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderRGBA32_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderStencilBits_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderStencilExport_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/SampleOrderVertex_spv.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { +namespace { + +constexpr VkAccessFlags anyWrite = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; +constexpr VkAccessFlags anyAccess = anyWrite | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_INPUT_ATTACHMENT_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_INDIRECT_COMMAND_READ_BIT; +constexpr VkPipelineStageFlags reorderStages = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; +constexpr std::uint32_t setsPerPool = 64; + +struct StoragePush { + std::array map; + std::array extent; + std::uint32_t count; +}; + +struct DepthPush { + std::array map; + std::uint32_t bit; +}; + +bool exactDepth(VkFormat format) { + return format == VK_FORMAT_D16_UNORM || format == VK_FORMAT_D32_SFLOAT || format == VK_FORMAT_D16_UNORM_S8_UINT || format == VK_FORMAT_D32_SFLOAT_S8_UINT; +} + +bool stencilOf(const Context& context, const DepthTarget& depth) { + return depth.stencilAddress != 0 && HasStencil(HostDepthFormat(context, depth.format)); +} + +bool depthSampleable(const Context& context, VkFormat format, std::uint32_t samples) { + if (context.imageFormatProperties == nullptr || context.formatProperties == nullptr) return false; + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + if ((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0) return false; + VkImageFormatProperties supported{}; + const VkImageCreateFlags flags = SampleLocationsCompatibleDepth(context, format, samples) ? VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT : 0u; + constexpr VkImageUsageFlags usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + if (context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, flags, &supported) != VK_SUCCESS) return false; + return (supported.sampleCounts & samples) != 0; +} + +std::array mapFor(const SampleOrder& order, bool toHost) { + std::array map{}; + for (std::size_t pixel = 0; pixel < map.size(); ++pixel) { + const auto word = order[pixel] == 0u ? KeptSampleOrder : order[pixel]; + map[pixel] = toHost ? word : InvertSampleOrder(word); + } + return map; +} + +VkExtent2D regionOf(const SampleOrderPass& pass, VkExtent2D extent) { + return {std::min(pass.extent.width, extent.width), std::min(pass.extent.height, extent.height)}; +} + +class CopyImage { +public: + CopyImage(const Context& context, VkFormat format, std::uint32_t samples, VkExtent2D extent, bool stencil) : context(context), format(format), samples(samples), extent(extent) { + try { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {extent.width, extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = static_cast(samples); + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage sample order copy"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory sample order copy"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory sample order copy"); + depthView = createView(VK_IMAGE_ASPECT_DEPTH_BIT); + if (stencil) stencilView = createView(VK_IMAGE_ASPECT_STENCIL_BIT); + } catch (...) { + release(); + throw; + } + } + ~CopyImage() { release(); } + CopyImage(const CopyImage&) = delete; + CopyImage& operator=(const CopyImage&) = delete; + + Context context; + VkFormat format; + std::uint32_t samples; + VkExtent2D extent; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkImageView depthView = VK_NULL_HANDLE; + VkImageView stencilView = VK_NULL_HANDLE; + bool general = false; + +private: + VkImageView createView(VkImageAspectFlags aspect) { + VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + info.image = image; + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.format = format; + info.subresourceRange = {aspect, 0, 1, 0, 1}; + VkImageView view = VK_NULL_HANDLE; + Check(context.Function("vkCreateImageView")(context.device, &info, nullptr, &view), "vkCreateImageView sample order copy"); + return view; + } + + void release() noexcept { + if (stencilView) context.Function("vkDestroyImageView")(context.device, stencilView, nullptr); + if (depthView) context.Function("vkDestroyImageView")(context.device, depthView, nullptr); + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + } +}; + +class SampleOrders { +public: + explicit SampleOrders(const Context& context) : context(context) { + this->context.bufferPool.reset(); + } + ~SampleOrders() { release(); } + SampleOrders(const SampleOrders&) = delete; + SampleOrders& operator=(const SampleOrders&) = delete; + + void Storage(VkCommandBuffer commands, const SampleOrderTarget& target, const std::array& map, VkExtent2D region) { + const auto pipeline = storagePipeline(target.format); + const auto set = allocate(storageSetLayout, {{VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, setsPerPool}}); + const VkDescriptorImageInfo imageInfo{VK_NULL_HANDLE, target.view, VK_IMAGE_LAYOUT_GENERAL}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = set; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + write.pImageInfo = &imageInfo; + context.Function("vkUpdateDescriptorSets")(context.device, 1, &write, 0, nullptr); + const StoragePush push{map, {region.width, region.height}, target.samples}; + context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); + context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, storagePipelineLayout, 0, 1, &set, 0, nullptr); + context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, storagePipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(push), &push); + context.Resolved(&DeviceFunctions::cmdDispatch, "vkCmdDispatch")(commands, (region.width + 7u) / 8u, (region.height + 7u) / 8u, 1); + } + + void Depth(VkCommandBuffer commands, const SampleOrderTarget& target, const std::array& map, VkExtent2D region, const SampleOrderPass& order, std::vector>& kept) { + const bool stencil = target.method != SampleOrderMethod::Depth; + auto copy = copyFor(target.format, target.samples, target.extent, stencil); + kept.push_back(copy); + const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); + if (!copy->general) { + VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toGeneral.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; + toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.image = copy->image; + toGeneral.subresourceRange = {aspects, 0, 1, 0, 1}; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + copy->general = true; + } + VkImageCopy region3d{}; + region3d.srcSubresource = {aspects, 0, 0, 1}; + region3d.dstSubresource = {aspects, 0, 0, 1}; + region3d.extent = {region.width, region.height, 1}; + context.Function("vkCmdCopyImage")(commands, target.image, VK_IMAGE_LAYOUT_GENERAL, copy->image, VK_IMAGE_LAYOUT_GENERAL, 1, ®ion3d); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT); + const auto& locations = order.locations; + const bool located = order.located; + const auto& pass = depthPass(target.format, target.samples, target.method, located, locations); + const auto set = allocate(sampledSetLayout, {{VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, setsPerPool * 2u}}); + std::array images{{{VK_NULL_HANDLE, copy->depthView, VK_IMAGE_LAYOUT_GENERAL}, {VK_NULL_HANDLE, stencil ? copy->stencilView : copy->depthView, VK_IMAGE_LAYOUT_GENERAL}}}; + std::array writes{}; + for (std::uint32_t binding = 0; binding < writes.size(); ++binding) { + writes[binding].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[binding].dstSet = set; + writes[binding].dstBinding = binding; + writes[binding].descriptorCount = 1; + writes[binding].descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + writes[binding].pImageInfo = &images[binding]; + } + context.Function("vkUpdateDescriptorSets")(context.device, stencil ? 2u : 1u, writes.data(), 0, nullptr); + const std::array views{target.view}; + auto framebuffer = std::make_shared(context, pass.renderPass, views, region); + kept.push_back(framebuffer); + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = pass.renderPass; + begin.framebuffer = framebuffer->Handle(); + begin.renderArea = {{0, 0}, region}; + const VkSampleLocationsInfoEXT info{VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast(target.samples), {1, 1}, target.samples, locations.data()}; + const VkAttachmentSampleLocationsEXT initial{0, info}; + const VkSubpassSampleLocationsEXT post{0, info}; + VkRenderPassSampleLocationsBeginInfoEXT sampleLocations{VK_STRUCTURE_TYPE_RENDER_PASS_SAMPLE_LOCATIONS_BEGIN_INFO_EXT}; + sampleLocations.attachmentInitialSampleLocationsCount = 1; + sampleLocations.pAttachmentInitialSampleLocations = &initial; + sampleLocations.postSubpassSampleLocationsCount = 1; + sampleLocations.pPostSubpassSampleLocations = &post; + if (located) begin.pNext = &sampleLocations; + context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + const VkViewport viewport{0.0f, 0.0f, static_cast(region.width), static_cast(region.height), 0.0f, 1.0f}; + const VkRect2D scissor{{0, 0}, region}; + context.Resolved(&DeviceFunctions::cmdSetViewport, "vkCmdSetViewport")(commands, 0, 1, &viewport); + context.Resolved(&DeviceFunctions::cmdSetScissor, "vkCmdSetScissor")(commands, 0, 1, &scissor); + context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, sampledPipelineLayout, 0, 1, &set, 0, nullptr); + const auto draw = [&](VkPipeline pipeline, std::uint32_t bit, std::uint32_t writeMask, std::uint32_t reference) { + context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + context.Resolved(&DeviceFunctions::cmdSetStencilWriteMask, "vkCmdSetStencilWriteMask")(commands, VK_STENCIL_FACE_FRONT_AND_BACK, writeMask); + context.Resolved(&DeviceFunctions::cmdSetStencilReference, "vkCmdSetStencilReference")(commands, VK_STENCIL_FACE_FRONT_AND_BACK, reference); + const DepthPush push{map, bit}; + context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, sampledPipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(push), &push); + context.Resolved(&DeviceFunctions::cmdDraw, "vkCmdDraw")(commands, 3, 1, 0, 0); + }; + draw(pass.depth, 0u, 0xffu, 0u); + if (target.method == SampleOrderMethod::DepthStencilBits) { + for (std::uint32_t bit = 0; bit < 8u; ++bit) draw(pass.stencilBits, bit, 1u << bit, 0xffu); + } + context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(commands); + } + + Context context; + +private: + struct Ring { + std::vector pools; + std::vector serials; + std::size_t current = 0; + std::uint32_t used = 0; + }; + + struct DepthPass { + VkRenderPass renderPass = VK_NULL_HANDLE; + VkPipeline depth = VK_NULL_HANDLE; + VkPipeline stencilBits = VK_NULL_HANDLE; + }; + + using DepthKey = std::tuple>; + using CopyKey = std::tuple; + + VkShaderModule module(const std::uint32_t* code, std::size_t bytes) { + VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + info.codeSize = bytes; + info.pCode = code; + VkShaderModule created = VK_NULL_HANDLE; + Check(context.Function("vkCreateShaderModule")(context.device, &info, nullptr, &created), "vkCreateShaderModule sample order"); + return created; + } + + void destroyModule(VkShaderModule shader) { + context.Function("vkDestroyShaderModule")(context.device, shader, nullptr); + } + + VkDescriptorSetLayout setLayout(VkDescriptorType type, std::uint32_t count, VkShaderStageFlags stage) { + std::array bindings{}; + for (std::uint32_t binding = 0; binding < count; ++binding) bindings[binding] = {binding, type, 1, stage, nullptr}; + VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + info.bindingCount = count; + info.pBindings = bindings.data(); + VkDescriptorSetLayout layout = VK_NULL_HANDLE; + Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &info, nullptr, &layout), "vkCreateDescriptorSetLayout sample order"); + return layout; + } + + VkPipelineLayout pipelineLayout(VkDescriptorSetLayout set, VkShaderStageFlags stage, std::uint32_t pushBytes) { + const VkPushConstantRange range{stage, 0, pushBytes}; + VkPipelineLayoutCreateInfo info{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + info.setLayoutCount = 1; + info.pSetLayouts = &set; + info.pushConstantRangeCount = 1; + info.pPushConstantRanges = ⦥ + VkPipelineLayout layout = VK_NULL_HANDLE; + Check(context.Function("vkCreatePipelineLayout")(context.device, &info, nullptr, &layout), "vkCreatePipelineLayout sample order"); + return layout; + } + + VkPipeline storagePipeline(VkFormat format) { + const std::array formats{VK_FORMAT_R8_UINT, VK_FORMAT_R16_UINT, VK_FORMAT_R32_UINT, VK_FORMAT_R32G32_UINT, VK_FORMAT_R32G32B32A32_UINT}; + const auto found = std::ranges::find(formats, format); + Require(found != formats.end(), "reordering the samples of a color target through a storage view of this format is not implemented"); + const auto index = static_cast(found - formats.begin()); + if (storagePipelines[index] != VK_NULL_HANDLE) return storagePipelines[index]; + if (storageSetLayout == VK_NULL_HANDLE) storageSetLayout = setLayout(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT); + if (storagePipelineLayout == VK_NULL_HANDLE) storagePipelineLayout = pipelineLayout(storageSetLayout, VK_SHADER_STAGE_COMPUTE_BIT, sizeof(StoragePush)); + const std::array, 5> code{{{SAMPLE_ORDER_R8_SPV, sizeof(SAMPLE_ORDER_R8_SPV)}, {SAMPLE_ORDER_R16_SPV, sizeof(SAMPLE_ORDER_R16_SPV)}, {SAMPLE_ORDER_R32_SPV, sizeof(SAMPLE_ORDER_R32_SPV)}, {SAMPLE_ORDER_RG32_SPV, sizeof(SAMPLE_ORDER_RG32_SPV)}, {SAMPLE_ORDER_RGBA32_SPV, sizeof(SAMPLE_ORDER_RGBA32_SPV)}}}; + const auto shader = module(code[index].first, code[index].second); + VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + info.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + info.stage.module = shader; + info.stage.pName = "main"; + info.layout = storagePipelineLayout; + VkResult created; + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + created = context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &info, nullptr, &storagePipelines[index]); + } + destroyModule(shader); + Check(created, "vkCreateComputePipelines sample order"); + return storagePipelines[index]; + } + + const DepthPass& depthPass(VkFormat format, std::uint32_t samples, SampleOrderMethod method, bool located, const std::array& locations) { + std::array standIn{}; + if (located) { + for (std::uint32_t sample = 0; sample < samples && sample < 8u; ++sample) { + standIn[sample * 2u] = std::bit_cast(locations[sample].x); + standIn[sample * 2u + 1u] = std::bit_cast(locations[sample].y); + } + } + const DepthKey key{format, samples, method, located, standIn}; + if (const auto found = depthPasses.find(key); found != depthPasses.end()) return found->second; + if (sampledSetLayout == VK_NULL_HANDLE) sampledSetLayout = setLayout(VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 2, VK_SHADER_STAGE_FRAGMENT_BIT); + if (sampledPipelineLayout == VK_NULL_HANDLE) sampledPipelineLayout = pipelineLayout(sampledSetLayout, VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(DepthPush)); + const bool stencil = method != SampleOrderMethod::Depth; + DepthPass pass; + try { + VkAttachmentDescription attachment{}; + attachment.format = format; + attachment.samples = static_cast(samples); + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.stencilLoadOp = stencil ? VK_ATTACHMENT_LOAD_OP_LOAD : VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.stencilStoreOp = stencil ? VK_ATTACHMENT_STORE_OP_STORE : VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachment.initialLayout = VK_IMAGE_LAYOUT_GENERAL; + attachment.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + if (HasStencil(format) && !stencil) { + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; + } + const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_GENERAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.pDepthStencilAttachment = &reference; + VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + passInfo.attachmentCount = 1; + passInfo.pAttachments = &attachment; + passInfo.subpassCount = 1; + passInfo.pSubpasses = &subpass; + Check(context.Function("vkCreateRenderPass")(context.device, &passInfo, nullptr, &pass.renderPass), "vkCreateRenderPass sample order"); + const auto create = [&](const std::uint32_t* code, std::size_t bytes, bool writesDepth) { + const auto vertex = module(SAMPLE_ORDER_VERTEX_SPV, sizeof(SAMPLE_ORDER_VERTEX_SPV)); + const auto fragment = module(code, bytes); + std::array stages{}; + stages[0] = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_VERTEX_BIT, vertex, "main", nullptr}; + stages[1] = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_FRAGMENT_BIT, fragment, "main", nullptr}; + VkPipelineVertexInputStateCreateInfo input{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 viewports{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewports.viewportCount = 1; + viewports.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 multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + multisample.rasterizationSamples = static_cast(samples); + multisample.sampleShadingEnable = VK_TRUE; + multisample.minSampleShading = 1.0f; + VkPipelineSampleLocationsStateCreateInfoEXT sampleLocations{VK_STRUCTURE_TYPE_PIPELINE_SAMPLE_LOCATIONS_STATE_CREATE_INFO_EXT}; + sampleLocations.sampleLocationsEnable = VK_TRUE; + sampleLocations.sampleLocationsInfo = {VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast(samples), {1, 1}, samples, locations.data()}; + if (located) multisample.pNext = &sampleLocations; + VkStencilOpState replace{}; + replace.failOp = VK_STENCIL_OP_REPLACE; + replace.passOp = VK_STENCIL_OP_REPLACE; + replace.depthFailOp = VK_STENCIL_OP_REPLACE; + replace.compareOp = VK_COMPARE_OP_ALWAYS; + replace.compareMask = 0xffu; + replace.writeMask = 0xffu; + VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; + depthStencil.depthTestEnable = writesDepth ? VK_TRUE : VK_FALSE; + depthStencil.depthWriteEnable = writesDepth ? VK_TRUE : VK_FALSE; + depthStencil.depthCompareOp = VK_COMPARE_OP_ALWAYS; + depthStencil.stencilTestEnable = stencil ? VK_TRUE : VK_FALSE; + depthStencil.front = replace; + depthStencil.back = replace; + const std::array dynamicStates{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR, VK_DYNAMIC_STATE_STENCIL_WRITE_MASK, VK_DYNAMIC_STATE_STENCIL_REFERENCE}; + VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; + dynamic.dynamicStateCount = static_cast(dynamicStates.size()); + dynamic.pDynamicStates = dynamicStates.data(); + VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + info.stageCount = static_cast(stages.size()); + info.pStages = stages.data(); + info.pVertexInputState = &input; + info.pInputAssemblyState = &assembly; + info.pViewportState = &viewports; + info.pRasterizationState = &raster; + info.pMultisampleState = &multisample; + info.pDepthStencilState = &depthStencil; + info.pDynamicState = &dynamic; + info.layout = sampledPipelineLayout; + info.renderPass = pass.renderPass; + VkPipeline pipeline = VK_NULL_HANDLE; + VkResult created; + { + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + created = context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &info, nullptr, &pipeline); + } + destroyModule(fragment); + destroyModule(vertex); + Check(created, "vkCreateGraphicsPipelines sample order"); + return pipeline; + }; + if (method == SampleOrderMethod::DepthStencilExport) pass.depth = create(SAMPLE_ORDER_STENCILEXPORT_SPV, sizeof(SAMPLE_ORDER_STENCILEXPORT_SPV), true); + else pass.depth = create(SAMPLE_ORDER_DEPTH_SPV, sizeof(SAMPLE_ORDER_DEPTH_SPV), true); + if (method == SampleOrderMethod::DepthStencilBits) pass.stencilBits = create(SAMPLE_ORDER_STENCILBITS_SPV, sizeof(SAMPLE_ORDER_STENCILBITS_SPV), false); + } catch (...) { + destroyPass(pass); + throw; + } + return depthPasses.emplace(key, pass).first->second; + } + + std::shared_ptr copyFor(VkFormat format, std::uint32_t samples, VkExtent2D extent, bool stencil) { + const CopyKey key{format, samples, extent.width, extent.height, stencil}; + if (const auto found = copies.find(key); found != copies.end()) { + found->second.second = ++uses; + return found->second.first; + } + constexpr std::size_t bound = 4; + while (copies.size() >= bound) { + const auto oldest = std::ranges::min_element(copies, [](const auto& a, const auto& b) { return a.second.second < b.second.second; }); + copies.erase(oldest); + } + auto copy = std::make_shared(context, format, samples, extent, stencil); + copies.emplace(key, std::pair{copy, ++uses}); + return copy; + } + + VkDescriptorSet allocate(VkDescriptorSetLayout layout, const std::vector& sizes) { + auto& ring = layout == storageSetLayout ? storageRing : sampledRing; + const auto* recorder = Recorder::Active(); + if (ring.pools.empty() || ring.used == setsPerPool) { + const auto completed = recorder != nullptr ? recorder->CompletedThrough() : 0; + auto next = ring.pools.size(); + for (std::size_t step = 1; step <= ring.pools.size(); ++step) { + const auto index = (ring.current + step) % ring.pools.size(); + if (ring.serials[index] <= completed) { + next = index; + break; + } + } + if (next < ring.pools.size()) { + Check(context.Function("vkResetDescriptorPool")(context.device, ring.pools[next], 0), "vkResetDescriptorPool sample order"); + } else { + VkDescriptorPoolCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + info.maxSets = setsPerPool; + info.poolSizeCount = static_cast(sizes.size()); + info.pPoolSizes = sizes.data(); + VkDescriptorPool pool = VK_NULL_HANDLE; + Check(context.Function("vkCreateDescriptorPool")(context.device, &info, nullptr, &pool), "vkCreateDescriptorPool sample order"); + ring.pools.push_back(pool); + ring.serials.push_back(0); + } + ring.current = next; + ring.used = 0; + } + VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocation.descriptorPool = ring.pools[ring.current]; + allocation.descriptorSetCount = 1; + allocation.pSetLayouts = &layout; + VkDescriptorSet set = VK_NULL_HANDLE; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &set), "vkAllocateDescriptorSets sample order"); + ++ring.used; + ring.serials[ring.current] = recorder != nullptr && recorder->Device() == context.device ? recorder->OpenSerial() : std::numeric_limits::max(); + return set; + } + + void destroyPass(const DepthPass& pass) noexcept { + if (pass.stencilBits) context.Function("vkDestroyPipeline")(context.device, pass.stencilBits, nullptr); + if (pass.depth) context.Function("vkDestroyPipeline")(context.device, pass.depth, nullptr); + if (pass.renderPass) context.Function("vkDestroyRenderPass")(context.device, pass.renderPass, nullptr); + } + + void release() noexcept { + copies.clear(); + for (const auto& [key, pass] : depthPasses) destroyPass(pass); + depthPasses.clear(); + for (const auto pipeline : storagePipelines) { + if (pipeline) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + } + for (auto* ring : {&storageRing, &sampledRing}) { + for (const auto pool : ring->pools) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); + ring->pools.clear(); + } + for (const auto layout : {storagePipelineLayout, sampledPipelineLayout}) { + if (layout) context.Function("vkDestroyPipelineLayout")(context.device, layout, nullptr); + } + for (const auto layout : {storageSetLayout, sampledSetLayout}) { + if (layout) context.Function("vkDestroyDescriptorSetLayout")(context.device, layout, nullptr); + } + } + + VkDescriptorSetLayout storageSetLayout = VK_NULL_HANDLE; + VkDescriptorSetLayout sampledSetLayout = VK_NULL_HANDLE; + VkPipelineLayout storagePipelineLayout = VK_NULL_HANDLE; + VkPipelineLayout sampledPipelineLayout = VK_NULL_HANDLE; + std::array storagePipelines{}; + std::map depthPasses; + std::map, std::uint64_t>> copies; + std::uint64_t uses = 0; + Ring storageRing; + Ring sampledRing; +}; + +std::mutex& ordersMutex() { + static std::mutex mutex; + return mutex; +} + +std::vector>& orders() { + static std::vector> list; + return list; +} + +SampleOrders& ordersFor(const Context& context) { + std::lock_guard lock(ordersMutex()); + for (const auto& entry : orders()) { + if (entry->context.device == context.device) return *entry; + } + orders().push_back(std::make_unique(context)); + return *orders().back(); +} + +} + +SampleOrderMethod DepthSampleOrderMethod(const Context& context, const DepthTarget& depth) { + if (!stencilOf(context, depth)) return SampleOrderMethod::Depth; + return context.shaderStencilExport ? SampleOrderMethod::DepthStencilExport : SampleOrderMethod::DepthStencilBits; +} + +bool MayReorderSamples(const Context& context, std::uint32_t samples) { + return samples > 1u && (context.sampleLocationSampleCounts & samples) != 0 && (context.sampleLocationGridSampleCounts & samples) == 0; +} + +VkFormat SampleOrderStorageFormat(const Context& context, VkFormat format, std::uint32_t elementBytes, std::uint32_t samples) { + if (!context.storageImageMultisample || context.imageFormatProperties == nullptr || samples <= 1u) return VK_FORMAT_UNDEFINED; + const auto alias = TextureDetiler::ImageElementFormat(elementBytes); + if (alias == VK_FORMAT_UNDEFINED) return VK_FORMAT_UNDEFINED; + VkImageFormatProperties supported{}; + if (context.imageFormatProperties(context.physical, alias, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_STORAGE_BIT, 0, &supported) != VK_SUCCESS || (supported.sampleCounts & samples) == 0) return VK_FORMAT_UNDEFINED; + constexpr VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_STORAGE_BIT; + constexpr VkImageCreateFlags flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT; + if (context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, flags, &supported) != VK_SUCCESS || (supported.sampleCounts & samples) == 0) return VK_FORMAT_UNDEFINED; + return alias; +} + +std::string SampleOrderRefusal(const Context& context, const State& state) { + if (!context.sampleRateShading) return "the device lacks sampleRateShading"; + for (const auto& color : state.colors) { + if (color.samples > 1u && SampleOrderStorageFormat(context, color.format, color.elementBytes, color.samples) == VK_FORMAT_UNDEFINED) return "color format " + std::to_string(color.format) + " has no storage view at " + std::to_string(color.samples) + " samples"; + } + if (state.depth && state.depth->samples > 1u) { + const auto host = HostDepthFormat(context, state.depth->format); + if (!exactDepth(host)) return "depth format " + std::to_string(host) + " is not written back exactly through FragDepth"; + if (!depthSampleable(context, host, state.depth->samples)) return "depth format " + std::to_string(host) + " cannot be sampled at " + std::to_string(state.depth->samples) + " samples"; + } + return {}; +} + +HostSampleLocations HostSampleLocationsFor(const Context& context, const State& state) { + if (!state.sampleLocationGrid || (context.sampleLocationGridSampleCounts & state.samples) != 0) return {SampleLocationGrid(state), {}}; + const auto order = SameSetSampleOrder(state); + const auto refusal = order ? SampleOrderRefusal(context, state) : std::string("its pixels hold different locations"); + static std::atomic reported{0}; + const auto bit = refusal.empty() ? state.samples : state.samples << 8u; + if ((reported.fetch_or(bit, std::memory_order_relaxed) & bit) == 0) { + if (refusal.empty()) std::fprintf(stderr, "[gpu] no 2x2 sample location grid at sample count %u on this device: pixel (0,0)'s sample locations stand in for every pixel, and each pixel's samples are put in the guest's order at the bounds of its render passes\n", state.samples); + else std::fprintf(stderr, "[gpu] no 2x2 sample location grid at sample count %u on this device: sample locations varying between pixels take pixel (0,0)'s in every pixel (%s)\n", state.samples, refusal.c_str()); + } + return {{1, 1}, refusal.empty() ? *order : SampleOrder{}}; +} + +std::vector PlanSampleOrder(const Context& context, const State& state, const SampleOrder& order, VkAttachmentLoadOp load, VkAttachmentStoreOp store) { + std::vector steps; + if (!ReordersSamples(order)) return steps; + const bool toHost = load == VK_ATTACHMENT_LOAD_OP_LOAD; + const bool toGuest = store == VK_ATTACHMENT_STORE_OP_STORE; + if (!toHost && !toGuest) return steps; + for (std::uint32_t index = 0; index < state.colors.size(); ++index) { + if (state.colors[index].samples > 1u) steps.push_back({index, SampleOrderMethod::Storage, toHost, toGuest}); + } + if (state.depth && state.depth->samples > 1u) steps.push_back({static_cast(state.colors.size()), DepthSampleOrderMethod(context, *state.depth), toHost, toGuest}); + return steps; +} + +SampleOrderPass SampleOrderPassFor(const Context& context, const State& state, const SampleOrder& order) { + return {order, state.renderExtent, SampleLocations(state), ReordersSamples(order) && ValidateSampleLocations(context, state)}; +} + +void RecordSampleOrder(const Context& context, VkCommandBuffer commands, const SampleOrderPass& pass, std::span targets, bool toHost, std::vector>& kept) { + if (targets.empty() || !ReordersSamples(pass.order)) return; + auto& reorder = ordersFor(context); + const auto map = mapFor(pass.order, toHost); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, reorderStages, anyWrite, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT); + for (const auto& target : targets) { + Require(target.image != VK_NULL_HANDLE && target.view != VK_NULL_HANDLE, "a reordered attachment has no image"); + const auto region = regionOf(pass, target.extent); + if (region.width == 0 || region.height == 0) continue; + if (target.method == SampleOrderMethod::Storage) reorder.Storage(commands, target, map, region); + else reorder.Depth(commands, target, map, region, pass, kept); + if (target.keep != nullptr) kept.push_back(target.keep); + } + RecordMemoryBarrier(context, commands, reorderStages, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, anyAccess); + if (Recorder::Active() != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); +} + +void ClearSampleOrders(VkDevice device) { + std::lock_guard lock(ordersMutex()); + std::erase_if(orders(), [&](const auto& entry) { return entry->context.device == device; }); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index c16eb7847a..6142051012 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -810,6 +810,53 @@ VkExtent2D SampleLocationGrid(const State& state) { return state.sampleLocationGrid ? VkExtent2D{2, 2} : VkExtent2D{1, 1}; } +std::optional SameSetSampleOrder(const State& state) { + if (!state.customSampleLocations || !state.sampleLocationGrid || state.samples < 2u || state.samples > 8u) return std::nullopt; + SampleOrder order{}; + for (std::uint32_t pixel = 0; pixel < order.size(); ++pixel) { + std::uint32_t word = KeptSampleOrder; + std::uint32_t taken = 0; + for (std::uint32_t sample = 0; sample < state.samples; ++sample) { + std::uint32_t guest = 0; + while (guest < state.samples && (((taken >> guest) & 1u) != 0u || state.sampleLocations[pixel * 8u + guest] != state.sampleLocations[sample])) ++guest; + if (guest == state.samples) return std::nullopt; + taken |= 1u << guest; + word = (word & ~(0xfu << (sample * 4u))) | (guest << (sample * 4u)); + } + order[pixel] = word; + } + return order; +} + +bool ReordersSamples(const SampleOrder& order) { + return std::ranges::any_of(order, [](std::uint32_t word) { return word != 0u && word != KeptSampleOrder; }); +} + +std::uint32_t InvertSampleOrder(std::uint32_t order) { + std::uint32_t inverse = 0; + for (std::uint32_t sample = 0; sample < 8u; ++sample) inverse |= sample << (((order >> (sample * 4u)) & 0x7u) * 4u); + return inverse; +} + +std::uint32_t HostSampleBits(std::uint32_t guestBits, std::uint32_t order) { + std::uint32_t host = 0; + for (std::uint32_t sample = 0; sample < 8u; ++sample) host |= ((guestBits >> ((order >> (sample * 4u)) & 0x7u)) & 1u) << sample; + return host | (guestBits & ~0xffu); +} + +bool SampleMaskFollowsOrder(const State& state, const SampleOrder& order) { + if (!ReordersSamples(order) || !PartialSampleMask(state) || !UniformSampleMask(state)) return false; + const auto full = (1u << state.samples) - 1u; + const auto mask = state.quadSampleMask[0] & full; + return std::ranges::any_of(order, [&](std::uint32_t word) { return (HostSampleBits(mask, word) & full) != mask; }); +} + +std::array QuadSampleMasks(const State& state, const SampleOrder& order) { + if (!SampleMaskFollowsOrder(state, order)) return QuadSampleMasks(state); + const auto mask = static_cast(state.quadSampleMask[0] & ((1u << state.samples) - 1u)); + return {mask, mask, mask, mask}; +} + std::array SampleLocations(const State& state) { Require(state.samples == 1 || state.samples == 2 || state.samples == 4 || state.samples == 8, "invalid sample count"); std::array locations{}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index e71366c10b..006a4850f6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -781,10 +781,13 @@ Texture::Texture(const Context& context, const std::shared_ptr& } } -Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer, std::shared_ptr owner) : context(context), owned(std::move(owner)) { +Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer, std::shared_ptr owner, VkImageUsageFlags usage) : context(context), owned(std::move(owner)) { Require(owned == nullptr || (owned->image == depthImage && owned->context.device == context.device), "a depth sampled view must retain its own device and image"); layout = VK_IMAGE_LAYOUT_GENERAL; VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + viewUsage.usage = usage; + if (usage != 0) viewInfo.pNext = &viewUsage; viewInfo.image = depthImage; viewInfo.viewType = viewType; viewInfo.format = depthFormat; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index f1cc82e635..90d08ca5e1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -5111,6 +5111,231 @@ void sampleLocationGridFallbackTests() { mock = MockVulkan{}; } +VkSampleCountFlags orderStorageCounts = VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; +bool orderSixteenBitStencil = true; +std::vector pipelineSampleMasks; + +VKAPI_ATTR VkResult VKAPI_CALL sampleOrderImageProperties(VkPhysicalDevice, VkFormat, VkImageType, VkImageTiling, VkImageUsageFlags usage, VkImageCreateFlags, VkImageFormatProperties* properties) { + *properties = {}; + properties->maxExtent = {16384, 16384, 1}; + properties->maxMipLevels = 1; + properties->maxArrayLayers = 1; + properties->sampleCounts = (usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0 ? orderStorageCounts : VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL sampleOrderFormatProperties(VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) { + *properties = {}; + if (format == VK_FORMAT_D16_UNORM_S8_UINT && !orderSixteenBitStencil) return; + properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT | VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT; +} + +VKAPI_ATTR VkResult VKAPI_CALL sampleOrderCreateGraphicsPipelines(VkDevice device, VkPipelineCache cache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks* allocator, VkPipeline* pipelines) { + const auto* samples = infos[0].pMultisampleState; + pipelineSampleMasks.push_back(samples != nullptr && samples->pSampleMask != nullptr ? *samples->pSampleMask : ~VkSampleMask{0}); + return sampleLocationCreateGraphicsPipelines(device, cache, count, infos, allocator, pipelines); +} + +PFN_vkVoidFunction VKAPI_CALL sampleOrderProc(VkDevice device, const char* name) { + if (std::string_view(name) == "vkCreateGraphicsPipelines") return reinterpret_cast(sampleOrderCreateGraphicsPipelines); + return sampleLocationProc(device, name); +} + +void sampleOrderTests() { + using namespace AgcDriver::Graphics; + auto queue = makeState(); + queue.context[0x2f8] = 0x00100001; + queue.context[0x292] = 3; + queue.context[0x3b8] = 0x0dc00000u; + queue.context[0x31d] = 0x9000; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + const auto decode = [&](std::array words) { + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = words[pixel]; + return DecodeState(queue); + }; + const auto village = decode({0x0c04u, 0x0c04u, 0x040cu, 0x040cu}); + const auto residentEvil4 = decode({0x040cu, 0x040cu, 0x0c04u, 0x0c04u}); + const auto uniform = decode({0x0c04u, 0x0c04u, 0x0c04u, 0x0c04u}); + const auto moved = decode({0x0c04u, 0x0c04u, 0x040bu, 0x040bu}); + const SampleOrder rowSwap{KeptSampleOrder, KeptSampleOrder, 0x76543201u, 0x76543201u}; + Require(SameSetSampleOrder(village) == rowSwap, "the 2x checkerboard (PPSA01556) did not reorder only its odd rows' two samples"); + Require(SameSetSampleOrder(residentEvil4) == rowSwap, "the 2x checkerboard with its rows swapped (PPSA07411) did not reorder only its odd rows' two samples"); + Require(!SameSetSampleOrder(uniform) && !SameSetSampleOrder(moved), "a uniform pattern or one whose odd rows hold other positions was taken as a reordering of one set"); + State four{}; + four.samples = 4; + four.customSampleLocations = true; + four.sampleLocationGrid = true; + const std::array positions{0x2eu, 0xe6u, 0x2au, 0x62u}; + const std::array, 4> placed{{{0, 1, 2, 3}, {1, 0, 3, 2}, {2, 3, 1, 0}, {3, 2, 0, 1}}}; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + for (std::uint32_t sample = 0; sample < 4; ++sample) four.sampleLocations[pixel * 8u + placed[pixel][sample]] = positions[sample]; + } + const SampleOrder fourOrder{KeptSampleOrder, 0x76542301u, 0x76540132u, 0x76541023u}; + Require(SameSetSampleOrder(four) == fourOrder, "a 4x pattern whose pixels hold one set of positions in four orders did not give each pixel its order"); + Require(InvertSampleOrder(0x76543201u) == 0x76543201u && InvertSampleOrder(0x76540132u) == 0x76541023u && InvertSampleOrder(KeptSampleOrder) == KeptSampleOrder, "inverting a sample order did not undo it"); + Require(HostSampleBits(0x1u, 0x76543201u) == 0x2u && HostSampleBits(0x1u, 0x76540132u) == 0x8u && HostSampleBits(0xffffffffu, 0x76540132u) == 0xffffffffu, "a guest sample mask did not move to the host samples at its guest samples' locations"); + Require(ReordersSamples(rowSwap) && !ReordersSamples(SampleOrder{}) && !ReordersSamples(SampleOrder{KeptSampleOrder, KeptSampleOrder, KeptSampleOrder, KeptSampleOrder}), "a sample order that keeps every index counted as a reordering"); + + const auto capable = [] { + auto context = mockContext(); + context.physical = makeHandle(); + context.device = makeHandle(); + context.deviceProc = sampleOrderProc; + context.imageFormatProperties = sampleOrderImageProperties; + context.formatProperties = sampleOrderFormatProperties; + context.sampleLocationSampleCounts = VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; + context.sampleLocationGridSampleCounts = 0; + context.storageImageMultisample = true; + context.sampleRateShading = true; + context.shaderStencilExport = true; + context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; + context.limits.framebufferDepthSampleCounts = VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; + context.limits.framebufferColorSampleCounts = VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; + context.limits.maxColorAttachments = 8; + context.limits.maxFramebufferWidth = 64; + context.limits.maxFramebufferHeight = 64; + context.limits.maxFramebufferLayers = 1; + return context; + }; + const auto attached = [](const State& decoded, VkFormat depthFormat, bool stencil) { + State state = decoded; + ColorTarget color{}; + color.address = 0x100000u; + color.extent = {64, 64}; + color.format = VK_FORMAT_R16G16B16A16_SFLOAT; + color.elementBytes = 8; + color.samples = state.samples; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + state.blends = {blend}; + state.renderExtent = {64, 64}; + if (depthFormat != VK_FORMAT_UNDEFINED) { + state.depth = DepthTarget{0x200000u, stencil ? 0x300000u : 0u, {64, 64}, depthFormat, 1.0f, 0}; + state.depth->samples = state.samples; + } + return state; + }; + const auto withStencil = attached(village, VK_FORMAT_D32_SFLOAT_S8_UINT, true); + { + const auto context = capable(); + const auto host = HostSampleLocationsFor(context, withStencil); + Require(host.grid.width == 1 && host.grid.height == 1 && host.order == rowSwap, "a 2x checkerboard on a device without a 2x2 grid that can reorder samples did not take the stand-in with its odd rows reordered"); + auto gridded = context; + gridded.sampleLocationGridSampleCounts = VK_SAMPLE_COUNT_2_BIT; + const auto native = HostSampleLocationsFor(gridded, withStencil); + Require(native.grid.width == 2 && native.grid.height == 2 && !ReordersSamples(native.order), "a 2x checkerboard on a device with a 2x2 grid was reordered"); + Require(!ReordersSamples(HostSampleLocationsFor(context, attached(uniform, VK_FORMAT_D32_SFLOAT, false)).order) && !ReordersSamples(HostSampleLocationsFor(context, attached(moved, VK_FORMAT_D32_SFLOAT, false)).order), "a uniform pattern, or one whose pixels hold other positions, was reordered"); + const auto refused = [&](Context device, const State& state, std::string_view reason, const std::string& what) { + Require(!ReordersSamples(HostSampleLocationsFor(device, state).order), what + " was reordered"); + Require(SampleOrderRefusal(device, state).find(reason) != std::string::npos, what + " was not refused for " + std::string(reason) + ": " + SampleOrderRefusal(device, state)); + }; + auto noStorage = context; + noStorage.storageImageMultisample = false; + refused(noStorage, withStencil, "has no storage view", "a color target on a device without multisampled storage images"); + auto noSampleShading = context; + noSampleShading.sampleRateShading = false; + refused(noSampleShading, withStencil, "sampleRateShading", "a device without sample-rate shading"); + orderStorageCounts = VK_SAMPLE_COUNT_4_BIT; + refused(context, withStencil, "has no storage view at 2 samples", "a color format without 2x storage"); + orderStorageCounts = VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT; + auto twentyFour = context; + twentyFour.physical = makeHandle(); + orderSixteenBitStencil = false; + refused(twentyFour, attached(village, VK_FORMAT_D16_UNORM_S8_UINT, true), "is not written back exactly", "16-bit depth with stencil kept as 24-bit depth"); + orderSixteenBitStencil = true; + auto sixteen = context; + sixteen.physical = makeHandle(); + Require(ReordersSamples(HostSampleLocationsFor(sixteen, attached(village, VK_FORMAT_D16_UNORM_S8_UINT, true)).order), "16-bit depth with stencil kept as 16-bit depth was not reordered"); + + using Method = SampleOrderMethod; + const auto steps = [](const std::vector& plan, std::initializer_list expected, const std::string& what) { + Require(plan.size() == expected.size(), what + " planned " + std::to_string(plan.size()) + " attachments"); + std::size_t index = 0; + for (const auto& step : expected) { + const auto& planned = plan[index++]; + Require(planned.attachment == step.attachment && planned.method == step.method && planned.toHost == step.toHost && planned.toGuest == step.toGuest, what + " planned attachment " + std::to_string(planned.attachment) + " differently"); + } + }; + steps(PlanSampleOrder(context, withStencil, rowSwap), {{0, Method::Storage, true, true}, {1, Method::DepthStencilExport, true, true}}, "a color and depth-stencil pass with stencil export"); + auto noExport = context; + noExport.shaderStencilExport = false; + steps(PlanSampleOrder(noExport, withStencil, rowSwap), {{0, Method::Storage, true, true}, {1, Method::DepthStencilBits, true, true}}, "a color and depth-stencil pass without stencil export"); + steps(PlanSampleOrder(context, attached(village, VK_FORMAT_D32_SFLOAT, false), rowSwap), {{0, Method::Storage, true, true}, {1, Method::Depth, true, true}}, "a color and depth pass"); + steps(PlanSampleOrder(context, attached(village, VK_FORMAT_D32_SFLOAT_S8_UINT, false), rowSwap), {{0, Method::Storage, true, true}, {1, Method::Depth, true, true}}, "a depth target whose host format has stencil but the guest's has none"); + steps(PlanSampleOrder(context, withStencil, rowSwap, VK_ATTACHMENT_LOAD_OP_CLEAR, VK_ATTACHMENT_STORE_OP_STORE), {{0, Method::Storage, false, true}, {1, Method::DepthStencilExport, false, true}}, "a pass that clears its attachments"); + steps(PlanSampleOrder(context, withStencil, rowSwap, VK_ATTACHMENT_LOAD_OP_LOAD, VK_ATTACHMENT_STORE_OP_DONT_CARE), {{0, Method::Storage, true, false}, {1, Method::DepthStencilExport, true, false}}, "a pass that discards its attachments"); + steps(PlanSampleOrder(context, withStencil, rowSwap, VK_ATTACHMENT_LOAD_OP_DONT_CARE, VK_ATTACHMENT_STORE_OP_DONT_CARE), {}, "a pass that neither loads nor stores"); + steps(PlanSampleOrder(context, withStencil, SampleOrder{}), {}, "a pass that keeps the guest's order"); + auto single = withStencil; + single.samples = 1; + single.colors[0].samples = 1; + single.depth->samples = 1; + steps(PlanSampleOrder(context, single, rowSwap), {}, "a single-sampled pass"); + } + + auto masked = attached(village, VK_FORMAT_UNDEFINED, false); + masked.quadSampleMask = {1, 1, 1, 1}; + Require(SampleMaskFollowsOrder(masked, rowSwap) && QuadSampleMasks(masked, rowSwap) == std::array{1, 1, 1, 1} && QuadSampleMasks(masked) == std::array{0xffffu, 0xffffu, 0xffffu, 0xffffu}, "a uniform mask of sample 0 did not move to the pixel shader under the checkerboard's order"); + auto empty = masked; + empty.quadSampleMask = {0, 0, 0, 0}; + Require(!SampleMaskFollowsOrder(empty, rowSwap) && !SampleMaskFollowsOrder(masked, SampleOrder{}), "an empty mask, or a mask without a reordering, moved to the pixel shader"); + + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.spirv = makeModule({}); + fragment.spirv = makeModule({.fragment = true}); + vertex.variantId = 1; + fragment.variantId = 2; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}}; + const std::array mainKey{41}; + const std::array pixelZero{{{0.75f, 0.5f}, {0.25f, 0.5f}}}; + for (const bool libraries : {false, true}) { + for (const bool maskedPass : {false, true}) { + mock = MockVulkan{}; + pipelineSampleLocations.clear(); + passSampleLocations.clear(); + pipelineSampleMasks.clear(); + auto context = capable(); + context.graphicsPipelineLibrary = libraries; + auto state = maskedPass ? masked : attached(village, VK_FORMAT_UNDEFINED, false); + const PipelineSetLayouts resources{makeHandle(), VK_NULL_HANDLE, false, mainKey, {}}; + std::size_t reorderedMasks = 0; + { + Pipeline pipeline(context, state, VertexInputLayout{}, resources, shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(pipeline.ReordersSamples() && pipeline.HostSampleOrder() == rowSwap, "a 2x checkerboard pipeline on a device without a 2x2 grid did not reorder its odd rows"); + reorderedMasks = pipelineSampleMasks.size(); + auto gridded = context; + gridded.sampleLocationGridSampleCounts = VK_SAMPLE_COUNT_2_BIT; + Pipeline native(gridded, state, VertexInputLayout{}, resources, shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(!native.ReordersSamples(), "a 2x checkerboard pipeline on a device with a 2x2 grid reordered its samples"); + } + ClearPipelineLibraries(context.device); + Require(mock.live == 0, "sample order pipeline tests leaked Vulkan objects"); + Require(!pipelineSampleLocations.empty(), "the reordered pipeline took no sample locations"); + const auto& standIn = pipelineSampleLocations.front(); + Require(standIn.grid.width == 1 && standIn.grid.height == 1 && standIn.locations.size() == 2 && standIn.locations[0].x == pixelZero[0].x && standIn.locations[1].x == pixelZero[1].x, "the reordered pipeline did not draw with pixel (0,0)'s locations"); + const auto keepsMask = [&](std::size_t begin, std::size_t end) { return std::any_of(pipelineSampleMasks.begin() + static_cast(begin), pipelineSampleMasks.begin() + static_cast(end), [](VkSampleMask mask) { return mask == 1u; }); }; + Require(reorderedMasks != 0 && !keepsMask(0, reorderedMasks), "the reordered pipeline kept a static sample mask its odd rows would apply to the other sample"); + Require(keepsMask(reorderedMasks, pipelineSampleMasks.size()) == maskedPass, "the pipeline on a device with a 2x2 grid did not keep the static sample mask exactly when the pass has one"); + } + } + mock = MockVulkan{}; + + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x20000u, {}, 0, {}}; + request.context.waveSize = 64; + request.context.pixel = ShaderRecompiler::ShaderPixelStageInfo{}; + const auto keptHash = ShaderRecompiler::RecompileCacheKey::ContextHash(request); + request.context.pixel->sampleOrder = rowSwap; + Require(ShaderRecompiler::RecompileCacheKey::ContextHash(request) != keptHash, "the pixel shader's sample order did not key its variant"); + const ShaderRecompiler::RequestSerializer serializer; + const auto back = serializer.Deserialize(serializer.Serialize(request)); + Require(back.request.context.pixel.has_value() && back.request.context.pixel->sampleOrder == rowSwap, "the recompile request lost the pixel shader's sample order"); +} + void pipelineLibrarySetTests() { using namespace AgcDriver::Graphics; for (const auto mask : {0u, 1u, 2u, 3u}) { @@ -7381,6 +7606,11 @@ int main(int argc, char** argv) { std::cout << "Sample location grid fallback checks passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--sample-order") { + sampleOrderTests(); + std::cout << "Sample order checks passed\n"; + return 0; + } { AgcDriver::Graphics::PipelineCacheSynchronization synchronization; auto pipelineUse = synchronization.LockForPipelineUse(); @@ -7514,6 +7744,7 @@ int main(int argc, char** argv) { framebufferLayerTests(); UnwrittenSampleLocationTests(); sampleLocationGridFallbackTests(); + sampleOrderTests(); quadPixelMaskTests(); ShaderStageTests(); NggVertexWorkgroupTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a3294a30fe..0798093f69 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3049,7 +3049,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQR4AAAA=", "new requests did not use serialization version 30"); + require(requestPrefix(encoded, 8u) == "NVNQQSAAAAA=", "new requests did not use serialization version 32"); const std::array fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -3167,7 +3167,7 @@ void verifyPixelRequestSerialization() { for (std::size_t bytes = 0; bytes < 45u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQR8AAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSEAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -3302,6 +3302,16 @@ void verifyVertexInfoVersion19() { require(allBitsOffset == flagOffset + 2u, "the front-face all-bits flag does not follow the dithered coverage flag"); require(blendOffset == allBitsOffset + 18u, "the reversed-order blend code is not serialized after the front-face all-bits flag, quad pixel mask and quad sample masks"); constexpr std::size_t sampleMaskBytes = 16u; + constexpr std::size_t sampleOrderBytes = 16u; + auto ordered = pixel; + ordered.context.pixel->sampleOrder = {0x76543210u, 0x76543210u, 0x76543201u, 0x76543201u}; + const auto orderedBytes = decodeBase64(serializer.Serialize(ordered)); + require(orderedBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + static_cast(blendOffset + 4u), orderedBytes.begin()) && orderedBytes[blendOffset + 4u] == 0x10u && orderedBytes[blendOffset + 15u] == 0x76u && orderedBytes[blendOffset + 16u] == 0x01u && std::equal(plainPixel.begin() + static_cast(blendOffset + 4u + sampleOrderBytes), plainPixel.end(), orderedBytes.begin() + static_cast(blendOffset + 4u + sampleOrderBytes)), "the sample order is not the four words the version 32 pixel info adds after the reversed-order blend code"); + auto version30 = orderedBytes; + version30.erase(version30.begin() + static_cast(blendOffset + 4u), version30.begin() + static_cast(blendOffset + 4u + sampleOrderBytes)); + version30[4] = 30u; + const auto replay30 = serializer.Deserialize(encodeBase64(version30)); + require(replay30.request.context.pixel.has_value() && replay30.request.context.pixel->sampleOrder == std::array{} && serializer.Serialize(replay30.request) == encodedPixel, "a version 30 request read a sample order or misread the fields after the reversed-order blend code"); auto maskedPixel = pixel; maskedPixel.context.pixel->quadPixelMask = 0x2u; const auto maskedBytes = decodeBase64(serializer.Serialize(maskedPixel)); @@ -3313,22 +3323,22 @@ void verifyVertexInfoVersion19() { const auto sampledBytes = decodeBase64(serializer.Serialize(sampledPixel)); require(std::equal(maskedBytes.begin(), maskedBytes.begin() + static_cast(maskOffset + 1u), sampledBytes.begin()) && sampledBytes[maskOffset + 1u] == 0x1u && sampledBytes[maskOffset + 5u] == 0x2u && sampledBytes[maskOffset + 9u] == 0x4u && sampledBytes[maskOffset + 13u] == 0x8u, "the quad sample masks are not the four words after the quad pixel mask"); auto version29 = sampledBytes; - version29.erase(version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u)); + version29.erase(version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + sampleOrderBytes)); version29[4] = 29u; const auto replay29 = serializer.Deserialize(encodeBase64(version29)); require(replay29.request.context.pixel.has_value() && replay29.request.context.pixel->quadSampleMask == std::array{0x1u, 0x2u, 0x4u, 0x8u} && replay29.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay29.request)) == sampledBytes, "a version 29 request read a reversed-order blend code or misread the fields after the quad sample masks"); auto version28 = sampledBytes; - version28.erase(version28.begin() + static_cast(maskOffset + 1u), version28.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u)); + version28.erase(version28.begin() + static_cast(maskOffset + 1u), version28.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + sampleOrderBytes)); version28[4] = 28u; const auto replay28 = serializer.Deserialize(encodeBase64(version28)); require(replay28.request.context.pixel.has_value() && replay28.request.context.pixel->quadPixelMask == 0x2u && replay28.request.context.pixel->quadSampleMask == std::array{0xffffu, 0xffffu, 0xffffu, 0xffffu} && replay28.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay28.request)) == maskedBytes, "a version 28 request read quad sample masks or a reversed-order blend code, or misread the fields after them"); auto version27 = maskedBytes; - version27.erase(version27.begin() + static_cast(maskOffset), version27.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u)); + version27.erase(version27.begin() + static_cast(maskOffset), version27.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + sampleOrderBytes)); version27[4] = 27u; const auto replay27 = serializer.Deserialize(encodeBase64(version27)); require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && replay27.request.context.pixel->reversedBlend == 0u && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or reversed-order blend code, or misread the fields after it"); auto legacyPixel = plainPixel; - legacyPixel.erase(legacyPixel.begin() + static_cast(allBitsOffset), legacyPixel.begin() + static_cast(allBitsOffset + 2u + sampleMaskBytes + 4u)); + legacyPixel.erase(legacyPixel.begin() + static_cast(allBitsOffset), legacyPixel.begin() + static_cast(allBitsOffset + 2u + sampleMaskBytes + 4u + sampleOrderBytes)); legacyPixel[4] = 26u; const auto replay26 = serializer.Deserialize(encodeBase64(legacyPixel)); require(replay26.request.context.pixel.has_value() && replay26.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replay26.request.context.pixel->frontFaceAllBits && replay26.request.context.pixel->reversedBlend == 0u, "a version 26 request lost its pixel primitive class or read a front-face all-bits flag"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleOrder.cpp new file mode 100644 index 0000000000..7a2d33251d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleOrder.cpp @@ -0,0 +1,1257 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include "SampleOrderTestFillDepth_spv.h" +#include "SampleOrderTestFillStencil_spv.h" +#include "SampleOrderTestFillVertex_spv.h" +#include "SampleOrderTestR16_spv.h" +#include "SampleOrderTestR32_spv.h" +#include "SampleOrderTestR8_spv.h" +#include "SampleOrderTestReadColor_spv.h" +#include "SampleOrderTestReadDepth_spv.h" +#include "SampleOrderTestReadStencil_spv.h" +#include "SampleOrderTestRG32_spv.h" +#include "SampleOrderTestRGBA32_spv.h" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Check; +using AgcDriver::Graphics::Context; +using AgcDriver::Graphics::KeptSampleOrder; +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::SampleOrder; +using AgcDriver::Graphics::SampleOrderMethod; +using AgcDriver::Graphics::SampleOrderPass; +using AgcDriver::Graphics::SampleOrderTarget; +using ShaderRecompiler::PixelInput; +using ShaderRecompiler::PixelInputBit; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t WaveSize = 64; +constexpr VkImageUsageFlags DepthUsage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; +constexpr VkImageUsageFlags ColorUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_STORAGE_BIT; + +SampleOrder OrderFor(std::uint32_t samples) { + if (samples == 2u) return {KeptSampleOrder, KeptSampleOrder, 0x76543201u, 0x76543201u}; + if (samples == 4u) return {KeptSampleOrder, 0x76542301u, 0x76540132u, 0x76541023u}; + return {0x01234567u, KeptSampleOrder, 0x10325476u, 0x45670123u}; +} + +std::uint32_t Pick(std::uint32_t word, std::uint32_t index) { + return (word >> (index * 4u)) & 7u; +} + +std::uint32_t Quad(std::uint32_t x, std::uint32_t y) { + return (x & 1u) | ((y & 1u) << 1u); +} + +std::string At(std::uint32_t x, std::uint32_t y, std::uint32_t sample) { + return "sample " + std::to_string(sample) + " of pixel (" + std::to_string(x) + ", " + std::to_string(y) + ")"; +} + +void FullBarrier(const Context& context, VkCommandBuffer commands) { + AgcDriver::Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); +} + +void HostBarrier(const Context& context, VkCommandBuffer commands) { + AgcDriver::Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); +} + +class Image { +public: + Image(const Context& context, VkFormat format, std::uint32_t samples, VkExtent2D extent, VkImageUsageFlags usage, VkImageCreateFlags flags) : context(context) { + try { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.flags = flags; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {extent.width, extent.height, 1u}; + info.mipLevels = 1u; + info.arrayLayers = 1u; + info.samples = static_cast(samples); + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = usage; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage sample order test"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory sample order test"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0u), "vkBindImageMemory sample order test"); + } catch (...) { + release(); + throw; + } + } + ~Image() { release(); } + Image(const Image&) = delete; + Image& operator=(const Image&) = delete; + VkImage Handle() const { return image; } + VkImageView View(VkFormat format, VkImageAspectFlags aspect, VkImageUsageFlags usage) { + VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + viewUsage.usage = usage; + VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + info.pNext = &viewUsage; + info.image = image; + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.format = format; + info.subresourceRange = {aspect, 0u, 1u, 0u, 1u}; + VkImageView view = VK_NULL_HANDLE; + Check(context.Function("vkCreateImageView")(context.device, &info, nullptr, &view), "vkCreateImageView sample order test"); + views.push_back(view); + return view; + } + void ToGeneral(VkCommandBuffer commands, VkImageAspectFlags aspect) const { + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange = {aspect, 0u, 1u, 0u, 1u}; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0u, 0u, nullptr, 0u, nullptr, 1u, &barrier); + } + +private: + void release() noexcept { + for (const auto view : views) context.Function("vkDestroyImageView")(context.device, view, nullptr); + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + } + Context context; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + std::vector views; +}; + +struct Binding { + std::uint32_t binding; + VkDescriptorType type; +}; + +class Program { +public: + Program(const Context& context, std::span compute, std::span bindings, std::uint32_t pushBytes, VkShaderStageFlags stages = VK_SHADER_STAGE_COMPUTE_BIT) : context(context), stages(stages), pushBytes(pushBytes) { + try { + std::vector layoutBindings; + std::vector sizes; + for (const auto& binding : bindings) { + layoutBindings.push_back({binding.binding, binding.type, 1u, stages, nullptr}); + sizes.push_back({binding.type, 1u}); + types.push_back(binding); + } + VkDescriptorSetLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + layoutInfo.bindingCount = static_cast(layoutBindings.size()); + layoutInfo.pBindings = layoutBindings.data(); + Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &layoutInfo, nullptr, &setLayout), "sample order test descriptor layout"); + VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + poolInfo.maxSets = 1u; + poolInfo.poolSizeCount = static_cast(sizes.size()); + poolInfo.pPoolSizes = sizes.data(); + Check(context.Function("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &pool), "sample order test descriptor pool"); + const VkDescriptorSetAllocateInfo allocate{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, nullptr, pool, 1u, &setLayout}; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocate, &set), "sample order test descriptor set"); + const VkPushConstantRange range{stages, 0u, pushBytes}; + VkPipelineLayoutCreateInfo layoutCreate{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutCreate.setLayoutCount = 1u; + layoutCreate.pSetLayouts = &setLayout; + layoutCreate.pushConstantRangeCount = 1u; + layoutCreate.pPushConstantRanges = ⦥ + Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutCreate, nullptr, &layout), "sample order test pipeline layout"); + if (compute.empty()) return; + const auto shader = Module(compute); + VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + info.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0u, VK_SHADER_STAGE_COMPUTE_BIT, shader, "main", nullptr}; + info.layout = layout; + const auto created = context.Function("vkCreateComputePipelines")(context.device, VK_NULL_HANDLE, 1u, &info, nullptr, &pipeline); + context.Function("vkDestroyShaderModule")(context.device, shader, nullptr); + Check(created, "sample order test compute pipeline"); + } catch (...) { + release(); + throw; + } + } + ~Program() { release(); } + Program(const Program&) = delete; + Program& operator=(const Program&) = delete; + + VkShaderModule Module(std::span code) const { + VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + info.codeSize = code.size_bytes(); + info.pCode = code.data(); + VkShaderModule module = VK_NULL_HANDLE; + Check(context.Function("vkCreateShaderModule")(context.device, &info, nullptr, &module), "sample order test shader module"); + return module; + } + void Image(std::uint32_t binding, VkImageView view) const { + const VkDescriptorImageInfo image{VK_NULL_HANDLE, view, VK_IMAGE_LAYOUT_GENERAL}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = set; + write.dstBinding = binding; + write.descriptorCount = 1u; + write.descriptorType = typeOf(binding); + write.pImageInfo = ℑ + context.Function("vkUpdateDescriptorSets")(context.device, 1u, &write, 0u, nullptr); + } + void Storage(std::uint32_t binding, VkBuffer buffer) const { + const VkDescriptorBufferInfo info{buffer, 0u, VK_WHOLE_SIZE}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = set; + write.dstBinding = binding; + write.descriptorCount = 1u; + write.descriptorType = typeOf(binding); + write.pBufferInfo = &info; + context.Function("vkUpdateDescriptorSets")(context.device, 1u, &write, 0u, nullptr); + } + void Bind(VkCommandBuffer commands, VkPipelineBindPoint point, const void* push) const { + context.Function("vkCmdBindDescriptorSets")(commands, point, layout, 0u, 1u, &set, 0u, nullptr); + context.Function("vkCmdPushConstants")(commands, layout, stages, 0u, pushBytes, push); + } + void Dispatch(VkCommandBuffer commands, const void* push, VkExtent2D extent) const { + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); + Bind(commands, VK_PIPELINE_BIND_POINT_COMPUTE, push); + context.Function("vkCmdDispatch")(commands, (extent.width + 7u) / 8u, (extent.height + 7u) / 8u, 1u); + } + VkPipelineLayout Layout() const { return layout; } + +private: + VkDescriptorType typeOf(std::uint32_t binding) const { + for (const auto& known : types) { + if (known.binding == binding) return known.type; + } + throw std::runtime_error("the sample order test program has no binding " + std::to_string(binding)); + } + void release() noexcept { + if (pipeline) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + if (layout) context.Function("vkDestroyPipelineLayout")(context.device, layout, nullptr); + if (pool) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); + if (setLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, setLayout, nullptr); + } + Context context; + VkShaderStageFlags stages; + std::uint32_t pushBytes; + std::vector types; + VkDescriptorSetLayout setLayout = VK_NULL_HANDLE; + VkDescriptorPool pool = VK_NULL_HANDLE; + VkDescriptorSet set = VK_NULL_HANDLE; + VkPipelineLayout layout = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; +}; + +class DepthFill { +public: + DepthFill(const Context& context, VkFormat format, std::uint32_t samples, VkExtent2D extent, VkImageView attachment, bool stencil, VkBuffer values) : context(context), extent(extent), samples(samples) { + const std::array bindings{{{0u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER}}}; + program = std::make_unique(context, std::span{}, bindings, 8u, VK_SHADER_STAGE_FRAGMENT_BIT); + program->Storage(0u, values); + try { + const bool hasStencil = AgcDriver::Graphics::HasStencil(format); + VkAttachmentDescription description{}; + description.format = format; + description.samples = static_cast(samples); + description.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + description.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + description.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + description.stencilStoreOp = hasStencil ? VK_ATTACHMENT_STORE_OP_STORE : VK_ATTACHMENT_STORE_OP_DONT_CARE; + description.initialLayout = VK_IMAGE_LAYOUT_GENERAL; + description.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + const VkAttachmentReference reference{0u, VK_IMAGE_LAYOUT_GENERAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.pDepthStencilAttachment = &reference; + VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + passInfo.attachmentCount = 1u; + passInfo.pAttachments = &description; + passInfo.subpassCount = 1u; + passInfo.pSubpasses = &subpass; + Check(context.Function("vkCreateRenderPass")(context.device, &passInfo, nullptr, &pass), "sample order test fill pass"); + VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebufferInfo.renderPass = pass; + framebufferInfo.attachmentCount = 1u; + framebufferInfo.pAttachments = &attachment; + framebufferInfo.width = extent.width; + framebufferInfo.height = extent.height; + framebufferInfo.layers = 1u; + Check(context.Function("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer), "sample order test fill framebuffer"); + const auto vertex = program->Module(SAMPLE_ORDER_TEST_FILLVERTEX_SPV); + const auto fragment = stencil ? program->Module(SAMPLE_ORDER_TEST_FILLSTENCIL_SPV) : program->Module(SAMPLE_ORDER_TEST_FILLDEPTH_SPV); + const std::array stages{{ + {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0u, VK_SHADER_STAGE_VERTEX_BIT, vertex, "main", nullptr}, + {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0u, VK_SHADER_STAGE_FRAGMENT_BIT, fragment, "main", nullptr}, + }}; + VkPipelineVertexInputStateCreateInfo input{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; + const VkViewport viewport{0.0f, 0.0f, static_cast(extent.width), static_cast(extent.height), 0.0f, 1.0f}; + const VkRect2D scissor{{0, 0}, extent}; + VkPipelineViewportStateCreateInfo viewports{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewports.viewportCount = 1u; + viewports.pViewports = &viewport; + viewports.scissorCount = 1u; + viewports.pScissors = &scissor; + 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 multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + multisample.rasterizationSamples = static_cast(samples); + multisample.sampleShadingEnable = VK_TRUE; + multisample.minSampleShading = 1.0f; + VkStencilOpState replace{}; + replace.failOp = VK_STENCIL_OP_REPLACE; + replace.passOp = VK_STENCIL_OP_REPLACE; + replace.depthFailOp = VK_STENCIL_OP_REPLACE; + replace.compareOp = VK_COMPARE_OP_ALWAYS; + replace.compareMask = 0xffu; + replace.writeMask = 0xffu; + VkPipelineDepthStencilStateCreateInfo depth{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; + depth.depthTestEnable = VK_TRUE; + depth.depthWriteEnable = VK_TRUE; + depth.depthCompareOp = VK_COMPARE_OP_ALWAYS; + depth.stencilTestEnable = stencil ? VK_TRUE : VK_FALSE; + depth.front = replace; + depth.back = replace; + VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + info.stageCount = static_cast(stages.size()); + info.pStages = stages.data(); + info.pVertexInputState = &input; + info.pInputAssemblyState = &assembly; + info.pViewportState = &viewports; + info.pRasterizationState = &raster; + info.pMultisampleState = &multisample; + info.pDepthStencilState = &depth; + info.pColorBlendState = &blend; + info.layout = program->Layout(); + info.renderPass = pass; + const auto created = context.Function("vkCreateGraphicsPipelines")(context.device, VK_NULL_HANDLE, 1u, &info, nullptr, &pipeline); + context.Function("vkDestroyShaderModule")(context.device, fragment, nullptr); + context.Function("vkDestroyShaderModule")(context.device, vertex, nullptr); + Check(created, "sample order test fill pipeline"); + } catch (...) { + release(); + throw; + } + } + ~DepthFill() { release(); } + DepthFill(const DepthFill&) = delete; + DepthFill& operator=(const DepthFill&) = delete; + void Run(VkCommandBuffer commands) const { + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = pass; + begin.framebuffer = framebuffer; + begin.renderArea = {{0, 0}, extent}; + context.Function("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + const std::array push{extent.width, samples}; + program->Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, push.data()); + context.Function("vkCmdDraw")(commands, 3u, 1u, 0u, 0u); + context.Function("vkCmdEndRenderPass")(commands); + } + +private: + void release() noexcept { + if (pipeline) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + if (framebuffer) context.Function("vkDestroyFramebuffer")(context.device, framebuffer, nullptr); + if (pass) context.Function("vkDestroyRenderPass")(context.device, pass, nullptr); + } + Context context; + VkExtent2D extent; + std::uint32_t samples; + std::unique_ptr program; + VkRenderPass pass = VK_NULL_HANDLE; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; +}; + +using Samples = std::vector>; + +template +void Submit(const Context& context, Record record) { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + AgcDriver::Graphics::CommandBatch batch(context); + record(batch.Handle()); + batch.SubmitAndWait(); +} + +Samples ReadValues(AgcDriver::Graphics::Buffer& buffer, std::size_t count) { + buffer.Invalidate(); + Samples values(count); + std::memcpy(values.data(), buffer.Bytes().data(), count * sizeof(values[0])); + return values; +} + +void CheckOrdered(const Samples& before, const Samples& after, VkExtent2D extent, VkExtent2D region, std::uint32_t samples, const SampleOrder& order, bool toHost, const std::string& what) { + std::uint32_t moved = 0; + for (std::uint32_t y = 0; y < extent.height; ++y) { + for (std::uint32_t x = 0; x < extent.width; ++x) { + const bool inside = x < region.width && y < region.height; + const auto word = order[Quad(x, y)]; + const auto map = toHost ? word : AgcDriver::Graphics::InvertSampleOrder(word); + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto base = (y * extent.width + x) * samples; + const auto source = inside ? Pick(map, sample) : sample; + if (after[base + sample] != before[base + source]) { + char held[96]; + std::snprintf(held, sizeof(held), " holds 0x%08x 0x%08x, expected 0x%08x 0x%08x (sample %u)", after[base + sample][0], after[base + sample][1], before[base + source][0], before[base + source][1], source); + throw std::runtime_error(what + ": " + At(x, y, sample) + held); + } + if (source != sample) ++moved; + } + } + } + Require(moved != 0, what + ": the order moved no sample"); +} + +const std::array, 5> ColorFormats{{ + {VK_FORMAT_R8_UNORM, 1u}, {VK_FORMAT_R16_SFLOAT, 2u}, {VK_FORMAT_R8G8B8A8_SRGB, 4u}, {VK_FORMAT_R16G16B16A16_SFLOAT, 8u}, {VK_FORMAT_R32G32B32A32_SFLOAT, 16u} +}}; + +std::span TexelCode(VkFormat alias) { + switch (alias) { + case VK_FORMAT_R8_UINT: return SAMPLE_ORDER_TEST_R8_SPV; + case VK_FORMAT_R16_UINT: return SAMPLE_ORDER_TEST_R16_SPV; + case VK_FORMAT_R32_UINT: return SAMPLE_ORDER_TEST_R32_SPV; + case VK_FORMAT_R32G32_UINT: return SAMPLE_ORDER_TEST_RG32_SPV; + case VK_FORMAT_R32G32B32A32_UINT: return SAMPLE_ORDER_TEST_RGBA32_SPV; + default: throw std::runtime_error("the sample order test has no texel program for this alias"); + } +} + +std::uint32_t AliasMask(std::uint32_t elementBytes, std::uint32_t component) { + if (elementBytes == 1u) return component == 0u ? 0xffu : 0u; + if (elementBytes == 2u) return component == 0u ? 0xffffu : 0u; + if (elementBytes == 4u) return component == 0u ? ~0u : 0u; + if (elementBytes == 8u) return component < 2u ? ~0u : 0u; + return ~0u; +} + +Samples Masked(Samples values, std::uint32_t elementBytes) { + for (auto& value : values) { + for (std::uint32_t component = 0; component < 4u; ++component) value[component] &= AliasMask(elementBytes, component); + } + return values; +} + +constexpr VkExtent2D SwapExtent{20u, 12u}; +constexpr VkExtent2D SwapRegion{16u, 10u}; + +bool ColorSwaps(const Context& context, VkFormat format, std::uint32_t elementBytes, std::uint32_t samples) { + const auto alias = AgcDriver::Graphics::SampleOrderStorageFormat(context, format, elementBytes, samples); + const auto name = "the " + std::to_string(samples) + "x color format " + std::to_string(format); + if (alias == VK_FORMAT_UNDEFINED) { + std::printf("%s has no %u-sample storage alias here, not swapped\n", name.c_str(), samples); + return false; + } + const auto order = OrderFor(samples); + Image image(context, format, samples, SwapExtent, ColorUsage, VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT); + const auto view = image.View(alias, VK_IMAGE_ASPECT_COLOR_BIT, VK_IMAGE_USAGE_STORAGE_BIT); + const auto count = static_cast(SwapExtent.width) * SwapExtent.height * samples; + AgcDriver::Graphics::Buffer values(context, count * 16u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Samples before(count); + for (std::size_t index = 0; index < count; ++index) { + for (std::uint32_t component = 0; component < 4u; ++component) before[index][component] = static_cast(index * 0x9e3779b1u + component * 0x85ebca6bu + 0x1000u * (index % samples)) & AliasMask(elementBytes, component); + before[index][0] = (before[index][0] & ~7u & AliasMask(elementBytes, 0u)) | static_cast(index % samples); + } + std::memcpy(values.Bytes().data(), before.data(), count * sizeof(before[0])); + const std::array bindings{{{0u, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}, {1u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER}}}; + Program texels(context, TexelCode(alias), bindings, 16u); + texels.Image(0u, view); + texels.Storage(1u, values.Handle()); + const std::array store{SwapExtent.width, SwapExtent.height, samples, 1u}; + const std::array load{SwapExtent.width, SwapExtent.height, samples, 0u}; + const SampleOrderTarget target{image.Handle(), view, alias, SwapExtent, samples, SampleOrderMethod::Storage, nullptr}; + const SampleOrderPass pass{order, SwapRegion, {}, false}; + std::vector> kept; + Submit(context, [&](VkCommandBuffer commands) { + image.ToGeneral(commands, VK_IMAGE_ASPECT_COLOR_BIT); + texels.Dispatch(commands, store.data(), SwapExtent); + FullBarrier(context, commands); + texels.Dispatch(commands, load.data(), SwapExtent); + HostBarrier(context, commands); + }); + Require(Masked(ReadValues(values, count), elementBytes) == before, name + ": the stored samples did not read back"); + Submit(context, [&](VkCommandBuffer commands) { + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, std::span(&target, 1u), true, kept); + FullBarrier(context, commands); + texels.Dispatch(commands, load.data(), SwapExtent); + HostBarrier(context, commands); + }); + CheckOrdered(before, Masked(ReadValues(values, count), elementBytes), SwapExtent, SwapRegion, samples, order, true, name + " put in the host's order"); + Submit(context, [&](VkCommandBuffer commands) { + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, std::span(&target, 1u), false, kept); + FullBarrier(context, commands); + texels.Dispatch(commands, load.data(), SwapExtent); + HostBarrier(context, commands); + }); + Require(Masked(ReadValues(values, count), elementBytes) == before, name + ": putting the samples back in the guest's order did not restore every sample bit for bit"); + std::printf("%s round-trips through its %u-byte storage alias\n", name.c_str(), elementBytes); + return true; +} + +bool DepthSampleable(const Context& context, VkFormat format, std::uint32_t samples) { + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + constexpr VkFormatFeatureFlags needed = VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; + if ((properties.optimalTilingFeatures & needed) != needed) return false; + VkImageFormatProperties supported{}; + return context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, DepthUsage, 0u, &supported) == VK_SUCCESS && (supported.sampleCounts & samples) != 0u; +} + +std::string MethodName(SampleOrderMethod method) { + switch (method) { + case SampleOrderMethod::Depth: return "a depth pass"; + case SampleOrderMethod::DepthStencilExport: return "a depth pass exporting stencil"; + case SampleOrderMethod::DepthStencilBits: return "a depth pass and eight stencil bit passes"; + default: return "storage"; + } +} + +bool DepthSwaps(const Context& context, VkFormat format, std::uint32_t samples, SampleOrderMethod method) { + const bool stencil = method != SampleOrderMethod::Depth; + const auto name = "the " + std::to_string(samples) + "x depth format " + std::to_string(format) + " through " + MethodName(method); + if (!DepthSampleable(context, format, samples)) { + std::printf("%s: the format cannot be sampled at this sample count here, not swapped\n", name.c_str()); + return false; + } + if (stencil && !context.shaderStencilExport) { + std::printf("%s: the device has no stencil export to fill the test's stencil, not swapped\n", name.c_str()); + return false; + } + const auto order = OrderFor(samples); + const bool hasStencil = AgcDriver::Graphics::HasStencil(format); + const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (hasStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); + Image image(context, format, samples, SwapExtent, DepthUsage, 0u); + const auto attachment = image.View(format, aspects, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); + const auto depthView = image.View(format, VK_IMAGE_ASPECT_DEPTH_BIT, VK_IMAGE_USAGE_SAMPLED_BIT); + const auto stencilView = stencil ? image.View(format, VK_IMAGE_ASPECT_STENCIL_BIT, VK_IMAGE_USAGE_SAMPLED_BIT) : VK_NULL_HANDLE; + const auto count = static_cast(SwapExtent.width) * SwapExtent.height * samples; + AgcDriver::Graphics::Buffer fill(context, count * 8u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + AgcDriver::Graphics::Buffer values(context, count * 16u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + std::vector> filled(count); + const bool unorm16 = format == VK_FORMAT_D16_UNORM || format == VK_FORMAT_D16_UNORM_S8_UINT; + for (std::size_t index = 0; index < count; ++index) { + const auto key = static_cast(index + 1u); + filled[index] = {std::bit_cast(unorm16 ? static_cast(key) / 65535.0f : static_cast(key) / 4096.0f), (key * 37u) & 0xffu}; + } + std::memcpy(fill.Bytes().data(), filled.data(), count * sizeof(filled[0])); + DepthFill raster(context, format, samples, SwapExtent, attachment, stencil, fill.Handle()); + const std::array bindings{{{0u, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, {1u, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, {2u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER}}}; + Program read(context, stencil ? std::span(SAMPLE_ORDER_TEST_READSTENCIL_SPV) : std::span(SAMPLE_ORDER_TEST_READDEPTH_SPV), bindings, 12u); + read.Image(0u, depthView); + if (stencil) read.Image(1u, stencilView); + read.Storage(2u, values.Handle()); + const std::array push{SwapExtent.width, SwapExtent.height, samples}; + const SampleOrderTarget target{image.Handle(), attachment, format, SwapExtent, samples, method, nullptr}; + const SampleOrderPass pass{order, SwapRegion, {}, false}; + std::vector> kept; + Submit(context, [&](VkCommandBuffer commands) { + image.ToGeneral(commands, aspects); + raster.Run(commands); + FullBarrier(context, commands); + read.Dispatch(commands, push.data(), SwapExtent); + HostBarrier(context, commands); + }); + const auto before = ReadValues(values, count); + for (std::size_t index = 0; index < count; ++index) { + const auto key = static_cast(index + 1u); + const auto depth = std::bit_cast(before[index][0]); + const bool depthHeld = unorm16 ? static_cast(depth * 65535.0f + 0.5f) == key : before[index][0] == filled[index][0]; + Require(depthHeld && (!stencil || before[index][1] == filled[index][1]), name + ": the filled sample " + std::to_string(index) + " did not read back"); + } + Submit(context, [&](VkCommandBuffer commands) { + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, std::span(&target, 1u), true, kept); + FullBarrier(context, commands); + read.Dispatch(commands, push.data(), SwapExtent); + HostBarrier(context, commands); + }); + CheckOrdered(before, ReadValues(values, count), SwapExtent, SwapRegion, samples, order, true, name + " put in the host's order"); + Submit(context, [&](VkCommandBuffer commands) { + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, std::span(&target, 1u), false, kept); + FullBarrier(context, commands); + read.Dispatch(commands, push.data(), SwapExtent); + HostBarrier(context, commands); + }); + Require(ReadValues(values, count) == before, name + ": putting the samples back in the guest's order did not restore every sample bit for bit"); + std::printf("%s round-trips\n", name.c_str()); + return true; +} + +std::vector SwapSampleCounts(const Context& context) { + std::vector counts; + for (const std::uint32_t samples : {2u, 4u, 8u}) { + if ((context.limits.framebufferColorSampleCounts & samples) != 0u && (context.limits.framebufferDepthSampleCounts & samples) != 0u) counts.push_back(samples); + } + return counts; +} + +std::uint32_t SwapPasses(const Context& context) { + std::uint32_t swapped = 0; + for (const auto samples : SwapSampleCounts(context)) { + for (const auto& [format, bytes] : ColorFormats) swapped += ColorSwaps(context, format, bytes, samples) ? 1u : 0u; + swapped += DepthSwaps(context, VK_FORMAT_D32_SFLOAT, samples, SampleOrderMethod::Depth) ? 1u : 0u; + swapped += DepthSwaps(context, VK_FORMAT_D16_UNORM, samples, SampleOrderMethod::Depth) ? 1u : 0u; + for (const auto format : {VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D16_UNORM_S8_UINT}) { + swapped += DepthSwaps(context, format, samples, SampleOrderMethod::Depth) ? 1u : 0u; + swapped += DepthSwaps(context, format, samples, SampleOrderMethod::DepthStencilExport) ? 1u : 0u; + swapped += DepthSwaps(context, format, samples, SampleOrderMethod::DepthStencilBits) ? 1u : 0u; + } + } + return swapped; +} + +using Vertices = std::vector>; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array NullExportCode{0xf8001890, 0x00000000, 0xbf810000}; + +constexpr std::uint32_t RemapWidth = 64; +constexpr std::uint32_t RemapHeight = 8; +alignas(256) std::array Invocations{}; +alignas(256) std::array, 4> StoreCodes{}; +alignas(256) std::array, 8> MaskCodes{}; +alignas(256) constexpr std::array SampleColorCode{0xd5480007, 0x02111106, 0x7e0e0d07, 0xf800180f, 0x07070100, 0xbf810000, 0xbf810000}; +alignas(256) std::array DepthMemory{}; + +std::span StoreCode(std::uint32_t samples) { + const auto shift = static_cast(std::countr_zero(samples)); + auto& code = StoreCodes[shift]; + code = {0x7e100f04, 0x7e120f05, 0x34121286, 0x4a101308, 0x34101080u | shift, 0xd548000a, 0x02111106, 0x4a101508, 0x34101082, 0x7e160281, 0xe0705000, 0x80000b08, 0xbf8c3f70, 0xf8001890, 0x00000000, 0xbf810000, 0xbf810000}; + return code; +} + +std::span MaskCode(std::size_t slot, std::uint32_t mask) { + auto& code = MaskCodes[slot]; + code = {0x7e1002ffu, mask, 0xf8001884u, 0x00080000u, 0xbf810000u}; + return code; +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, stride == 0u ? 0x31016facu : 0x01016facu}; +} + +struct Pass { + const Vertices* vertices = nullptr; + std::span code; + bool perSample = false; + std::uint32_t shaderControl = 0u; + std::uint32_t colorFormat = 0u; + std::uint32_t samples = 4u; + VkExtent2D extent{RemapWidth, RemapHeight}; + std::optional depth; + bool depthWrite = false; + VkCompareOp depthCompare = VK_COMPARE_OP_ALWAYS; + std::optional color; + std::optional> locations; + std::optional order; + bool storesInvocations = false; +}; + +void Draw(AgcDriver::VulkanDevice& device, const Context& context, const Pass& pass) { + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(pass.vertices->data(), 16u, static_cast(pass.vertices->size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.hasColorTarget = pass.color.has_value(); + if (pass.color) { + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + state.colors = {*pass.color}; + state.color = *pass.color; + state.blends = {blend}; + state.blend = blend; + } + state.renderExtent = pass.extent; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(pass.extent.height), static_cast(pass.extent.width), -static_cast(pass.extent.height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, pass.extent}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blendConstants = {}; + state.samples = pass.samples; + state.perSampleShading = pass.perSample; + state.pixelEarlyZ = AgcDriver::Graphics::PixelEarlyZ(pass.shaderControl); + if (pass.locations) { + state.customSampleLocations = true; + state.sampleLocations = *pass.locations; + for (std::uint32_t pixel = 1; pixel < 4u; ++pixel) { + for (std::uint32_t sample = 0; sample < pass.samples; ++sample) state.sampleLocationGrid |= state.sampleLocations[pixel * 8u + sample] != state.sampleLocations[sample]; + } + } + if (pass.depth) { + state.depth = pass.depth; + state.depthTest = true; + state.depthWrite = pass.depthWrite; + state.depthCompare = pass.depthCompare; + } + + const std::uint32_t inputs = pass.perSample ? PixelInputBit(PixelInput::PerspectiveSample) | PixelInputBit(PixelInput::LinearSample) | PixelInputBit(PixelInput::PositionX) | PixelInputBit(PixelInput::PositionY) | PixelInputBit(PixelInput::Ancillary) : 0u; + const AgcDriver::Registers registers{{0x1b3, inputs}, {0x1b4, inputs}, {0x1b6, 0u}, {0x1c5, pass.colorFormat}, {0x203, pass.shaderControl}}; + auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(registers, AgcDriver::Graphics::ExportMappings(state)); + Require(pixel.sampleShading == pass.perSample, "the pixel inputs did not select the requested shading rate"); + const auto order = pass.order ? *pass.order : state.sampleLocationGrid ? AgcDriver::Graphics::HostSampleLocationsFor(context, state).order : SampleOrder{}; + pixel.sampleOrder = order; + pixel.quadPixelMask = AgcDriver::Graphics::QuadPixelMask(state); + pixel.quadSampleMask = AgcDriver::Graphics::QuadSampleMasks(state, order); + std::vector pixelUserData(4, 0u); + if (pass.storesInvocations) { + const auto invocations = BufferDescriptor(Invocations.data(), 0u, static_cast(sizeof(Invocations))); + std::copy(invocations.begin(), invocations.end(), pixelUserData.begin()); + } + const std::array pixelMemory{{{reinterpret_cast(pass.code.data()), std::as_bytes(pass.code)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pass.code.data()), pass.code, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + std::vector shaders{{ShaderStage::Vertex, &vertexResult, 0}}; + ShaderRecompiler::RecompileResult geometry; + if (pixelResult.barycentricEmulation.active) { + geometry = ShaderRecompiler::BuildBarycentricGeometryShader(vertexResult, pixelResult, target, device.GeometryLimits()); + shaders.push_back({ShaderStage::Geometry, &geometry, 0}); + } + shaders.push_back({ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}); + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(pass.vertices->size()), 0, 1, 0, false}; + AgcDriver::Graphics::Draw(context, state, draw, shaders); +} + +void Settle(AgcDriver::VulkanDevice& device) { + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + } + device.WaitIdle(); +} + +Vertices Rectangle(float left, float right, float z) { + return {{left, -1.0f, z, 1.0f}, {right, -1.0f, z, 1.0f}, {right, 1.0f, z, 1.0f}, {left, -1.0f, z, 1.0f}, {right, 1.0f, z, 1.0f}, {left, 1.0f, z, 1.0f}}; +} + +Vertices Screen(float z) { + return {{-1.0f, -1.0f, z, 1.0f}, {3.0f, -1.0f, z, 1.0f}, {-1.0f, 3.0f, z, 1.0f}}; +} + +float StandardX(std::uint32_t samples, std::uint32_t sample) { + static constexpr std::array two{0.75f, 0.25f}; + static constexpr std::array four{0.375f, 0.875f, 0.125f, 0.625f}; + static constexpr std::array eight{0.5625f, 0.4375f, 0.8125f, 0.3125f, 0.1875f, 0.0625f, 0.6875f, 0.9375f}; + return samples == 2u ? two[sample] : samples == 4u ? four[sample] : eight[sample]; +} + +void CheckStored(std::uint32_t samples, const std::string& what, const std::function& expected) { + for (std::uint32_t y = 0; y < RemapHeight; ++y) { + for (std::uint32_t x = 0; x < RemapWidth; ++x) { + const auto mask = expected(x, y); + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto stored = Invocations[(y * RemapWidth + x) * samples + sample]; + Require(stored == ((mask >> sample) & 1u), what + ": guest " + At(x, y, sample) + (stored != 0u ? " ran" : " did not run") + ", expected guest mask " + std::to_string(mask)); + } + } + } +} + +std::uint32_t GuestBits(std::uint32_t hostBits, std::uint32_t word, std::uint32_t samples) { + std::uint32_t guest = 0; + for (std::uint32_t host = 0; host < samples; ++host) { + if (((hostBits >> host) & 1u) != 0u) guest |= 1u << Pick(word, host); + } + return guest; +} + +void SampleIndexRemap(AgcDriver::VulkanDevice& device, const Context& context, std::uint32_t samples) { + const auto order = OrderFor(samples); + const auto toNdc = [](float pixel) { return pixel * 2.0f / static_cast(RemapWidth) - 1.0f; }; + const auto rectangle = Rectangle(toNdc(2.5f), toNdc(5.5f), 0.5f); + for (const bool remapped : {false, true}) { + Invocations.fill(0u); + Pass pass; + pass.vertices = &rectangle; + pass.code = StoreCode(samples); + pass.perSample = true; + pass.samples = samples; + pass.order = remapped ? order : SampleOrder{}; + pass.storesInvocations = true; + Draw(device, context, pass); + Settle(device); + CheckStored(samples, std::to_string(samples) + "x SampleID " + (remapped ? "under the test's sample order" : "in the host's order"), [&](std::uint32_t x, std::uint32_t y) { + std::uint32_t host = 0; + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto at = static_cast(x) + StandardX(samples, sample); + if (at > 2.5f && at < 5.5f) host |= 1u << sample; + } + return remapped ? GuestBits(host, order[Quad(x, y)], samples) : host; + }); + } + std::printf("%ux SampleID reads the guest's sample index in all four pixels of the 2x2 grid\n", samples); +} + +void CoverageRemap(AgcDriver::VulkanDevice& device, const Context& context, std::uint32_t samples) { + const auto order = OrderFor(samples); + const auto back = Screen(0.25f), middle = Screen(0.5f), probe = Screen(0.375f); + const AgcDriver::Graphics::DepthTarget depth = [&] { + AgcDriver::Graphics::DepthTarget target{reinterpret_cast(DepthMemory.data()) + 256u * 1024u * static_cast(std::countr_zero(samples)), 0u, {RemapWidth, RemapHeight}, VK_FORMAT_D32_SFLOAT, 0.0f, 0}; + target.samples = samples; + return target; + }(); + const std::array masks{1u, samples == 2u ? 2u : samples == 4u ? 6u : 0x96u}; + std::size_t slot = 0; + for (const auto mask : masks) { + Pass reset; + reset.vertices = &back; + reset.code = NullExportCode; + reset.samples = samples; + reset.depth = depth; + reset.depthWrite = true; + Draw(device, context, reset); + Pass masked = reset; + masked.vertices = &middle; + masked.code = MaskCode(slot++ + (samples == 2u ? 0u : samples == 4u ? 2u : 4u), mask); + masked.shaderControl = 0x100u; + masked.order = order; + Draw(device, context, masked); + Settle(device); + Invocations.fill(0u); + Pass check; + check.vertices = &probe; + check.code = StoreCode(samples); + check.perSample = true; + check.shaderControl = 0x10u; + check.samples = samples; + check.depth = depth; + check.depthCompare = VK_COMPARE_OP_LESS; + check.order = order; + check.storesInvocations = true; + Draw(device, context, check); + Settle(device); + CheckStored(samples, std::to_string(samples) + "x samples a pixel shader's exported guest mask " + std::to_string(mask) + " wrote depth into", [&](std::uint32_t, std::uint32_t) { return mask; }); + } + std::printf("%ux exported sample masks reach the host samples at the guest samples' locations\n", samples); +} + +std::uint32_t ShaderRemaps(AgcDriver::VulkanDevice& device, const Context& context) { + std::uint32_t checked = 0; + for (const std::uint32_t samples : {2u, 4u, 8u}) { + if ((context.limits.framebufferDepthSampleCounts & samples) == 0u || (context.limits.framebufferNoAttachmentsSampleCounts & samples) == 0u) continue; + if (context.limits.standardSampleLocations) { + SampleIndexRemap(device, context, samples); + ++checked; + } else { + std::printf("skipped the %ux SampleID check: the device has no standard sample locations\n", samples); + } + CoverageRemap(device, context, samples); + ++checked; + } + return checked; +} + +constexpr std::size_t BlockBytes = 65536; + +class GuestBlock { +public: + explicit GuestBlock(std::size_t bytes) : bytes((bytes + BlockBytes - 1u) & ~(BlockBytes - 1u)) { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, this->bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, this->bytes)); +#endif + Require(block != nullptr, "sample order test: cannot allocate a guest block"); + std::memset(block, 0, this->bytes); + GuestAllocations::Mutation().Add(block, this->bytes, true, true, true); + } + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::size_t bytes; + std::byte* block = nullptr; +}; + +GuestBlock& NewBlock(std::size_t bytes) { + static auto* blocks = new std::deque(); + return blocks->emplace_back(bytes); +} + +constexpr VkExtent2D ReferenceExtent{64u, 32u}; +constexpr std::array VillageLocations{0x04, 0x0c, 0, 0, 0, 0, 0, 0, 0x04, 0x0c, 0, 0, 0, 0, 0, 0, 0x0c, 0x04, 0, 0, 0, 0, 0, 0, 0x0c, 0x04, 0, 0, 0, 0, 0, 0}; +constexpr float ClearDepth = 0.9375f; +constexpr std::size_t SceneSlots = 6u; + +struct Rendered { + Samples color; + Samples depth; +}; + +Samples ReadTarget(const Context& context, VkImageView view, bool color, std::uint32_t samples) { + const auto count = static_cast(ReferenceExtent.width) * ReferenceExtent.height * samples; + AgcDriver::Graphics::Buffer values(context, count * 16u, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const std::array bindings{{{0u, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, {2u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER}}}; + Program read(context, color ? std::span(SAMPLE_ORDER_TEST_READCOLOR_SPV) : std::span(SAMPLE_ORDER_TEST_READDEPTH_SPV), bindings, 12u); + read.Image(0u, view); + read.Storage(2u, values.Handle()); + const std::array push{ReferenceExtent.width, ReferenceExtent.height, samples}; + Submit(context, [&](VkCommandBuffer commands) { + FullBarrier(context, commands); + read.Dispatch(commands, push.data(), ReferenceExtent); + HostBarrier(context, commands); + }); + return ReadValues(values, count); +} + +Rendered RenderCheckerboard(AgcDriver::VulkanDevice& device, const Context& context, std::uint32_t samples, std::size_t run, const std::optional>& locations) { + AgcDriver::Graphics::ColorTarget color{}; + color.extent = ReferenceExtent; + color.surfaceExtent = ReferenceExtent; + color.format = VK_FORMAT_R16G16B16A16_SFLOAT; + color.componentMapping = 0xe4u; + color.tileMode = AgcDriver::Graphics::ColorTileMode::RenderTarget; + color.elementBytes = 8u; + color.samples = samples; + color.bytes = AgcDriver::Graphics::ColorTargetLayout(ReferenceExtent.width, ReferenceExtent.height, color.tileMode, color.elementBytes).Bytes(); + auto& block = NewBlock(AgcDriver::Graphics::MultisampledColorBytes(color)); + color.address = block.Address(); + color.surfaceAddress = color.address; + AgcDriver::Graphics::DepthTarget depth{reinterpret_cast(DepthMemory.data()) + 256u * 1024u * run, 0u, ReferenceExtent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + depth.samples = samples; + const Vertices clear = Screen(ClearDepth); + const Vertices slopes{{-1.0f, -1.0f, 0.125f, 1.0f}, {1.0f, -0.8125f, 0.875f, 1.0f}, {-0.5625f, 1.0f, 0.5f, 1.0f}, {1.0f, -1.0f, 0.75f, 1.0f}, {0.6875f, 1.0f, 0.0625f, 1.0f}, {-1.0f, 0.4375f, 0.3125f, 1.0f}}; + const Vertices shade{{-0.9375f, -0.875f, 0.25f, 1.0f}, {0.9375f, -0.625f, 0.6875f, 1.0f}, {0.0625f, 0.9375f, 0.4375f, 1.0f}}; + Pass depthOnly; + depthOnly.vertices = &clear; + depthOnly.code = NullExportCode; + depthOnly.samples = samples; + depthOnly.extent = ReferenceExtent; + depthOnly.depth = depth; + depthOnly.depthWrite = true; + depthOnly.locations = locations; + Draw(device, context, depthOnly); + depthOnly.vertices = &slopes; + depthOnly.depthCompare = VK_COMPARE_OP_LESS; + Draw(device, context, depthOnly); + Pass colorPass; + colorPass.vertices = &clear; + colorPass.code = SampleColorCode; + colorPass.perSample = true; + colorPass.colorFormat = 9u; + colorPass.samples = samples; + colorPass.extent = ReferenceExtent; + colorPass.depth = depth; + colorPass.depthCompare = VK_COMPARE_OP_ALWAYS; + colorPass.color = color; + colorPass.locations = locations; + Draw(device, context, colorPass); + colorPass.vertices = &shade; + colorPass.depthCompare = VK_COMPARE_OP_LESS_OR_EQUAL; + Draw(device, context, colorPass); + Settle(device); + Rendered rendered; + rendered.color = ReadTarget(context, AgcDriver::Graphics::MultisampleTargetView(context, color), true, samples); + rendered.depth = ReadTarget(context, AgcDriver::Graphics::DepthSurfaceOrderTarget(context, depth).view, false, samples); + return rendered; +} + +std::size_t Differences(const Samples& reference, const Samples& rendered, std::uint32_t samples, const std::string& what, bool report) { + std::size_t differences = 0; + for (std::size_t index = 0; index < reference.size(); ++index) { + if (reference[index] == rendered[index]) continue; + if (report && differences < 4u) { + const auto pixel = static_cast(index / samples); + std::printf("%s: %s is 0x%08x 0x%08x 0x%08x 0x%08x, the reference's is 0x%08x 0x%08x 0x%08x 0x%08x\n", what.c_str(), At(pixel % ReferenceExtent.width, pixel / ReferenceExtent.width, static_cast(index % samples)).c_str(), rendered[index][0], rendered[index][1], rendered[index][2], rendered[index][3], reference[index][0], reference[index][1], reference[index][2], reference[index][3]); + } + ++differences; + } + return differences; +} + +struct ClearTargets { + AgcDriver::VulkanDevice& device; + VkDevice handle; + ~ClearTargets() { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + device.WaitIdle(); + AgcDriver::Graphics::ClearMultisampleTargets(handle); + AgcDriver::Graphics::ClearCachedTextures(handle); + } +}; + +void CheckScene(const Rendered& rendered, std::uint32_t samples, const std::string& what) { + std::size_t cleared = 0; + for (std::size_t index = 0; index < rendered.depth.size(); ++index) { + const auto sample = static_cast(index % samples); + const auto pixel = static_cast(index / samples); + const auto at = At(pixel % ReferenceExtent.width, pixel / ReferenceExtent.width, sample); + const auto id = static_cast(sample); + Require(std::bit_cast(rendered.color[index][2]) == id && std::bit_cast(rendered.color[index][3]) == id, what + ": " + at + " does not hold its own sample index in color"); + const auto depth = std::bit_cast(rendered.depth[index][0]); + Require(depth >= 0.0f && depth <= ClearDepth, what + ": " + at + " holds depth " + std::to_string(depth)); + if (depth == ClearDepth) ++cleared; + } + Require(cleared != 0u && cleared != rendered.depth.size(), what + ": the slopes did not leave depth both at and below the clear value"); +} + +std::uint32_t SceneChecks(AgcDriver::VulkanDevice& device, const Context& context) { + ClearTargets clearTargets{device, context.device}; + std::uint32_t checked = 0; + for (const std::uint32_t samples : {2u, 4u, 8u}) { + if ((context.limits.framebufferColorSampleCounts & samples) == 0u || (context.limits.framebufferDepthSampleCounts & samples) == 0u || !DepthSampleable(context, VK_FORMAT_D32_SFLOAT, samples)) continue; + CheckScene(RenderCheckerboard(device, context, samples, SceneSlots + static_cast(std::countr_zero(samples)), std::nullopt), samples, std::to_string(samples) + "x scene with the standard locations"); + ++checked; + } + return checked; +} + +std::array UniformLocations(const std::array& pattern, std::uint32_t pixel) { + std::array uniform{}; + for (std::uint32_t target = 0; target < 4u; ++target) std::copy_n(pattern.begin() + 8u * pixel, 8u, uniform.begin() + 8u * target); + return uniform; +} + +Rendered QuadComposite(AgcDriver::VulkanDevice& device, const Context& context, std::uint32_t samples, std::size_t run, const std::array& pattern) { + std::array parts; + for (std::uint32_t pixel = 0; pixel < parts.size(); ++pixel) parts[pixel] = RenderCheckerboard(device, context, samples, run + pixel, UniformLocations(pattern, pixel)); + auto composite = parts[0]; + for (std::size_t index = 0; index < composite.color.size(); ++index) { + const auto pixel = static_cast(index / samples); + const auto& part = parts[Quad(pixel % ReferenceExtent.width, pixel / ReferenceExtent.width)]; + composite.color[index] = part.color[index]; + composite.depth[index] = part.depth[index]; + } + return composite; +} + +int GridReference(AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + ClearTargets clearTargets{device, context.device}; + constexpr std::uint32_t samples = 2u; + if ((context.limits.framebufferColorSampleCounts & samples) == 0u || (context.limits.framebufferDepthSampleCounts & samples) == 0u) { + std::printf("skipped, the device has no 2x color and depth targets\n"); + return VulkanTestSkipped; + } + if ((context.sampleLocationSampleCounts & samples) == 0u) { + std::printf("skipped, the device has no custom sample locations at 2 samples\n"); + return VulkanTestSkipped; + } + auto emulated = context; + emulated.sampleLocationGridSampleCounts = 0u; + auto fallback = emulated; + fallback.storageImageMultisample = false; + AgcDriver::Graphics::State probe{}; + probe.samples = samples; + probe.customSampleLocations = true; + probe.sampleLocationGrid = true; + probe.sampleLocations = VillageLocations; + AgcDriver::Graphics::ColorTarget color{}; + color.format = VK_FORMAT_R16G16B16A16_SFLOAT; + color.elementBytes = 8u; + color.samples = samples; + probe.colors = {color}; + probe.depth = AgcDriver::Graphics::DepthTarget{0x100000u, 0u, ReferenceExtent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + probe.depth->samples = samples; + const auto refusal = AgcDriver::Graphics::SampleOrderRefusal(emulated, probe); + if (!refusal.empty()) { + std::printf("skipped, the device does not put the checkerboard's samples in the guest's order: %s\n", refusal.c_str()); + return VulkanTestSkipped; + } + Require(AgcDriver::Graphics::ReordersSamples(AgcDriver::Graphics::HostSampleLocationsFor(emulated, probe).order) && !AgcDriver::Graphics::ReordersSamples(AgcDriver::Graphics::HostSampleLocationsFor(fallback, probe).order), "the emulated and fallback contexts did not take the exact and approximate paths"); + const auto reference = QuadComposite(device, context, samples, 3u, VillageLocations); + const auto exact = RenderCheckerboard(device, emulated, samples, 1u, VillageLocations); + const auto approximate = RenderCheckerboard(device, fallback, samples, 2u, VillageLocations); + CheckScene(reference, samples, "the composite of uniform renders"); + CheckScene(exact, samples, "the exact path"); + CheckScene(approximate, samples, "pixel (0,0)'s locations alone"); + const bool grid = (context.sampleLocationGridSampleCounts & samples) != 0u; + std::size_t gridDifferences = 0; + if (grid) { + const auto native = RenderCheckerboard(device, context, samples, 0u, VillageLocations); + CheckScene(native, samples, "the 2x2 grid"); + gridDifferences = Differences(reference.color, native.color, samples, "color through the device's 2x2 grid", true) + Differences(reference.depth, native.depth, samples, "depth through the device's 2x2 grid", true); + const auto exactGridDifferences = Differences(native.color, exact.color, samples, "color", false) + Differences(native.depth, exact.depth, samples, "depth", false); + std::printf("2x checkerboard at %ux%u: the device's 2x2 grid differs from the composite of four uniform renders in %zu samples and from the exact path in %zu samples\n", ReferenceExtent.width, ReferenceExtent.height, gridDifferences, exactGridDifferences); + } else { + std::printf("the device has no 2x2 sample location grid at 2 samples: the composite of four uniform renders is the reference\n"); + } + const auto colorDifferences = Differences(reference.color, exact.color, samples, "color with the samples put in the guest's order", true); + const auto depthDifferences = Differences(reference.depth, exact.depth, samples, "depth with the samples put in the guest's order", true); + const auto approximateDifferences = Differences(reference.color, approximate.color, samples, "color", false) + Differences(reference.depth, approximate.depth, samples, "depth", false); + std::printf("2x checkerboard at %ux%u: the exact path differs from the reference in %zu color and %zu depth samples; pixel (0,0)'s locations alone differ in %zu samples\n", ReferenceExtent.width, ReferenceExtent.height, colorDifferences, depthDifferences, approximateDifferences); + Require(approximateDifferences != 0u, "pixel (0,0)'s locations alone rendered the checkerboard like the reference: the scene does not tell the paths apart"); + Require(gridDifferences == 0u, "the composite of four uniform renders does not render the checkerboard like the device's 2x2 grid"); + Require(colorDifferences == 0u && depthDifferences == 0u, "the exact path did not render the checkerboard bit for bit like the reference"); + std::puts(grid ? "the exact path renders Village's 2x checkerboard like the device's 2x2 sample location grid, bit for bit" : "the exact path renders Village's 2x checkerboard like four uniform renders composed by pixel parity, bit for bit"); + return 0; +} + +int Bench(AgcDriver::VulkanDevice& device, VkExtent2D extent, std::uint32_t samples, std::uint32_t frames) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if ((context.limits.framebufferColorSampleCounts & samples) == 0u || (context.limits.framebufferDepthSampleCounts & samples) == 0u) { + std::printf("skipped, the device has no %ux color and depth targets\n", samples); + return VulkanTestSkipped; + } + const auto alias = AgcDriver::Graphics::SampleOrderStorageFormat(context, VK_FORMAT_R16G16B16A16_SFLOAT, 8u, samples); + if (alias == VK_FORMAT_UNDEFINED || !DepthSampleable(context, VK_FORMAT_D32_SFLOAT, samples) || !context.limits.timestampComputeAndGraphics) { + std::printf("skipped, the device has no %ux storage alias for RGBA16F, sampleable D32F or timestamps\n", samples); + return VulkanTestSkipped; + } + Image color(context, VK_FORMAT_R16G16B16A16_SFLOAT, samples, extent, ColorUsage, VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT); + Image depth(context, VK_FORMAT_D32_SFLOAT, samples, extent, DepthUsage, 0u); + const auto storage = color.View(alias, VK_IMAGE_ASPECT_COLOR_BIT, VK_IMAGE_USAGE_STORAGE_BIT); + const auto attachment = depth.View(VK_FORMAT_D32_SFLOAT, VK_IMAGE_ASPECT_DEPTH_BIT, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); + const std::array targets{{ + {color.Handle(), storage, alias, extent, samples, SampleOrderMethod::Storage, nullptr}, + {depth.Handle(), attachment, VK_FORMAT_D32_SFLOAT, extent, samples, SampleOrderMethod::Depth, nullptr}, + }}; + const SampleOrderPass pass{OrderFor(samples), extent, {}, false}; + VkQueryPoolCreateInfo queryInfo{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO}; + queryInfo.queryType = VK_QUERY_TYPE_TIMESTAMP; + queryInfo.queryCount = 5u; + VkQueryPool queries = VK_NULL_HANDLE; + Check(context.Function("vkCreateQueryPool")(context.device, &queryInfo, nullptr, &queries), "vkCreateQueryPool sample order bench"); + std::vector> phases; + std::vector> kept; + for (std::uint32_t frame = 0; frame <= frames; ++frame) { + kept.clear(); + Submit(context, [&](VkCommandBuffer commands) { + if (frame == 0u) { + color.ToGeneral(commands, VK_IMAGE_ASPECT_COLOR_BIT); + depth.ToGeneral(commands, VK_IMAGE_ASPECT_DEPTH_BIT); + } + context.Function("vkCmdResetQueryPool")(commands, queries, 0u, 5u); + const auto stamp = [&](std::uint32_t query) { + context.Function("vkCmdWriteTimestamp")(commands, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queries, query); + }; + stamp(0u); + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, std::span(targets).subspan(1u), true, kept); + stamp(1u); + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, std::span(targets).subspan(1u), false, kept); + stamp(2u); + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, targets, true, kept); + stamp(3u); + AgcDriver::Graphics::RecordSampleOrder(context, commands, pass, targets, false, kept); + stamp(4u); + }); + std::array ticks{}; + Check(context.Function("vkGetQueryPoolResults")(context.device, queries, 0u, 5u, sizeof(ticks), ticks.data(), sizeof(std::uint64_t), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT), "vkGetQueryPoolResults sample order bench"); + if (frame == 0u) continue; + std::array phase{}; + for (std::size_t index = 0; index < phase.size(); ++index) phase[index] = static_cast(ticks[index + 1u] - ticks[index]) * context.limits.timestampPeriod / 1.0e6; + phases.push_back(phase); + } + context.Function("vkDestroyQueryPool")(context.device, queries, nullptr); + std::array median{}; + for (std::size_t index = 0; index < median.size(); ++index) { + std::vector values; + for (const auto& phase : phases) values.push_back(phase[index]); + std::nth_element(values.begin(), values.begin() + static_cast(values.size() / 2u), values.end()); + median[index] = values[values.size() / 2u]; + } + std::printf("sample order swaps at %ux%u, %ux, median of %u frames (GPU ms): depth-only pass in %.3f, out %.3f; color+depth pass in %.3f, out %.3f; per frame with one pass of each %.3f\n", extent.width, extent.height, samples, frames, median[0], median[1], median[2], median[3], median[0] + median[1] + median[2] + median[3]); + return 0; +} + +} + +int main(int argc, char** argv) { + try { + const std::string_view mode = argc > 1 ? std::string_view(argv[1]) : std::string_view(); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (mode == "--grid-reference") return GridReference(*device); + if (mode == "--bench") { + VkExtent2D extent{1920u, 2160u}; + if (argc > 2) Require(std::sscanf(argv[2], "%ux%u", &extent.width, &extent.height) == 2, "--bench takes WIDTHxHEIGHT"); + const auto samples = argc > 3 ? static_cast(std::strtoul(argv[3], nullptr, 10)) : 2u; + const auto frames = argc > 4 ? static_cast(std::strtoul(argv[4], nullptr, 10)) : 50u; + return Bench(*device, extent, samples, frames); + } + Require(mode.empty(), "unknown mode " + std::string(mode)); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (!context.sampleRateShading) { + std::printf("skipped, the device lacks sampleRateShading\n"); + return VulkanTestSkipped; + } + const auto swapped = SwapPasses(context); + const auto remapped = ShaderRemaps(*device, context); + const auto scenes = SceneChecks(*device, context); + if (swapped == 0u && remapped == 0u && scenes == 0u) { + std::printf("skipped, the device has no multisampled targets the sample order tests can use\n"); + return VulkanTestSkipped; + } + std::printf("sample order tests passed: %u swap round trips, %u shader remap checks, %u scene checks\n", swapped, remapped, scenes); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderFill.frag b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderFill.frag new file mode 100644 index 0000000000..0f0df0cae8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderFill.frag @@ -0,0 +1,22 @@ +#version 450 +#ifdef STENCIL +#extension GL_ARB_shader_stencil_export : require +#endif + +layout(std430, binding = 0) readonly buffer Values { + uvec2 values[]; +}; + +layout(push_constant) uniform Fill { + uint width; + uint count; +} fill; + +void main() { + const uvec2 at = uvec2(gl_FragCoord.xy); + const uint slot = (at.y * fill.width + at.x) * fill.count + uint(gl_SampleID); + gl_FragDepth = uintBitsToFloat(values[slot].x); +#ifdef STENCIL + gl_FragStencilRefARB = int(values[slot].y); +#endif +} diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderFill.vert b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderFill.vert new file mode 100644 index 0000000000..05606ecec4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderFill.vert @@ -0,0 +1,6 @@ +#version 450 + +void main() { + const vec2 corner = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2)); + gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderRead.comp b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderRead.comp new file mode 100644 index 0000000000..89a0fed927 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderRead.comp @@ -0,0 +1,39 @@ +#version 450 +#extension GL_EXT_samplerless_texture_functions : require + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +#ifdef COLOR +layout(binding = 0) uniform texture2DMS colors; +#else +layout(binding = 0) uniform texture2DMS depths; +#ifdef STENCIL +layout(binding = 1) uniform utexture2DMS stencils; +#endif +#endif + +layout(std430, binding = 2) buffer Values { + uvec4 values[]; +}; + +layout(push_constant) uniform Read { + uvec2 extent; + uint count; +} read; + +void main() { + const uvec2 at = gl_GlobalInvocationID.xy; + if (at.x >= read.extent.x || at.y >= read.extent.y) return; + for (uint index = 0u; index < read.count; ++index) { + const uint slot = (at.y * read.extent.x + at.x) * read.count + index; +#ifdef COLOR + values[slot] = floatBitsToUint(texelFetch(colors, ivec2(at), int(index))); +#else + uvec4 value = uvec4(floatBitsToUint(texelFetch(depths, ivec2(at), int(index)).r), 0u, 0u, 0u); +#ifdef STENCIL + value.y = texelFetch(stencils, ivec2(at), int(index)).r; +#endif + values[slot] = value; +#endif + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderTexels.comp b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderTexels.comp new file mode 100644 index 0000000000..b169405ba9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/shaders/SampleOrderTexels.comp @@ -0,0 +1,25 @@ +#version 450 + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +layout(binding = 0, TEXEL_FORMAT) uniform restrict uimage2DMS texels; + +layout(std430, binding = 1) buffer Values { + uvec4 values[]; +}; + +layout(push_constant) uniform Access { + uvec2 extent; + uint count; + uint store; +} access; + +void main() { + const uvec2 at = gl_GlobalInvocationID.xy; + if (at.x >= access.extent.x || at.y >= access.extent.y) return; + for (uint index = 0u; index < access.count; ++index) { + const uint slot = (at.y * access.extent.x + at.x) * access.count + index; + if (access.store != 0u) imageStore(texels, ivec2(at), int(index), values[slot]); + else values[slot] = imageLoad(texels, ivec2(at), int(index)); + } +} diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 9a05433674..45283ed818 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -194,6 +194,7 @@ private: append(key, value.quadPixelMask); append(key, value.quadSampleMask); append(key, value.targetOutputMode); + append(key, value.sampleOrder); } static void append(std::vector& key, const ShaderVertexResourceDestination& value) { diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 20c4e29f51..b6412f02b2 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -343,6 +343,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteU8(info.quadPixelMask); for (const std::uint16_t mask : info.quadSampleMask) writer.WriteU32(mask); writer.WriteU32(info.reversedBlend); + for (const std::uint32_t order : info.sampleOrder) writer.WriteU32(order); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -405,6 +406,9 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { } } if (version >= 30u) info.reversedBlend = reader.ReadU32(); + if (version >= 32u) { + for (std::uint32_t& order : info.sampleOrder) order = reader.ReadU32(); + } if (version < 5u) { const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; }; info.inputAddr = input(PixelInput::PerspectiveSample, info.hasPerspectiveCenterVgpr && inputAddrOrCenterVgpr == 2u) | input(PixelInput::PerspectiveCenter, info.hasPerspectiveCenterVgpr) | @@ -776,7 +780,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(30u); + writer.WriteU32(32u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -809,7 +813,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 30u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 32u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 72fe2675ee..9120908bd6 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -184,12 +184,17 @@ struct ShaderPixelInputInfo { bool psDitheredAlphaToCoverage = false; std::uint8_t quadPixelMask = 0xf; std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; + std::array sampleOrder{}; ShaderStageRuntime stage; bool HasPositionInput() const { return psPosX || psPosY || psPosZ || psPosW; } + [[nodiscard]] bool ReordersSamples() const { + return std::ranges::any_of(sampleOrder, [](std::uint32_t order) { return order != 0u && order != 0x76543210u; }); + } + [[nodiscard]] bool QuadSampleMaskPartial() const { return std::ranges::any_of(quadSampleMask, [](std::uint16_t mask) { return mask != 0xffffu; }); } diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index fac95cb5d4..d302f46fd8 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -237,7 +237,7 @@ bool IsPixelParameterFlat(const ShaderPixelInputInfo& pixel, std::uint32_t input } void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pixel, ShaderInfo& info) { - if (pixel->HasPositionInput() || pixel->psDitheredAlphaToCoverage || pixel->QuadMasked()) { + if (pixel->HasPositionInput() || pixel->psDitheredAlphaToCoverage || pixel->QuadMasked() || pixel->ReordersSamples()) { AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord"); } if (pixel->psFrontFace) { diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 157ab11feb..eb1391c38b 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -172,6 +172,7 @@ struct ShaderPixelStageInfo { std::uint8_t quadPixelMask = 0xf; std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; std::uint32_t reversedBlend = 0; + std::array sampleOrder{}; }; struct ShaderVertexBufferResource { diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index b03d85a809..9cad08bd0c 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 36; +inline constexpr std::uint32_t FormatVersion = 40; enum class LoadStatus { Loaded, diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index b85eb66e9f..1ce27f077e 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -182,6 +182,7 @@ struct SpirvEmitterState { std::uint32_t quadPixelUncovered = 0; std::uint32_t sampleMaskInputVariable = 0; std::uint32_t quadSampleCoverage = 0; + std::uint32_t sampleOrderWord = 0; std::uint32_t subgroupLocalInvocationIdVariable = 0; std::uint32_t perVertexVariable = 0; std::uint32_t pointSizeVariable = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index edabae4d45..4fdf48a401 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -38,6 +38,31 @@ const ShaderWorkgroupInputInfo* ShaderWorkgroupInputFor(const SpirvEmitterState& } } +namespace { + +std::uint32_t GuestSampleIndex(SpirvEmitterState& state, std::uint32_t hostIndex) { + if (state.sampleOrderWord == 0u) return hostIndex; + const auto shift = EmitBinaryU32(state, spv::OpShiftLeftLogical, EmitBinaryU32(state, spv::OpBitwiseAnd, hostIndex, ConstantU32(state, 7u)), ConstantU32(state, 2u)); + return EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, state.sampleOrderWord, shift), ConstantU32(state, 7u)); +} + +std::uint32_t ReorderedSampleBits(SpirvEmitterState& state, std::uint32_t bits, bool toHost) { + auto reordered = EmitBinaryU32(state, spv::OpBitwiseAnd, bits, ConstantU32(state, ~0xffu)); + for (std::uint32_t sample = 0; sample < 8u; ++sample) { + const auto guest = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, state.sampleOrderWord, ConstantU32(state, sample * 4u)), ConstantU32(state, 7u)); + if (toHost) { + const auto bit = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, bits, guest), ConstantU32(state, 1u)); + reordered = EmitBinaryU32(state, spv::OpBitwiseOr, reordered, EmitBinaryU32(state, spv::OpShiftLeftLogical, bit, ConstantU32(state, sample))); + } else { + const auto bit = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, bits, ConstantU32(state, sample)), ConstantU32(state, 1u)); + reordered = EmitBinaryU32(state, spv::OpBitwiseOr, reordered, EmitBinaryU32(state, spv::OpShiftLeftLogical, bit, guest)); + } + } + return reordered; +} + +} + std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std::uint32_t component) { if (kind == StageInputKind::LocalInvocationIndex) { return EmitLocalInvocationIndex(state); @@ -97,7 +122,7 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: const auto bits = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeI32(state), value, variable); state.module.AddFunction(spv::OpBitcast, TypeU32(state), bits, value); - return bits; + return kind == StageInputKind::SampleId ? GuestSampleIndex(state, bits) : bits; } if (kind == StageInputKind::FragCoord || kind == StageInputKind::TessCoord) { const auto pointer = state.module.AllocateId(); @@ -289,6 +314,7 @@ bool AlphaToCoverage(const SpirvEmitterState& state) { void StoreCoverage(SpirvEmitterState& state, std::uint32_t mask) { if (state.quadSampleCoverage != 0u) mask = Binary(state, spv::OpBitwiseAnd, TypeI32(state), mask, state.quadSampleCoverage); + if (state.sampleOrderWord != 0u) mask = Unary(state, spv::OpBitcast, TypeI32(state), ReorderedSampleBits(state, Unary(state, spv::OpBitcast, TypeU32(state), mask), true)); const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeI32(state)), pointer, state.sampleMaskVariable, ConstantU32(state, 0u)); state.module.AddFunction(spv::OpStore, pointer, mask); @@ -1181,6 +1207,18 @@ void EmitProgram(SpirvEmitterState& state) { if (state.pixelValidMaskVariable != 0u) { state.module.AddFunction(spv::OpStore, state.pixelValidMaskVariable, ConstantU32(state, 1u)); } + if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->ReordersSamples()) { + const auto& order = state.inputInfo.pixel->sampleOrder; + const auto parity = [&](std::uint32_t component) { + const auto value = Unary(state, spv::OpBitcast, TypeF32(state), EmitBuiltinU32(state, StageInputKind::FragCoord, component)); + return EmitBinaryU32(state, spv::OpBitwiseAnd, Unary(state, spv::OpConvertFToU, TypeU32(state), value), ConstantU32(state, 1u)); + }; + const auto quad = EmitBinaryU32(state, spv::OpBitwiseOr, parity(0u), EmitBinaryU32(state, spv::OpShiftLeftLogical, parity(1u), ConstantU32(state, 1u))); + const auto word = [&](std::size_t pixel) { return ConstantU32(state, order[pixel] == 0u ? 0x76543210u : order[pixel]); }; + auto selected = word(3u); + for (std::uint32_t pixel = 3u; pixel-- > 0u;) selected = Select(state, TypeU32(state), EmitCompareU32Constant(state, spv::OpIEqual, quad, pixel), word(pixel), selected); + state.sampleOrderWord = selected; + } if (AlphaToCoverage(state)) StoreCoverage(state, ConstantI32(state, -1)); if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpBeginInvocationInterlockEXT); @@ -1205,7 +1243,9 @@ void EmitProgram(SpirvEmitterState& state) { state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeI32(state)), pointer, state.sampleMaskInputVariable, ConstantU32(state, 0u)); const auto rasterized = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeI32(state), rasterized, pointer); - bit = EmitBinaryU32(state, spv::OpBitwiseAnd, Unary(state, spv::OpBitcast, TypeU32(state), rasterized), covered); + auto guestRasterized = Unary(state, spv::OpBitcast, TypeU32(state), rasterized); + if (state.sampleOrderWord != 0u) guestRasterized = ReorderedSampleBits(state, guestRasterized, false); + bit = EmitBinaryU32(state, spv::OpBitwiseAnd, guestRasterized, covered); state.quadSampleCoverage = Unary(state, spv::OpBitcast, TypeI32(state), covered); } const auto mask = static_cast(pixel.quadPixelMask); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 5de4543f7b..5b1ff637cc 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -150,6 +150,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psDitheredAlphaToCoverage = pixel.ditheredAlphaToCoverage; pixelStorage.quadPixelMask = pixel.quadPixelMask; pixelStorage.quadSampleMask = pixel.quadSampleMask; + pixelStorage.sampleOrder = pixel.sampleOrder; ShaderStageInputInfo result; result.pixel = &pixelStorage; return result; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index b8d4b0e10e..4f507c411d 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -47,6 +47,7 @@ void Require(bool condition) { void InsertPixelDefaultsAfterFrontFace(std::vector& bytes) { bytes.insert(bytes.begin() + 261u, {0x0fu, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0u, 0u, 0u, 0u}); + bytes.insert(bytes.begin() + 282u, 16u, 0u); } std::vector DecodeBase64(std::string_view text) { @@ -206,7 +207,7 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 30u; + upgraded[4] = 32u; if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 5u, 0u); @@ -245,7 +246,7 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 30u; + layered[4] = 32u; layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); if (fixture.tessellation) layered.insert(layered.end() - 35u, 5u, 0u); @@ -270,7 +271,7 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 30u; + bytes[4] = 32u; bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); bytes.insert(bytes.end(), sizeof(std::uint64_t), 0u); @@ -394,7 +395,7 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 30u; + upgraded[4] = 32u; if (fixture.pixel) upgraded.insert(upgraded.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); upgraded.insert(upgraded.end(), sizeof(std::uint64_t), 0u); @@ -407,7 +408,7 @@ int main() { const auto replay25 = serializer.Deserialize(EncodeBase64(main25)); Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u)); Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u)); - main25[4] = 30u; + main25[4] = 32u; if (fixture.pixel) { main25.insert(main25.begin() + 260u, 0u); InsertPixelDefaultsAfterFrontFace(main25); @@ -426,9 +427,9 @@ int main() { const auto replay = serializer.Deserialize(encoded); Require(replay.request.target.float16DenormPreserve == preserve); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 30u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 32u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto future = bytes; - future[4] = 31u; + future[4] = 33u; bool futureRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(future))); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 83a88c9982..232bed7ec7 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -106,7 +106,7 @@ - [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. - [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. - [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. -- [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2. On other devices every pixel takes the pattern's pixel (0,0) locations (`PA_SC_AA_SAMPLE_LOCS_PIXEL_X0Y0_*`, its first registers and the corner the grid is anchored at), and the first such pipeline of each sample count logs `[gpu] no 2x2 sample location grid`. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each; an RTX 3070 Laptop GPU (driver 617.42) reports 1x1 for every count. With that stand-in the top-left pixel of every 2x2 quad matches the console and the other three use its positions. A pattern that only reorders one set of positions between pixels, as the 2x checkerboard of PPSA01556 and PPSA27836 puts its samples at x = +4/16 and -4/16 and swaps them on odd rows, keeps every position and the number of samples a primitive covers in each pixel, so a resolve that averages a pixel's samples, or takes their minimum or maximum, sees the console's values. Sample i of an odd row then holds what the console stores in sample 1 - i: a pass that reads the samples by index, as checkerboard reconstruction does, exchanges the two half-pixels of odd rows, which can show as combing along vertical edges, and per-sample shading, sample-mask exports and coverage inputs see the swapped indices too. A pattern whose pixels hold different positions draws pixel (0,0)'s positions in each pixel whose own differ. A device's pipelines and render pass begins take the same stand-in for a given pattern, so a sample-location-compatible depth image is not drawn with one set of locations and transitioned or tested with another. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. +- [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2. On other devices every pixel takes the pattern's pixel (0,0) locations (`PA_SC_AA_SAMPLE_LOCS_PIXEL_X0Y0_*`, its first registers and the corner the grid is anchored at), and the first such pipeline of each sample count logs `[gpu] no 2x2 sample location grid`. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each; an RTX 3070 Laptop GPU (driver 617.42) reports 1x1 for every count. A pattern that only reorders one set of positions between pixels, as the 2x checkerboard of PPSA01556 and PPSA27836 puts its samples at x = +4/16 and -4/16 and swaps them on odd rows, is drawn exactly through that stand-in: before every render pass that draws with it each pixel's samples are put in the stand-in's order and after it back in the guest's, through a storage view of a multisampled color target and through a copy and a per-sample pass for depth (writing stencil through VK_EXT_shader_stencil_export, or one pass per bit without it), while the pixel shader reads SampleID, applies the sample mask and exports coverage in the guest's order, and the second log line names the reordering. That costs two passes over each multisampled attachment per render pass, and on those devices every multisampled color target is created with a mutable format and storage usage for its view, which a driver may answer by not compressing it. It needs shaderStorageImageMultisample with a storage view of the color format at the sample count, sampleRateShading, and a depth format a FragDepth write gives back exactly (16-bit, or 32-bit float; not 24-bit depth, the stand-in for 16-bit depth with stencil where the device has neither); without them, and for patterns whose pixels hold different positions, the stand-in alone is used, and the log line says why. Then the top-left pixel of every 2x2 quad matches the console and the other three use its positions. A same-set pattern keeps every position and the number of samples a primitive covers in each pixel, so a resolve that averages a pixel's samples, or takes their minimum or maximum, sees the console's values, but sample i of an odd row holds what the console stores in sample 1 - i: a pass that reads the samples by index, as checkerboard reconstruction does, exchanges the two half-pixels of odd rows, which can show as combing along vertical edges, and per-sample shading, sample-mask exports and coverage inputs see the swapped indices too. A pattern whose pixels hold different positions draws pixel (0,0)'s positions in each pixel whose own differ. A device's pipelines and render pass begins take the same stand-in for a given pattern, so a sample-location-compatible depth image is not drawn with one set of locations and transitioned or tested with another. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. - [DCC display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. - [Storage image write-back](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. - [16-bit float display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0xC001000622000000` and `0xC001000600000000` are taken as R16G16B16A16 and B16G16R16A16 float, eight bytes per texel, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps them and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgba16Float`, `Bgra16Float`); PPSA11386 registers 3840x2160 tiled buffers of the first. The tiled layout is assumed to be the 8-byte SW_64KB_R_X equation of `TextureSwizzleEquations.hpp` applied to full coordinates, as addrlib applies it, by analogy with the 32-bit scanout; `ColorTargetLayout` applies the equations to in-block coordinates, which for 8-byte texels differs in every odd 64-row block row (address bit 11 takes y bit 6) and for 16-byte texels also differs, so a float display buffer written through `WriteColorTarget` and presented from guest memory would show those rows misplaced. The values are assumed to be linear light and are encoded with the sRGB curve for the 8-bit output. Bits 32-39 of these formats are `0x06`, as in the 10-bit formats SharpProspero lists without sRGB transfer, while its sRGB formats have `0x00`, its 10-bit PQ formats `0x04` (with `0x07` in bits 40-47) and its float BT.2100 PQ formats `0x07`: they show only that these formats are not the sRGB ones. NaN and negative values give 0, values of 1 and above give 255, alpha is linear. Neither the layout nor the transfer is verified against console or title frames, and HDR output is not modelled. DCC metadata on a float buffer throws.