diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp index 3d3794279..0e55dd5ea 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp @@ -47,6 +47,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta checkFeature(spv::CapabilityFloat64, core.shaderFloat64 == VK_TRUE, "shaderFloat64"); checkFeature(spv::CapabilityTessellation, core.tessellationShader == VK_TRUE, "tessellationShader"); checkFeature(spv::CapabilityImageGatherExtended, core.shaderImageGatherExtended == VK_TRUE, "shaderImageGatherExtended"); + checkFeature(spv::CapabilityClipDistance, core.shaderClipDistance == VK_TRUE, "shaderClipDistance"); checkFeature(spv::CapabilityMinLod, core.shaderResourceMinLod == VK_TRUE, "shaderResourceMinLod"); checkFeature(spv::CapabilityStorageImageMultisample, core.shaderStorageImageMultisample == VK_TRUE, "shaderStorageImageMultisample"); checkFeature(spv::CapabilityStorageImageReadWithoutFormat, core.shaderStorageImageReadWithoutFormat == VK_TRUE, "shaderStorageImageReadWithoutFormat"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 8a1600be3..fa9d8b151 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -262,6 +262,7 @@ struct VulkanDevice::State { bool fragmentShaderBarycentric = false; bool geometryShader = false; bool sampleRateShading = false; + bool shaderClipDistance = false; bool shaderClock = false; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; @@ -1348,6 +1349,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->sampleRateShading = enabled.sampleRateShading == VK_TRUE; if (enabled.geometryShader) state->capabilities.push_back(spv::CapabilityGeometry); enabled.shaderClipDistance = available.shaderClipDistance; + state->shaderClipDistance = enabled.shaderClipDistance == VK_TRUE; + if (enabled.shaderClipDistance) state->capabilities.push_back(spv::CapabilityClipDistance); if (enabled.shaderStorageImageWriteWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageWriteWithoutFormat); if (enabled.shaderStorageImageReadWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageReadWithoutFormat); state->storageImageWithoutFormat = enabled.shaderStorageImageWriteWithoutFormat == VK_TRUE && enabled.shaderStorageImageReadWithoutFormat == VK_TRUE; @@ -3012,6 +3015,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.imageInt64Atomics = state->imageInt64Atomics; context.geometryShader = state->geometryShader; context.sampleRateShading = state->sampleRateShading; + context.shaderClipDistance = state->shaderClipDistance; context.nullDescriptors = state->shaderProfile != nullptr && state->shaderProfile->NullDescriptors(); context.bufferInt64Atomics = state->bufferInt64Atomics; context.placeholderImages = state->placeholderImages.get(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 38049d9e6..d1e641ab6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -158,6 +158,7 @@ struct Context { bool imageInt64Atomics = false; bool geometryShader = false; bool sampleRateShading = false; + bool shaderClipDistance = false; bool nullDescriptors = false; bool bufferInt64Atomics = false; const PlaceholderImages* placeholderImages = nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index bb817d437..49fb7769c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -110,7 +110,7 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment); bool ShadersWriteMemory(std::span shaders); // Returns the color attachment locations the pixel shader writes. -std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false); +std::set ValidateShaders(std::span 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); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 09fc3ffbc..a25b9ec2f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -732,7 +732,7 @@ std::set CachedFragmentOutputs(const Context& context, std::span< } std::set outputs; try { - outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics); + outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.shaderClipDistance); } catch (const std::exception& error) { if (keyed) { std::lock_guard lock(validationMutex()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index c5d9168d1..d30076237 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -43,6 +43,7 @@ struct Module { std::map outputs; std::optional secondSource; bool position = false; + bool clipDistance = false; bool primitiveIndices = false; bool fragmentBarycentric = false; std::map> modes; @@ -109,6 +110,11 @@ struct Module { if (!input) position = true; return; } + if (value == spv::BuiltInClipDistance && !input && (vertex || stage == Stage::TessellationEvaluation)) { + Require(!clipDistance && Signature(type) == "f32[1]", "invalid or duplicate clip distance output"); + clipDistance = true; + return; + } if (stage == Stage::TessellationControl || stage == Stage::TessellationEvaluation || stage == Stage::Mesh) { const auto signature = Signature(type); if (!input && value == spv::BuiltInPosition) { @@ -136,7 +142,7 @@ struct Module { } }; -Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics) { +Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance) { using Stage = ShaderRecompiler::ShaderStage; Require(compiled.program != nullptr, "missing compiled shader"); const auto& shader = *compiled.program; @@ -232,6 +238,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi capability == spv::CapabilityImage1D || capability == spv::CapabilityImageGatherExtended || capability == spv::CapabilityImageQuery || + ((vertex || evaluation) && shaderClipDistance && capability == spv::CapabilityClipDistance) || (fragment && geometryShader && capability == spv::CapabilityGeometry) || (fragment && sampleRateShading && capability == spv::CapabilitySampleRateShading) || capability == spv::CapabilityImageMSArray || @@ -547,7 +554,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } -std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics) { +std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance) { using Stage = ShaderRecompiler::ShaderStage; const bool tessellation = state.stages.path == ShaderPath::Tessellation; const bool mesh = state.stages.path == ShaderPath::Geometry; @@ -568,7 +575,7 @@ std::set ValidateShaders(std::span shaders, Require(shaders[i].program != nullptr, "missing compiled shader"); Require(shaders[i].stage == expected, "graphics stage order disagrees"); for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == 0, "graphics resource uses a descriptor set other than zero"); - const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics); + const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance); if (i != 0) { for (const auto& [location, signature] : current.inputs) { const auto output = previous.outputs.find(location); diff --git a/core/libs/prx/libSceAgcDriver/tests/PositionInvariance.cpp b/core/libs/prx/libSceAgcDriver/tests/PositionInvariance.cpp index 2c9b168bb..5b6d1b3be 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PositionInvariance.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PositionInvariance.cpp @@ -36,6 +36,8 @@ void requireInvariantPositions(const ShaderRecompiler::RecompileResult& result, constexpr std::array PositionExport{0xf80008cfu, 0u, 0xbf810000u}; +constexpr std::array VertexCapabilities{spv::CapabilityClipDistance}; + ShaderRecompiler::RecompileRequest baseRequest(ShaderRecompiler::ShaderStage stage) { using namespace ShaderRecompiler; RecompileRequest request{}; @@ -45,6 +47,7 @@ ShaderRecompiler::RecompileRequest baseRequest(ShaderRecompiler::ShaderStage sta request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 64; + request.target.supportedCapabilities = VertexCapabilities; request.layout.pushConstantSizeBytes = 128; request.useCache = false; return request; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 2a86162fa..1526845de 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -50,7 +50,7 @@ namespace { ShaderRecompiler::SpirvTarget BufferTarget() { - static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityImageGatherExtended, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityImageGatherExtended, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance}; static constexpr std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; ShaderRecompiler::SpirvTarget target{}; target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; @@ -1202,7 +1202,7 @@ void verifyLanesOutsideHostSubgroup() { void verifyHalfWaveReduction() { using namespace ShaderRecompiler; - static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic}; + static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityClipDistance}; const auto pixel = [](std::span code, std::uint32_t subgroupSize, bool arithmetic) { ShaderPixelStageInfo info{}; info.inputAddr = PixelInputBit(PixelInput::PositionX); @@ -1364,7 +1364,7 @@ void verifyHalfWaveReduction() { void verifyNarrowVertexWave() { using namespace ShaderRecompiler; - static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic}; + static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityClipDistance}; static constexpr std::array userData{0u, 0u, 0u, 0x2020u}; const auto vertex = [](std::span code, std::uint32_t waveSize, std::uint32_t subgroupSize, std::uint32_t userDataBase) { RecompileRequest request{}; @@ -1411,7 +1411,7 @@ void verifyNarrowVertexWave() { const auto wide64 = prepared(culling, 64u); require(!wide64.empty() && reductions(wide64) == 0u, "narrow vertex wave: the NGG culling scan, keyed on lanes below 64 on 64 lanes, did not keep its DPP scan"); require(prepares(patched({{6, 0x80060015u}}), 32u), "narrow vertex wave: the scan keyed on lanes below the vertex count in s3 did not prepare on 32 lanes"); - static constexpr std::array noArithmetic{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle}; + static constexpr std::array noArithmetic{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityClipDistance}; const auto otherKeys = patched({{7, 0x360e0a82u}}); auto withoutArithmetic = vertex(otherKeys, 64u, 32u, 8u); withoutArithmetic.target.supportedCapabilities = noArithmetic; diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp index daa5ac849..ae99defb9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvNarrowConstantStore.cpp @@ -50,8 +50,8 @@ void CheckStorageOnlyModule(const RecompileResult& result, std::string_view name } SpirvTarget StorageOnlyTarget() { - static constexpr std::array capabilities{ - spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, + static constexpr std::array capabilities{ + spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance, }; static constexpr std::array extensions{ "SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer", diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp index 8d591fe05..453fbbab7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VertexBarrier.cpp @@ -12,6 +12,8 @@ namespace { constexpr std::array BarrierExport{0xbf8a0000u, 0xf80008cfu, 0u, 0xbf810000u}; constexpr std::array BarrierLdsExport{0xd8340000u, 0u, 0xbf8a0000u, 0xf80008cfu, 0u, 0xbf810000u}; +constexpr std::array VertexCapabilities{spv::CapabilityClipDistance}; + ShaderRecompiler::RecompileRequest vertexRequest(std::span code) { using namespace ShaderRecompiler; RecompileRequest request{}; @@ -21,6 +23,7 @@ ShaderRecompiler::RecompileRequest vertexRequest(std::span request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 32; + request.target.supportedCapabilities = VertexCapabilities; request.layout.pushConstantSizeBytes = 128; request.useCache = false; return request; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp new file mode 100644 index 000000000..b5c7ad5c5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ZeroPositionVertex.cpp @@ -0,0 +1,137 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Extent = 64u; +alignas(256) std::array pixels{}; +alignas(256) std::array vertices{}; +constexpr std::array vertexCode{0xf4080100u, 0xfa000000u, 0x4a0a0a02u, 0x4a0a0b08u, 0x4a0a0a03u, 0xe00c2000u, 0x80010005u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u}; +constexpr std::array pixelCode{0x7e0002f2u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u}; + +using Position = std::array; + +struct Counts { + unsigned total = 0; + unsigned upper = 0; + std::array, 16> map{}; +}; + +Counts RunCase(AgcDriver::VulkanDevice& device, const char* name, std::uint32_t records, const std::vector& positions, std::size_t stored, VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) { + vertices.fill(std::byte{0}); + constexpr std::uint32_t stride = 16u; + for (std::size_t index = 0; index < stored; ++index) std::memcpy(vertices.data() + index * stride, positions[index].data(), 16u); + std::array descriptor{}; + const auto tableAddress = reinterpret_cast(descriptor.data()); + std::array userData{static_cast(tableAddress), static_cast(tableAddress >> 32u), 0u, 0u}; + const std::array memory{{{tableAddress, std::as_bytes(std::span(descriptor))}}}; + ShaderRecompiler::ShaderVertexStageInfo vertexInfo{}; + vertexInfo.fetchEmbedded = true; + vertexInfo.resourcesNum = 1u; + vertexInfo.resourcesDst[0] = {0, 4, 0, 0}; + ShaderRecompiler::RecompileRequest request{{ShaderStage::Vertex, reinterpret_cast(vertexCode.data()), vertexCode, 0, {}}, {32u, 0u, userData, std::nullopt, std::nullopt, vertexInfo, memory}, device.Target(), {0u, 0u, 0u, 96u}}; + const auto address = reinterpret_cast(vertices.data()); + descriptor = {static_cast(address), static_cast(address >> 32u) | (stride << 16u), records, 0x01000facu | (77u << 12u)}; + request.context.vertex->resources[0].fields = descriptor; + const auto vertex = ShaderRecompiler::Recompile(request); + const auto pushBytes = static_cast(vertex.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixelInfo{}; + pixelInfo.wave32 = true; + pixelInfo.targetOutputMode[0] = 9u; + pixelInfo.targetExportMapping.fill(0xe4u); + ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), {0u, 0u, pushBytes, 128u - pushBytes}}; + const auto pixel = ShaderRecompiler::Recompile(pixelRequest); + const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, pushBytes}}}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(pixels.data()), {Extent, Extent}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Extent, Extent}; + state.topology = topology; + state.viewport = {0.0f, static_cast(Extent), static_cast(Extent), -static_cast(Extent), 0.0f, 1.0f}; + state.scissor = {{0, 0}, {Extent, Extent}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15u; + state.blends = {state.blend}; + AgcDriver::Pm4::DrawParameters draw{0u, static_cast(positions.size()), 0u, 1u, 0u, false}; + draw.firstInstance = 0u; + pixels.fill(std::byte{0}); + device.Draw(state, draw, shaders); + device.WaitIdle(); + Counts counts; + for (std::uint32_t y = 0; y < Extent; ++y) { + for (std::uint32_t x = 0; x < Extent; ++x) { + const auto index = (y * Extent + x) * 4u; + if (pixels[index] == std::byte{0} && pixels[index + 3u] == std::byte{0}) continue; + ++counts.total; + const float ndcX = (static_cast(x) + 0.5f) / Extent * 2.0f - 1.0f; + const float ndcY = 1.0f - (static_cast(y) + 0.5f) / Extent * 2.0f; + if (ndcX + ndcY > -1.0f) ++counts.upper; + ++counts.map[y / 4u][x / 4u]; + } + } + std::cout << "case " << name << ": records=" << records << " total=" << counts.total << " outsideFirstTriangle(x+y>-1)=" << counts.upper << "\n"; + for (const auto& row : counts.map) { + std::string line; + for (const auto count : row) line += count == 0u ? '.' : count == 16u ? '#' : '+'; + std::cout << " " << line << "\n"; + } + return counts; +} + +void Run(AgcDriver::VulkanDevice& device) { + std::cout << "device: " << device.DeviceName() << "\n"; + const Position v0{-1.0f, -1.0f, 0.0f, 1.0f}; + const Position v1{0.0f, -1.0f, 0.0f, 1.0f}; + const Position v2{-1.0f, 0.0f, 0.0f, 1.0f}; + const auto make = [&](const Position& last) { return std::vector{v0, v1, v2, last}; }; + const auto a = RunCase(device, "A out-of-range vertex 3", 3u, make({0.0f, 0.0f, 0.0f, 0.0f}), 3u); + const auto b = RunCase(device, "B in-range (0,0,0,0)", 4u, make({0.0f, 0.0f, 0.0f, 0.0f}), 4u); + const auto c = RunCase(device, "C repeat of v1", 4u, make(v1), 4u); + const auto d = RunCase(device, "D (0,0,0,1) at the centre", 4u, make({0.0f, 0.0f, 0.0f, 1.0f}), 4u); + const auto e = RunCase(device, "E (1,1,0,1) real triangle", 4u, make({1.0f, 1.0f, 0.0f, 1.0f}), 4u); + const Position zero{0.0f, 0.0f, 0.0f, 0.0f}; + const auto f = RunCase(device, "F negative zeros", 4u, make({-0.0f, 0.0f, -0.0f, -0.0f}), 4u); + const auto g = RunCase(device, "G triangle list, second triangle has (0,0,0,0)", 6u, {v0, v1, v2, v1, v2, zero}, 6u, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST); + const auto h = RunCase(device, "H strip with two (0,0,0,0) vertices", 5u, {v0, v1, v2, zero, zero}, 5u); + const auto p = RunCase(device, "P point list, (-0.5,-0.5,0,1) and (0,0,0,0)", 2u, {{-0.5f, -0.5f, 0.0f, 1.0f}, zero}, 2u, VK_PRIMITIVE_TOPOLOGY_POINT_LIST); + Require(e.upper > 0u, "probe cannot see the second triangle"); + Require(c.total > 0u && c.upper == 0u, "the first triangle is not drawn alone"); + Require(a.upper == 0u && a.total == c.total, "a vertex fetched past num_records must clip the primitive away"); + Require(b.upper == 0u && b.total == c.total, "a stored (0,0,0,0) vertex must clip the primitive away"); + Require(f.upper == 0u && f.total == c.total, "a stored (-0,0,-0,-0) vertex must clip the primitive away"); + Require(g.upper == 0u && g.total == c.total, "a triangle list with a (0,0,0,0) vertex must keep only the first triangle"); + Require(h.upper == 0u && h.total == c.total, "a strip with two (0,0,0,0) vertices must keep only the first triangle"); + Require(d.upper == 496u, "a (0,0,0,1) vertex must not be clipped away"); + Require(p.total == 1u, "a point at (0,0,0,0) must not be drawn"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index a876a4062..ca69aa93c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -164,6 +165,7 @@ struct SpirvEmitterState { std::uint32_t perVertexVariable = 0; std::uint32_t pointSizeVariable = 0; std::uint32_t clipDistanceVariable = 0; + std::optional zeroPositionClipIndex; std::uint32_t cullDistanceVariable = 0; std::uint32_t layerVariable = 0; std::uint32_t viewportIndexVariable = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 3cd67e86d..c484fa2b8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -291,6 +291,8 @@ void DefineInputs(SpirvEmitterState& state) { } } +constexpr std::uint32_t MaxClipDistances = 8u; + void DefineOutputs(SpirvEmitterState& state) { state.outputs.reserve(state.program.Info().outputs.size()); std::uint32_t clipDistanceCount = 0u; @@ -307,6 +309,18 @@ void DefineOutputs(SpirvEmitterState& state) { DefineMeshOutputs(state); return; } + const auto stage = state.program.Resources().stage; + const bool exportsPosition = std::ranges::any_of(state.outputs, [](const SpirvOutputBinding& output) { return output.kind == StageOutputKind::Position; }); + if (exportsPosition && (stage == IrShaderStage::Vertex || stage == IrShaderStage::TessellationEvaluation)) { + if (std::ranges::find(state.supportedCapabilities, spv::CapabilityClipDistance) == state.supportedCapabilities.end()) { + FailEmit("clipping a primitive with an all-zero clip position needs shaderClipDistance on the target device"); + } + state.zeroPositionClipIndex = clipDistanceCount; + clipDistanceCount += 1u; + if (clipDistanceCount > MaxClipDistances) { + FailEmit("guest clip distances plus the all-zero clip position distance exceed " + std::to_string(MaxClipDistances) + " clip distances"); + } + } const auto BuiltIn = [&](std::uint32_t& variable, std::uint32_t type, const char* name, std::uint32_t builtin) { if (variable == 0u) { variable = DefineInterfaceVariable(state, type, spv::StorageClassOutput, name); @@ -355,6 +369,9 @@ void DefineOutputs(SpirvEmitterState& state) { } } } + if (state.zeroPositionClipIndex) { + BuiltIn(state.clipDistanceVariable, F32ArrayType(state, clipDistanceCount), "gl_ClipDistance", spv::BuiltInClipDistance); + } if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psAlphaToCoverage) { 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); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 8a507d409..1793ea2f8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -417,6 +417,32 @@ void EmitAuxPositionExport(SpirvValueEmitContext& ctx, std::uint32_t data, const } } +void StoreZeroPositionClipDistance(SpirvEmitterState& state, std::uint32_t position) { + const auto bits = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitcast, TypeU32Vector(state, 4u), bits, position); + std::uint32_t combined = 0u; + for (std::uint32_t component = 0; component < 4u; component++) { + const auto raw = state.module.AllocateId(); + const auto magnitude = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), raw, bits, component); + state.module.AddFunction(spv::OpBitwiseAnd, TypeU32(state), magnitude, raw, ConstantU32(state, 0x7fffffffu)); + if (component == 0u) { + combined = magnitude; + continue; + } + const auto merged = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitwiseOr, TypeU32(state), merged, combined, magnitude); + combined = merged; + } + const auto allZero = state.module.AllocateId(); + const auto distance = state.module.AllocateId(); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpIEqual, TypeBool(state), allZero, combined, ConstantU32(state, 0u)); + state.module.AddFunction(spv::OpSelect, TypeF32(state), distance, allZero, ConstantF32Value(state, -1.0f), ConstantF32Value(state, 0.0f)); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeF32(state)), pointer, state.clipDistanceVariable, ConstantU32(state, *state.zeroPositionClipIndex)); + state.module.AddFunction(spv::OpStore, pointer, distance); +} + std::uint32_t ConvertClipCoordinate(SpirvEmitterState& state, std::uint32_t coordinate, float scale, float offset, float halfExtent) { const auto window = state.module.AllocateId(); const auto biased = state.module.AllocateId(); @@ -1312,6 +1338,9 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { value = ConvertPositionToClipSpace(state, value); } } + if (exp.kind == ExportTargetKind::Position && state.zeroPositionClipIndex) { + StoreZeroPositionClipDistance(state, value); + } if (state.program.Resources().stage == IrShaderStage::Mesh) { const auto kind = exp.kind == ExportTargetKind::Position ? StageOutputKind::Position : StageOutputKind::Parameter; state.module.AddFunction(spv::OpStore, MeshOutputPointer(state, kind, exp.index), value); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c1247e46b..f5709b025 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -625,6 +625,7 @@ Throughout the project, every function at every stage either **does exactly what - The [s_swappc_b64 fetch-shader call at vertex/local/tessellation/mesh entry](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) assumes the called fetch-shader blob ends in `s_setpc_b64` to the link pair (the GNM fetch-shader shape shadPS4's `EmitFetch` copies); the blob is not part of the shader program, so its return closes the pairing by assumption and any in-program reader of the link pair throws. Attribute fetches are inlined from the request's parsed fetch-shader plan through the same `GetAttribute` path as the embedded fetch analyzer, honoring the buffer resource `dst_sel`; an attribute writing more than four registers, or an entry call without a parsed plan, throws. The vertex/instance offset accumulator inside the fetch blob is not replayed for external blobs. - The scalar aperture sources [src_shared_base, src_shared_limit, src_private_base, src_private_limit](../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler) decode to their own operand kinds (scalar source codes 235 to 238, named as in LLVM's `SIRegisterInfo.td`), but their value is not modelled: reading one refuses with the source it refused named, because the wave's shared and private aperture bases and limits are unknown. Both the scalar-code read and the raw-operand read refuse; a shader that only carries the value into an address still prepares, one that reads it does not - [VertexBufferReadSize](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) and the [vertex staging copy](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a vertex fetch whose index is at or past the descriptor's `num_records` no longer throws for `OOB_SELECT` 0 and 1. The copy still spans the whole fetch, but the bytes from `num_records * stride` onward read as zeros, so the out-of-range vertex is `(0, 0, 0, 0)`. Measured with `buffer_load_format_xyzw` (`idxen`) through the [hardware oracle](HW_ORACLE.md) on RDNA2 gfx1036 for PPSA01325's word 3 `0x0004dfac` (`OOB_SELECT` 0) in #2241: indices 3, 4 and 5 over 3 records each return `00000000 00000000 00000000 00000000`, W included, unlike Vulkan `robustBufferAccess`, which returns `(0, 0, 0, 1)`. The zeros come from the record check, not from memory: stored values placed past the records are ignored, and a `num_records` 6 control returns them. `RESOURCE_LEVEL` 1, `OOB_SELECT` 1 and FORMAT 74 `32_32_32_FLOAT` give the same zeros, in wave32 and wave64; gfx1013 (the console) is not measured. `OOB_SELECT` 2 returns the stored values past the records and `OOB_SELECT` 3 compares `num_records` against the byte offset, both measured on the same setup in #2241; the two keep throwing past `num_records` here, since each needs its own rule. Zero-stride descriptors (`stride` 0) keep their separate byte-count check and are not covered here. Each out-of-range fetch is copied alone and outside the draw-snapshot cache, which is neither read nor written for it: a reused snapshot would carry another draw's bytes into the zero region, and a stored zero-padded copy would hand zeros to a later in-range draw at the same address. The valid prefix is still flushed for GPU writes before it is read. +- [EmitSetAttribute](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a primitive with a vertex whose clip position is (0,0,0,0) is clipped away through an extra `gl_ClipDistance` element on vertex and tessellation evaluation shaders, matching what a Radeon 610M (RDNA2, RADV) and llvmpipe rasterise; NVIDIA 615.78.08 without it draws the vertex at the viewport centre. Measured for triangles (strip and list) and for a point at (0,0,0,0), which the Radeon 610M does not draw, NVIDIA draws at the viewport centre and llvmpipe at the top-left corner; lines with such a vertex are clipped by the same element and were not measured. Mesh, geometry and the generated rect-list and barycentric stages do not write it, a device without `shaderClipDistance` cannot compile vertex shaders (the compile throws), and guest clip distances are still refused by the pipeline validator. The PS5 rasteriser was not measured. - [sceImeKeyboardGetResourceId](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) reports that no USB keyboard is connected (`0x80bc0004`, as [shadPS4](https://github.com/shadps4-emu/shadPS4) does) because IME keyboard events are not delivered to the handler, although [libSceKeyboard](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) exposes the host keyboard - [Player review dialog](../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) shows nothing: `scePlayerReviewDialogOpen` finishes at once and the result reports that the user canceled, as `sceWebBrowserDialogOpen` does - [NP reachability callbacks](../../core/libs/prx/libSceNpManager/Export.cpp) retain one callback and its user data; duplicate registration and unregistering an absent callback return the errors used by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/np/np_manager.cpp). No callback is delivered because the existing NP model remains signed out with reachability unavailable.