perf(agc): use device-local tile buffers and cache spir-v validation

This commit is contained in:
boykopovar 2026-09-25 09:14:03 +03:00
parent 7ed5a0beea
commit 46c799da67
7 changed files with 142 additions and 14 deletions

View File

@ -33,6 +33,7 @@ private:
std::unique_ptr<Buffer> tiled;
std::unique_ptr<Buffer> linear;
std::unique_ptr<Buffer> readback;
std::unique_ptr<Buffer> upload;
std::uint32_t width = 0;
std::uint32_t height = 0;
ColorTileMode mode = ColorTileMode::Linear;

View File

@ -29,6 +29,7 @@ private:
VkDeviceSize allocationBytes = 0;
VkBufferUsageFlags usage;
VkMemoryPropertyFlags properties;
bool reusable = false;
std::shared_ptr<BufferPool> cache;
};

View File

@ -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);
}

View File

@ -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<Buffer>(context, layout.Bytes(), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
auto newLinear = std::make_unique<Buffer>(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<Buffer>(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<Buffer>(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<Buffer>(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<Buffer>(context, layout.Bytes(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
const std::array<VkDescriptorBufferInfo, 2> buffers{{{newTiled->Handle(), 0, layout.Bytes()}, {newLinear->Handle(), 0, layout.LinearBytes()}}};
std::array<VkWriteDescriptorSet, 2> 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<PFN_vkCmdDispatch>("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<PFN_vkCmdPipelineBarrier>("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<PFN_vkCmdCopyBuffer>("vkCmdCopyBuffer")(commands, upload->Handle(), tiled->Handle(), 1, &copy);
convert(commands, false, swapRedBlue);
}

View File

@ -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<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory buffer");
Check(context.Function<PFN_vkBindBufferMemory>("vkBindBufferMemory")(context.device, buffer, memory, 0), "vkBindBufferMemory");
initializeAddress(usage);
Check(context.Function<PFN_vkMapMemory>("vkMapMemory")(context.device, memory, 0, VK_WHOLE_SIZE, 0, &mapping), "vkMapMemory");
if ((properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0) Check(context.Function<PFN_vkMapMemory>("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<std::byte> Buffer::Bytes() {
Require(mapping != nullptr, "GPU-only buffer has no CPU mapping");
return {static_cast<std::byte*>(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;

View File

@ -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 <spirv/unified1/spirv.hpp>
#include <algorithm>
#include <map>
@ -8,6 +9,8 @@
#include <set>
#include <string>
#include <vector>
#include <memory>
#include <type_traits>
namespace AgcDriver::Graphics {
namespace {
@ -463,6 +466,76 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
}
namespace {
struct ValidatedInterface {
std::map<std::uint32_t, std::string> inputs;
std::map<std::uint32_t, std::string> outputs;
};
std::shared_ptr<const ValidatedInterface> 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 = [&]<typename TValue>(const TValue& value) {
static_assert(std::is_trivially_copyable_v<TValue>);
key.append(reinterpret_cast<const char*>(&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<const char*>(code.data()), code.size());
timing.Mark("key", key.size());
static thread_local std::map<std::string, std::shared_ptr<const ValidatedInterface>> 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<const ValidatedInterface>(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<const CompiledShader> 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<const CompiledShader> shaders, const State& state
Require(shaders.size() == (tessellation || state.rectList ? 4u : 2u), "incorrect graphics stage count");
const std::array<Stage, 4> tessStages{Stage::Local, Stage::TessellationControl, Stage::TessellationEvaluation, Stage::Fragment};
static_cast<void>(AssemblePushConstants(shaders));
Module previous;
std::shared_ptr<const ValidatedInterface> 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) {

View File

@ -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<void>(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<PFN_vkCmdPipelineBarrier>("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<PFN_vkCmdCopyBuffer>("vkCmdCopyBuffer");
transfer(batch.Handle(), upload.Handle(), local.Handle(), 1, &copy);
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, &copy);
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);