feat(agc-driver): execute boolean SET_PREDICATION

SET_PREDICATION threw, and packets with the predicate header bit were
rejected. The executor now keeps the queue's predication: operations 3
and 4 read a 64- or 32-bit value and bit 8 selects whether predicated
packets run when it is non-zero or zero; operation 0 clears it. Before
a predicated packet runs while predication is active, queued labels are
recorded and the value is read, as for COND_EXEC; the packet is skipped
when the predicate fails.

Indirect buffers are inlined when a submission is copied, so a
predicated INDIRECT_BUFFER call keeps its header in front of the
inlined commands and records their end like a COND_EXEC range; the
block runs or is skipped on one evaluation, as on the command
processor. Query predication, a predicated COND_EXEC, predicated chains
and flips inside predicated command buffers throw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
alfalfa-star 2026-10-05 21:02:47 +02:00
parent 823e01e3c1
commit 124be3e7e8
8 changed files with 159 additions and 10 deletions

View File

@ -88,6 +88,8 @@ bool AccessesMemory(std::uint32_t header);
// synchronization): such a packet needs a pipeline barrier, not a device drain.
bool UsesGpuCacheBarrier(std::span<const std::uint32_t> packet);
bool IsTagMarker(std::span<const std::uint32_t> packet);
inline bool Predicated(std::uint32_t header) { return !FillerPacket(header) && (header & 1u) != 0; }
bool PredicationPasses(const QueueState& queue);
bool WaitSatisfied(std::span<const std::uint32_t> packet);
// WaitSatisfied for a polling loop: the caller has validated the address once, so the value is read
// directly instead of through the checked guest memory path.

View File

@ -167,6 +167,12 @@ inline Registers InitialContextRegisters() {
return result;
}
struct Predication {
std::uint64_t address = 0;
std::uint32_t operation = 0;
bool executeWhenSet = false;
};
struct QueueState {
Registers shader;
Registers context = InitialContextRegisters();
@ -180,6 +186,7 @@ struct QueueState {
std::uint32_t indexType = 0;
std::uint32_t instanceCount = 1;
std::vector<std::string> markers;
Predication predication;
void ClearContext() {
context = InitialContextRegisters();

View File

@ -117,6 +117,17 @@ void Driver::execute(const Submission& submission) {
GuestMemory::SetCurrentPacket(header == FlipPacketHeader ? 0xffffu : opcode, submission.queue);
CaptureTrace::Log("packet submission=%llu queue=%x offset=%zu header=%08x words=%zu", static_cast<unsigned long long>(submission.serial), submission.queue, cursor, header, packet.size());
if (Pm4::Predicated(header) && queue.predication.operation != 0) {
recordQueuedLabelsBeforeRead(submission.queue);
if (!Pm4::PredicationPasses(queue)) {
cursor = opcode == 0x3f ? submission.conditionalEnds.at(cursor) : nextCursor;
continue;
}
}
if (opcode == 0x3f) {
cursor = nextCursor;
continue;
}
const auto flushStart = profilePackets ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
flushBetweenPackets(submission.queue, header, opcode == 0x49 || opcode == 0x37);

View File

@ -50,6 +50,19 @@ bool Driver::copySegment(Submission& submission, const std::uint32_t* guest, std
const bool chain = (guest[cursor + 3] & (1u << 20u)) != 0;
require(!chain || guarded.empty(), "a chained INDIRECT_BUFFER inside a conditional execution range is not implemented");
GuestMemory::CheckRange(target, targetWords * sizeof(std::uint32_t), alignof(std::uint32_t));
if (Pm4::Predicated(header)) {
require(!chain, "predicated command buffer chains are not implemented");
const auto start = submission.commands.size();
submission.commands.insert(submission.commands.end(), guest + cursor, guest + cursor + count);
require(!copySegment(submission, target, targetWords, budget), "a REWIND inside a predicated command buffer is not implemented");
for (auto inner = start + count; inner < submission.commands.size(); inner += Pm4::PacketWords(submission.commands[inner])) {
const auto innerHeader = submission.commands[inner];
require(innerHeader != FlipPacketHeader && innerHeader != RenderingWaitPacketHeader, "flips and rendering waits in predicated command buffers are not implemented");
}
submission.conditionalEnds.emplace(start, submission.commands.size());
cursor += count;
continue;
}
if (copySegment(submission, target, targetWords, budget)) {
require(guarded.empty(), "a REWIND inside a conditional execution range is not implemented");
return true;

View File

@ -166,7 +166,7 @@ std::string_view UnsupportedReason(std::uint32_t header) {
}
}
switch (opcode) {
case 0x11: case 0x12: case 0x13: case 0x15: case 0x16: case 0x22: case 0x26: case 0x27:
case 0x11: case 0x12: case 0x13: case 0x15: case 0x16: case 0x20: case 0x22: case 0x26: case 0x27:
case 0x2a: case 0x2d: case 0x2f: case 0x35: case 0x37: case 0x40: case 0x42: case 0x46: case 0x50:
case 0x58: case 0x63: case 0x64: case 0x69: case 0x76: case 0x79: case 0x7a:
case 0x81: case 0x83: case 0x9f: return {};
@ -175,7 +175,6 @@ std::string_view UnsupportedReason(std::uint32_t header) {
return {};
case 0x3a: case 0x8d:
return "graphics draw, shader stages and guest render-target materialization are not implemented";
case 0x20: return "GPU query predication is not implemented";
case 0x33: case 0x3f: return "nested command buffers, branching and nested flip reservation are not implemented";
case 0x3c: case 0x93: return {};
case 0x39:
@ -213,7 +212,7 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
const auto size = [&](std::size_t count) { require(packet.size() == count, "invalid packet size"); };
const auto graphics = [&] { require(queue == 0, "graphics packet in compute queue"); };
const auto reason = UnsupportedReason(header);
if (!reason.empty()) throw std::runtime_error(std::string(reason));
if (!reason.empty() && !(opcode == 0x3f && Predicated(header))) throw std::runtime_error(std::string(reason));
if (opcode == 0x10) {
require((header & 3u) == 0, "unsupported NOP header flags");
switch ((header >> 2u) & 0x3fu) {
@ -239,11 +238,11 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
return;
}
// Header bit 1 selects the compute shader type and bit 2 (register writes) resets the filter CAM;
// neither changes what the packet writes. Predication (bit 0) is not implemented.
// neither changes what the packet writes.
const auto flags = header & 0xffu;
const bool registerWrite = opcode == 0x69 || opcode == 0x76 || opcode == 0x79 || opcode == 0x7a;
auto allowedFlags = opcode == 0x11 ? 2u : registerWrite ? 6u : opcode == 0x3c || opcode == 0x93 ? 2u : 0u;
if (IsTagMarker(packet)) allowedFlags |= 1u;
if (opcode != 0x20 && opcode != 0x22) allowedFlags |= 1u;
if ((flags & ~allowedFlags) != 0) throw std::runtime_error("PM4 header flags 0x" + ToHex(flags) + " are not implemented");
switch (opcode) {
case 0x11:
@ -252,6 +251,17 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
if ((header & 2u) == 0) graphics();
break;
case 0x12: graphics(); size(2); require((packet[1] & ~0xfu) == 0, "unsupported CLEAR_STATE payload bits"); break;
case 0x20: {
graphics();
size(4);
const auto operation = (packet[1] >> 16u) & 7u;
require(operation != 1 && operation != 2, "GPU query predication is not implemented");
require(operation == 0 || operation == 3 || operation == 4, "invalid predication operation");
require((packet[1] & ~0x71100u) == 0, "unsupported SET_PREDICATION bits");
if (operation != 0) require(address(packet[2], packet[3]) != 0 && (packet[2] & 0xfu) == 0, "unaligned predication address");
break;
}
case 0x3f: size(4); break;
case 0x13: case 0x2f: graphics(); size(2); break;
case 0x26: graphics(); size(3); break;
case 0x27:
@ -476,6 +486,16 @@ std::size_t ConditionalWords(std::span<const std::uint32_t> packet) {
return packet[4] & ConditionalWordsMask;
}
bool PredicationPasses(const QueueState& queue) {
const auto& predication = queue.predication;
if (predication.operation == 0) return true;
const std::size_t bytes = predication.operation == 3 ? 8 : 4;
std::uint64_t value = 0;
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Wait);
GuestMemory::Read(predication.address, std::as_writable_bytes(std::span(&value, 1)).first(bytes), bytes);
return (value != 0) == predication.executeWhenSet;
}
std::uint32_t ReadCondition(std::span<const std::uint32_t> packet) {
require(packet.size() == 5 && ((packet[0] >> 8u) & 0xffu) == 0x22u, "expected COND_EXEC packet");
std::uint32_t value = 0;
@ -725,6 +745,12 @@ void Execute(std::span<const std::uint32_t> packet, QueueState& queue) {
APS5_LOG_OUT_DEBUG("CLEAR_STATE targetMaskBefore=0x%x shaderMaskBefore=0x%x", queue.context.contains(0x8e) ? queue.context.at(0x8e) : 0u, queue.context.contains(0x8f) ? queue.context.at(0x8f) : 0u);
queue.ClearContext(); return;
case 0x13: queue.indexBufferSize = packet[1]; return;
case 0x20: {
const auto operation = (packet[1] >> 16u) & 7u;
queue.predication = {operation == 0 ? 0 : address(packet[2], packet[3]), operation, (packet[1] & 0x100u) != 0};
return;
}
case 0x3f: throw std::runtime_error(std::string(UnsupportedReason(packet[0])));
case 0x26: queue.indexBase = address(packet[1], packet[2]); return;
case 0x2a: queue.indexType = packet[1]; return;
case 0x2f: queue.instanceCount = packet[1]; return;

View File

@ -180,7 +180,7 @@ void testAutoDraw() {
}
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x2d, {3}), 0); }, "packet size");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x2d, {3, 2, 0}), 0); }, "packet size");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x2d, {3, 2}, 1), 0); }, "header flags");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x2d, {3, 2}, 2), 0); }, "header flags");
state.userConfig[0x24a] = std::numeric_limits<std::uint32_t>::max();
check(AgcDriver::Pm4::ResolveDraw(makePacket(0x2d, {1, 2}), state).firstVertex == std::numeric_limits<std::uint32_t>::max(), "last vertex rejected");
expectFailure([&] { AgcDriver::Pm4::ResolveDraw(makePacket(0x2d, {2, 2}), state); }, "vertex range overflow");
@ -508,7 +508,7 @@ void testEventWrite() {
for (const auto bit : {0x40u, 0x80u, 0x800u, 0x80000000u}) {
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x410u | bit}), 0); }, "reserved bits");
}
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x410}, 1), 0); }, "header flags");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x410}, 2), 0); }, "header flags");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x410, 0, 0}), 0); }, "packet size");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x13a, 0, 0}), 0); }, "event type 58");
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x0d}), 0); }, "event type 13");
@ -541,7 +541,7 @@ void testAcquireMem() {
packet[index] = value;
expectFailure([&] { AgcDriver::Pm4::Validate(packet, 0); }, reason);
};
invalidWord(0, captured[0] | 1u, "header flags");
invalidWord(0, captured[0] | 2u, "header flags");
invalidWord(1, 4, "control flags");
invalidWord(1, 0x00800000, "control flags");
invalidWord(3, 1, "above 40 bits");
@ -554,6 +554,50 @@ void testAcquireMem() {
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x58, {0, 0, 0, 0, 0, 0, 0, 0}), 0); }, "packet size");
}
void testPredication() {
alignas(16) std::uint64_t flag[4] = {0, 0, 0, 0};
auto* flag32 = reinterpret_cast<std::uint32_t*>(&flag[2]);
const auto setPredication = [&](std::uint32_t operation, bool executeWhenSet, const void* address) {
return makePacket(0x20, {(operation << 16u) | (executeWhenSet ? 0x100u : 0u) | 0x1000u, low(address), high(address)});
};
AgcDriver::QueueState state;
check(AgcDriver::Pm4::PredicationPasses(state), "inactive predication must pass");
execute(state, setPredication(3, true, &flag[0]));
check(state.predication.operation == 3 && state.predication.executeWhenSet && state.predication.address == reinterpret_cast<std::uintptr_t>(&flag[0]), "BOOL64 predication state mismatch");
check(!AgcDriver::Pm4::PredicationPasses(state), "zero BOOL64 value must skip draw-visible packets");
flag[0] = 1ull << 40u;
check(AgcDriver::Pm4::PredicationPasses(state), "upper BOOL64 bits must count");
execute(state, setPredication(3, false, &flag[0]));
check(!AgcDriver::Pm4::PredicationPasses(state), "non-zero BOOL64 value must skip draw-not-visible packets");
flag[0] = 0;
check(AgcDriver::Pm4::PredicationPasses(state), "zero BOOL64 value must run draw-not-visible packets");
execute(state, setPredication(4, true, &flag[2]));
flag[2] = 0xffffffff00000000ull;
check(*flag32 == 0 && !AgcDriver::Pm4::PredicationPasses(state), "BOOL32 must read only 32 bits");
*flag32 = 7;
check(AgcDriver::Pm4::PredicationPasses(state), "non-zero BOOL32 value must run draw-visible packets");
execute(state, setPredication(0, false, nullptr));
check(state.predication.operation == 0 && AgcDriver::Pm4::PredicationPasses(state), "clear must end predication");
expectFailure([&] { AgcDriver::Pm4::Validate(setPredication(1, true, &flag[0]), 0); }, "query predication");
expectFailure([&] { AgcDriver::Pm4::Validate(setPredication(2, true, &flag[0]), 0); }, "query predication");
expectFailure([&] { AgcDriver::Pm4::Validate(setPredication(5, true, &flag[0]), 0); }, "invalid predication operation");
expectFailure([&] { AgcDriver::Pm4::Validate(setPredication(3, true, &flag[0]), 0x20); }, "compute queue");
expectFailure([&] { AgcDriver::Pm4::Validate(setPredication(3, true, nullptr), 0); }, "unaligned predication address");
expectFailure([&] { AgcDriver::Pm4::Validate(setPredication(3, true, reinterpret_cast<const std::uint8_t*>(&flag[0]) + 8), 0); }, "unaligned predication address");
auto continued = setPredication(3, true, &flag[0]);
continued[1] |= 1u << 31u;
expectFailure([&] { AgcDriver::Pm4::Validate(continued, 0); }, "SET_PREDICATION bits");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x20, {3u << 16u, low(&flag[0]), high(&flag[0])}, 1), 0); }, "header flags");
AgcDriver::Pm4::Validate(makePacket(0x2d, {3, 2}, 1), 0);
AgcDriver::Pm4::Validate(makePacket(0x46, {0x410}, 1), 0);
const auto call = makePacket(0x3f, {0x1000, 0, 0x0f200010}, 1);
AgcDriver::Pm4::Validate(call, 0);
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x3f, {0x1000, 0, 0x0f200010}), 0); }, "nested command buffers");
expectFailure([&] { AgcDriver::Pm4::Execute(call, state); }, "nested command buffers");
}
void testDriverSubmission() {
std::array<std::uint32_t, 2> source{0x10, 73};
std::array<std::uint32_t, 1> destination{};
@ -693,6 +737,49 @@ void testConditionalSubmission() {
check(results[13] == 0, "a rejected conditional submission executed a guarded packet");
}
void testPredicatedSubmission() {
alignas(16) std::uint64_t flag[2] = {0, 0};
alignas(16) std::array<std::uint32_t, 4> written{};
alignas(16) std::array<std::uint32_t, 1> nestedWritten{};
const auto nested = makePacket(0x37, {0x100, low(nestedWritten.data()), high(nestedWritten.data()), 3});
const auto call = makePacket(0x3f, {low(nested.data()), high(nested.data()), 0x0f200000u | static_cast<std::uint32_t>(nested.size())}, 1);
const auto submit = [](std::vector<std::uint32_t>& commands) {
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
check(sceAgcDriverSubmitDcb(&packet) == 0, "predicated submission failed");
AgcDriverWaitIdle_nid_postfix();
};
std::vector<std::uint32_t> commands;
for (const auto& packet : {
makePacket(0x20, {0x31100, low(&flag[0]), high(&flag[0])}),
makePacket(0x37, {0x100, low(&written[0]), high(&written[0]), 7}, 1),
call,
makePacket(0x20, {0, 0, 0}),
makePacket(0x37, {0x100, low(&written[1]), high(&written[1]), 9}, 1)
}) commands.insert(commands.end(), packet.begin(), packet.end());
submit(commands);
check(written[0] == 0, "packet with a false predicate was executed");
check(nestedWritten[0] == 0, "command buffer with a false predicate was executed");
check(written[1] == 9, "cleared predication skipped a packet");
flag[0] = 1;
commands.clear();
for (const auto& packet : {
makePacket(0x20, {0x31100, low(&flag[0]), high(&flag[0])}),
makePacket(0x37, {0x100, low(&written[2]), high(&written[2]), 5}, 1),
call,
makePacket(0x20, {0x31000, low(&flag[0]), high(&flag[0])}),
makePacket(0x37, {0x100, low(&written[3]), high(&written[3]), 6}, 1),
makePacket(0x20, {0, 0, 0})
}) commands.insert(commands.end(), packet.begin(), packet.end());
submit(commands);
check(written[2] == 5, "packet with a true predicate was skipped");
check(nestedWritten[0] == 3, "command buffer with a true predicate was skipped");
check(written[3] == 0, "draw-not-visible packet ran with a set value");
auto chain = makePacket(0x3f, {low(nested.data()), high(nested.data()), 0x0f300000u | static_cast<std::uint32_t>(nested.size())}, 1);
Packet chained{chain.data(), static_cast<std::uint32_t>(chain.size()), 0, {}};
expectFailure([&] { sceAgcDriverSubmitDcb(&chained); }, "predicated command buffer chains");
AgcDriverWaitIdle_nid_postfix();
}
void testAsyncMemoryFailure() {
auto commands = makePacket(0x37, {0x100, 0x1000, 0, 1});
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
@ -728,7 +815,9 @@ int main(int argc, char** argv) {
testConditionReadSynchronization();
testEventWrite();
testAcquireMem();
testPredication();
testDriverSubmission();
testPredicatedSubmission();
testConditionalSubmission();
LibcRunShutdown_nid_postfix();
std::puts("PM4 catalog, registers, state, memory, conditional execution and submission tests passed");

View File

@ -76,7 +76,7 @@ void testValidation() {
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
commands[0] = 0xc001105c;
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
commands[0] = 0xc0017601;
commands[0] = 0xc0017608;
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
commands[0] = 0xc0017600;
commands[1] = 0x10000;
@ -110,7 +110,7 @@ void testClearState() {
words[0] = 0xc0011200;
packet.dw_num = 3;
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
words[0] = 0xc0001201;
words[0] = 0xc0001202;
packet.dw_num = 2;
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
words[0] = 0xc0001200;

View File

@ -33,6 +33,7 @@ Throughout the project, every function at every stage either **does exactly what
- [sceFontGetRenderScaledKerning](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - arguments, the cleared output and the bound renderer check taken from `cellFontGetRenderScaledKerning` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellFont.cpp) (PS3 predecessor of the library), not confirmed on a title. The kerning is the `sceFontGetKerning` one at the render scale
- [sceFontGetPixelResolution](../../core/libs/prx/libSceFont/src/Library.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title. The value is the one the library's driver reports (64 for FreeType); the argument checks and the cleared output follow `sceFontGetLibrary`
- [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted
- [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
- [sceAgcGetIsTrinityMode](../../core/libs/prx/libSceAgc/Misc/src/Platform.cpp) (libSceAgc) - signature from the only PPSA26344 call (a pointer to a one-byte flag, result ignored); a null pointer throws because its error code is unknown
- [zARR5aCmkoY](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown name, signature
- [qj7QZpgr9Uw](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - unknown name