fix(agc): keep the guest's sample order where pixel (0,0)'s locations stand in for a 2x2 grid

Village's 2x checkerboard puts its two samples at x = +4/16 and -4/16
and swaps their indices on odd rows. On a device without a 2x2 sample
location grid every pixel takes pixel (0,0)'s locations, so the odd
rows hold the console's sample 1 in sample 0 and the reverse: a pass
that reads the samples by index, as checkerboard reconstruction does,
exchanges the two half-pixels of every odd row, and SampleID, sample
masks and exported coverage name the wrong samples there.

Such a pattern holds one set of positions in every pixel, so drawing
with pixel (0,0)'s locations is exact up to the order of each pixel's
samples. The draw now keeps the guest's order: the pixel shader reads
SampleID, applies the sample mask and exports coverage through each
pixel's order, and every render pass drawing with the stand-in puts the
samples of its multisampled attachments in the stand-in's order before
it and back in the guest's after it. Color targets do that through a
storage view of their texels as unsigned integers, depth through a copy
and a per-sample pass writing FragDepth, and stencil through
VK_EXT_shader_stencil_export or one pass per bit.

It needs shaderStorageImageMultisample with a storage view of the color
format, sampleRateShading, and a depth format FragDepth writes back
exactly (16-bit or 32-bit float); otherwise, and for patterns whose
pixels hold different positions, the stand-in alone is used, and the
log line says why. Devices with the grid are unchanged.

Recompile requests carry the order as serialization version 32 (#6470
takes 31), and the shader cache format becomes 40 (open pull requests
take 37 and 39).
This commit is contained in:
Yunus Shaikh committed 2026-10-15 17:53:11 +00:00
1 parent 668ecaeca2
commit 57ceb9e370
41 files changed
+2659 -41

No files matched your search

@@ -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 "$<TARGET_FILE:glslang-standalone>" -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 "$<TARGET_FILE:glslang-standalone>" -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_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_buffer_descriptor_runtime_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
@@ -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:$<TARGET_FILE_DIR:libc>")
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:$<TARGET_FILE_DIR:libc>")
set_tests_properties(agc_driver_covered_centroid_state PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_tests_properties(agc_driver_sample_location_grid_fallback PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_tests_properties(agc_driver_sample_order_planning PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests)
set_tests_properties(agc_compute_pipeline_capture PROPERTIES TIMEOUT 30)
@@ -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<ShaderRecompiler::GeometryStageLimits> GeometryLimits() const;
const std::uint32_t* WorkGroupCountLimit() const;
std::uint32_t VendorId() const;
@@ -130,7 +130,11 @@ std::shared_ptr<DrawDecode> 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<std::uint32_t>{} : localDevice->Target().supportedCapabilities;
@@ -76,6 +76,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
mix(pixel.ditheredAlphaToCoverage);
mix(pixel.quadPixelMask);
for (const auto mask : pixel.quadSampleMask) mix(mask);
for (const auto order : pixel.sampleOrder) mix(order);
std::lock_guard cacheLock(drawCacheMutex);
++drawEntryCounters.lookups;
const auto found = drawCache.find(drawKey);
@@ -196,7 +196,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) {
const auto& y = b.programs[i];
if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset || x.tailContinuation != y.tailContinuation) return false;
}
if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask) return false;
if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.sampleOrder != q.sampleOrder) return false;
if (s.samples != t.samples || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false;
return true;
}
@@ -23,6 +23,7 @@
#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp"
#include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp"
#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp"
@@ -306,6 +307,8 @@ struct VulkanDevice::State {
bool storageImageWithoutFormat = false;
VkSampleCountFlags sampleLocationSampleCounts = 0;
VkSampleCountFlags sampleLocationGridSampleCounts = 0;
bool storageImageMultisample = false;
bool shaderStencilExport = false;
bool depthClamp = false;
bool depthBounds = false;
bool depthBiasClamp = false;
@@ -709,6 +712,7 @@ struct VulkanDevice::State {
Graphics::ClearCachedPipelines(device);
Graphics::ClearDepthSurfaces(device);
Graphics::ClearMultisampleTargets(device);
Graphics::ClearSampleOrders(device);
{
std::lock_guard pipelines(computePipelinesMutex);
computePipelines.clear();
@@ -1185,6 +1189,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
deviceExtensions.push_back(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME);
state->capabilities.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<PFN_vkGetPhysicalDeviceMemoryProperties2>("vkGetPhysicalDeviceMemoryProperties2") : nullptr;
context.storageImageMultisample = state->storageImageMultisample;
context.shaderStencilExport = state->shaderStencilExport;
context.memoryProperties2 =state->memoryBudget ? state->InstanceFunction<PFN_vkGetPhysicalDeviceMemoryProperties2>("vkGetPhysicalDeviceMemoryProperties2") : nullptr;
context.singlePassStorage = state->storageImageWithoutFormat;
return context;
}
@@ -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 {
@@ -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 <array>
#include <cstdint>
@@ -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);
@@ -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 <cstddef>
#include <cstdint>
@@ -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<Texture> 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);
@@ -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<const std::byte, PipelinePushConstantBytes> 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<std::uint32_t> colorSlots;
bool sampleLocationsEnabled = false;
VkExtent2D sampleLocationGrid{1, 1};
SampleOrder sampleOrder{};
std::vector<CachedFramebuffer> framebuffers;
};
@@ -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 <array>
#include <cstdint>
#include <memory>
#include <span>
#include <string>
#include <vector>
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<VkSampleLocationEXT, 32> 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<void> 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<SampleOrderStep> 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<const SampleOrderTarget> targets, bool toHost, std::vector<std::shared_ptr<void>>& kept);
void ClearSampleOrders(VkDevice device);
}
#endif
@@ -151,6 +151,14 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup =
State DecodeState(const QueueState& queue, bool vertexWorkgroup = false);
std::array<VkSampleLocationEXT, 32> SampleLocations(const State& state);
VkExtent2D SampleLocationGrid(const State& state);
using SampleOrder = std::array<std::uint32_t, 4>;
inline constexpr std::uint32_t KeptSampleOrder = 0x76543210u;
std::optional<SampleOrder> 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<std::uint16_t, 4> QuadSampleMasks(const State& state, const SampleOrder& order);
std::array<std::uint8_t, 8> ExportMappings(const State& state);
std::array<ShaderRecompiler::ColorExportPacking, 8> ExportPackings(const State& state);
ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state);
@@ -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<StorageTexture>& 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<OwnedImage> 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<OwnedImage> owner = {}, VkImageUsageFlags usage = 0);
struct DepthCopyTag {};
Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag);
static VkFormat DepthCopyFormat(VkFormat colorFormat);
@@ -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]);
}
@@ -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
}
@@ -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);
}
@@ -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<const std::uint32_t> code, std::span<const std::uint32_t> userData, const std::array<std::uint32_t, 3>& numThreads) {
static constexpr std::array<std::uint32_t, 17> 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;
@@ -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<std::byte, PipelinePushConstantBytes>* pushBytes = nullptr;
VkShaderStageFlags pushStages = 0;
std::vector<std::pair<std::uint32_t, std::shared_ptr<Pipeline>>> stencilReplay;
std::vector<SampleOrderTarget> reordered;
SampleOrderPass reorderPass;
};
void recordStencilReplay(const Context& context, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, const DrawInputs& inputs, std::span<const std::pair<std::uint32_t, std::shared_ptr<Pipeline>>> replay, std::span<const CompiledShader> shaders, const ShaderResources& resources, const std::array<std::byte, PipelinePushConstantBytes>* 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<std::shared_ptr<void>> 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<void(VkCommandBuffer)> 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<std::shared_ptr<void>> 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<SampleOrderTarget> 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<DrawRecipe>();
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, &copy);
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<std::shared_ptr<void>> 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) {
@@ -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<PFN_vkCreateImageView>("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<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &storageView), "vkCreateImageView multisampled target storage");
return storageView;
}
std::shared_ptr<Texture> 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<std::uint32_t, 4> key{static_cast<std::uint32_t>(components.r), static_cast<std::uint32_t>(components.g), static_cast<std::uint32_t>(components.b), static_cast<std::uint32_t>(components.a)};
if (const auto found = textures.find(key); found != textures.end()) return found->second;
auto texture = std::make_shared<Texture>(context, image, target.format, VK_IMAGE_ASPECT_COLOR_BIT, components);
auto texture = std::make_shared<Texture>(context, image, target.format, VK_IMAGE_ASPECT_COLOR_BIT, components, VK_IMAGE_VIEW_TYPE_2D, 0, std::shared_ptr<OwnedImage>{}, 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<PFN_vkDestroyRenderPass>("vkDestroyRenderPass")(context.device, resolvePass, nullptr);
if (storageView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, storageView, nullptr);
if (view) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, view, nullptr);
if (image) context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, image, nullptr);
if (memory) context.Function<PFN_vkFreeMemory>("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<Texture> MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components) {
std::lock_guard lock(targetsMutex());
const auto format = ResolveTextureFormat(resource.format);
@@ -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 <algorithm>
#include <array>
@@ -160,10 +161,11 @@ bool ShadersWriteMemory(std::span<const CompiledShader> shaders) {
void ValidateSampleMask(const Context& context, const State& state, std::span<const CompiledShader> 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<std::uint32_t> 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<typename TValue>
void appendKey(std::vector<std::byte>& key, const TValue& value) {
static_assert(std::is_trivially_copyable_v<TValue>);
@@ -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<const CompiledShader> 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<VkPipelineColorBlendAttachmentState> firstChannels(state.blends.begin(), state.blends.end());
@@ -894,6 +897,10 @@ void pipelineKey(std::vector<std::byte>& 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<std::byte>& key) {
@@ -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 <algorithm>
#include <array>
#include <atomic>
#include <bit>
#include <cstdio>
#include <limits>
#include <map>
#include <mutex>
#include <tuple>
#include <utility>
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<std::uint32_t, 4> map;
std::array<std::uint32_t, 2> extent;
std::uint32_t count;
};
struct DepthPush {
std::array<std::uint32_t, 4> 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<std::uint32_t, 4> mapFor(const SampleOrder& order, bool toHost) {
std::array<std::uint32_t, 4> 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<VkSampleCountFlagBits>(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<PFN_vkCreateImage>("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage sample order copy");
VkMemoryRequirements requirements{};
context.Function<PFN_vkGetImageMemoryRequirements>("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<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory sample order copy");
Check(context.Function<PFN_vkBindImageMemory>("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<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &info, nullptr, &view), "vkCreateImageView sample order copy");
return view;
}
void release() noexcept {
if (stencilView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, stencilView, nullptr);
if (depthView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, depthView, nullptr);
if (image) context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, image, nullptr);
if (memory) context.Function<PFN_vkFreeMemory>("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<std::uint32_t, 4>& 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<PFN_vkUpdateDescriptorSets>("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<std::uint32_t, 4>& map, VkExtent2D region, const SampleOrderPass& order, std::vector<std::shared_ptr<void>>& 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<PFN_vkCmdCopyImage>("vkCmdCopyImage")(commands, target.image, VK_IMAGE_LAYOUT_GENERAL, copy->image, VK_IMAGE_LAYOUT_GENERAL, 1, &region3d);
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<VkDescriptorImageInfo, 2> images{{{VK_NULL_HANDLE, copy->depthView, VK_IMAGE_LAYOUT_GENERAL}, {VK_NULL_HANDLE, stencil ? copy->stencilView : copy->depthView, VK_IMAGE_LAYOUT_GENERAL}}};
std::array<VkWriteDescriptorSet, 2> 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<PFN_vkUpdateDescriptorSets>("vkUpdateDescriptorSets")(context.device, stencil ? 2u : 1u, writes.data(), 0, nullptr);
const std::array<VkImageView, 1> views{target.view};
auto framebuffer = std::make_shared<Framebuffer>(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<VkSampleCountFlagBits>(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<float>(region.width), static_cast<float>(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<VkDescriptorPool> pools;
std::vector<std::uint64_t> 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<VkFormat, std::uint32_t, SampleOrderMethod, bool, std::array<std::uint32_t, 16>>;
using CopyKey = std::tuple<VkFormat, std::uint32_t, std::uint32_t, std::uint32_t, bool>;
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<PFN_vkCreateShaderModule>("vkCreateShaderModule")(context.device, &info, nullptr, &created), "vkCreateShaderModule sample order");
return created;
}
void destroyModule(VkShaderModule shader) {
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, shader, nullptr);
}
VkDescriptorSetLayout setLayout(VkDescriptorType type, std::uint32_t count, VkShaderStageFlags stage) {
std::array<VkDescriptorSetLayoutBinding, 2> 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<PFN_vkCreateDescriptorSetLayout>("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 = &range;
VkPipelineLayout layout = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(context.device, &info, nullptr, &layout), "vkCreatePipelineLayout sample order");
return layout;
}
VkPipeline storagePipeline(VkFormat format) {
const std::array<VkFormat, 5> 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<std::size_t>(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<std::pair<const std::uint32_t*, std::size_t>, 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<PFN_vkCreateComputePipelines>("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<VkSampleLocationEXT, 32>& locations) {
std::array<std::uint32_t, 16> standIn{};
if (located) {
for (std::uint32_t sample = 0; sample < samples && sample < 8u; ++sample) {
standIn[sample * 2u] = std::bit_cast<std::uint32_t>(locations[sample].x);
standIn[sample * 2u + 1u] = std::bit_cast<std::uint32_t>(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<VkSampleCountFlagBits>(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<PFN_vkCreateRenderPass>("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<VkPipelineShaderStageCreateInfo, 2> 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<VkSampleCountFlagBits>(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<VkSampleCountFlagBits>(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<VkDynamicState, 4> 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<std::uint32_t>(dynamicStates.size());
dynamic.pDynamicStates = dynamicStates.data();
VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
info.stageCount = static_cast<std::uint32_t>(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<PFN_vkCreateGraphicsPipelines>("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<CopyImage> 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<CopyImage>(context, format, samples, extent, stencil);
copies.emplace(key, std::pair{copy, ++uses});
return copy;
}
VkDescriptorSet allocate(VkDescriptorSetLayout layout, const std::vector<VkDescriptorPoolSize>& 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<PFN_vkResetDescriptorPool>("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<std::uint32_t>(sizes.size());
info.pPoolSizes = sizes.data();
VkDescriptorPool pool = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreateDescriptorPool>("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<PFN_vkAllocateDescriptorSets>("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<std::uint64_t>::max();
return set;
}
void destroyPass(const DepthPass& pass) noexcept {
if (pass.stencilBits) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, pass.stencilBits, nullptr);
if (pass.depth) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, pass.depth, nullptr);
if (pass.renderPass) context.Function<PFN_vkDestroyRenderPass>("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<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, pipeline, nullptr);
}
for (auto* ring : {&storageRing, &sampledRing}) {
for (const auto pool : ring->pools) context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")(context.device, pool, nullptr);
ring->pools.clear();
}
for (const auto layout : {storagePipelineLayout, sampledPipelineLayout}) {
if (layout) context.Function<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout")(context.device, layout, nullptr);
}
for (const auto layout : {storageSetLayout, sampledSetLayout}) {
if (layout) context.Function<PFN_vkDestroyDescriptorSetLayout>("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<VkPipeline, 5> storagePipelines{};
std::map<DepthKey, DepthPass> depthPasses;
std::map<CopyKey, std::pair<std::shared_ptr<CopyImage>, std::uint64_t>> copies;
std::uint64_t uses = 0;
Ring storageRing;
Ring sampledRing;
};
std::mutex& ordersMutex() {
static std::mutex mutex;
return mutex;
}
std::vector<std::unique_ptr<SampleOrders>>& orders() {
static std::vector<std::unique_ptr<SampleOrders>> 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<SampleOrders>(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<std::uint32_t> 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<SampleOrderStep> PlanSampleOrder(const Context& context, const State& state, const SampleOrder& order, VkAttachmentLoadOp load, VkAttachmentStoreOp store) {
std::vector<SampleOrderStep> 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<std::uint32_t>(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<const SampleOrderTarget> targets, bool toHost, std::vector<std::shared_ptr<void>>& 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; });
}
}
@@ -810,6 +810,53 @@ VkExtent2D SampleLocationGrid(const State& state) {
return state.sampleLocationGrid ? VkExtent2D{2, 2} : VkExtent2D{1, 1};
}
std::optional<SampleOrder> 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<std::uint16_t, 4> QuadSampleMasks(const State& state, const SampleOrder& order) {
if (!SampleMaskFollowsOrder(state, order)) return QuadSampleMasks(state);
const auto mask = static_cast<std::uint16_t>(state.quadSampleMask[0] & ((1u << state.samples) - 1u));
return {mask, mask, mask, mask};
}
std::array<VkSampleLocationEXT, 32> SampleLocations(const State& state) {
Require(state.samples == 1 || state.samples == 2 || state.samples == 4 || state.samples == 8, "invalid sample count");
std::array<VkSampleLocationEXT, 32> locations{};
@@ -781,10 +781,13 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
}
}
Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer, std::shared_ptr<OwnedImage> 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<OwnedImage> 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;
@@ -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<VkSampleMask> 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<PFN_vkVoidFunction>(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<std::uint32_t, 4> 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<std::uint8_t, 4> positions{0x2eu, 0xe6u, 0x2au, 0x62u};
const std::array<std::array<std::uint32_t, 4>, 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<VkPhysicalDevice>();
context.device = makeHandle<VkDevice>();
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<VkPhysicalDevice>();
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<VkPhysicalDevice>();
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<SampleOrderStep>& plan, std::initializer_list<SampleOrderStep> 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<std::uint16_t, 4>{1, 1, 1, 1} && QuadSampleMasks(masked) == std::array<std::uint16_t, 4>{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<CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}};
const std::array<std::uint32_t, 1> mainKey{41};
const std::array<VkSampleLocationEXT, 2> 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<VkDescriptorSetLayout>(), 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<std::ptrdiff_t>(begin), pipelineSampleMasks.begin() + static_cast<std::ptrdiff_t>(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();
@@ -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<std::uint32_t, 2> 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<void>(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<void>(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<std::ptrdiff_t>(blendOffset + 4u), orderedBytes.begin()) && orderedBytes[blendOffset + 4u] == 0x10u && orderedBytes[blendOffset + 15u] == 0x76u && orderedBytes[blendOffset + 16u] == 0x01u && std::equal(plainPixel.begin() + static_cast<std::ptrdiff_t>(blendOffset + 4u + sampleOrderBytes), plainPixel.end(), orderedBytes.begin() + static_cast<std::ptrdiff_t>(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<std::ptrdiff_t>(blendOffset + 4u), version30.begin() + static_cast<std::ptrdiff_t>(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<std::uint32_t, 4>{} && 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<std::ptrdiff_t>(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<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u));
version29.erase(version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast<std::ptrdiff_t>(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<std::uint16_t, 4>{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<std::ptrdiff_t>(maskOffset + 1u), version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u));
version28.erase(version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u), version28.begin() + static_cast<std::ptrdiff_t>(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<std::uint16_t, 4>{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<std::ptrdiff_t>(maskOffset), version27.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u));
version27.erase(version27.begin() + static_cast<std::ptrdiff_t>(maskOffset), version27.begin() + static_cast<std::ptrdiff_t>(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<std::ptrdiff_t>(allBitsOffset), legacyPixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset + 2u + sampleMaskBytes + 4u));
legacyPixel.erase(legacyPixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset), legacyPixel.begin() + static_cast<std::ptrdiff_t>(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");
File diff suppressed because it is too large. Load diff
@@ -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
}
@@ -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);
}
@@ -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
}
}
@@ -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));
}
}
+1
View File
@@ -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<std::uint64_t>& key, const ShaderVertexResourceDestination& value) {
@@ -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);
@@ -184,12 +184,17 @@ struct ShaderPixelInputInfo {
bool psDitheredAlphaToCoverage = false;
std::uint8_t quadPixelMask = 0xf;
std::array<std::uint16_t, 4> quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu};
std::array<std::uint32_t, 4> 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; });
}
@@ -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) {
+1
View File
@@ -172,6 +172,7 @@ struct ShaderPixelStageInfo {
std::uint8_t quadPixelMask = 0xf;
std::array<std::uint16_t, 4> quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu};
std::uint32_t reversedBlend = 0;
std::array<std::uint32_t, 4> sampleOrder{};
};
struct ShaderVertexBufferResource {
+1 -1
View File
@@ -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,
@@ -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;
@@ -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<std::uint32_t>(pixel.quadPixelMask);
@@ -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;
@@ -47,6 +47,7 @@ void Require(bool condition) {
void InsertPixelDefaultsAfterFrontFace(std::vector<std::uint8_t>& 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<std::uint8_t> 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<void>(serializer.Deserialize(EncodeBase64(future)));
+1 -1
View File
@@ -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.