fix(agc): register the null pixel program with the inputs and user SGPRs its draws use

Since #1178 the SPI_PS_INPUT_ENA/ADDR emplace calls did nothing, so the registered variant had no pixel inputs, and null pixel draws took the last pixel shader's user SGPR count and the bound targets' export mapping. Registration now overwrites those registers, the draw gives the null program no user SGPRs and an empty export mapping, and the at-draw preparation is left for wave32 only. Root cause found by @Raghav314.
This commit is contained in:
Joey Fisher 2026-10-09 09:38:18 -06:00 committed by Adrià Franch
parent bbfd61430a
commit e2664bef83
5 changed files with 71 additions and 22 deletions

View File

@ -18,6 +18,10 @@ namespace ShaderRecompiler {
class ShaderPreparationContext;
}
namespace AgcDriver {
class VulkanDevice;
}
namespace AgcDriver::DriverDetail {
struct ShaderSnapshot;
@ -78,6 +82,7 @@ std::shared_ptr<const ShaderSnapshot> ReadRawComputeShader(std::uint64_t address
std::shared_ptr<const ShaderRecompiler::SourceHandle> PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation = nullptr);
std::uint64_t NullPixelProgramAddress();
ShaderSnapshot PrepareNullPixelProgram(const VulkanDevice& device);
std::optional<ShaderRecompiler::ShaderFloatMode> RegisteredFloatMode(const ShaderSnapshot& snapshot);
void PublishRegisteredShader(std::shared_ptr<ShaderRegistry>& registry, const std::shared_ptr<const ShaderSnapshot>& snapshot);

View File

@ -35,8 +35,8 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const
require(address - snapshot.codeAddress < snapshot.code.size() * sizeof(std::uint32_t), "graphics program is outside registered shader code");
require((address - snapshot.codeAddress) % sizeof(std::uint32_t) == 0, "graphics entry point is not dword aligned");
require(snapshot.type == type, "graphics program refers to an incompatible shader binary type");
Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, rsrc2);
const auto resources = nullPixel && !queue.shader.contains(rsrc2) ? 0u : ReadGraphicsRegister(queue.shader, rsrc2);
if (!nullPixel) Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, rsrc2);
const auto resources = nullPixel ? 0u : ReadGraphicsRegister(queue.shader, rsrc2);
const auto userCount = ((resources >> 1u) & 0x1fu) | (((resources >> 27u) & 1u) << 5u);
require(userCount <= 32, "graphics user SGPR count exceeds the register bank");
const auto codeOffset = static_cast<std::size_t>((address - snapshot.codeAddress) / sizeof(std::uint32_t));

View File

@ -182,8 +182,8 @@ void PublishRegisteredShader(std::shared_ptr<ShaderRegistry>& registry, const st
namespace {
bool PreparedAtUse(const ShaderSnapshot& snapshot, const ShaderRecompiler::RecompileRequest& request) {
if (snapshot.codeAddress == NullPixelProgramAddress() && request.shader.stage == ShaderRecompiler::ShaderStage::Fragment) {
APS5_LOG_ERR("The null pixel program has no artifact for this draw (wave %u, %zu user SGPRs); preparing it at draw", request.context.waveSize, request.context.userData.size());
if (snapshot.codeAddress == NullPixelProgramAddress() && request.shader.stage == ShaderRecompiler::ShaderStage::Fragment && request.context.waveSize == 32u) {
APS5_LOG_ERR("The null pixel program has no wave%u artifact for this draw; preparing it at draw", request.context.waveSize);
return true;
}
if (!snapshot.header.empty()) return snapshot.prepared->deferred;
@ -718,6 +718,26 @@ void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr<
transaction.Commit();
}
ShaderSnapshot PrepareNullPixelProgram(const VulkanDevice& device) {
ShaderSnapshot null{NullPixelProgramAddress(), reinterpret_cast<std::uintptr_t>(&NullPixelShader), NullPixelShader.type, {}, {}};
null.code.assign(std::begin(NullPixelCode), std::end(NullPixelCode));
null.header.resize(sizeof(Shader));
std::memcpy(null.header.data(), &NullPixelShader, sizeof(Shader));
auto nullRegisteredState = DecodeRegisteredState(null);
nullRegisteredState.shader.insert_or_assign(0x008u, static_cast<std::uint32_t>(null.codeAddress >> 8u));
nullRegisteredState.shader.insert_or_assign(0x009u, static_cast<std::uint32_t>(null.codeAddress >> 40u));
nullRegisteredState.shader.insert_or_assign(0x00bu, 0u);
nullRegisteredState.context.insert_or_assign(0x1b3u, 0x2u);
nullRegisteredState.context.insert_or_assign(0x1b4u, 0x2u);
null.registeredState = std::make_shared<const RegisteredShaderState>(std::move(nullRegisteredState));
QueueState nullState{};
nullState.shader = null.registeredState->shader;
nullState.context = null.registeredState->context;
nullState.userConfig = null.registeredState->userConfig;
null.prepared->entries = PrepareRegistered(null, device, nullState, true);
return null;
}
void Driver::RegisterShader(const Shader* shader) {
PerformanceContext timingContext(FrameTiming::Preparation());
PerformanceTimer timing("Shader.Register");
@ -789,23 +809,7 @@ void Driver::RegisterShader(const Shader* shader) {
rethrowFailure();
PublishRegisteredShader(shaders, std::make_shared<const ShaderSnapshot>(std::move(snapshot)));
if (shaders->find(NullPixelProgramAddress()) == shaders->end()) {
ShaderSnapshot null{NullPixelProgramAddress(), reinterpret_cast<std::uintptr_t>(&NullPixelShader), NullPixelShader.type, {}, {}};
null.code.assign(std::begin(NullPixelCode), std::end(NullPixelCode));
null.header.resize(sizeof(Shader));
std::memcpy(null.header.data(), &NullPixelShader, sizeof(Shader));
auto nullRegisteredState = DecodeRegisteredState(null);
nullRegisteredState.shader.emplace(0x008u, static_cast<std::uint32_t>(null.codeAddress >> 8u));
nullRegisteredState.shader.emplace(0x009u, static_cast<std::uint32_t>(null.codeAddress >> 40u));
nullRegisteredState.shader.emplace(0x00bu, 0u);
nullRegisteredState.context.emplace(0x1b3u, 0x2u);
nullRegisteredState.context.emplace(0x1b4u, 0x2u);
null.registeredState = std::make_shared<const RegisteredShaderState>(std::move(nullRegisteredState));
QueueState nullState{};
nullState.shader = null.registeredState->shader;
nullState.context = null.registeredState->context;
nullState.userConfig = null.registeredState->userConfig;
null.prepared->entries = PrepareRegistered(null, *localDevice, nullState, true);
PublishRegisteredShader(shaders, std::make_shared<const ShaderSnapshot>(std::move(null)));
PublishRegisteredShader(shaders, std::make_shared<const ShaderSnapshot>(PrepareNullPixelProgram(*localDevice)));
}
transaction.Commit();
}

View File

@ -158,7 +158,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
.conservativeZExport = static_cast<ShaderRecompiler::ConservativeZExport>(conservativeZExport),
.orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0,
.targetOutputMode = targetOutputMode,
.targetExportMapping = exportMappings
.targetExportMapping = nullProgram ? std::array<std::uint8_t, 8>{} : exportMappings
};
}

View File

@ -5,6 +5,7 @@
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
#include "Optimization/ResourceProgram.hpp"
#include <spirv/unified1/spirv.hpp>
#include <algorithm>
#include <array>
#include <vector>
#include <cstring>
@ -247,6 +248,44 @@ void Check(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ShaderPath path
Reject([&] { DecodeGraphicsPrograms(invalid, queue, registry, true, true); }, "reserved graphics program address");
}
void NullPixelMatchesRegistration(AgcDriver::VulkanDevice& device) {
using namespace AgcDriver::DriverDetail;
Fixture front;
front.Initialize(2u);
const auto null = std::make_shared<ShaderSnapshot>(PrepareNullPixelProgram(device));
const auto registered = null->prepared->entries;
Require(!registered.empty(), "the null pixel program was not prepared at registration");
ShaderRegistry registry;
registry.emplace(front.snapshot->codeAddress, front.snapshot);
registry.emplace(null->codeAddress, null);
AgcDriver::QueueState queue{};
queue.context[0x8e] = 0xfu;
queue.context[0x8f] = 0xfu;
front.Bind(queue, 0xc8u, 0x8bu);
alignas(8) const std::array<std::uint32_t, 2> merged{};
const auto mergedAddress = reinterpret_cast<std::uintptr_t>(merged.data());
queue.shader[0x82u] = static_cast<std::uint32_t>(mergedAddress);
queue.shader[0x83u] = static_cast<std::uint32_t>(mergedAddress >> 32u);
queue.shader[0x008u] = 0u;
queue.shader[0x009u] = 0u;
queue.shader[0x00bu] = 4u << 1u;
queue.context[0x1b3u] = 0x30u;
queue.context[0x1b4u] = 0x30u;
DrawDecode draw{};
draw.state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex;
draw.state.stages.vertexWaveSize = 32u;
draw.state.stages.fragmentWaveSize = 64u;
std::array<std::uint8_t, 8> mappings{};
mappings.fill(0xe4u);
draw.pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, mappings, true);
DecodeGraphicsPrograms(draw, queue, registry, false, true);
const auto& fragment = draw.programs.back();
Require(fragment.snapshot == null && fragment.userData.empty(), "a null pixel draw took the last pixel shader's user SGPRs");
const auto stages = PrepareGraphicsStages(draw, device.Target());
const auto& handle = stages.back().entry.handle;
Require(std::ranges::any_of(registered, [&](const auto& entry) { return entry.handle == handle; }), "a null pixel draw did not match the variant prepared at registration");
}
}
int main(int argc, char** argv) {
@ -255,6 +294,7 @@ int main(int argc, char** argv) {
if (!device) return VulkanTestSkipped;
Require(argc <= 2, "invalid test arguments");
const auto dump = argc == 2 ? std::filesystem::path(argv[1]) : std::filesystem::path{};
NullPixelMatchesRegistration(*device);
Check(*device, AgcDriver::Graphics::ShaderPath::Vertex, dump);
Check(*device, AgcDriver::Graphics::ShaderPath::Geometry, dump);
Check(*device, AgcDriver::Graphics::ShaderPath::Tessellation, dump);