From 46c799da67f087bce0c32be998002f7347daddce Mon Sep 17 00:00:00 2001 From: boykopovar Date: Fri, 25 Sep 2026 09:14:03 +0300 Subject: [PATCH] perf(agc): use device-local tile buffers and cache spir-v validation --- .../Graphics/include/GpuColorTransfer.hpp | 1 + .../Graphics/include/Resources.hpp | 1 + .../Graphics/src/BufferPool.cpp | 2 +- .../Graphics/src/GpuColorTransfer.cpp | 19 +++-- .../Graphics/src/Resources.cpp | 8 +- .../Graphics/src/ShaderValidation.cpp | 85 +++++++++++++++++-- .../tests/ColorTransferTests.cpp | 40 +++++++++ 7 files changed, 142 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp index d0a507650..10dc5511f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp @@ -33,6 +33,7 @@ private: std::unique_ptr tiled; std::unique_ptr linear; std::unique_ptr readback; + std::unique_ptr upload; std::uint32_t width = 0; std::uint32_t height = 0; ColorTileMode mode = ColorTileMode::Linear; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp index 5aca2f213..841435d03 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp @@ -29,6 +29,7 @@ private: VkDeviceSize allocationBytes = 0; VkBufferUsageFlags usage; VkMemoryPropertyFlags properties; + bool reusable = false; std::shared_ptr cache; }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index 92555d838..147caafbb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -11,7 +11,7 @@ BufferPool::~BufferPool() { } void BufferPool::destroy(const BufferAllocation& allocation) noexcept { - unmap(device, allocation.memory); + if (allocation.mapping != nullptr) unmap(device, allocation.memory); destroyBuffer(device, allocation.buffer, nullptr); freeMemory(device, allocation.memory, nullptr); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp index 40cdff05e..8f778b199 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp @@ -72,9 +72,10 @@ void GpuColorTransfer::prepare(std::uint32_t newWidth, std::uint32_t newHeight, Require(layout.Bytes() <= context.limits.maxStorageBufferRange && layout.LinearBytes() <= context.limits.maxStorageBufferRange, "color transfer exceeds storage buffer limits"); Require((newWidth + 7u) / 8u <= context.limits.maxComputeWorkGroupCount[0] && (newHeight + 7u) / 8u <= context.limits.maxComputeWorkGroupCount[1], "color transfer exceeds workgroup limits"); if (tiled && width == newWidth && height == newHeight && mode == newMode) return; - auto newTiled = std::make_unique(context, layout.Bytes(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); - auto newLinear = std::make_unique(context, layout.LinearBytes(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + auto newTiled = std::make_unique(context, layout.Bytes(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + auto newLinear = std::make_unique(context, layout.LinearBytes(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); auto newReadback = std::make_unique(context, layout.Bytes(), VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT); + auto newUpload = std::make_unique(context, layout.Bytes(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT); const std::array buffers{{{newTiled->Handle(), 0, layout.Bytes()}, {newLinear->Handle(), 0, layout.LinearBytes()}}}; std::array writes{}; for (std::uint32_t i = 0; i < writes.size(); ++i) { @@ -89,6 +90,7 @@ void GpuColorTransfer::prepare(std::uint32_t newWidth, std::uint32_t newHeight, tiled = std::move(newTiled); linear = std::move(newLinear); readback = std::move(newReadback); + upload = std::move(newUpload); width = newWidth; height = newHeight; mode = newMode; @@ -99,7 +101,7 @@ void GpuColorTransfer::Upload(std::uint64_t address, std::uint32_t newWidth, std prepare(newWidth, newHeight, newMode); timing.Mark("prepare"); const ColorTargetLayout layout(width, height, mode); - GuestMemory::Read(address, tiled->Bytes(), layout.Alignment()); + GuestMemory::Read(address, upload->Bytes(), layout.Alignment()); timing.Mark("guest_read", layout.Bytes()); } @@ -117,11 +119,18 @@ void GpuColorTransfer::convert(VkCommandBuffer commands, bool toTiled, bool swap context.Function("vkCmdDispatch")(commands, (width + 7u) / 8u, (height + 7u) / 8u, 1); VkMemoryBarrier after{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; after.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; - after.dstAccessMask = toTiled ? VK_ACCESS_HOST_READ_BIT : VK_ACCESS_TRANSFER_READ_BIT; - barrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, toTiled ? VK_PIPELINE_STAGE_HOST_BIT : VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &after, 0, nullptr, 0, nullptr); + after.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &after, 0, nullptr, 0, nullptr); } void GpuColorTransfer::Detile(VkCommandBuffer commands, bool swapRedBlue) { + Require(commands != VK_NULL_HANDLE && upload && tiled, "color upload is not prepared"); + VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + before.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; + before.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &before, 0, nullptr, 0, nullptr); + const VkBufferCopy copy{0, 0, upload->Bytes().size()}; + context.Function("vkCmdCopyBuffer")(commands, upload->Handle(), tiled->Handle(), 1, ©); convert(commands, false, swapRedBlue); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp index 5e244016f..b219865d9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp @@ -16,6 +16,7 @@ Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usag mapping = allocation->mapping; deviceAddress = allocation->address; allocationBytes = allocation->allocationBytes; + reusable = true; return; } try { @@ -35,7 +36,8 @@ Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usag Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory buffer"); Check(context.Function("vkBindBufferMemory")(context.device, buffer, memory, 0), "vkBindBufferMemory"); initializeAddress(usage); - Check(context.Function("vkMapMemory")(context.device, memory, 0, VK_WHOLE_SIZE, 0, &mapping), "vkMapMemory"); + if ((properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0) Check(context.Function("vkMapMemory")(context.device, memory, 0, VK_WHOLE_SIZE, 0, &mapping), "vkMapMemory"); + reusable = true; } catch (...) { release(); throw; @@ -47,7 +49,7 @@ Buffer::~Buffer() { } void Buffer::release() noexcept { - if (mapping && buffer && memory && cache) { + if (reusable && buffer && memory && cache) { cache->Put({buffer, memory, mapping, deviceAddress, allocationBytes, size, usage, properties}); return; } @@ -61,10 +63,12 @@ VkBuffer Buffer::Handle() const { } std::span Buffer::Bytes() { + Require(mapping != nullptr, "GPU-only buffer has no CPU mapping"); return {static_cast(mapping), size}; } void Buffer::Invalidate() { + Require(mapping != nullptr, "cannot invalidate an unmapped GPU buffer"); VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE}; range.memory = memory; range.size = VK_WHOLE_SIZE; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index b325febbd..e12e886ab 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -1,6 +1,7 @@ #include "BdaAbi.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include #include #include @@ -8,6 +9,8 @@ #include #include #include +#include +#include namespace AgcDriver::Graphics { namespace { @@ -463,6 +466,76 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } +namespace { + +struct ValidatedInterface { + std::map inputs; + std::map outputs; +}; + +std::shared_ptr inspectCached(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric) { + PerformanceTimer timing("Graphics.ShaderValidation"); + Require(compiled.program != nullptr, "missing compiled shader"); + const auto& shader = *compiled.program; + std::string key; + const auto append = [&](const TValue& value) { + static_assert(std::is_trivially_copyable_v); + key.append(reinterpret_cast(&value), sizeof(value)); + }; + append(compiled.stage); + append(state.rectList); + append(state.stages.mesh.has_value()); + if (state.stages.mesh) { + append(state.stages.mesh->threadsPerGroup); + append(state.stages.mesh->maxVertices); + append(state.stages.mesh->maxPrimitives); + } + append(state.stages.tessellation.has_value()); + if (state.stages.tessellation) { + append(state.stages.tessellation->inputControlPoints); + append(state.stages.tessellation->outputControlPoints); + } + append(subgroup.supportedStages); + append(subgroup.supportedOperations); + append(fragmentShaderBarycentric); + append(shader.bdaAbiVersion); + append(shader.pushConstants.empty()); + append(shader.bindings.size()); + for (const auto& binding : shader.bindings) { + append(binding.kind); + append(binding.role); + append(binding.descriptorSet); + append(binding.binding); + append(binding.count); + append(binding.readOnly); + append(binding.guestDescriptor.empty()); + } + append(shader.vertexAttributes.size()); + for (const auto& attribute : shader.vertexAttributes) { + append(attribute.location); + append(attribute.components); + append(attribute.resource.fields[3]); + } + append(shader.spirv.size()); + const auto code = std::as_bytes(std::span(shader.spirv)); + if (!code.empty()) key.append(reinterpret_cast(code.data()), code.size()); + timing.Mark("key", key.size()); + static thread_local std::map> cache; + const auto found = cache.find(key); + if (found != cache.end()) { + timing.Mark("hit"); + return found->second; + } + auto module = Inspect(compiled, state, subgroup, fragmentShaderBarycentric); + auto result = std::make_shared(ValidatedInterface{std::move(module.inputs), std::move(module.outputs)}); + if (cache.size() >= 128) cache.clear(); + cache.emplace(std::move(key), result); + timing.Mark("inspect"); + return result; +} + +} + void ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric) { using Stage = ShaderRecompiler::ShaderStage; const bool tessellation = state.stages.path == ShaderPath::Tessellation; @@ -473,22 +546,22 @@ void ValidateShaders(std::span shaders, const State& state Require(shaders.size() == (tessellation || state.rectList ? 4u : 2u), "incorrect graphics stage count"); const std::array tessStages{Stage::Local, Stage::TessellationControl, Stage::TessellationEvaluation, Stage::Fragment}; static_cast(AssemblePushConstants(shaders)); - Module previous; + std::shared_ptr previous; for (std::size_t i = 0; i < shaders.size(); ++i) { const auto expected = state.rectList ? (i == 0 ? Stage::Vertex : tessStages[i]) : tessellation ? tessStages[i] : i == 1 ? Stage::Fragment : state.stages.path == ShaderPath::Geometry ? Stage::Mesh : Stage::Vertex; Require(shaders[i].program != nullptr, "missing compiled shader"); Require(shaders[i].stage == expected, "graphics stage order disagrees"); for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == 0, "graphics resource uses a descriptor set other than zero"); - const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric); + const auto current = inspectCached(shaders[i], state, subgroup, fragmentShaderBarycentric); if (i != 0) { - for (const auto& [location, signature] : current.inputs) { - const auto output = previous.outputs.find(location); - Require(output != previous.outputs.end() && output->second == signature, "graphics interfaces disagree at location " + std::to_string(location)); + for (const auto& [location, signature] : current->inputs) { + const auto output = previous->outputs.find(location); + Require(output != previous->outputs.end() && output->second == signature, "graphics interfaces disagree at location " + std::to_string(location)); } } previous = current; } - Require(previous.outputs.size() == 1 && previous.outputs.contains(0) && previous.outputs.at(0) == "vertex:f32x4", "fragment shader must export one float4 color at location zero"); + Require(previous->outputs.size() == 1 && previous->outputs.contains(0) && previous->outputs.at(0) == "vertex:f32x4", "fragment shader must export one float4 color at location zero"); } void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment) { diff --git a/core/libs/prx/libSceAgcDriver/tests/ColorTransferTests.cpp b/core/libs/prx/libSceAgcDriver/tests/ColorTransferTests.cpp index e756c1cf5..6a032c776 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ColorTransferTests.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ColorTransferTests.cpp @@ -68,10 +68,50 @@ void checkBufferReuse(const Context& context) { Require(simultaneous.Handle() != reused.Handle(), "buffer cache reused an active allocation"); } +void checkDeviceBuffer(const Context& context) { + constexpr std::size_t bytes = 4096; + constexpr auto usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + VkBuffer handle = VK_NULL_HANDLE; + { + Buffer local(context, bytes, usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + handle = local.Handle(); + bool rejected = false; + try { static_cast(local.Bytes()); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected, "GPU-only buffer exposed a CPU mapping"); + rejected = false; + try { local.Invalidate(); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected, "GPU-only buffer accepted host invalidation"); + Buffer upload(context, bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + Buffer readback(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + std::fill(upload.Bytes().begin(), upload.Bytes().end(), std::byte{0x5a}); + CommandBatch batch(context); + VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + barrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + const auto sync = context.Function("vkCmdPipelineBarrier"); + sync(batch.Handle(), VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr); + const VkBufferCopy copy{0, 0, bytes}; + const auto transfer = context.Function("vkCmdCopyBuffer"); + transfer(batch.Handle(), upload.Handle(), local.Handle(), 1, ©); + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + sync(batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr); + transfer(batch.Handle(), local.Handle(), readback.Handle(), 1, ©); + barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT; + sync(batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr); + batch.SubmitAndWait(); + Require(std::equal(upload.Bytes().begin(), upload.Bytes().end(), readback.Bytes().begin()), "device-local buffer transfer corrupted data"); + } + Buffer reused(context, bytes, usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Require(reused.Handle() == handle, "device-local buffer was not retained for reuse"); +} + } void RunColorTransferTests(const AgcDriver::Graphics::Context& context) { checkBufferReuse(context); + checkDeviceBuffer(context); checkConversion(context, 130, 129, AgcDriver::Graphics::ColorTileMode::RenderTarget, false); checkConversion(context, 257, 17, AgcDriver::Graphics::ColorTileMode::RenderTarget, true); checkConversion(context, 192, 13, AgcDriver::Graphics::ColorTileMode::Linear, false);