perf(agc): handle the zero dword fill kernel as a buffer fill
A wave64 compute kernel that stores dword 0 at index group * 64 + thread through a 4-byte stride buffer is run as a buffer fill. Palworld (PPSA23126) clears its DCC keys with it every frame; as a shader dispatch its writes were unknown to the DCC key proofs, so every refresh of those targets waited for the GPU. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c7632f036f
commit
501453573c
|
|
@ -18,6 +18,7 @@ struct DwordPatternFill {
|
|||
};
|
||||
|
||||
std::optional<DwordPatternFill> MatchDwordPatternFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
|
||||
std::optional<DwordPatternFill> MatchZeroDwordFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,23 @@ std::optional<DwordPatternFill> MatchDwordPatternFill(std::span<const std::uint3
|
|||
return fill;
|
||||
}
|
||||
|
||||
std::optional<DwordPatternFill> MatchZeroDwordFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) {
|
||||
static const bool enabled = std::getenv("APS5_NO_FILL_HLE") == nullptr;
|
||||
if (!enabled || userData.size() != 4 || packet.size() < 5 || !compute.groupIdEnable[0] || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1) return std::nullopt;
|
||||
static constexpr std::array<std::uint32_t, 6> kernel{0xd7460000u, 0x04010c04u, 0x7e020280u, 0xe0102000u, 0x80000100u, 0xbf810000u};
|
||||
if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt;
|
||||
const auto stride = (userData[1] >> 16u) & 0x3fffu;
|
||||
const bool swizzled = ((userData[1] >> 31u) & 1u) != 0;
|
||||
const bool addTid = ((userData[3] >> 23u) & 1u) != 0;
|
||||
const auto outOfBounds = (userData[3] >> 28u) & 3u;
|
||||
const auto type = userData[3] >> 30u;
|
||||
if (type != 0 || stride != 4 || swizzled || addTid || outOfBounds != 0 || (packet[4] & 0x20u) != 0 || packet[2] != 1 || packet[3] != 1) return std::nullopt;
|
||||
const auto dwords = std::min<std::uint64_t>(static_cast<std::uint64_t>(packet[1]) * 64u, userData[2]);
|
||||
const auto base = userData[0] | (static_cast<std::uint64_t>(userData[1] & 0xffffu) << 32u);
|
||||
if (dwords == 0 || dwords % 4u != 0 || base % 16u != 0) return std::nullopt;
|
||||
return DwordPatternFill{base, static_cast<std::size_t>(dwords * 4u), {}};
|
||||
}
|
||||
|
||||
bool Driver::fillClearEnabled() {
|
||||
static const bool enabled = std::getenv("APS5_NO_FILL_CLEAR") == nullptr;
|
||||
return enabled;
|
||||
|
|
@ -125,7 +142,8 @@ void Driver::fillClearCount(const Graphics::StorageTexture::FillCoverage& covera
|
|||
}
|
||||
|
||||
bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::shared_ptr<VulkanDevice>& localDevice) {
|
||||
const auto dwordFill = MatchDwordPatternFill(packet, code, userData, compute);
|
||||
auto dwordFill = MatchDwordPatternFill(packet, code, userData, compute);
|
||||
if (!dwordFill) dwordFill = MatchZeroDwordFill(packet, code, userData, compute);
|
||||
if (!dwordFill && !matchesFillKernel(code, userData, compute)) return false;
|
||||
std::uint64_t base;
|
||||
std::size_t bytes;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using AgcDriver::DriverDetail::MatchZeroDwordFill;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 6> Code{0xd7460000u, 0x04010c04u, 0x7e020280u, 0xe0102000u, 0x80000100u, 0xbf810000u};
|
||||
constexpr std::uint32_t Untouched = 0xcafebabeu;
|
||||
alignas(256) std::array<std::uint32_t, 1028> Output{};
|
||||
|
||||
std::vector<std::uint32_t> UserData(void* output, std::uint32_t records, std::uint32_t stride = 4, std::uint32_t format = 0x00014204u) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(output);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), records, format};
|
||||
}
|
||||
|
||||
void MatchTests() {
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{64, 1, 1}, 0, {true, false, false}, false, 1};
|
||||
const std::array<std::uint32_t, 5> packet{0xc0031500u, 4, 1, 1, 0x41u};
|
||||
const auto data = UserData(Output.data(), 1024);
|
||||
const auto fill = MatchZeroDwordFill(packet, Code, data, compute);
|
||||
Require(fill && fill->base == reinterpret_cast<std::uintptr_t>(Output.data()) && fill->bytes == 1024 && fill->pattern == std::array<std::uint32_t, 4>{}, "the zero dword fill kernel was not recognized as a zero fill");
|
||||
for (std::size_t word = 0; word < Code.size(); ++word) {
|
||||
auto altered = Code;
|
||||
altered[word] ^= 1u;
|
||||
Require(!MatchZeroDwordFill(packet, altered, data, compute), "a different shader matched the zero dword fill kernel");
|
||||
}
|
||||
Require(!MatchZeroDwordFill(packet, std::span(Code).first(5), data, compute), "a truncated shader matched");
|
||||
Require(!MatchZeroDwordFill(packet, Code, UserData(Output.data(), 1024, 8), compute), "a strided descriptor matched a contiguous fill");
|
||||
Require(!MatchZeroDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x00814204u), compute), "an add-tid descriptor matched");
|
||||
Require(!MatchZeroDwordFill(packet, Code, UserData(Output.data(), 1024, 4, 0x30014204u), compute), "a raw out-of-bounds mode matched");
|
||||
Require(!MatchZeroDwordFill(packet, Code, UserData(Output.data() + 1, 1024), compute), "an unaligned destination matched");
|
||||
Require(!MatchZeroDwordFill(packet, Code, UserData(Output.data(), 102), compute), "a record count not filling 16-byte units matched");
|
||||
auto extra = data;
|
||||
extra.push_back(0);
|
||||
Require(!MatchZeroDwordFill(packet, Code, extra, compute), "a different user SGPR count matched (the group id moves)");
|
||||
auto partial = packet;
|
||||
partial[4] |= 0x20u;
|
||||
Require(!MatchZeroDwordFill(partial, Code, data, compute), "a partial dispatch matched");
|
||||
auto noGroup = compute;
|
||||
noGroup.groupIdEnable[0] = false;
|
||||
Require(!MatchZeroDwordFill(packet, Code, data, noGroup), "a dispatch without the group id matched");
|
||||
const auto clamped = MatchZeroDwordFill(packet, Code, UserData(Output.data(), 64), compute);
|
||||
Require(clamped && clamped->bytes == 256, "the fill exceeded the descriptor's records");
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> Commands(const std::vector<std::uint32_t>& data, std::uint32_t groups, bool partial, std::uint32_t threads) {
|
||||
std::vector<std::uint32_t> words;
|
||||
const auto registers = [&](std::uint32_t first, std::span<const std::uint32_t> values) {
|
||||
words.push_back(0xc0007600u | (static_cast<std::uint32_t>(values.size()) << 16u));
|
||||
words.push_back(first);
|
||||
words.insert(words.end(), values.begin(), values.end());
|
||||
};
|
||||
const std::array<std::uint32_t, 3> shape{64, 1, 1};
|
||||
registers(0x207, shape);
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(Code.data());
|
||||
const std::array<std::uint32_t, 2> program{static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>(address >> 40u)};
|
||||
registers(0x20c, program);
|
||||
const std::array<std::uint32_t, 1> resource{(static_cast<std::uint32_t>(data.size()) << 1u) | (1u << 7u)};
|
||||
registers(0x213, resource);
|
||||
registers(0x240, data);
|
||||
words.insert(words.end(), {0xc0031500u, partial ? threads : groups, 1, 1, 0x41u | (partial ? 0x20u : 0u)});
|
||||
return words;
|
||||
}
|
||||
|
||||
void ExecuteTests() {
|
||||
struct Case {
|
||||
std::uint32_t groups, records, stride;
|
||||
bool partial;
|
||||
std::uint32_t threads;
|
||||
};
|
||||
constexpr std::array<Case, 8> cases{{
|
||||
{4, 1024, 4, false, 0},
|
||||
{16, 1024, 4, false, 0},
|
||||
{4, 64, 4, false, 0},
|
||||
{4, 100, 4, false, 0},
|
||||
{4, 102, 4, false, 0},
|
||||
{2, 1024, 8, false, 0},
|
||||
{0, 1024, 4, true, 70},
|
||||
{0, 1024, 4, false, 0},
|
||||
}};
|
||||
for (const auto& item : cases) {
|
||||
Output.fill(Untouched);
|
||||
const auto words = Commands(UserData(Output.data(), item.records, item.stride), item.groups, item.partial, item.threads);
|
||||
Packet packet{const_cast<std::uint32_t*>(words.data()), static_cast<std::uint32_t>(words.size()), 0, {}};
|
||||
AgcDriver::Submit(&packet, 0);
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(Output.data()), sizeof(Output));
|
||||
const auto launched = item.partial ? item.threads : item.groups * 64u;
|
||||
const auto filled = std::min(launched, item.records);
|
||||
const auto step = item.stride / 4u;
|
||||
for (std::uint32_t i = 0; i < Output.size(); ++i) {
|
||||
const bool written = i % step == 0 && i / step < filled;
|
||||
const auto expected = written ? 0u : Untouched;
|
||||
Require(Output[i] == expected, "zero dword fill mismatch at " + std::to_string(i) + " (groups " + std::to_string(item.groups) + ", records " + std::to_string(item.records) + ", stride " + std::to_string(item.stride) + "): got " + std::to_string(Output[i]) + ", expected " + std::to_string(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
if (std::getenv("APS5_NO_FILL_HLE") == nullptr) MatchTests();
|
||||
{
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
}
|
||||
ExecuteTests();
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
std::puts("zero dword fill tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue