mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-11-07 13:39:55 +00:00
fix(agc): check GPU-side indirect dispatch group counts against the device limit
A DISPATCH_INDIRECT read on the GPU took its group counts unchecked. A count buffer that a skipped producer never wrote (PPSA08427: 0xffffffff left by skipped depth-plane passes) launched up to 2^32 groups and kept the GPU busy until the title hung. The CPU path already refuses counts above maxComputeWorkGroupCount. - Recorder::RecordDispatchArguments copies the three counts from the import into a 12-byte indirect buffer with a one-thread kernel (DispatchArguments.comp), all three 0 when one exceeds the device limit, as RecordMeshArguments does for mesh indirect records. - VulkanDevice records the dispatch from that buffer; without the kernel the import is read directly, as before. - agc_driver_indirect_dispatch_limit: 0xffffffff groups run nothing, one group still runs. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Q8tinbP1MUpUaDsbNWnGnu
This commit is contained in:
1 parent
4224d240aa
commit
e54d2f35fe
6 files changed
+189
-1
No files matched your search
@@ -83,6 +83,19 @@ add_custom_command(
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
set(agcDispatchArgumentsShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/DispatchArguments.comp)
|
||||
set(agcDispatchArgumentsShaderSpv ${agcTextureDetileGeneratedDir}/DispatchArguments.spv)
|
||||
set(agcDispatchArgumentsShaderHeader ${agcTextureDetileGeneratedDir}/DispatchArguments_spv.h)
|
||||
add_custom_command(
|
||||
OUTPUT "${agcDispatchArgumentsShaderHeader}"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}"
|
||||
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 -o "${agcDispatchArgumentsShaderSpv}" "${agcDispatchArgumentsShaderSource}"
|
||||
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcDispatchArgumentsShaderSpv} -DOUTPUT=${agcDispatchArgumentsShaderHeader}
|
||||
-DSYMBOL=DISPATCH_ARGUMENTS_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
|
||||
DEPENDS "${agcDispatchArgumentsShaderSource}" glslang-standalone
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
set(agcSampleCounterShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/SampleCounter.comp)
|
||||
set(agcSampleCounterShaderSpv ${agcTextureDetileGeneratedDir}/SampleCounter.spv)
|
||||
set(agcSampleCounterShaderHeader ${agcTextureDetileGeneratedDir}/SampleCounter_spv.h)
|
||||
@@ -230,6 +243,7 @@ set(agcDriverSources
|
||||
${agcCmaskClearShaderHeader}
|
||||
${agcMeshArgumentsShaderHeader}
|
||||
${agcTextureDetileImageShaderHeader}
|
||||
${agcDispatchArgumentsShaderHeader}
|
||||
${agcSampleCounterShaderHeader}
|
||||
${agcSampleDumpsShaderHeader}
|
||||
${agcPresentationShaderHeaders}
|
||||
|
||||
@@ -4364,6 +4364,16 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
|
||||
recorder.EndGpuTiming(timing);
|
||||
++d.preBarriersRecorded;
|
||||
}
|
||||
VkBuffer indirectBuffer = argumentImport != nullptr ? argumentImport->buffer : VK_NULL_HANDLE;
|
||||
VkDeviceSize indirectOffset = argumentImport != nullptr ? arguments - argumentImport->base : 0;
|
||||
if (argumentImport != nullptr && argumentImport->address != 0) {
|
||||
auto checked = std::make_shared<Graphics::Buffer>(context, 12, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
|
||||
if (recorder.RecordDispatchArguments(commands, argumentImport->address + (arguments - argumentImport->base), checked->DeviceAddress())) {
|
||||
indirectBuffer = checked->Handle();
|
||||
indirectOffset = 0;
|
||||
recorder.Keep(std::move(checked));
|
||||
}
|
||||
}
|
||||
context.Resolved(&Graphics::DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->pipeline);
|
||||
resources.Bind(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->layout);
|
||||
if (const auto dwords = record.shader->program->workgroupMemoryDwords; dwords != 0) {
|
||||
@@ -4377,7 +4387,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
|
||||
const auto gpuTiming = recorder.BeginGpuTiming(record.programAddress != 0 ? record.programAddress : record.shader->program->variantId);
|
||||
if (state->checkpointMarker != nullptr) state->checkpointMarker(commands, reinterpret_cast<const void*>(static_cast<std::uintptr_t>(record.programAddress)));
|
||||
if (argumentImport != nullptr) {
|
||||
context.Resolved(&Graphics::DeviceFunctions::cmdDispatchIndirect, "vkCmdDispatchIndirect")(commands, argumentImport->buffer, arguments - argumentImport->base);
|
||||
context.Resolved(&Graphics::DeviceFunctions::cmdDispatchIndirect, "vkCmdDispatchIndirect")(commands, indirectBuffer, indirectOffset);
|
||||
} else {
|
||||
const auto* limit = state->properties.limits.maxComputeWorkGroupCount;
|
||||
if (record.x <= limit[0] && record.y <= limit[1] && record.z <= limit[2]) {
|
||||
|
||||
@@ -189,6 +189,7 @@ public:
|
||||
// ever submitted); WaitSerial(serial) then covers all recorded work.
|
||||
std::uint64_t SubmitAndEpoch();
|
||||
bool HasTimeline() const { return timeline != VK_NULL_HANDLE; }
|
||||
bool RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress source, VkDeviceAddress arguments);
|
||||
// Waits until every batch up to `serial` completed on the GPU. Called WITHOUT GuestMemory::GpuMutex:
|
||||
// it touches only the immutable timeline semaphore (the caller keeps the device alive). Requires
|
||||
// HasTimeline(); the debug switch APS5_NO_TIMELINE makes the device create the recorder without one.
|
||||
@@ -703,6 +704,9 @@ private:
|
||||
VkPipelineLayout dumpLayout = VK_NULL_HANDLE;
|
||||
VkPipeline dumpPipeline = VK_NULL_HANDLE;
|
||||
std::shared_ptr<void> segmentPools;
|
||||
int dispatchArgumentState = 0;
|
||||
VkPipelineLayout dispatchArgumentLayout = VK_NULL_HANDLE;
|
||||
VkPipeline dispatchArgumentPipeline = VK_NULL_HANDLE;
|
||||
// BeginGpuTiming on the open batch without Commands() (RecordStore times its own run, which
|
||||
// Commands() would close).
|
||||
std::uint32_t beginTiming(std::uint64_t key);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#version 450
|
||||
#extension GL_EXT_buffer_reference : require
|
||||
|
||||
layout(local_size_x = 1) in;
|
||||
layout(buffer_reference, std430, buffer_reference_align = 4) buffer Words { uint words[]; };
|
||||
layout(push_constant) uniform Parameters {
|
||||
Words source;
|
||||
Words arguments;
|
||||
uint maxX;
|
||||
uint maxY;
|
||||
uint maxZ;
|
||||
uint unused;
|
||||
} parameters;
|
||||
|
||||
void main() {
|
||||
uint x = parameters.source.words[0];
|
||||
uint y = parameters.source.words[1];
|
||||
uint z = parameters.source.words[2];
|
||||
bool valid = x <= parameters.maxX && y <= parameters.maxY && z <= parameters.maxZ;
|
||||
parameters.arguments.words[0] = valid ? x : 0u;
|
||||
parameters.arguments.words[1] = valid ? y : 0u;
|
||||
parameters.arguments.words[2] = valid ? z : 0u;
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp"
|
||||
#include "prx/libc/include/CpuTopology.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/shaders/DispatchArguments_spv.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
@@ -1168,6 +1169,8 @@ Recorder::~Recorder() {
|
||||
context.Function<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout")(context.device, dumpLayout, nullptr);
|
||||
sampleCounter.reset();
|
||||
}
|
||||
if (dispatchArgumentPipeline != VK_NULL_HANDLE) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, dispatchArgumentPipeline, nullptr);
|
||||
if (dispatchArgumentLayout != VK_NULL_HANDLE) context.Function<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout")(context.device, dispatchArgumentLayout, nullptr);
|
||||
for (auto& [commands, fence] : spare) {
|
||||
context.Function<PFN_vkFreeCommandBuffers>("vkFreeCommandBuffers")(context.device, context.pool, 1, &commands);
|
||||
context.Function<PFN_vkDestroyFence>("vkDestroyFence")(context.device, fence, nullptr);
|
||||
@@ -1946,6 +1949,52 @@ void Recorder::EndGpuTiming(std::uint32_t index, std::uint64_t bytes) {
|
||||
context.Function<PFN_vkCmdWriteTimestamp>("vkCmdWriteTimestamp")(open->commands, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, open->queries, index * 2 + 1);
|
||||
}
|
||||
|
||||
bool Recorder::RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress source, VkDeviceAddress arguments) {
|
||||
constexpr std::uint32_t PushBytes = 32;
|
||||
if (dispatchArgumentState == 0) {
|
||||
dispatchArgumentState = -1;
|
||||
VkShaderModule module = VK_NULL_HANDLE;
|
||||
try {
|
||||
const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, PushBytes};
|
||||
VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
|
||||
layoutInfo.pushConstantRangeCount = 1;
|
||||
layoutInfo.pPushConstantRanges = &push;
|
||||
Check(context.Function<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &dispatchArgumentLayout), "vkCreatePipelineLayout dispatch arguments");
|
||||
VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
moduleInfo.codeSize = sizeof(DISPATCH_ARGUMENTS_SPV);
|
||||
moduleInfo.pCode = DISPATCH_ARGUMENTS_SPV;
|
||||
Check(context.Function<PFN_vkCreateShaderModule>("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule dispatch arguments");
|
||||
VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
|
||||
pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
|
||||
pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
|
||||
pipelineInfo.stage.module = module;
|
||||
pipelineInfo.stage.pName = "main";
|
||||
pipelineInfo.layout = dispatchArgumentLayout;
|
||||
Check(context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &dispatchArgumentPipeline), "vkCreateComputePipelines dispatch arguments");
|
||||
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
|
||||
dispatchArgumentState = 1;
|
||||
} catch (const std::exception& error) {
|
||||
if (module != VK_NULL_HANDLE) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
|
||||
std::fprintf(stderr, "[gpu] indirect dispatches read their group counts unchecked: %s\n", error.what());
|
||||
}
|
||||
}
|
||||
if (dispatchArgumentState < 0) return false;
|
||||
recordBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT);
|
||||
function(cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, dispatchArgumentPipeline);
|
||||
const auto* limit = context.limits.maxComputeWorkGroupCount;
|
||||
struct {
|
||||
VkDeviceAddress source;
|
||||
VkDeviceAddress arguments;
|
||||
std::array<std::uint32_t, 4> limits;
|
||||
} parameters{source, arguments, {limit[0], limit[1], limit[2], 0u}};
|
||||
static_assert(sizeof(parameters) == PushBytes);
|
||||
function(cmdPushConstants, "vkCmdPushConstants")(commands, dispatchArgumentLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, PushBytes, ¶meters);
|
||||
function(cmdDispatch, "vkCmdDispatch")(commands, 1, 1, 1);
|
||||
recordBarrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_INDIRECT_COMMAND_READ_BIT);
|
||||
CountBarriers(CommandClass::DispatchLeading, 2);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Recorder::CountSamples() {
|
||||
GuestMemory::AssertGpuLockHeld("Recorder::CountSamples");
|
||||
gpuSampleCounter();
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <new>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 64;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
alignas(256) std::array<std::uint32_t, Threads> Output{};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 5> StoreCode{0x34020082u, 0x7e040287u, 0xe0701000u, 0x80010201u, 0xbf810000u};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t count) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), count * 4u, 0x31016facu};
|
||||
}
|
||||
|
||||
int Run(AgcDriver::VulkanDevice& device, std::uint32_t* block, std::array<std::uint32_t, 3> groups) {
|
||||
Output.fill(0xdeadbeefu);
|
||||
std::memcpy(block, groups.data(), sizeof(groups));
|
||||
std::vector<std::uint32_t> userData(8, 0u);
|
||||
const auto output = BufferDescriptor(Output.data(), Threads);
|
||||
std::copy(output.begin(), output.end(), userData.begin() + 4);
|
||||
const std::span<const std::uint32_t> code(StoreCode);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{64, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
const auto outcome = device.DispatchIndirect(result, reinterpret_cast<std::uintptr_t>(block), {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
return outcome.cpuReason;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
auto* block = static_cast<std::uint32_t*>(::operator new(BlockBytes, std::align_val_t{BlockBytes}));
|
||||
std::memset(block, 0, BlockBytes);
|
||||
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
||||
GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, BlockBytes, true, true, true);
|
||||
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
||||
int failures = 0;
|
||||
try {
|
||||
const int invalidPath = Run(*device, block, {0xffffffffu, 1u, 1u});
|
||||
if (invalidPath != 0) {
|
||||
std::printf("skipped, the group counts were read on the CPU (reason %d): the device cannot import the block\n", invalidPath);
|
||||
} else {
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) Require(Output[tid] == 0xdeadbeefu, "indirect dispatch limit: thread " + std::to_string(tid) + " ran with 0xffffffff groups");
|
||||
Require(Run(*device, block, {1u, 1u, 1u}) == 0, "indirect dispatch limit: valid counts left the GPU path");
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) Require(Output[tid] == 7u, "indirect dispatch limit: thread " + std::to_string(tid) + " stored " + std::to_string(Output[tid]) + " with one group, expected 7");
|
||||
}
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
failures = 1;
|
||||
}
|
||||
mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
||||
GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block);
|
||||
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
||||
::operator delete(block, std::align_val_t{BlockBytes});
|
||||
if (failures != 0) return 1;
|
||||
std::puts("indirect dispatch limit tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user