Merge pull request #1825 from TheAggie/feat/agc-ngg-vertex-mesh-v2

feat(agc): run unregistered NGG vertex programs that need a workgroup on the mesh path
This commit is contained in:
Adrià 2026-10-11 09:35:19 +02:00 committed by GitHub
commit 2bb78d2c7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 93 additions and 11 deletions

View File

@ -81,6 +81,7 @@ struct ShaderSnapshot {
std::vector<std::byte> header;
std::shared_ptr<PreparedShaders> prepared = std::make_shared<PreparedShaders>();
std::shared_ptr<const RegisteredShaderState> registeredState;
std::shared_ptr<std::atomic<std::uint8_t>> workgroupUse = std::make_shared<std::atomic<std::uint8_t>>(0u);
};
@ -88,6 +89,7 @@ std::shared_ptr<const ShaderSnapshot> ReadRawShader(std::uint64_t address);
std::shared_ptr<const ShaderSnapshot> ProgramSnapshot(const ShaderRegistry& shaders, std::uint64_t address, std::initializer_list<std::uint8_t> types);
void NoteRawGraphicsDraw();
bool RawGraphicsDrawn();
bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset);
std::shared_ptr<const ShaderRecompiler::SourceHandle> PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation = nullptr);

View File

@ -109,7 +109,16 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const
std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Submission& submission) {
auto product = std::make_shared<DrawDecode>();
product->state = Graphics::DecodeState(queue);
bool vertexWorkgroup = false;
const auto stages = queue.context.find(0x2d5);
const auto low = queue.shader.find(0xc8);
const auto high = queue.shader.find(0xc9);
if (stages != queue.context.end() && (stages->second & 0x02002024u) == 0x2000u && low != queue.shader.end() && high != queue.shader.end()) {
const auto address = (static_cast<std::uint64_t>(low->second) << 8u) | (static_cast<std::uint64_t>(high->second & 0xffu) << 40u);
const auto front = ProgramSnapshot(*submission.shaders, address, {2});
vertexWorkgroup = front->header.empty() && ProgramUsesWorkgroup(*front, static_cast<std::size_t>((address - front->codeAddress) / sizeof(std::uint32_t)));
}
product->state = Graphics::DecodeState(queue, vertexWorkgroup);
DecodeGraphicsPrograms(*product, queue, *submission.shaders, false, true);
product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(product->state), Graphics::PixelProgramSkipped(queue));
product->pixel.targetExportPacking = Graphics::ExportPackings(product->state);

View File

@ -15,6 +15,7 @@
#include <condition_variable>
#include <cstdio>
#include <cstddef>
#include <span>
#include <cstdlib>
#include <cstring>
#include <deque>
@ -131,6 +132,21 @@ void AwaitRegisteredPreparation(PreparedShaders& prepared, std::unique_lock<std:
}
bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset) {
constexpr std::uint8_t Unknown = 0, Unused = 1, Used = 2;
if (codeOffset == 0) {
const auto cached = snapshot.workgroupUse->load(std::memory_order_acquire);
if (cached != Unknown) return cached == Used;
}
const auto decoded = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(std::span(snapshot.code).subspan(codeOffset));
const bool used = std::any_of(decoded.instructions.begin(), decoded.instructions.end(), [](const auto& instruction) {
return instruction.family == ShaderRecompiler::RdnaInstructionFamily::DS || instruction.op == ShaderRecompiler::RdnaOpcode::SBarrier;
});
if (codeOffset == 0) snapshot.workgroupUse->store(used ? Used : Unused, std::memory_order_release);
return used;
}
struct ShaderPreparationTransaction::State {
struct Change {
std::shared_ptr<PreparedShaders> destination;

View File

@ -110,8 +110,8 @@ struct State {
std::array<float, 4> blendConstants;
};
ShaderStages DecodeShaderStages(const QueueState& queue);
State DecodeState(const QueueState& queue);
ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false);
State DecodeState(const QueueState& queue, bool vertexWorkgroup = false);
std::array<std::uint8_t, 8> ExportMappings(const State& state);
std::array<ShaderRecompiler::ColorExportPacking, 8> ExportPackings(const State& state);
ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot);

View File

@ -478,7 +478,7 @@ void intersect(VkRect2D& result, const Registers& registers, std::uint32_t offse
}
ShaderStages DecodeShaderStages(const QueueState& queue) {
ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) {
const auto value = read(queue.context, 0x2d5);
const auto validate = [&](bool condition, const char* reason) {
if (condition) return;
@ -490,7 +490,8 @@ ShaderStages DecodeShaderStages(const QueueState& queue) {
validate((value & 3u) != 3u && ((value >> 3u) & 3u) != 3u && ((value >> 6u) & 3u) != 3u, "reserved LS_EN, ES_EN or VS_EN encoding");
const auto primitive = read(queue.userConfig, 0x242, RegisterBank::UserConfig);
const bool tessellation = primitive == 9;
const bool geometry = (value & 0x20u) != 0;
validate(!vertexWorkgroup || ((value & 0x02002020u) == 0x2000u && !tessellation), "a vertex program that needs a workgroup requires PRIMGEN_EN without GS_EN, PRIMGEN_PASSTHRU_EN or tessellation");
const bool geometry = (value & 0x20u) != 0 || vertexWorkgroup;
validate(tessellation == ((value & 4u) != 0), "Patch topology and HS_EN disagree");
validate(!tessellation || !geometry, "combined tessellation and geometry is unsupported by the reference path");
const auto path = tessellation ? ShaderPath::Tessellation : geometry ? ShaderPath::Geometry : ShaderPath::Vertex;
@ -514,15 +515,16 @@ ShaderStages DecodeShaderStages(const QueueState& queue) {
const auto vertices = (group >> 9u) & 0x1ffu;
const auto primitives = group & 0x1ffu;
const auto maxVertices = read(queue.context, 0x1ff);
const auto verticesPerPrimitive = read(queue.context, 0x2ce);
const auto verticesPerPrimitive = vertexWorkgroup ? 3u : read(queue.context, 0x2ce);
validate((primitive == 1 || primitive == 2 || primitive == 4 || primitive == 5 || primitive == 6) && read(queue.context, 0x29b) == 2 && verticesPerPrimitive >= 3, "unsupported geometry input or output assembly");
const auto inputSize = primitive == 1 ? 1u : primitive == 2 ? 2u : 3u;
validate(vertices >= inputSize && maxVertices != 0 && maxVertices <= 256 && verticesPerPrimitive <= 256, "invalid geometry subgroup output");
validate(vertices >= inputSize && maxVertices != 0 && (!vertexWorkgroup || maxVertices >= inputSize) && maxVertices <= 256 && verticesPerPrimitive <= 256, "invalid geometry subgroup output");
const auto inputStep = primitive == 5 || primitive == 6 ? 1u : inputSize;
const auto groupPrimitives = std::min({primitives, (vertices - inputSize) / inputStep + 1u, maxVertices / verticesPerPrimitive});
const auto outputLimit = vertexWorkgroup ? (maxVertices - inputSize) / inputStep + 1u : maxVertices / verticesPerPrimitive;
const auto groupPrimitives = std::min({primitives, (vertices - inputSize) / inputStep + 1u, outputLimit});
validate(groupPrimitives != 0, "geometry subgroup contains no primitives");
const auto resources = read(queue.shader, 0x8b, RegisterBank::Shader);
validate(((read(queue.shader, 0x8a, RegisterBank::Shader) >> 29u) & 3u) == 3 && ((resources >> 16u) & 3u) == 3, "unsupported geometry VGPR allocation");
validate(vertexWorkgroup || (((read(queue.shader, 0x8a, RegisterBank::Shader) >> 29u) & 3u) == 3 && ((resources >> 16u) & 3u) == 3), "unsupported geometry VGPR allocation");
const auto esgsItemSize = read(queue.context, 0x2ab);
validate(esgsItemSize != 0 && esgsItemSize * vertices <= 0xffffu, "invalid VGT_ESGS_RING_ITEMSIZE");
const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, primitives * (verticesPerPrimitive - 2u)});
@ -540,12 +542,12 @@ std::string DepthMaintenanceRejection(const QueueState& queue) {
return zeroMessage(0x000, control->second, "DB_RENDER_CONTROL depth copy, resummarize or decompress");
}
State DecodeState(const QueueState& queue) {
State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
if (auto reason = DepthMaintenanceRejection(queue); !reason.empty()) throw std::runtime_error(reason);
const auto& cx = queue.context;
State result{};
result.conservativeRasterization = decodeConservativeRasterization(queue);
result.stages = DecodeShaderStages(queue);
result.stages = DecodeShaderStages(queue, vertexWorkgroup);
const auto primitive = read(queue.userConfig, 0x242, RegisterBank::UserConfig);
APS5_LOG_OUT_DEBUG("DecodeState primitive=%u path=%u vertexWave=%u", primitive, static_cast<unsigned>(result.stages.path), result.stages.vertexWaveSize);
switch (primitive) {

View File

@ -468,6 +468,41 @@ void ShaderStageTests() {
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register");
}
void NggVertexWorkgroupTests() {
auto queue = makeState();
queue.context[0x2d5] = 0x12010;
queue.userConfig[0x242] = 4;
queue.userConfig[0x25b] = 0x10080;
queue.context[0x1ff] = 0x80;
queue.context[0x2ce] = 0;
queue.context[0x29b] = 2;
queue.context[0x2ab] = 1;
queue.shader[0x8a] = 0x222c0009;
queue.shader[0x8b] = 0x43001e;
queue.context[0x1b3] = 2;
queue.context[0x1b4] = 2;
Require(AgcDriver::Graphics::DecodeState(queue).stages.path == AgcDriver::Graphics::ShaderPath::Vertex, "an NGG vertex program without a workgroup left the vertex path");
auto state = AgcDriver::Graphics::DecodeState(queue, true);
Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Geometry && state.stages.mesh, "an NGG vertex program that needs a workgroup did not take the mesh path");
auto mesh = *state.stages.mesh;
Require(mesh.inputPrimitive == 4 && mesh.primitivesPerGroup == 42 && mesh.verticesPerGroup == 126 && mesh.maxVertices == 128 && mesh.maxPrimitives == 128 && mesh.threadsPerGroup == 128 && mesh.ldsSizeDwords == 1024 && mesh.esgsItemSize == 1, "the NGG vertex subgroup of a triangle list changed");
queue.userConfig[0x242] = 6;
mesh = *AgcDriver::Graphics::DecodeState(queue, true).stages.mesh;
Require(mesh.primitivesPerGroup == 126 && mesh.verticesPerGroup == 128 && mesh.maxPrimitives == 128, "the NGG vertex subgroup of a triangle strip is not bounded by its vertex budget");
queue.shader[0x8a] = 0x222c0005;
queue.shader[0x8b] = 0x400030;
Require(AgcDriver::Graphics::DecodeState(queue, true).stages.mesh.has_value(), "an NGG vertex program that loads only its vertex ID was rejected");
queue.shader[0x8a] = 0x222c0009;
queue.shader[0x8b] = 0x43001e;
queue.context[0x1ff] = 2;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue, true); }, "invalid geometry subgroup output");
queue.context[0x1ff] = 0x80;
for (const auto stages : {0x02012010u, 0x00012030u, 0x00010010u}) {
queue.context[0x2d5] = stages;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue, true); }, "requires PRIMGEN_EN without GS_EN, PRIMGEN_PASSTHRU_EN or tessellation");
}
}
constexpr std::array<std::uint8_t, 8> IdentityExports{0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u};
std::vector<spv::BuiltIn> pixelBuiltinsRead(std::uint32_t ena, std::uint32_t addr, std::uint32_t source) {
@ -4071,6 +4106,7 @@ int main() {
uint16ExportTests();
uint8x4TargetTests();
ShaderStageTests();
NggVertexWorkgroupTests();
TuningFieldTests();
PixelInputLayoutTests();
ColorPipeBankXorTests();

View File

@ -602,6 +602,21 @@ void testProgramSnapshots() {
check(AgcDriver::DriverDetail::DecodeProgramsCurrent(decode, shaders), "a decode with the rewritten program was not current");
}
void testProgramWorkgroupUse() {
const auto snapshot = [](std::vector<std::uint32_t> code) {
AgcDriver::DriverDetail::ShaderSnapshot result{0x1000, 0, 2, std::move(code), {}};
return result;
};
const auto plain = snapshot({0xbf800000, 0xbf810000});
check(!AgcDriver::DriverDetail::ProgramUsesWorkgroup(plain, 0) && !AgcDriver::DriverDetail::ProgramUsesWorkgroup(plain, 0), "a program without LDS or barriers needs a workgroup");
const auto barrier = snapshot({0xbf8a0000, 0xbf810000});
check(AgcDriver::DriverDetail::ProgramUsesWorkgroup(barrier, 0), "an s_barrier did not need a workgroup");
const auto lds = snapshot({0xd8340000, 0x00000100, 0xbf810000});
check(AgcDriver::DriverDetail::ProgramUsesWorkgroup(lds, 0) && AgcDriver::DriverDetail::ProgramUsesWorkgroup(lds, 0), "an LDS write did not need a workgroup");
const auto entry = snapshot({0xbf8a0000, 0xbf810000, 0xbf800000, 0xbf810000});
check(!AgcDriver::DriverDetail::ProgramUsesWorkgroup(entry, 2) && AgcDriver::DriverDetail::ProgramUsesWorkgroup(entry, 0), "the workgroup check did not start at the program's entry");
}
void testWorkerFailure() {
std::array<std::uint32_t, 5> words{0xc0031500, 1, 1, 1, 0x41};
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
@ -694,6 +709,7 @@ int main() {
check(!unterminated.empty() && bounded->code.size() == 64, "raw compute crossed inaccessible memory or missed its last instruction");
#endif
testProgramSnapshots();
testProgramWorkgroupUse();
testEvents();
testValidation();
testClearState();

View File

@ -132,6 +132,7 @@ Throughout the project, every function at every stage either **does exactly what
- [SET_PREDICATION](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw
- [sceAgcGetGsOversubscription](../../core/libs/prx/libSceAgc/Shader/src/GsOversubscription.cpp) (libSceAgc) - signature and occupancy formula (base and expanded vertex/export capacities, budget shift, `GE_PC_ALLOC` / `SPI_SHADER_PGM_RSRC4_GS` encoding) follow KytyPS5 only, not confirmed on hardware or a title; a negative or NaN factor that gives a negative target, a missing GS context register or a zero `GE_MAX_OUTPUT_PER_SUBGROUP` throws
- [Vertex entry merged wave info](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - `s3` of every vertex-path shader is set to `waveSize | waveSize << 8` (vertex and primitive count of a full wave, wave index 0). Layout taken from the prologue Mesa's ACO emits for NGG vertex shaders (`s_bfe_u64 exec` from `s3`), not measured on hardware. The driver gives every graphics program user data from `s8`, so the condition does not tell NGG from legacy vertex shaders: they just never read `s3`. A draw whose last wave is partial still reports a full wave.
- [NGG vertex programs on the mesh path](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - an unregistered vertex program with `PRIMGEN_EN` that uses LDS or `s_barrier` runs on the geometry (mesh) path. Each input primitive is assumed to yield at most one output primitive, the subgroup is bounded by `GE_MAX_OUTPUT_PER_SUBGROUP` as a vertex budget, and the GS/ES VGPR count checks are skipped, because the merged inputs keep their positions. Taken from the code Mesa's ACO emits for NGG culling and from PPSA59322 and PPSA99010 draws, not measured on hardware. Registered programs keep the vertex path
- [s_barrier in vertex-path programs](../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Programs with LDS, GDS or memory writes throw. Not measured on hardware
- [Strided buffer overhang](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a strided buffer descriptor that ends less than one stride past the end of the readable registered allocation holding its base is bound only up to that allocation's end, even when another readable allocation starts right there. This assumes the bytes past the end are the rounded-up `num_records` of the last record (RADV rounds it up), which no fetch inside the buffer reads; a guest that really keeps a vertex split across two adjacent allocations would read zeros for the cut tail. Taken from the RetroArch Dolphin core case in #2912, not measured on hardware.
- [Zero-stride vertex fetch range](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - for a stride-0 V# with OOB_SELECT 2 the whole fetch reads memory unless NUM_RECORDS is 0; with OOB_SELECT 3 it reads zero when NUM_RECORDS is 0 and throws when the fetch only partly fits. Rules taken from the GFX10 OOB_SELECT table in Mesa [ac_descriptors.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/ac_descriptors.c), not measured on hardware. Whether OOB_SELECT 3 checks each dword of a multi-dword fetch separately is unknown. OOB_SELECT 0 and 1 keep the structured record checks.