fix(shader): clip primitives with an all-zero clip position

This commit is contained in:
fma-rc 2026-10-11 20:05:44 +02:00
parent e013f8c9c5
commit 9db923e907
No known key found for this signature in database
15 changed files with 216 additions and 11 deletions

View File

@ -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");

View File

@ -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();

View File

@ -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;

View File

@ -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<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);
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);
}

View File

@ -732,7 +732,7 @@ std::set<std::uint32_t> CachedFragmentOutputs(const Context& context, std::span<
}
std::set<std::uint32_t> 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());

View File

@ -43,6 +43,7 @@ struct Module {
std::map<std::uint32_t, std::string> outputs;
std::optional<std::string> secondSource;
bool position = false;
bool clipDistance = false;
bool primitiveIndices = false;
bool fragmentBarycentric = false;
std::map<std::uint32_t, std::vector<std::uint32_t>> 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<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) {
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) {
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<std::uint32_t> ValidateShaders(std::span<const CompiledShader> 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);

View File

@ -36,6 +36,8 @@ void requireInvariantPositions(const ShaderRecompiler::RecompileResult& result,
constexpr std::array<std::uint32_t, 3> PositionExport{0xf80008cfu, 0u, 0xbf810000u};
constexpr std::array<std::uint32_t, 1> 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;

View File

@ -50,7 +50,7 @@
namespace {
ShaderRecompiler::SpirvTarget BufferTarget() {
static constexpr std::array<std::uint32_t, 5> capabilities{spv::CapabilityShader, spv::CapabilityImageGatherExtended, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess};
static constexpr std::array<std::uint32_t, 6> capabilities{spv::CapabilityShader, spv::CapabilityImageGatherExtended, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance};
static constexpr std::array<std::string_view, 2> 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<std::uint32_t, 4> capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic};
static constexpr std::array<std::uint32_t, 5> capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityClipDistance};
const auto pixel = [](std::span<const std::uint32_t> 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<std::uint32_t, 4> capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic};
static constexpr std::array<std::uint32_t, 5> capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityClipDistance};
static constexpr std::array<std::uint32_t, 4> userData{0u, 0u, 0u, 0x2020u};
const auto vertex = [](std::span<const std::uint32_t> 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<std::uint32_t, 3> noArithmetic{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle};
static constexpr std::array<std::uint32_t, 4> 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;

View File

@ -50,8 +50,8 @@ void CheckStorageOnlyModule(const RecompileResult& result, std::string_view name
}
SpirvTarget StorageOnlyTarget() {
static constexpr std::array<std::uint32_t, 3> capabilities{
spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess,
static constexpr std::array<std::uint32_t, 4> capabilities{
spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance,
};
static constexpr std::array<std::string_view, 4> extensions{
"SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer",

View File

@ -12,6 +12,8 @@ namespace {
constexpr std::array<std::uint32_t, 4> BarrierExport{0xbf8a0000u, 0xf80008cfu, 0u, 0xbf810000u};
constexpr std::array<std::uint32_t, 6> BarrierLdsExport{0xd8340000u, 0u, 0xbf8a0000u, 0xf80008cfu, 0u, 0xbf810000u};
constexpr std::array<std::uint32_t, 1> VertexCapabilities{spv::CapabilityClipDistance};
ShaderRecompiler::RecompileRequest vertexRequest(std::span<const std::uint32_t> code) {
using namespace ShaderRecompiler;
RecompileRequest request{};
@ -21,6 +23,7 @@ ShaderRecompiler::RecompileRequest vertexRequest(std::span<const std::uint32_t>
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;

View File

@ -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 <array>
#include <cstdint>
#include <cstring>
#include <exception>
#include <iostream>
#include <optional>
#include <span>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Extent = 64u;
alignas(256) std::array<std::byte, Extent * Extent * 4u> pixels{};
alignas(256) std::array<std::byte, 512> vertices{};
constexpr std::array<std::uint32_t, 11> vertexCode{0xf4080100u, 0xfa000000u, 0x4a0a0a02u, 0x4a0a0b08u, 0x4a0a0a03u, 0xe00c2000u, 0x80010005u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u};
constexpr std::array<std::uint32_t, 5> pixelCode{0x7e0002f2u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u};
using Position = std::array<float, 4>;
struct Counts {
unsigned total = 0;
unsigned upper = 0;
std::array<std::array<unsigned, 16>, 16> map{};
};
Counts RunCase(AgcDriver::VulkanDevice& device, const char* name, std::uint32_t records, const std::vector<Position>& 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<std::uint32_t, 4> descriptor{};
const auto tableAddress = reinterpret_cast<std::uintptr_t>(descriptor.data());
std::array<std::uint32_t, 4> userData{static_cast<std::uint32_t>(tableAddress), static_cast<std::uint32_t>(tableAddress >> 32u), 0u, 0u};
const std::array<ShaderRecompiler::MemoryRegion, 1> 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<std::uintptr_t>(vertexCode.data()), vertexCode, 0, {}}, {32u, 0u, userData, std::nullopt, std::nullopt, vertexInfo, memory}, device.Target(), {0u, 0u, 0u, 96u}};
const auto address = reinterpret_cast<std::uintptr_t>(vertices.data());
descriptor = {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(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<std::uint32_t>(vertex.pushConstants.size());
ShaderRecompiler::ShaderPixelStageInfo pixelInfo{};
pixelInfo.wave32 = true;
pixelInfo.targetOutputMode[0] = 9u;
pixelInfo.targetExportMapping.fill(0xe4u);
ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(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<AgcDriver::Graphics::CompiledShader, 2> 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<std::uintptr_t>(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<float>(Extent), static_cast<float>(Extent), -static_cast<float>(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<std::uint32_t>(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<float>(x) + 0.5f) / Extent * 2.0f - 1.0f;
const float ndcY = 1.0f - (static_cast<float>(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<Position>{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;
}
}

View File

@ -8,6 +8,7 @@
#include <cstddef>
#include <cstdint>
#include <map>
#include <optional>
#include <span>
#include <string_view>
#include <unordered_map>
@ -164,6 +165,7 @@ struct SpirvEmitterState {
std::uint32_t perVertexVariable = 0;
std::uint32_t pointSizeVariable = 0;
std::uint32_t clipDistanceVariable = 0;
std::optional<std::uint32_t> zeroPositionClipIndex;
std::uint32_t cullDistanceVariable = 0;
std::uint32_t layerVariable = 0;
std::uint32_t viewportIndexVariable = 0;

View File

@ -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);

View File

@ -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);

View File

@ -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.