feat(agc): emulate per-pixel multisample coverage masks

Decode the four PA_SC_AA_MASK fields at the rasterized sample count. Use the native Vulkan mask when all four pixels agree and apply varying masks in the fragment shader, preserving helpers and combining coverage with alpha-to-coverage and shader mask exports. Add the normalized masks to pipeline, draw and shader keys and the versioned replay request.

Refuse unsupported conservative-raster, centroid and early depth/stencil memory-write cases. Synthetic readbacks cover driver submission, depth/stencil suppression, fragment stores and one-sample and four-sample coverage.
This commit is contained in:
fdfawef324 authored and Dean Galvin committed 2026-10-15 13:00:34 +13:00
1 parent 90783f2748
commit 1ffc5f672b
29 files changed
+1015 -42

No files matched your search

@@ -126,6 +126,8 @@ std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Su
product->pixel.targetExportPacking = Graphics::ExportPackings(product->state);
product->pixel.dualSourceBlend = product->state.dualSourceBlend;
product->pixel.alphaToCoverage = product->state.alphaToCoverage;
product->pixel.quadPixelMask = Graphics::QuadPixelMask(product->state);
product->pixel.quadSampleMask = Graphics::QuadSampleMasks(product->state);
if (product->pixel.ancillary) product->pixel.rasterPrimitive = Graphics::RasterPrimitiveOf(product->state);
if (product->pixel.ancillary) product->pixel.layerExported = product->state.layerExports != 0;
product->pixel.ditheredAlphaToCoverage = product->state.ditheredAlphaToCoverage;
@@ -73,6 +73,8 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
mix(pixel.alphaToCoverage);
mix(static_cast<std::uint64_t>(pixel.rasterPrimitive));
mix(pixel.ditheredAlphaToCoverage);
mix(pixel.quadPixelMask);
for (const auto mask : pixel.quadSampleMask) mix(mask);
std::lock_guard cacheLock(drawCacheMutex);
++drawEntryCounters.lookups;
const auto found = drawCache.find(drawKey);
@@ -144,7 +144,8 @@ 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.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage) return false;
if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask) return false;
if (s.samples != t.samples || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false;
return true;
}
@@ -283,6 +283,7 @@ struct VulkanDevice::State {
bool float64DenormPreserve = false;
bool float16DenormPreserve = false;
bool demoteToHelperInvocation = false;
bool earlyFragmentSampleMaskTestBeforeSampleCounting = false;
// VK_EXT_descriptor_indexing with non-uniform image array indexing (bindless image tables in
// graphics stages, and compute workgroups wider than a wave).
bool descriptorIndexing = false;
@@ -1153,6 +1154,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
}
demoteFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DEMOTE_TO_HELPER_INVOCATION_FEATURES_EXT};
demoteFeatures.shaderDemoteToHelperInvocation = VK_TRUE;
if (hasExtension(VK_KHR_MAINTENANCE_5_EXTENSION_NAME)) {
VkPhysicalDeviceMaintenance5PropertiesKHR maintenance5{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_PROPERTIES_KHR};
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &maintenance5};
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &properties);
state->earlyFragmentSampleMaskTestBeforeSampleCounting = maintenance5.earlyFragmentSampleMaskTestBeforeSampleCounting == VK_TRUE;
}
std::vector<const char*> deviceExtensions;
if (window != nullptr) deviceExtensions.assign(presentationExtensions.begin(), presentationExtensions.end());
if (window != nullptr && hasExtension(VK_EXT_HDR_METADATA_EXTENSION_NAME)) {
@@ -3449,6 +3456,7 @@ Graphics::Context VulkanDevice::buildContext() const {
context.placeholderImages = state->placeholderImages.get();
context.viewportIndexLayer = state->viewportIndexLayer;
context.demoteToHelperInvocation = state->demoteToHelperInvocation;
context.earlyFragmentSampleMaskTestBeforeSampleCounting = state->earlyFragmentSampleMaskTestBeforeSampleCounting;
context.primitiveListRestart = state->primitiveListRestart;
context.imageViewMinLod = state->imageViewMinLod;
context.pipelineExecutableInfo = state->pipelineExecutableInfo;
@@ -215,6 +215,7 @@ struct Context {
const PlaceholderImages* placeholderImages = nullptr;
bool viewportIndexLayer = false;
bool demoteToHelperInvocation = false;
bool earlyFragmentSampleMaskTestBeforeSampleCounting = false;
bool primitiveListRestart = false;
bool imageViewMinLod = false;
bool pipelineExecutableInfo = false;
@@ -123,6 +123,8 @@ bool ValidateSampleLocations(const Context& context, const State& state);
void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment);
bool ShadersWriteMemory(std::span<const CompiledShader> shaders);
bool DeclaresEarlyFragmentTests(const ShaderRecompiler::RecompileResult& program);
void ValidateSampleMask(const Context& context, const State& state, std::span<const CompiledShader> shaders);
// Returns the color attachment locations the pixel shader writes.
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false, bool shaderCullDistance = false, bool viewportIndexLayer = false, bool runtimeDescriptorArray = false, bool demoteToHelperInvocation = false);
@@ -11,6 +11,7 @@
namespace AgcDriver::Graphics {
ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array<std::uint8_t, 8>& exportMappings, bool nullProgram = false);
bool PixelEarlyZ(std::uint32_t shaderControl);
ShaderRecompiler::ShaderPixelStageInfo DefaultUnexportedInputs(ShaderRecompiler::ShaderPixelStageInfo pixel, std::span<const std::uint32_t> parameterExports);
ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& shader, std::span<const std::byte> header);
// A guest range DecodeVertexStageInfo read (an attribute word, a vertex V#) with the bytes as read:
@@ -129,6 +129,8 @@ struct State {
bool customSampleLocations = false;
bool sampleLocationGrid = false;
std::uint32_t paClVsOutCntl = 0;
std::array<std::uint16_t, 4> quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu};
bool pixelEarlyZ = false;
};
// PA_CL_VS_OUT_CNTL bits that place position exports (clip and cull distances, the misc and the two
@@ -142,6 +144,11 @@ VkExtent2D SampleLocationGrid(const State& state);
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);
bool PartialSampleMask(const State& state);
bool UniformSampleMask(const State& state);
VkSampleMask HostSampleMask(const State& state);
std::uint8_t QuadPixelMask(const State& state);
std::array<std::uint16_t, 4> QuadSampleMasks(const State& state);
ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot, bool allowMultisampleDcc = false);
std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height);
std::uint32_t ColorWriteMask(const Registers& context);
@@ -1059,6 +1059,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
// Viewport and scissor are dynamic pipeline state, so their limits are checked here per draw.
ValidateViewport(context, state.viewport);
ValidateDepthBounds(context, state);
ValidateSampleMask(context, state, shaders);
timer.phase(PhaseValidate);
inputs.maxIndex = draw.indexed ? 0u : draw.firstVertex + draw.indexCount - 1u;
if (draw.indexed) {
@@ -158,6 +158,20 @@ bool ShadersWriteMemory(std::span<const CompiledShader> shaders) {
return false;
}
void ValidateSampleMask(const Context& context, const State& state, std::span<const CompiledShader> shaders) {
if (!PartialSampleMask(state)) return;
for (const auto& shader : shaders) {
if (shader.stage != ShaderRecompiler::ShaderStage::Fragment) continue;
Require(shader.program != nullptr, "missing compiled fragment shader");
if (UniformSampleMask(state)) {
Require(context.earlyFragmentSampleMaskTestBeforeSampleCounting || !DeclaresEarlyFragmentTests(*shader.program), "a sample mask with early fragment tests needs VkPhysicalDeviceMaintenance5Properties::earlyFragmentSampleMaskTestBeforeSampleCounting, which the device lacks");
continue;
}
const bool tested = state.depth.has_value() && (state.depthTest || state.stencilTest || state.depthBoundsTest);
Require(!state.pixelEarlyZ || !tested || !ShadersWriteMemory(std::span(&shader, 1)), "a sample mask that differs between the pixels of a quad, from a pixel program with early depth or stencil tests that writes memory, is unsupported: the masked pixels are removed in the shader, after the tests");
}
}
namespace {
thread_local bool prebuilding = false;
@@ -242,6 +256,8 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State&
}
appendKey(keys.renderPass, state.depth.has_value());
if (state.depth) appendKey(keys.renderPass, state.depth->format);
appendKey(keys.fragmentShader, HostSampleMask(state));
appendKey(keys.fragmentOutput, HostSampleMask(state));
if (!resources.split) {
appendKey(keys.layout, pushStagesOf(context));
for (const auto word : resources.mainKey) appendKey(keys.layout, word);
@@ -460,6 +476,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
depthStencil.stencilTestEnable = state.stencilTest;
depthStencil.front = state.stencilFront;
depthStencil.back = state.stencilBack;
const VkSampleMask sampleMask = HostSampleMask(state);
if (PartialSampleMask(state) && UniformSampleMask(state)) 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());
@@ -866,6 +884,7 @@ void pipelineKey(std::vector<std::byte>& key, const Context& context, const Stat
append(key, tessellation.primitiveGroupSize);
append(key, tessellation.breakAtInstance);
}
append(key, HostSampleMask(state));
}
std::uint64_t hashKey(const std::vector<std::byte>& key) {
@@ -144,11 +144,6 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
const bool depthExportEnable = (shaderControl & 0x1u) != 0;
const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0;
const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0;
const bool depthBeforeShader = ((shaderControl >> 12u) & 0x1u) != 0;
const auto zOrder = (shaderControl >> 4u) & 0x3u;
const auto aaMask = read(context, 0x30e, RegisterBank::Context);
const auto aaMaskBottom = read(context, 0x30f, RegisterBank::Context);
const auto quadPixelMask = static_cast<std::uint8_t>((aaMask & 1u) | (((aaMask >> 16u) & 1u) << 1u) | ((aaMaskBottom & 1u) << 2u) | (((aaMaskBottom >> 16u) & 1u) << 3u));
const auto loaded = [&](PixelInput input) { return (activeInputs & PixelInputBit(input)) != 0; };
return ShaderRecompiler::ShaderPixelStageInfo{
.interpolatorCount = inputNum,
@@ -170,17 +165,26 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
.pixelKillEnable = pixelKillEnable,
.depthExportEnable = depthExportEnable,
.sampleMaskExportEnable = sampleMaskExportEnable,
.earlyZ = depthBeforeShader || ((zOrder & 1u) != 0u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable),
.earlyZ = PixelEarlyZ(shaderControl),
.executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0,
.conservativeZExport = static_cast<ShaderRecompiler::ConservativeZExport>(conservativeZExport),
.orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0,
.targetOutputMode = targetOutputMode,
.targetExportMapping = nullProgram ? std::array<std::uint8_t, 8>{} : exportMappings,
.stencilRefExportEnable = stencilRefExportEnable,
.quadPixelMask = quadPixelMask
.stencilRefExportEnable = stencilRefExportEnable
};
}
bool PixelEarlyZ(std::uint32_t shaderControl) {
const bool pixelKillEnable = ((shaderControl >> 6u) & 0x1u) != 0;
const bool depthExportEnable = (shaderControl & 0x1u) != 0;
const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0;
const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0;
const bool depthBeforeShader = ((shaderControl >> 12u) & 0x1u) != 0;
const auto zOrder = (shaderControl >> 4u) & 0x3u;
return depthBeforeShader || ((zOrder & 1u) != 0u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable);
}
ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const std::byte> header, std::uint64_t headerAddress, std::span<const std::uint32_t> userData, std::vector<DecodeRead>* reads, bool staticAbi, std::uint32_t hiddenUserWords) {
if (header.empty()) return {};
if (header.size() < sizeof(Shader)) throw std::runtime_error("AGC graphics: shader header is smaller than the fixed AGC header");
@@ -146,7 +146,7 @@ struct Module {
Require(value == spv::BuiltInPosition && signature == "f32x4" && !position, "unsupported or duplicate vertex built-in output");
position = true;
} else {
Require((storage == spv::StorageClassInput && ((value == spv::BuiltInFragCoord && signature == "f32x4") || ((value == spv::BuiltInFrontFacing || value == spv::BuiltInHelperInvocation) && signature == "bool") || ((value == spv::BuiltInSampleId || value == spv::BuiltInLayer) && signature == "i32"))) || (storage == spv::StorageClassOutput && ((value == spv::BuiltInFragDepth && signature == "f32") || (value == spv::BuiltInFragStencilRefEXT && signature == "i32") || (value == spv::BuiltInSampleMask && (signature == "i32[1]" || signature == "u32[1]")))), "unsupported fragment built-in");
Require((storage == spv::StorageClassInput && ((value == spv::BuiltInFragCoord && signature == "f32x4") || ((value == spv::BuiltInFrontFacing || value == spv::BuiltInHelperInvocation) && signature == "bool") || ((value == spv::BuiltInSampleId || value == spv::BuiltInLayer) && signature == "i32") || (value == spv::BuiltInSampleMask && signature == "i32[1]"))) || (storage == spv::StorageClassOutput && ((value == spv::BuiltInFragDepth && signature == "f32") || (value == spv::BuiltInFragStencilRefEXT && signature == "i32") || (value == spv::BuiltInSampleMask && (signature == "i32[1]" || signature == "u32[1]")))), "unsupported fragment built-in");
}
}
};
@@ -601,6 +601,20 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
}
bool DeclaresEarlyFragmentTests(const ShaderRecompiler::RecompileResult& program) {
const auto& words = program.spirv.Words();
Require(words.size() >= 5 && words[0] == spv::MagicNumber, "the fragment shader is not a SPIR-V module");
for (std::size_t cursor = 5; cursor < words.size();) {
const auto count = words[cursor] >> 16u;
Require(count != 0 && count <= words.size() - cursor, "truncated SPIR-V instruction");
const auto op = static_cast<spv::Op>(words[cursor] & 0xffffu);
if (op == spv::OpFunction) return false;
if (op == spv::OpExecutionMode && count >= 3 && words[cursor + 2] == spv::ExecutionModeEarlyFragmentTests) return true;
cursor += count;
}
return false;
}
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation) {
using Stage = ShaderRecompiler::ShaderStage;
const bool tessellation = state.stages.path == ShaderPath::Tessellation;
@@ -1,5 +1,6 @@
#include <cstdio>
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp"
@@ -410,6 +411,40 @@ bool coveredCentroidSupported(const QueueState& queue) {
return ena != queue.context.end() && addr != queue.context.end() && (ena->second & addr->second & CentroidInputs) == 0;
}
std::array<std::uint32_t, 4> sampleMaskFields(std::uint32_t top, std::uint32_t bottom) {
return {top & 0xffffu, top >> 16u, bottom & 0xffffu, bottom >> 16u};
}
std::string sampleMaskRejection(const QueueState& queue, std::uint32_t config, std::uint32_t top, std::uint32_t bottom) {
const auto log2 = config & 7u;
if (log2 > 3u) return {};
const auto full = (1u << (1u << log2)) - 1u;
auto masks = sampleMaskFields(top, bottom);
for (auto& mask : masks) mask &= full;
if (std::ranges::all_of(masks, [&](std::uint32_t mask) { return mask == full; })) return {};
const bool uniform = std::ranges::all_of(masks, [&](std::uint32_t mask) { return mask == masks[0]; });
const bool wholePixels = std::ranges::all_of(masks, [&](std::uint32_t mask) { return mask == 0u || mask == full; });
char detail[96];
std::snprintf(detail, sizeof(detail), " (PA_SC_AA_MASK_X0Y0_X1Y0=0x%08x, PA_SC_AA_MASK_X0Y1_X1Y1=0x%08x)", top, bottom);
const auto conservative = find(queue.context, 0x313);
if (conservative != queue.context.end() && (conservative->second & ~0x001f6000u) != 0) return std::string("AGC graphics: sample masks with conservative rasterization are unsupported") + detail;
if (!wholePixels && uniform && !PixelProgramSkipped(queue)) {
const auto ena = find(queue.context, 0x1b3);
const auto addr = find(queue.context, 0x1b4);
if (ena != queue.context.end() && addr != queue.context.end() && (ena->second & addr->second & CentroidInputs) != 0) return std::string("AGC graphics: sample masks that remove some samples of a pixel with centroid interpolation are unsupported") + detail;
}
return {};
}
std::array<std::uint16_t, 4> decodeQuadSampleMask(const Registers& cx, std::uint32_t samples) {
const bool singleSample = (read(cx, 0x2f8) & 7u) == 0u;
const auto full = (1u << samples) - 1u;
const auto fields = sampleMaskFields(read(cx, 0x30e), read(cx, 0x30f));
std::array<std::uint16_t, 4> masks{};
for (std::size_t pixel = 0; pixel < masks.size(); ++pixel) masks[pixel] = static_cast<std::uint16_t>(singleSample ? ((fields[pixel] & 1u) != 0u ? full : 0u) : fields[pixel] & full);
return masks;
}
std::uint32_t decodeSamples(const Registers& cx) {
const auto config = read(cx, 0x2f8);
zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u | CoveredCentroidIsCenter), "coverage conversion or nonstandard multisampling");
@@ -816,7 +851,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
Require(read(cx, 0x83) == 0xffffu, "clip rectangles are unsupported");
Require((read(cx, 0x8c) & 0xfu) == 0xau, "nonstandard triangle edge rules are unsupported");
Require(read(cx, 0x2f9) == 0x2du, "nonstandard pixel center or vertex quantization is unsupported");
Require((read(cx, 0x2f8) & 7u) == 0 || (read(cx, 0x30e) == 0xffffffffu && read(cx, 0x30f) == 0xffffffffu), "sample masks of multisampled draws are unsupported");
if (auto reason = sampleMaskRejection(queue, read(cx, 0x2f8), read(cx, 0x30e), read(cx, 0x30f)); !reason.empty()) throw std::runtime_error(reason);
const auto viewportControl = read(cx, 0x206);
if (viewportControl != 0x43fu) throw std::runtime_error(vteMessage(viewportControl));
zero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags");
@@ -903,6 +938,8 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
APS5_LOG_OUT_DEBUG("Render extent from screen=%ux%u", result.renderExtent.width, result.renderExtent.height);
Require(result.renderExtent.width != 0 && result.renderExtent.height != 0, "empty framebuffer extent for a draw without color writes");
}
result.quadSampleMask = decodeQuadSampleMask(cx, result.samples);
result.pixelEarlyZ = !PixelProgramSkipped(queue) && PixelEarlyZ(read(cx, 0x203));
if ((read(cx, 0x207) & LayerExports & ~result.layerExports & ~(1u << 21u)) != 0) {
static bool reported = false;
if (!reported) {
@@ -1037,6 +1074,43 @@ std::array<ShaderRecompiler::ColorExportPacking, 8> ExportPackings(const State&
return packings;
}
bool PartialSampleMask(const State& state) {
const auto full = (1u << state.samples) - 1u;
return std::ranges::any_of(state.quadSampleMask, [&](std::uint16_t mask) { return (mask & full) != full; });
}
bool UniformSampleMask(const State& state) {
const auto full = (1u << state.samples) - 1u;
return std::ranges::all_of(state.quadSampleMask, [&](std::uint16_t mask) { return (mask & full) == (state.quadSampleMask[0] & full); });
}
VkSampleMask HostSampleMask(const State& state) {
const auto full = (1u << state.samples) - 1u;
return UniformSampleMask(state) ? state.quadSampleMask[0] & full : full;
}
std::uint8_t QuadPixelMask(const State& state) {
if (!PartialSampleMask(state) || UniformSampleMask(state)) return 0xfu;
const auto full = (1u << state.samples) - 1u;
std::uint8_t pixels = 0;
for (std::uint32_t pixel = 0; pixel < state.quadSampleMask.size(); ++pixel) {
if ((state.quadSampleMask[pixel] & full) != 0u) pixels = static_cast<std::uint8_t>(pixels | (1u << pixel));
}
return pixels;
}
std::array<std::uint16_t, 4> QuadSampleMasks(const State& state) {
std::array<std::uint16_t, 4> masks{0xffffu, 0xffffu, 0xffffu, 0xffffu};
if (!PartialSampleMask(state) || UniformSampleMask(state)) return masks;
const auto full = (1u << state.samples) - 1u;
if (std::ranges::all_of(state.quadSampleMask, [&](std::uint16_t mask) { return (mask & full) == 0u || (mask & full) == full; })) return masks;
for (std::size_t pixel = 0; pixel < masks.size(); ++pixel) {
const auto mask = state.quadSampleMask[pixel] & full;
masks[pixel] = static_cast<std::uint16_t>(mask == full ? 0xffffu : mask);
}
return masks;
}
ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state) {
if (state.rectList) return ShaderRecompiler::RasterPrimitive::Rectangle;
if (state.stages.mesh.has_value() || state.stages.tessellation.has_value()) return ShaderRecompiler::RasterPrimitive::Triangle;
@@ -1390,8 +1464,10 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
if (value(cx, 0x8c, word) && (word & 0xfu) != 0xau) return require(false, "nonstandard triangle edge rules are unsupported");
if (value(cx, 0x2f9, word) && word != 0x2du) return require(false, "nonstandard pixel center or vertex quantization is unsupported");
if (value(cx, 0x313, word) && (word & ~0x001f6000u) != 0 && word != 0x6001u) return conservativeMessage(word);
std::uint32_t other = 0;
if (value(cx, 0x2f8, word) && (word & 7u) != 0 && value(cx, 0x30e, word) && value(cx, 0x30f, other) && (word != 0xffffffffu || other != 0xffffffffu)) return require(false, "sample masks of multisampled draws are unsupported");
std::uint32_t config = 0, other = 0;
if (value(cx, 0x2f8, config) && value(cx, 0x30e, word) && value(cx, 0x30f, other)) {
if (auto reason = sampleMaskRejection(queue, config, word, other); !reason.empty()) return reason;
}
if (value(cx, 0x206, word) && word != 0x43fu) return vteMessage(word);
if (auto reason = nonzero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); !reason.empty()) return reason;
std::uint32_t targetMask = 0, shaderMask = 0;
+148 -12
View File
@@ -2781,18 +2781,149 @@ void framebufferLayerTests() {
}
void quadPixelMaskTests() {
auto queue = makeState();
queue.context[0x30e] = 0xffff0000u;
queue.context[0x30f] = 0u;
queue.context[0x1b3] = 2;
queue.context[0x1b4] = 2;
static_cast<void>(AgcDriver::Graphics::DecodeState(queue));
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a single-sample quad pixel mask was refused by the precheck");
Require(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, {}).quadPixelMask == 0x2u, "PA_SC_AA_MASK did not decode to the covered quad pixel");
queue = makeState();
queue.context[0x1b3] = 2;
queue.context[0x1b4] = 2;
Require(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, {}).quadPixelMask == 0xfu, "full sample masks did not cover the whole quad");
using AgcDriver::Graphics::DecodeState;
using AgcDriver::Graphics::DrawRejection;
using AgcDriver::Graphics::HostSampleMask;
using AgcDriver::Graphics::PartialSampleMask;
using AgcDriver::Graphics::QuadPixelMask;
using AgcDriver::Graphics::UniformSampleMask;
using Masks = std::array<std::uint16_t, 4>;
const auto masked = [](std::uint32_t top, std::uint32_t bottom, std::uint32_t log2 = 0u) {
auto queue = makeState();
queue.context[0x30e] = top;
queue.context[0x30f] = bottom;
queue.context[0x1b3] = 2;
queue.context[0x1b4] = 2;
if (log2 != 0u) {
queue.context[0x2f8] = (log2 << 20u) | log2;
queue.context[0x292] = 3;
queue.context[0x3b8] = 0x0dc00000u;
queue.context[0x31d] = (log2 << 12u) | (log2 << 15u);
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = log2 == 1u ? 0xcc44u : 0x622ae6aeu;
}
return queue;
};
const auto accepted = [](const AgcDriver::QueueState& queue, const std::string& what) {
std::vector<AgcDriver::Graphics::RegisterRead> reads;
AgcDriver::Graphics::RegisterReadLog() = &reads;
const auto state = DecodeState(queue);
const auto rejection = DrawRejection(queue, false);
AgcDriver::Graphics::RegisterReadLog() = nullptr;
Require(rejection.empty(), what + " was refused by the precheck: " + rejection);
for (const auto read : reads) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a register decoded for " + what);
return state;
};
const auto refused = [](const AgcDriver::QueueState& queue, std::string_view reason) {
expectFailure([&] { DecodeState(queue); }, reason);
Require(DrawRejection(queue, false).find(reason) != std::string::npos, "the precheck accepted a draw DecodeState refuses for " + std::string(reason));
};
auto state = accepted(masked(0x0000ffffu, 0u), "the X0Y0 single-sample mask");
Require(state.quadSampleMask == Masks{1, 0, 0, 0} && PartialSampleMask(state) && !UniformSampleMask(state) && QuadPixelMask(state) == 0x1u, "PA_SC_AA_MASK 0x0000ffff/0 did not cover only the even-x, even-y pixel of each quad");
state = accepted(masked(0xffff0000u, 0u), "the X1Y0 single-sample mask");
Require(QuadPixelMask(state) == 0x2u, "PA_SC_AA_MASK 0xffff0000/0 did not cover only the odd-x, even-y pixel of each quad");
state = accepted(masked(0xfffe0001u, 0x0001fffeu), "a single-sample mask with bits above sample 0");
Require(state.quadSampleMask == Masks{1, 0, 0, 1} && QuadPixelMask(state) == 0x9u, "a single-sample mask did not keep only bit 0 of each pixel's field");
state = accepted(masked(0x00010001u, 0x00010001u), "a single-sample mask with only sample 0 set");
Require(!PartialSampleMask(state) && QuadPixelMask(state) == 0xfu && HostSampleMask(state) == 1u, "PA_SC_AA_MASK 0x00010001 at one sample is not a full mask");
state = accepted(masked(0u, 0u), "an empty single-sample mask");
Require(PartialSampleMask(state) && UniformSampleMask(state) && HostSampleMask(state) == 0u && QuadPixelMask(state) == 0xfu, "an empty mask equal for the whole quad did not become a host sample mask of 0");
Require(AgcDriver::Graphics::DecodePixelStageInfo(masked(0x0000ffffu, 0u).context, {}).quadPixelMask == 0xfu, "the pixel stage decode read the sample mask registers instead of the decoded state");
auto queue = masked(0x0000ffffu, 0u);
queue.context[0x203] = 0x10u;
Require(accepted(queue, "an early-Z masked draw").pixelEarlyZ, "DB_SHADER_CONTROL Z_ORDER EARLY_Z_THEN_LATE_Z did not decode as early Z");
queue.context[0x203] = 0x50u;
Require(!accepted(queue, "a killing masked draw").pixelEarlyZ, "a pixel program with kill decoded as early Z");
queue.context[0x313] = 0x6001u;
refused(queue, "sample masks with conservative rasterization");
queue = masked(0xffffffffu, 0xffffffffu);
queue.context[0x313] = 0x6001u;
Require(!PartialSampleMask(accepted(queue, "a full mask with conservative rasterization")), "a full mask with conservative rasterization decoded as partial");
state = accepted(masked(0x00050005u, 0x00050005u, 2u), "a 4x mask equal for the whole quad");
Require(state.samples == 4 && PartialSampleMask(state) && UniformSampleMask(state) && HostSampleMask(state) == 0x5u && QuadPixelMask(state) == 0xfu, "a 4x mask 0x5 equal for the whole quad did not become the host sample mask 0x5");
state = accepted(masked(0xfff5fff5u, 0xfff5fff5u, 2u), "a 4x mask with bits above sample 3");
Require(HostSampleMask(state) == 0x5u && state.quadSampleMask == Masks{5, 5, 5, 5}, "a 4x mask kept bits of samples the rasterizer does not have");
state = accepted(masked(0x0000000fu, 0u, 2u), "a 4x mask that covers whole pixels");
Require(!UniformSampleMask(state) && QuadPixelMask(state) == 0x1u && state.quadSampleMask == Masks{0xf, 0, 0, 0}, "a 4x mask covering all samples of one quad pixel did not become a quad pixel mask");
state = accepted(masked(0x00000003u, 0u, 1u), "a 2x mask that covers whole pixels");
Require(QuadPixelMask(state) == 0x1u, "a 2x mask covering both samples of one quad pixel did not become a quad pixel mask");
using AgcDriver::Graphics::QuadSampleMasks;
constexpr Masks NoSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu};
state = accepted(masked(0x00020001u, 0x00080004u, 2u), "a 4x mask with one sample per quad pixel");
Require(QuadPixelMask(state) == 0xfu && QuadSampleMasks(state) == Masks{1, 2, 4, 8}, "a 4x mask of sample q for quad pixel q did not become per-pixel sample masks");
state = accepted(masked(0x0000000fu, 0x00030000u, 2u), "a 4x mask mixing full, empty and partial pixels");
Require(QuadPixelMask(state) == 0x9u && QuadSampleMasks(state) == Masks{0xffff, 0, 0, 3}, "a 4x mask mixing full, empty and partial pixels did not keep full pixels as all samples");
Require(QuadSampleMasks(accepted(masked(0x0000000fu, 0u, 2u), "a whole-pixel 4x mask")) == NoSampleMask && QuadSampleMasks(accepted(masked(0x00050005u, 0x00050005u, 2u), "a uniform 4x mask")) == NoSampleMask && QuadSampleMasks(accepted(masked(0x0000ffffu, 0u), "a single-sample mask")) == NoSampleMask, "a mask without partial pixels that differ produced per-pixel sample masks");
for (const auto centroid : {4u, 64u}) {
queue = masked(0x00020001u, 0x00080004u, 2u);
queue.context[0x1b3] = 2u | centroid;
queue.context[0x1b4] = 2u | centroid;
Require(QuadSampleMasks(accepted(queue, "centroid interpolation under per-pixel sample masks")) == Masks{1, 2, 4, 8}, "centroid interpolation refused per-pixel sample masks");
}
for (const auto centroid : {4u, 64u}) {
queue = masked(0x00050005u, 0x00050005u, 2u);
queue.context[0x1b3] = 2u | centroid;
queue.context[0x1b4] = 2u | centroid;
refused(queue, "sample masks that remove some samples of a pixel with centroid interpolation");
queue.context[0x1b4] = 2u;
Require(HostSampleMask(accepted(queue, "an unaddressed centroid input")) == 0x5u, "an unaddressed centroid input refused a 4x mask");
queue = masked(0x0000000fu, 0u, 2u);
queue.context[0x1b3] = 2u | centroid;
queue.context[0x1b4] = 2u | centroid;
Require(QuadPixelMask(accepted(queue, "centroid interpolation under a whole-pixel mask")) == 0x1u, "centroid interpolation refused a mask that covers whole pixels");
}
queue = masked(0x0000ffffu, 0u);
queue.context[0x31d] = 0x12000;
state = accepted(queue, "a single-sample mask into a 4x target");
Require(state.samples == 4 && state.quadSampleMask == Masks{0xf, 0, 0, 0} && QuadPixelMask(state) == 0x1u, "single-sample rasterization into a 4x target did not apply each pixel's sample 0 bit to all of its samples");
queue.context[0x30e] = queue.context[0x30f] = 0xfffefffeu;
state = accepted(queue, "an empty single-sample mask into a 4x target");
Require(UniformSampleMask(state) && HostSampleMask(state) == 0u, "an empty single-sample mask into a 4x target did not remove every sample");
AgcDriver::Graphics::Context context{};
ShaderRecompiler::RecompileResult early;
early.spirv = std::vector<std::uint32_t>{spv::MagicNumber, 0x10300u, 0u, 2u, 0u, (3u << 16u) | spv::OpExecutionMode, 1u, spv::ExecutionModeEarlyFragmentTests, (5u << 16u) | spv::OpFunction, 0u, 1u, 0u, 0u};
ShaderRecompiler::RecompileResult late;
late.spirv = std::vector<std::uint32_t>{spv::MagicNumber, 0x10300u, 0u, 2u, 0u, (3u << 16u) | spv::OpExecutionMode, 1u, spv::ExecutionModeOriginUpperLeft};
Require(AgcDriver::Graphics::DeclaresEarlyFragmentTests(early) && !AgcDriver::Graphics::DeclaresEarlyFragmentTests(late), "early fragment tests were not read from the SPIR-V execution modes");
using Stage = ShaderRecompiler::ShaderStage;
AgcDriver::Graphics::State uniform{};
uniform.samples = 4;
uniform.quadSampleMask = {5, 5, 5, 5};
const std::array<AgcDriver::Graphics::CompiledShader, 1> earlyShaders{{{Stage::Fragment, &early, 0}}};
const std::array<AgcDriver::Graphics::CompiledShader, 1> lateShaders{{{Stage::Fragment, &late, 0}}};
expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, uniform, earlyShaders); }, "earlyFragmentSampleMaskTestBeforeSampleCounting");
AgcDriver::Graphics::ValidateSampleMask(context, uniform, lateShaders);
context.earlyFragmentSampleMaskTestBeforeSampleCounting = true;
AgcDriver::Graphics::ValidateSampleMask(context, uniform, earlyShaders);
ShaderRecompiler::RecompileResult stores;
ShaderRecompiler::DescriptorBinding written{};
written.kind = ShaderRecompiler::DescriptorKind::StorageBuffer;
written.role = ShaderRecompiler::DescriptorRole::GuestBuffers;
written.count = 1;
written.bufferWritten = {true};
stores.bindings.push_back(written);
const std::array<AgcDriver::Graphics::CompiledShader, 2> storing{{{Stage::Vertex, &late, 0}, {Stage::Fragment, &stores, 0}}};
const std::array<AgcDriver::Graphics::CompiledShader, 2> vertexStoring{{{Stage::Vertex, &stores, 0}, {Stage::Fragment, &late, 0}}};
AgcDriver::Graphics::State varying{};
varying.quadSampleMask = {1, 0, 0, 0};
varying.pixelEarlyZ = true;
varying.depth = AgcDriver::Graphics::DepthTarget{};
varying.depthTest = true;
expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, varying, storing); }, "differs between the pixels of a quad");
AgcDriver::Graphics::ValidateSampleMask(context, varying, vertexStoring);
for (const auto change : {0, 1, 2}) {
auto relaxed = varying;
if (change == 0) relaxed.pixelEarlyZ = false;
if (change == 1) relaxed.depthTest = false;
if (change == 2) relaxed.quadSampleMask = {1, 1, 1, 1};
AgcDriver::Graphics::ValidateSampleMask(context, relaxed, storing);
}
varying.depthTest = false;
varying.stencilTest = true;
expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, varying, storing); }, "differs between the pixels of a quad");
varying.stencilTest = false;
varying.depthBoundsTest = true;
expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, varying, storing); }, "differs between the pixels of a quad");
}
void DepthClipTests() {
@@ -5120,6 +5251,11 @@ void earlyFragmentTestsTests() {
request.context.pixel->alphaToCoverage = true;
const auto replayed = serializer.Deserialize(serializer.Serialize(request));
Require(!declaresEarlyFragmentTests(request) && !declaresEarlyFragmentTests(replayed.request), "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with alpha-to-coverage declared early fragment tests");
request = depthExportRequest(shaderControl);
request.context.pixel->quadPixelMask = 0x1u;
const auto masked = serializer.Deserialize(serializer.Serialize(request));
Require(!declaresEarlyFragmentTests(request) && !declaresEarlyFragmentTests(masked.request), "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with a quad pixel mask declared early fragment tests");
Require(AgcDriver::Graphics::DeclaresEarlyFragmentTests(ShaderRecompiler::Recompile(depthExportRequest(shaderControl))), "the driver did not read early fragment tests from a recompiled pixel shader");
}
}
@@ -2605,6 +2605,12 @@ void verifyMeshConfiguration() {
std::vector<std::uint64_t> full;
RecompileCacheKey::Build(fragment, full);
require(masked != full, "the quad pixel mask is not part of the recompile cache key");
fragment.context.pixel->quadSampleMask = {0x1u, 0x2u, 0x0u, 0xffffu};
const auto sampled = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(fragment));
require(sampled.request.context.pixel.has_value() && sampled.request.context.pixel->quadSampleMask == std::array<std::uint16_t, 4>{0x1u, 0x2u, 0x0u, 0xffffu}, "the quad sample masks were lost in serialization");
std::vector<std::uint64_t> partial;
RecompileCacheKey::Build(fragment, partial);
require(partial != full, "the quad sample masks are not part of the recompile cache key");
}
std::vector<std::uint64_t> key;
RecompileCacheKey::Build(request, key);
@@ -2986,7 +2992,7 @@ void verifyPixelRequestSerialization() {
minimal.context.pixel->layerExported = true;
minimal.context.pixel->ditheredAlphaToCoverage = true;
const auto encoded = serializer.Serialize(minimal);
require(requestPrefix(encoded, 8u) == "NVNQQRwAAAA=", "new requests did not use serialization version 28");
require(requestPrefix(encoded, 8u) == "NVNQQR0AAAA=", "new requests did not use serialization version 29");
const std::array<std::uint32_t, 2> fallbacks{0x40u, 0x1ac4u};
auto withFallbacks = minimal;
withFallbacks.context.linkedCallFallbacks = fallbacks;
@@ -3101,10 +3107,10 @@ void verifyPixelRequestSerialization() {
require(RecompileCacheKey::ContextHash(legacy19.request) == RecompileCacheKey::ContextHash(upgraded19.request), "upgrading version19 changed shader identity");
}
constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u;
for (std::size_t bytes = 0; bytes < 25u; ++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, or quad pixel mask was accepted");
for (std::size_t bytes = 0; bytes < 41u; ++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=", "NVNQQR0AAAA="}) {
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQR4AAAA="}) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted");
}
static constexpr std::string_view version18Request{
@@ -3229,7 +3235,7 @@ void verifyVertexInfoVersion19() {
std::size_t allBitsOffset = 0;
while (allBitsOffset < version26Pixel.size() && version26Pixel[allBitsOffset] == allBitsBytes[allBitsOffset]) ++allBitsOffset;
require(allBitsOffset == flagOffset + 2u, "the front-face all-bits flag does not follow the dithered coverage flag");
version26Pixel.erase(version26Pixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset), version26Pixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset + 2u));
version26Pixel.erase(version26Pixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset), version26Pixel.begin() + static_cast<std::ptrdiff_t>(allBitsOffset + 2u + 16u));
version26Pixel[4] = 26u;
const auto replay26 = serializer.Deserialize(encodeBase64(version26Pixel));
require(replay26.request.context.pixel.has_value() && replay26.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replay26.request.context.pixel->frontFaceAllBits, "a version 26 request lost its pixel primitive class or read a front-face all-bits flag");
@@ -3240,12 +3246,21 @@ void verifyVertexInfoVersion19() {
std::size_t maskOffset = 0;
while (maskOffset < legacyPixel.size() && legacyPixel[maskOffset] == maskedBytes[maskOffset]) ++maskOffset;
require(maskOffset == flagOffset + 3u && maskOffset < legacyPixel.size(), "the quad pixel mask is not the byte after the front-face all-bits flag");
constexpr std::size_t sampleMaskBytes = 16u;
auto sampledPixel = maskedPixel;
sampledPixel.context.pixel->quadSampleMask = {0x1u, 0x2u, 0x4u, 0x8u};
auto version28 = decodeBase64(serializer.Serialize(sampledPixel));
require(std::equal(maskedBytes.begin(), maskedBytes.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u), version28.begin()) && version28[maskOffset + 1u] == 0x1u && version28[maskOffset + 5u] == 0x2u && version28[maskOffset + 9u] == 0x4u && version28[maskOffset + 13u] == 0x8u, "the quad sample masks are not the four words after the quad pixel mask");
version28.erase(version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u), version28.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes));
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} && decodeBase64(serializer.Serialize(replay28.request)) == maskedBytes, "a version 28 request read quad sample masks or misread the fields after them");
auto version27 = maskedBytes;
version27.erase(version27.begin() + static_cast<std::ptrdiff_t>(maskOffset));
version27.erase(version27.begin() + static_cast<std::ptrdiff_t>(maskOffset), version27.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes));
version27[4] = 27u;
const auto replay27 = serializer.Deserialize(encodeBase64(version27));
require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or misread the fields after it");
legacyPixel.erase(legacyPixel.begin() + static_cast<std::ptrdiff_t>(flagOffset), legacyPixel.begin() + static_cast<std::ptrdiff_t>(flagOffset + 4u));
legacyPixel.erase(legacyPixel.begin() + static_cast<std::ptrdiff_t>(flagOffset), legacyPixel.begin() + static_cast<std::ptrdiff_t>(flagOffset + 4u + sampleMaskBytes));
legacyPixel.resize(legacyPixel.size() - sizeof(std::uint64_t));
for (const auto version : {19u, 20u, 21u, 22u}) {
auto versionPixel = legacyPixel;
@@ -0,0 +1,215 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <cstdio>
#include <functional>
#include <iostream>
#include <span>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Width = 64;
constexpr std::uint32_t Height = 16;
constexpr std::uint32_t WaveSize = 64;
constexpr std::uint32_t EarlyZThenLateZ = 0x10u;
constexpr std::uint8_t Reference = 0x5au;
constexpr std::byte Kept{0x40};
alignas(256) std::array<std::byte, Width * Height * 4> Pixels{};
alignas(256) std::array<std::byte, 128 * 128 * 4> DepthMemory{};
alignas(256) std::array<std::byte, 256 * 256> StencilMemory{};
alignas(256) std::array<std::uint32_t, Width * Height> Stores{};
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 4> PlainPixelCode{
0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 12> StoringPixelCode{
0x7e040f00, 0x7e060f01, 0x34060686, 0x4a040702, 0x34040482, 0x7e080281, 0xe0705000, 0x80000402,
0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000,
};
using Triangle = std::array<std::array<float, 4>, 3>;
alignas(256) constexpr Triangle Middle{{{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}}};
alignas(256) constexpr Triangle Near{{{-1.0f, -1.0f, 0.25f, 1.0f}, {3.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 3.0f, 0.25f, 1.0f}}};
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, stride == 0u ? 0x31016facu : 0x01016facu};
}
struct Pass {
std::array<std::uint16_t, 4> quadSampleMask{1, 1, 1, 1};
const Triangle* triangle = &Middle;
std::span<const std::uint32_t> pixelCode = PlainPixelCode;
std::uint32_t shaderControl = EarlyZThenLateZ;
bool depthTest = false;
bool depthWrite = false;
VkCompareOp depthCompare = VK_COMPARE_OP_ALWAYS;
VkStencilOpState stencil{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0u, 0u};
};
void Draw(AgcDriver::VulkanDevice& device, const Pass& pass) {
Pixels.fill(Kept);
Stores.fill(0);
const auto target = device.Target();
std::vector<std::uint32_t> vertexUserData(4, 0u);
const auto vertexBuffer = BufferDescriptor(pass.triangle->data(), 16u, static_cast<std::uint32_t>(pass.triangle->size()));
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}};
ShaderRecompiler::RecompileRequest vertex{
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
{WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
target,
{0, 0, 0, 64}
};
vertex.useCache = false;
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
AgcDriver::Graphics::State state{};
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt};
state.color = {reinterpret_cast<std::uintptr_t>(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u};
state.colors = {state.color};
state.hasColorTarget = true;
state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast<std::uintptr_t>(DepthMemory.data()), reinterpret_cast<std::uintptr_t>(StencilMemory.data()), {Width, Height}, VK_FORMAT_D32_SFLOAT_S8_UINT, 0.0f, 0};
state.depthTest = pass.depthTest;
state.depthWrite = pass.depthWrite;
state.depthCompare = pass.depthCompare;
state.stencilTest = true;
state.stencilFront = pass.stencil;
state.stencilBack = pass.stencil;
state.renderExtent = {Width, Height};
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1};
state.negativeOneToOne = false;
state.scissor = {{0, 0}, {Width, Height}};
state.cullMode = VK_CULL_MODE_NONE;
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
state.blend.colorWriteMask = 15;
state.blends = {state.blend};
state.blendConstants = {};
state.quadSampleMask = pass.quadSampleMask;
state.pixelEarlyZ = AgcDriver::Graphics::PixelEarlyZ(pass.shaderControl);
const AgcDriver::Registers context{{0x1b3, 0x300u}, {0x1b4, 0x300u}, {0x1b6, 0u}, {0x1c5, 9u}, {0x203, pass.shaderControl}};
auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state));
Require(pixel.earlyZ == state.pixelEarlyZ, "the pixel stage and the decoded state disagree on early Z");
pixel.quadPixelMask = AgcDriver::Graphics::QuadPixelMask(state);
std::vector<std::uint32_t> pixelUserData(4, 0u);
const auto stores = BufferDescriptor(Stores.data(), 0u, static_cast<std::uint32_t>(sizeof(Stores)));
std::copy(stores.begin(), stores.end(), pixelUserData.begin());
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(pass.pixelCode.data()), std::as_bytes(pass.pixelCode)}}};
ShaderRecompiler::RecompileRequest fragment{
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pass.pixelCode.data()), pass.pixelCode, 0, {}},
{WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
target,
PixelPushLayout(vertexPush, target)
};
fragment.useCache = false;
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
{ShaderStage::Vertex, &vertexResult, 0},
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}
}};
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(pass.triangle->size()), 0, 1, 0, false};
device.Draw(state, draw, shaders);
device.WaitIdle();
}
void Check(const std::function<bool(std::uint32_t, std::uint32_t)>& drawn, const std::string& what) {
for (std::uint32_t y = 0; y < Height; ++y) {
for (std::uint32_t x = 0; x < Width; ++x) {
const auto expected = drawn(x, y) ? std::byte{255} : Kept;
const auto actual = Pixels[(y * Width + x) * 4u];
Require(actual == expected, what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + std::to_string(static_cast<unsigned>(actual)) + ", expected " + (drawn(x, y) ? "drawn" : "kept"));
}
}
}
bool Covered(std::uint8_t quadPixelMask, std::uint32_t x, std::uint32_t y) {
return ((quadPixelMask >> ((x & 1u) | ((y & 1u) << 1u))) & 1u) != 0u;
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
const VkStencilOpState replace{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, Reference};
const VkStencilOpState equal{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, Reference};
const VkStencilOpState always{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0u, 0u};
const VkStencilOpState zero{VK_STENCIL_OP_ZERO, VK_STENCIL_OP_ZERO, VK_STENCIL_OP_ZERO, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 0u};
for (const std::uint8_t quadPixelMask : {std::uint8_t{0x1}, std::uint8_t{0x2}, std::uint8_t{0x9}}) {
const auto mask = "quad pixel mask " + std::to_string(quadPixelMask);
Pass reset;
reset.triangle = &Near;
reset.depthTest = true;
reset.depthWrite = true;
reset.stencil = zero;
Draw(*device, reset);
Check([](std::uint32_t, std::uint32_t) { return true; }, mask + ", the full-screen draw that resets depth to 0.25 and stencil to 0");
Pass gbuffer;
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) gbuffer.quadSampleMask[pixel] = static_cast<std::uint16_t>((quadPixelMask >> pixel) & 1u);
gbuffer.depthTest = true;
gbuffer.depthWrite = true;
gbuffer.depthCompare = VK_COMPARE_OP_GREATER_OR_EQUAL;
gbuffer.stencil = replace;
Draw(*device, gbuffer);
Check([&](std::uint32_t x, std::uint32_t y) { return Covered(quadPixelMask, x, y); }, mask + ", the early-Z GEQUAL draw with depth writes and stencil REPLACE");
Pass stencilProbe;
stencilProbe.stencil = equal;
Draw(*device, stencilProbe);
Check([&](std::uint32_t x, std::uint32_t y) { return Covered(quadPixelMask, x, y); }, mask + ", stencil equal to the replaced reference");
Pass depthProbe;
depthProbe.triangle = &Near;
depthProbe.depthTest = true;
depthProbe.depthCompare = VK_COMPARE_OP_LESS;
depthProbe.stencil = always;
Draw(*device, depthProbe);
Check([&](std::uint32_t x, std::uint32_t y) { return Covered(quadPixelMask, x, y); }, mask + ", depth 0.25 less than the stored depth");
depthProbe.depthCompare = VK_COMPARE_OP_GREATER_OR_EQUAL;
Draw(*device, depthProbe);
Check([&](std::uint32_t x, std::uint32_t y) { return !Covered(quadPixelMask, x, y); }, mask + ", depth 0.25 at least the stored depth");
}
Pass storing;
storing.quadSampleMask = {1, 0, 0, 0};
storing.pixelCode = StoringPixelCode;
storing.depthTest = true;
storing.depthCompare = VK_COMPARE_OP_ALWAYS;
storing.stencil = always;
try {
Draw(*device, storing);
} catch (const std::exception& error) {
Require(std::string(error.what()).find("differs between the pixels of a quad") != std::string::npos, std::string("a storing early-Z pixel program under a quad mask failed for another reason: ") + error.what());
storing.shaderControl = 0u;
Draw(*device, storing);
Check([](std::uint32_t x, std::uint32_t y) { return Covered(0x1, x, y); }, "the storing late-Z pixel program under quad pixel mask 1");
for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) {
const auto expected = Covered(0x1, pixel % Width, pixel / Width) ? 1u : 0u;
Require(Stores[pixel] == expected, "the storing pixel program under quad pixel mask 1 stored " + std::to_string(Stores[pixel]) + " at pixel " + std::to_string(pixel) + ", expected " + std::to_string(expected));
}
std::puts("quad sample mask depth and stencil tests passed");
return 0;
}
Require(false, "a quad-masked early-Z pixel program that stores to memory was drawn with depth tests");
return 1;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
@@ -0,0 +1,213 @@
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
#include "prx/libc/include/GuestArena.hpp"
#include "prx/libc/include/GuestWriteWatch.hpp"
#include "VulkanTestDevice.hpp"
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <sys/mman.h>
#endif
#include <algorithm>
#include <array>
#include <bit>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
constexpr std::uint32_t Width = 64;
constexpr std::uint32_t Height = 16;
constexpr std::uint32_t SetContextReg = 0x69u;
constexpr std::uint32_t SetShReg = 0x76u;
constexpr std::uint32_t SetUconfigReg = 0x79u;
constexpr std::uint32_t DrawIndexAuto = 0x2du;
constexpr std::size_t Block = 65536;
constexpr std::size_t ColorBytes = Width * Height * 4;
std::byte* ColorMemory = nullptr;
alignas(64) volatile std::uint32_t EndOfPipe = 0;
std::byte* AllocateWatched(std::size_t bytes) {
#ifdef _WIN32
void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block);
GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes);
#else
void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (raw == MAP_FAILED) throw std::runtime_error("cannot map the watched block");
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
const auto aligned = (begin + Block - 1) & ~static_cast<std::uintptr_t>(Block - 1);
if (aligned != begin) munmap(raw, aligned - begin);
if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast<void*>(aligned + bytes), begin + Block - aligned);
void* block = reinterpret_cast<void*>(aligned);
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes);
#endif
return static_cast<std::byte*>(block);
}
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
0xe0382000, 0x80020005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 7> PixelCode{
0x7e0002f2, 0x7e0202f2, 0x7e0402f2, 0x7e0602f2, 0xf800080f, 0x03020100, 0xbf810000,
};
alignas(256) constexpr std::array<std::array<float, 4>, 3> Vertices{{
{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f},
}};
struct Step {
const char* name;
std::uint32_t top;
std::uint32_t bottom;
std::uint32_t shaderControl;
std::uint8_t quadPixels;
};
const std::array<Step, 8> Steps{{
{"PA_SC_AA_MASK 0x0000ffff/0 with early Z (GTA V, X0Y0)", 0x0000ffffu, 0u, 0x10u, 0x1u},
{"PA_SC_AA_MASK 0x0000ffff/0", 0x0000ffffu, 0u, 0x800u, 0x1u},
{"PA_SC_AA_MASK 0xffff0000/0 (X1Y0)", 0xffff0000u, 0u, 0x800u, 0x2u},
{"PA_SC_AA_MASK 0/0xffff0000 (X1Y1)", 0u, 0xffff0000u, 0x800u, 0x8u},
{"PA_SC_AA_MASK 0xfffe0001/0x0001fffe (X0Y0 and X1Y1 by bit 0)", 0xfffe0001u, 0x0001fffeu, 0x800u, 0x9u},
{"PA_SC_AA_MASK 0x00010001/0x00010001 (full at one sample)", 0x00010001u, 0x00010001u, 0x800u, 0xfu},
{"PA_SC_AA_MASK 0/0 (empty)", 0u, 0u, 0x800u, 0x0u},
{"full PA_SC_AA_MASK", 0xffffffffu, 0xffffffffu, 0x800u, 0xfu},
}};
struct RegisteredShader {
Shader shader;
ShaderUserData userData;
};
RegisteredShader VertexShader{};
RegisteredShader PixelShader{};
const std::vector<std::pair<std::uint32_t, std::uint32_t>> ContextBaseline{
{0x2d5, 0x2000},
{0x1b6, 0}, {0x207, 0}, {0x200, 0},
{0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0},
{0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa},
{0x2f9, 0x2d}, {0x313, 0x6000},
{0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240},
{0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010},
{0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028},
{0x31b, 0}, {0x31d, 0}, {0x3b8, 0x9000000}, {0x1e0, 0},
{0xc, 0},
{0x81, 0x80000000}, {0x90, 0x80000000}, {0x94, 0x80000000},
{0xb4, 0}, {0xb5, std::bit_cast<std::uint32_t>(1.0f)}, {0x114, 0}, {0x113, std::bit_cast<std::uint32_t>(1.0f)},
};
void Register(RegisteredShader& registered, const void* code, std::uint32_t bytes, std::uint8_t type) {
registered.shader.file_header = 0x34333231u;
registered.shader.version = 0x18u;
registered.shader.user_data = &registered.userData;
registered.shader.code = code;
registered.shader.header_size = sizeof(RegisteredShader);
registered.shader.shader_size = bytes;
registered.shader.type = type;
AgcDriverRegisterShader_nid_postfix(&registered.shader);
}
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu};
}
void SetRegister(std::vector<std::uint32_t>& words, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) {
words.push_back(0xc0010000u | (opcode << 8u));
words.push_back(offset);
words.push_back(value);
}
std::vector<std::uint32_t> Frame(const Step& step) {
const auto colorAddress = reinterpret_cast<std::uintptr_t>(ColorMemory);
const auto vertexAddress = reinterpret_cast<std::uintptr_t>(VertexCode.data());
const auto pixelAddress = reinterpret_cast<std::uintptr_t>(PixelCode.data());
std::vector<std::uint32_t> words;
for (const auto& [offset, value] : ContextBaseline) SetRegister(words, SetContextReg, offset, value);
SetRegister(words, SetContextReg, 0x203, step.shaderControl);
SetRegister(words, SetContextReg, 0x30e, step.top);
SetRegister(words, SetContextReg, 0x30f, step.bottom);
SetRegister(words, SetContextReg, 0x318, static_cast<std::uint32_t>(colorAddress >> 8u));
SetRegister(words, SetContextReg, 0x390, static_cast<std::uint32_t>(colorAddress >> 40u));
SetRegister(words, SetContextReg, 0x3b0, ((Width - 1u) << 14u) | (Height - 1u));
SetRegister(words, SetContextReg, 0x10f, std::bit_cast<std::uint32_t>(Width / 2.0f));
SetRegister(words, SetContextReg, 0x110, std::bit_cast<std::uint32_t>(Width / 2.0f));
SetRegister(words, SetContextReg, 0x111, std::bit_cast<std::uint32_t>(-(Height / 2.0f)));
SetRegister(words, SetContextReg, 0x112, std::bit_cast<std::uint32_t>(Height / 2.0f));
const auto extent = (Height << 16u) | Width;
SetRegister(words, SetContextReg, 0xd, extent);
SetRegister(words, SetContextReg, 0x82, extent);
SetRegister(words, SetContextReg, 0x91, extent);
SetRegister(words, SetContextReg, 0x95, extent);
SetRegister(words, SetUconfigReg, 0x242, 4u);
SetRegister(words, SetShReg, 0x008, static_cast<std::uint32_t>(pixelAddress >> 8u));
SetRegister(words, SetShReg, 0x009, static_cast<std::uint32_t>(pixelAddress >> 40u));
SetRegister(words, SetShReg, 0x00b, 0u);
SetRegister(words, SetShReg, 0x0c8, static_cast<std::uint32_t>(vertexAddress >> 8u));
SetRegister(words, SetShReg, 0x0c9, static_cast<std::uint32_t>(vertexAddress >> 40u));
SetRegister(words, SetShReg, 0x08b, 4u << 1u);
const auto descriptor = BufferDescriptor(Vertices.data(), 16u, static_cast<std::uint32_t>(Vertices.size()));
for (std::uint32_t i = 0; i < descriptor.size(); ++i) SetRegister(words, SetShReg, 0x08c + i, descriptor[i]);
words.insert(words.end(), {0xc0000000u | (1u << 16u) | (DrawIndexAuto << 8u), static_cast<std::uint32_t>(Vertices.size()), 2u});
const auto labelAddress = reinterpret_cast<std::uintptr_t>(&EndOfPipe);
words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast<std::uint32_t>(labelAddress), static_cast<std::uint32_t>(static_cast<std::uint64_t>(labelAddress) >> 32u), 1u, 0u, 0u});
return words;
}
void Check(const Step& step, const std::vector<std::byte>& colors) {
const auto what = std::string("sample mask through the driver, ") + step.name;
for (std::uint32_t y = 0; y < Height; ++y) {
for (std::uint32_t x = 0; x < Width; ++x) {
const auto value = std::to_integer<std::uint8_t>(colors[(y * Width + x) * 4u]);
const bool expected = ((step.quadPixels >> ((x & 1u) | ((y & 1u) << 1u))) & 1u) != 0u;
Require(value == (expected ? 255u : 0u), what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") stored " + std::to_string(value) + ", expected " + (expected ? "written" : "clear"));
}
}
}
}
int main() {
try {
{
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
}
ColorMemory = AllocateWatched(Block);
Register(VertexShader, VertexCode.data(), sizeof(VertexCode), 2);
Register(PixelShader, PixelCode.data(), sizeof(PixelCode), 1);
for (int round = 0; round < 2; ++round) {
for (const auto& step : Steps) {
std::fill_n(ColorMemory, ColorBytes, std::byte{0});
EndOfPipe = 0;
auto words = Frame(step);
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
Require(sceAgcDriverSubmitDcb(&packet) == 0, std::string("DCB submission failed for ") + step.name);
AgcDriverWaitIdle_nid_postfix();
Require(EndOfPipe == 1u, std::string("the end-of-pipe label was not written for ") + step.name);
std::vector<std::byte> colors(ColorBytes);
AgcDriver::GuestMemory::Read(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(ColorMemory)), colors);
Check(step, colors);
}
}
AgcDriverShutdown_nid_postfix();
std::puts("sample mask draws through the driver passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
AgcDriverShutdown_nid_postfix();
return 1;
}
}
@@ -0,0 +1,209 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <cstdio>
#include <iostream>
#include <span>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::PixelInput;
using ShaderRecompiler::PixelInputBit;
using ShaderRecompiler::ShaderStage;
using Masks = std::array<std::uint16_t, 4>;
constexpr std::uint32_t Width = 64;
constexpr std::uint32_t Height = 16;
constexpr std::uint32_t Samples = 4;
constexpr std::uint32_t WaveSize = 64;
constexpr std::uint32_t EarlyZThenLateZ = 0x10u;
constexpr Masks Full{0xf, 0xf, 0xf, 0xf};
alignas(256) std::array<std::uint32_t, Width * Height * Samples> Invocations{};
alignas(256) std::array<std::byte, 256 * 1024> DepthMemory{};
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 17> SampleStoreCode{
0x7e100f04, 0x7e120f05, 0x34121286, 0x4a101308, 0x34101082, 0xd548000a, 0x02111106, 0x4a101508,
0x34101082, 0x7e160281, 0xe0705000, 0x80000b08, 0xbf8c3f70, 0xf8001890, 0x00000000, 0xbf810000,
0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 3> NullExportCode{0xf8001890, 0x00000000, 0xbf810000};
using Triangle = std::array<std::array<float, 4>, 3>;
alignas(256) constexpr Triangle Far{{{-1.0f, -1.0f, 0.25f, 1.0f}, {3.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 3.0f, 0.25f, 1.0f}}};
alignas(256) constexpr Triangle Probe{{{-1.0f, -1.0f, 0.375f, 1.0f}, {3.0f, -1.0f, 0.375f, 1.0f}, {-1.0f, 3.0f, 0.375f, 1.0f}}};
alignas(256) constexpr Triangle Middle{{{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}}};
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, stride == 0u ? 0x31016facu : 0x01016facu};
}
struct Pass {
Masks quadSampleMask = Full;
bool perSample = true;
std::uint32_t shaderControl = 0u;
const Triangle* triangle = &Middle;
bool depth = false;
bool depthWrite = false;
VkCompareOp depthCompare = VK_COMPARE_OP_ALWAYS;
};
void Draw(AgcDriver::VulkanDevice& device, const Pass& pass) {
Invocations.fill(0);
const auto target = device.Target();
std::vector<std::uint32_t> vertexUserData(4, 0u);
const auto vertexBuffer = BufferDescriptor(pass.triangle->data(), 16u, static_cast<std::uint32_t>(pass.triangle->size()));
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}};
ShaderRecompiler::RecompileRequest vertex{
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
{WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
target,
{0, 0, 0, 64}
};
vertex.useCache = false;
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
AgcDriver::Graphics::State state{};
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt};
state.hasColorTarget = false;
state.renderExtent = {Width, Height};
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1};
state.negativeOneToOne = false;
state.scissor = {{0, 0}, {Width, Height}};
state.cullMode = VK_CULL_MODE_NONE;
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
state.blendConstants = {};
state.samples = Samples;
state.quadSampleMask = pass.quadSampleMask;
state.pixelEarlyZ = AgcDriver::Graphics::PixelEarlyZ(pass.shaderControl);
if (pass.depth) {
state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast<std::uintptr_t>(DepthMemory.data()), 0u, {Width, Height}, VK_FORMAT_D32_SFLOAT, 0.0f, 0};
state.depth->samples = Samples;
state.depthTest = true;
state.depthWrite = pass.depthWrite;
state.depthCompare = pass.depthCompare;
}
const std::uint32_t inputs = pass.perSample ? PixelInputBit(PixelInput::PerspectiveSample) | PixelInputBit(PixelInput::LinearSample) | PixelInputBit(PixelInput::PositionX) | PixelInputBit(PixelInput::PositionY) | PixelInputBit(PixelInput::Ancillary) : PixelInputBit(PixelInput::PerspectiveCenter);
const AgcDriver::Registers context{{0x1b3, inputs}, {0x1b4, inputs}, {0x1b6, 0u}, {0x1c5, 0u}, {0x203, pass.shaderControl}};
auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state));
Require(pixel.sampleShading == pass.perSample, "the pixel inputs did not select the requested shading rate");
pixel.quadPixelMask = AgcDriver::Graphics::QuadPixelMask(state);
pixel.quadSampleMask = AgcDriver::Graphics::QuadSampleMasks(state);
std::vector<std::uint32_t> pixelUserData(4, 0u);
const auto invocations = BufferDescriptor(Invocations.data(), 0u, static_cast<std::uint32_t>(sizeof(Invocations)));
std::copy(invocations.begin(), invocations.end(), pixelUserData.begin());
const std::span<const std::uint32_t> code = pass.perSample ? std::span<const std::uint32_t>(SampleStoreCode) : std::span<const std::uint32_t>(NullExportCode);
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
ShaderRecompiler::RecompileRequest fragment{
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
{WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
target,
PixelPushLayout(vertexPush, target)
};
fragment.useCache = false;
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
{ShaderStage::Vertex, &vertexResult, 0},
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}
}};
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(pass.triangle->size()), 0, 1, 0, false};
device.Draw(state, draw, shaders);
device.WaitIdle();
}
void Check(const Masks& quadSampleMask, const std::string& what) {
for (std::uint32_t y = 0; y < Height; ++y) {
for (std::uint32_t x = 0; x < Width; ++x) {
const auto mask = quadSampleMask[(x & 1u) | ((y & 1u) << 1u)];
for (std::uint32_t sample = 0; sample < Samples; ++sample) {
const auto expected = (mask >> sample) & 1u;
const auto ran = Invocations[(y * Width + x) * Samples + sample];
Require(ran == expected, what + ": sample " + std::to_string(sample) + " of pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") " + (ran != 0u ? "stored" : "did not store") + " (" + std::to_string(ran) + "), the quad pixel's mask is " + std::to_string(mask));
}
}
}
}
void DepthWritten(AgcDriver::VulkanDevice& device, const Masks& mask, const std::string& what) {
Pass reset;
reset.perSample = false;
reset.triangle = &Far;
reset.depth = true;
reset.depthWrite = true;
Draw(device, reset);
Pass masked = reset;
masked.quadSampleMask = mask;
masked.triangle = &Middle;
Draw(device, masked);
Pass probe;
probe.shaderControl = EarlyZThenLateZ;
probe.triangle = &Probe;
probe.depth = true;
probe.depthCompare = VK_COMPARE_OP_LESS;
Draw(device, probe);
Check(mask, "the samples a pixel-rate draw with " + what + " wrote depth 0.5 into, probed by early-Z per-sample stores");
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
struct Case {
const char* name;
Masks mask;
};
const std::array<Case, 9> cases{{
{"no sample mask", Full},
{"samples 0 and 2 of every pixel", {0x5, 0x5, 0x5, 0x5}},
{"sample 3 of every pixel", {0x8, 0x8, 0x8, 0x8}},
{"no sample of any pixel", {0x0, 0x0, 0x0, 0x0}},
{"every sample of the even-x, even-y pixel of each quad", {0xf, 0x0, 0x0, 0x0}},
{"every sample of the odd-x pixels of each quad", {0x0, 0xf, 0x0, 0xf}},
{"sample q of quad pixel q", {0x1, 0x2, 0x4, 0x8}},
{"all, none, none and samples 0 and 1", {0xf, 0x0, 0x0, 0x3}},
{"samples 1 and 3, 0 and 2, 2 and 3, and 0", {0xa, 0x5, 0xc, 0x1}},
}};
for (const auto& test : cases) {
Pass pass;
pass.quadSampleMask = test.mask;
Draw(*device, pass);
Check(test.mask, std::string("4x per-sample shading with ") + test.name);
}
for (const auto& test : cases) DepthWritten(*device, test.mask, test.name);
Pass early;
early.quadSampleMask = {0x6, 0x6, 0x6, 0x6};
early.shaderControl = EarlyZThenLateZ;
try {
Draw(*device, early);
} catch (const std::exception& error) {
Require(std::string(error.what()).find("earlyFragmentSampleMaskTestBeforeSampleCounting") != std::string::npos, std::string("a sample mask with early fragment tests failed for another reason: ") + error.what());
std::puts("4x sample mask tests passed (early fragment tests refused: the device does not order the sample mask test before sample counting)");
return 0;
}
Check(early.quadSampleMask, "4x per-sample shading with early fragment tests and samples 1 and 2 of every pixel");
std::puts("4x sample mask tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
+1
View File
@@ -192,6 +192,7 @@ private:
append(key, value.stencilRefExportEnable);
append(key, value.ditheredAlphaToCoverage);
append(key, value.quadPixelMask);
append(key, value.quadSampleMask);
append(key, value.targetOutputMode);
}
@@ -341,6 +341,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) {
writer.WriteBool(info.ditheredAlphaToCoverage);
writer.WriteBool(info.frontFaceAllBits);
writer.WriteU8(info.quadPixelMask);
for (const std::uint16_t mask : info.quadSampleMask) writer.WriteU32(mask);
}
ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) {
@@ -395,6 +396,13 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) {
if (version >= 25u) info.ditheredAlphaToCoverage = reader.ReadBool();
if (version >= 27u) info.frontFaceAllBits = reader.ReadBool();
if (version >= 28u) info.quadPixelMask = reader.ReadU8();
if (version >= 29u) {
for (std::uint16_t& mask : info.quadSampleMask) {
const auto value = reader.ReadU32();
if (value > 0xffffu) throw std::runtime_error("invalid recompile request quad sample mask");
mask = static_cast<std::uint16_t>(value);
}
}
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) |
@@ -766,7 +774,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const
std::string buffer;
Writer writer(buffer);
writer.WriteU32(0x41505335u);
writer.WriteU32(28u);
writer.WriteU32(29u);
writeShaderBinary(writer, request.shader);
writeGuestContext(writer, request.context);
writeSpirvTarget(writer, request.target);
@@ -799,7 +807,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 > 28u) throw std::runtime_error("unsupported recompile request serialization version");
if (version < 1u || version > 29u) 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);
@@ -3,6 +3,7 @@
#include "IntermediateRepresentation/IrMetadata.hpp"
#include "Recompiler.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <limits>
@@ -182,12 +183,21 @@ struct ShaderPixelInputInfo {
bool psAlphaToCoverage = false;
bool psDitheredAlphaToCoverage = false;
std::uint8_t quadPixelMask = 0xf;
std::array<std::uint16_t, 4> quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu};
ShaderStageRuntime stage;
bool HasPositionInput() const {
return psPosX || psPosY || psPosZ || psPosW;
}
[[nodiscard]] bool QuadSampleMaskPartial() const {
return std::ranges::any_of(quadSampleMask, [](std::uint16_t mask) { return mask != 0xffffu; });
}
[[nodiscard]] bool QuadMasked() const {
return quadPixelMask != 0xf || QuadSampleMaskPartial();
}
[[nodiscard]] bool InputIsDefault(std::uint32_t input) const {
return input < inputNum && input < 32u && (interpolatorSettings[input] & 0x420u) == 0x20u;
}
@@ -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->quadPixelMask != 0xf) {
if (pixel->HasPositionInput() || pixel->psDitheredAlphaToCoverage || pixel->QuadMasked()) {
AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord");
}
if (pixel->psFrontFace) {
+1
View File
@@ -155,6 +155,7 @@ struct ShaderPixelStageInfo {
bool layerExported = false;
bool ditheredAlphaToCoverage = false;
std::uint8_t quadPixelMask = 0xf;
std::array<std::uint16_t, 4> quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu};
};
struct ShaderVertexBufferResource {
@@ -180,6 +180,8 @@ struct SpirvEmitterState {
const IrBlock* currentBlock = nullptr;
std::uint32_t pixelValidMaskVariable = 0;
std::uint32_t quadPixelUncovered = 0;
std::uint32_t sampleMaskInputVariable = 0;
std::uint32_t quadSampleCoverage = 0;
std::uint32_t subgroupLocalInvocationIdVariable = 0;
std::uint32_t perVertexVariable = 0;
std::uint32_t pointSizeVariable = 0;
@@ -298,6 +298,11 @@ void DefineInputs(SpirvEmitterState& state) {
state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationFlat);
}
}
if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->QuadSampleMaskPartial()) {
state.sampleMaskInputVariable = DefineInterfaceVariable(state, SampleMaskArrayType(state), spv::StorageClassInput, "gl_SampleMaskIn");
state.module.AddAnnotation(spv::OpDecorate, state.sampleMaskInputVariable, spv::DecorationBuiltIn, spv::BuiltInSampleMask);
state.module.AddAnnotation(spv::OpDecorate, state.sampleMaskInputVariable, spv::DecorationFlat);
}
}
constexpr std::uint32_t MaxClipDistances = 8u;
@@ -399,6 +404,9 @@ void DefineOutputs(SpirvEmitterState& state) {
if (state.inputInfo.pixel->psSampleMaskExportEnable) throw std::runtime_error("alpha-to-coverage with a sample-mask export is unsupported");
BuiltIn(state.sampleMaskVariable, SampleMaskArrayType(state), "gl_SampleMask", spv::BuiltInSampleMask);
}
if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->QuadSampleMaskPartial()) {
BuiltIn(state.sampleMaskVariable, SampleMaskArrayType(state), "gl_SampleMask", spv::BuiltInSampleMask);
}
}
void DefineDescriptors(SpirvEmitterState& state) {
@@ -196,7 +196,7 @@ void DefineModule(SpirvEmitterState& state) {
state.module.EmitExtension("SPV_EXT_shader_stencil_export");
state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeStencilRefReplacingEXT);
}
if (pixel.psEarlyZ && !pixel.psAlphaToCoverage && pixel.quadPixelMask == 0xf) {
if (pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) {
state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests);
}
if (pixel.psOrderedPixelShader) {
@@ -288,6 +288,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);
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);
@@ -1185,14 +1186,29 @@ void EmitProgram(SpirvEmitterState& state) {
state.module.AddFunction(spv::OpBeginInvocationInterlockEXT);
}
EmitMemoryOffsets(state);
if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->quadPixelMask != 0xfu) {
if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->QuadMasked()) {
const auto& pixel = *state.inputInfo.pixel;
const auto coordinate = [&](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, coordinate(0u), EmitBinaryU32(state, spv::OpShiftLeftLogical, coordinate(1u), ConstantU32(state, 1u)));
const auto bit = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, ConstantU32(state, state.inputInfo.pixel->quadPixelMask), quad), ConstantU32(state, 1u));
const auto mask = static_cast<std::uint32_t>(state.inputInfo.pixel->quadPixelMask);
auto bit = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, ConstantU32(state, pixel.quadPixelMask), quad), ConstantU32(state, 1u));
if (pixel.QuadSampleMaskPartial()) {
const auto top = ConstantU32(state, static_cast<std::uint32_t>(pixel.quadSampleMask[0]) | (static_cast<std::uint32_t>(pixel.quadSampleMask[1]) << 16u));
const auto bottom = ConstantU32(state, static_cast<std::uint32_t>(pixel.quadSampleMask[2]) | (static_cast<std::uint32_t>(pixel.quadSampleMask[3]) << 16u));
const auto lower = EmitCompareU32Constant(state, spv::OpIEqual, EmitBinaryU32(state, spv::OpBitwiseAnd, quad, ConstantU32(state, 2u)), 2u);
const auto row = Select(state, TypeU32(state), lower, bottom, top);
const auto shift = EmitBinaryU32(state, spv::OpShiftLeftLogical, EmitBinaryU32(state, spv::OpBitwiseAnd, quad, ConstantU32(state, 1u)), ConstantU32(state, 4u));
const auto covered = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, row, shift), ConstantU32(state, 0xffffu));
const auto pointer = state.module.AllocateId();
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);
state.quadSampleCoverage = Unary(state, spv::OpBitcast, TypeI32(state), covered);
}
const auto mask = static_cast<std::uint32_t>(pixel.quadPixelMask);
const bool demoteCapability = std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast<std::uint32_t>(spv::CapabilityDemoteToHelperInvocation)) != state.supportedCapabilities.end();
const bool demoteCore = state.spirvVersion >= 0x00010600u;
const bool demoteExtension = std::find(state.supportedExtensions.begin(), state.supportedExtensions.end(), "SPV_EXT_demote_to_helper_invocation") != state.supportedExtensions.end();
@@ -1217,6 +1233,7 @@ void EmitProgram(SpirvEmitterState& state) {
state.module.AddFunction(spv::OpKill);
}
EmitLabel(state, mergeLabel);
if (state.quadSampleCoverage != 0u) StoreCoverage(state, ConstantI32(state, -1));
}
if (program.BlockOrder().empty()) {
if (OrderedPixelShader(state)) {
@@ -1479,10 +1496,8 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) {
if ((exp.en & 4u) != 0u && state.sampleMaskVariable != 0u) {
const auto raw = ExportRawComponent(ctx, data, 2);
const auto value = state.module.AllocateId();
const auto pointer = state.module.AllocateId();
state.module.AddFunction(spv::OpBitcast, TypeI32(state), value, raw);
state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeI32(state)), pointer, state.sampleMaskVariable, ConstantU32(state, 0u));
state.module.AddFunction(spv::OpStore, pointer, value);
StoreCoverage(state, value);
}
return;
}
@@ -149,6 +149,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues
pixelStorage.psAlphaToCoverage = pixel.alphaToCoverage;
pixelStorage.psDitheredAlphaToCoverage = pixel.ditheredAlphaToCoverage;
pixelStorage.quadPixelMask = pixel.quadPixelMask;
pixelStorage.quadSampleMask = pixel.quadSampleMask;
ShaderStageInputInfo result;
result.pixel = &pixelStorage;
return result;
+1
View File
@@ -47,6 +47,7 @@
- [Unexported pixel inputs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver): a pixel input whose `SPI_PS_INPUT_CNTL` parameter the previous stage does not export is programmed as a default-value input (`OFFSET` bit 5, `DEFAULT_VAL` 0, FP16 high half too) and reads (0, 0, 0, 0), as radeonsi's `SI_PS_INPUT_CNTL_UNUSED` does and as rect-list draws already do. The console's value for an unwritten parameter cache entry is not measured on hardware.
- [Per-sample pixel shading](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): `PA_SC_MODE_CNTL_1.PS_ITER_SAMPLE` (bit 16, [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json)) on a multisampled draw enables Vulkan sample shading with a minimum rate of 1.0, so the pixel program runs once per sample. The draw throws when the device lacks `sampleRateShading`. On a single-sample draw the bit has no effect and is dropped. That the console runs the pixel program once per covered sample, and what it does with the other multisample registers in that mode, is taken from the register name and Vulkan's definition, not measured on hardware.
- [Unused pixel programs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): a bound pixel program is skipped when color masks have no intersection and DB export and execution enables are clear, regardless of retained SPI_SHADER_Z_FORMAT. This follows [AMD PAL DbShaderControl](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9AbiToPipelineRegisters.h) and its [GFX10 register definitions](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/chip/gfx9_plus_merged_registers.h), not a hardware measurement. Enabled DB_ALPHA_TO_MASK conservatively retains the program even with ALPHA_TO_MASK_DISABLE, because DecodeState currently enables Vulkan alpha-to-coverage without applying that override. Console behavior for these combinations has not been measured.
- [Sample masks](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `PA_SC_AA_MASK_X0Y0_X1Y0` and `PA_SC_AA_MASK_X0Y1_X1Y1` hold one 16-bit sample mask for each pixel of the aligned 2x2 quad ([Mesa gfx10.json](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/registers/gfx10.json#L13512)); only the bits of the `MSAA_NUM_SAMPLES` rasterized samples count, and at one sample bit 0 of a pixel's field covers every sample of the target the draw is raised to. A mask equal for the four pixels is the Vulkan `pSampleMask`, the case [radv](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_cmd_buffer.c#L13633) and [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/gallium/drivers/radeonsi/si_state.c#L4218) program; with early fragment tests it needs `earlyFragmentSampleMaskTestBeforeSampleCounting` ([radv](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_physical_device.c#L2079) reports it). A mask that differs between the pixels is applied in the pixel shader without early fragment tests: a pixel whose mask covers all or none of its samples is demoted to a helper invocation by `gl_FragCoord` parity, and otherwise the pixel's mask is ANDed into `gl_SampleMask` (with alpha-to-coverage and the mask export) after demoting invocations whose `SampleMask` input it leaves empty. A pixel program with early Z that writes memory under depth, stencil or depth-bounds tests is therefore refused. That XnYm is the pixel at (x mod 2, y mod 2) = (n, m) is read from the field names; that the mask removes samples before the depth and stencil tests and occlusion counting is inferred from radv implementing the Vulkan sample mask test with it; and that it takes no part in the centroid (`AA_MASK_CENTROID_DTMN` 0) is inferred from radv setting that bit only for conservative rasterization. None of it is measured on gfx1013. A mask equal for the four pixels that removes some but not all samples of a pixel is refused with active centroid inputs, since where Vulkan puts the centroid relative to `pSampleMask` is implementation-defined, and any partial mask is refused with conservative rasterization (radv sets `PREZ_AA_MASK_ENABLE` and `POSTZ_AA_MASK_ENABLE` [there](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_cmd_buffer.c#L13653), which 0x6001 leaves clear).
- [Covered-centroid raster state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) accepts PA_SC_AA_CONFIG.COVERED_CENTROID_IS_CENTER only when the pixel program is skipped or SPI_PS_INPUT_ENA and SPI_PS_INPUT_ADDR have no active perspective or linear centroid input. Bit 29 follows [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json); [RADV](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/radv_cmd_buffer.c) sets it on GFX10.3 and later. Active centroid interpolation and unspecified input registers still throw. Console covered-pixel centroid results were not measured.
- [Compute LDS beyond the device's shared-memory limit](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware.
- [AGC buffer allocations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request.