feat(agc): read selected live images without guest writeback

This commit is contained in:
JJenkx committed 2026-10-26 04:42:43 -04:00
1 parent b27cc8416f
commit 61ba654e90
4 files changed
+121 -1

No files matched your search

@@ -2885,6 +2885,13 @@ if(BUILD_TESTING)
if(WIN32)
set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_volume_direct_readback COMMAND agc_driver_layered_volume_draw_tests)
set_tests_properties(agc_driver_volume_direct_readback PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_TEST_DIRECT_READBACK=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")
if(WIN32)
set_tests_properties(agc_driver_volume_direct_readback PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_f16_to_f32_unpack_spirv COMMAND agc_driver_f16_to_f32_unpack_tests --verify-spirv)
set_tests_properties(agc_driver_f16_to_f32_unpack_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
@@ -20,6 +20,7 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp"
#include <algorithm>
#include <filesystem>
#include <array>
#include <bit>
#include <chrono>
@@ -4497,11 +4498,78 @@ bool StorageDumpSelects(const GuestTextureResource& descriptor) {
return selector.width != 0 && descriptor.width == selector.width && descriptor.height == selector.height && descriptor.depthOrLastArray + 1 == selector.layers && (selector.anyFormat || descriptor.format == selector.format);
}
bool StorageDumpReady() {
const char* gate = std::getenv("APS5_DUMP_STORAGE_GATE");
std::error_code error;
return gate == nullptr || std::filesystem::exists(gate, error);
}
}
void StorageTexture::FlushDumpSelected(const char* reason) {
const auto& selector = StorageDumpSelection();
if (selector.address == 0 && selector.width == 0) return;
if (!StorageDumpReady()) return;
if (std::getenv("APS5_DUMP_STORAGE_GATE") != nullptr) {
std::lock_guard gpu(GuestMemory::GpuMutex());
static std::vector<std::uint64_t> captured;
std::vector<std::shared_ptr<StorageTexture>> selected;
{
auto& live = Live();
std::lock_guard lock(live.mutex);
for (auto* texture : live.textures) {
if (selected.size() + captured.size() >= 8) break;
if (!StorageDumpSelects(texture->descriptor) || texture->sliceLinearBytes * texture->arrayLayers > (8u << 20u)) continue;
if (std::find(captured.begin(), captured.end(), texture->descriptor.baseAddress) != captured.end()) continue;
if (auto alive = texture->weak_from_this().lock()) selected.push_back(std::move(alive));
}
}
if (!selected.empty()) {
if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) recorder->Submit();
for (const auto& texture : selected) {
const auto& context = texture->context;
Check(context.Function<PFN_vkDeviceWaitIdle>("vkDeviceWaitIdle")(context.device), "private image readback wait");
Buffer host(context, static_cast<std::size_t>(texture->sliceLinearBytes * texture->arrayLayers), VK_BUFFER_USAGE_TRANSFER_DST_BIT);
CommandBatch batch(context);
const auto commands = batch.Handle();
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
barrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL;
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = texture->image;
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, texture->descriptor.mipCount, 0, texture->geometry.imageLayers};
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
const auto regions = texture->CopyRegions();
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, texture->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, host.Handle(), static_cast<std::uint32_t>(regions.size()), regions.data());
barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
const auto hostBarrier = WholeBufferBarrier(host.Handle(), VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, 0, 0, nullptr, 1, &hostBarrier, 1, &barrier);
batch.SubmitAndWait();
char name[80];
std::snprintf(name, sizeof(name), "storage_direct_%llx_%zu.raw", static_cast<unsigned long long>(texture->descriptor.baseAddress), captured.size());
std::FILE* file = std::fopen(name, "wb");
Require(file != nullptr, "cannot open private image readback file");
const auto& mip = texture->mips[0];
const std::uint32_t header[3] = {mip.pitchBytes / static_cast<std::uint32_t>(BytesPerElement(texture->descriptor.format)), mip.height, static_cast<std::uint32_t>(texture->storageFormat)};
bool written = std::fwrite(header, sizeof(header), 1, file) == 1;
for (std::uint32_t layer = 0; layer < texture->arrayLayers; ++layer) {
if (!texture->geometry.HasLayer(0, layer)) continue;
written = std::fwrite(host.Bytes().data() + texture->geometry.LinearLayerOffset(layer) + mip.linearOffset, 1, static_cast<std::size_t>(mip.linearSize), file) == mip.linearSize && written;
}
const bool closed = std::fclose(file) == 0;
Require(written && closed, "cannot complete private image readback file");
captured.push_back(texture->descriptor.baseAddress);
std::fprintf(stderr, "[storage-dump] direct=%s bytes=%llu\n", name, static_cast<unsigned long long>(mip.linearSize * texture->arrayLayers));
}
}
return;
}
std::vector<std::pair<std::uint64_t, std::size_t>> ranges;
{
auto& pending = Pending();
@@ -6274,7 +6342,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
detiler.Dispatch(commands, descriptor.tileMode, elementBytes, linear.Handle(), geometry.LinearLayerOffset(layer) + mip.linearOffset, tiled.Handle(), geometry.GuestLayerOffset(layer) + mip.tiledOffset, mip, true, layer, geometry.thick, {.pipeBankXor = descriptor.pipeBankXor});
}
}
const bool dumpSelected = StorageDumpSelects(descriptor);
const bool dumpSelected = StorageDumpSelects(descriptor) && StorageDumpReady();
static int dumps = 0;
std::unique_ptr<Buffer> dump;
if (dumpSelected && dumps < 8) {
@@ -20,6 +20,7 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <iostream>
#include <span>
#include <string>
@@ -269,6 +270,17 @@ void Check(const std::byte* volume, const AgcDriver::Graphics::ColorTarget& colo
int main(int argc, char** argv) {
try {
const bool directReadback = std::getenv("APS5_TEST_DIRECT_READBACK") != nullptr;
if (directReadback) {
std::remove("private-readback.ready");
char extent[64];
std::snprintf(extent, sizeof(extent), "%ux%ux%u", Width, Height, Slices);
#ifdef _WIN32
Require(_putenv_s("APS5_DUMP_STORAGE", extent) == 0 && _putenv_s("APS5_DUMP_STORAGE_GATE", "private-readback.ready") == 0, "cannot set private readback options");
#else
Require(setenv("APS5_DUMP_STORAGE", extent, 1) == 0 && setenv("APS5_DUMP_STORAGE_GATE", "private-readback.ready", 1) == 0, "cannot set private readback options");
#endif
}
const bool vertexOnly = argc == 2 && std::strcmp(argv[1], "--vertex-only") == 0;
Require(argc == 1 || vertexOnly, "usage: layered volume draw [--vertex-only]");
const auto device = OpenVulkanTestDevice();
@@ -283,6 +295,38 @@ int main(int argc, char** argv) {
const auto color = VolumeTarget(volume.Data());
Run(*device, color, false, PixelCode, false);
Check(volume.Data(), color, "vertex draw", WhiteBands);
if (directReadback) {
char name[80];
std::snprintf(name, sizeof(name), "storage_direct_%llx_0.raw", static_cast<unsigned long long>(color.address));
std::remove(name);
AgcDriver::Graphics::StorageTexture::FlushDumpSelected("closed readback gate fixture");
Require(!std::filesystem::exists(name), "private readback ignored its closed gate");
auto* trigger = std::fopen("private-readback.ready", "wb");
Require(trigger != nullptr, "cannot create private readback trigger");
Require(std::fclose(trigger) == 0, "cannot close private readback trigger");
AgcDriver::Graphics::StorageTexture::FlushDumpSelected("private readback fixture");
std::remove("private-readback.ready");
auto* file = std::fopen(name, "rb");
Require(file != nullptr, "clean volume image was not exported by direct readback");
std::array<std::uint32_t, 3> header{};
Require(std::fread(header.data(), sizeof(header), 1, file) == 1, "private readback header is incomplete");
Require(header[0] >= Width && header[1] == Height && header[2] == VK_FORMAT_R8G8B8A8_UNORM, "private readback header disagrees with the volume");
std::vector<std::uint8_t> pixels(static_cast<std::size_t>(header[0]) * Height * Slices * 4u);
Require(std::fread(pixels.data(), 1, pixels.size(), file) == pixels.size(), "private readback pixels are incomplete");
Require(std::fgetc(file) == EOF, "private readback has unexpected trailing pixels");
Require(std::fclose(file) == 0, "cannot close private readback");
for (std::uint32_t z = 0; z < Slices; ++z) {
for (std::uint32_t y = 0; y < Height; ++y) {
for (std::uint32_t x = 0; x < Width; ++x) {
const bool drawn = z >= FirstSlice && x / BandWidth == z - FirstSlice;
const auto expected = drawn ? WhiteBands[z - FirstSlice] : Kept;
for (std::uint32_t channel = 0; channel < 4; ++channel) Require(pixels[((z * Height + y) * header[0] + x) * 4u + channel] == expected, "private readback changed a volume texel");
}
}
}
std::puts("private clean-volume direct readback pixels verified");
return 0;
}
const GuestVolume fieldVolume((VolumeTarget(nullptr).bytes + 65535u) & ~std::size_t{65535u});
const auto fieldColor = VolumeTarget(fieldVolume.Data());
Run(*device, fieldColor, false, LayerFieldPixelCode, true);
+1
View File
@@ -143,6 +143,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti
| `APS5_DUMP_SCALE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 4 | keeps every n-th pixel in each direction of a dumped frame |
| `APS5_DUMP_SHADERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | writes each prepared shader's request to shader_<address>.req |
| `APS5_DUMP_STORAGE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | selects storage textures to dump by hex address or WxHxL[:format] extent as storage_<address>_<n>.raw |
| `APS5_DUMP_STORAGE_GATE` | path | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | with `APS5_DUMP_STORAGE`, waits for this path to exist and reads matching live images directly without guest writeback; at most eight addresses, 8 MiB linear storage per image; writes `storage_direct_<address>_<n>.raw` with the existing pitch, height and VkFormat header and first-mip pixels |
| `APS5_DUMP_SUBMITS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp` | | directory in which every submission is captured for replay |
| `APS5_DUMP_TARGETS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | 0 | dumps the first n renders of each non-resident color target as target_<address>_<n>.raw in the working directory; a non-zero value also runs every draw synchronously, and in practice it needs APS5_NO_RESIDENT_TARGETS=1 |
| `APS5_DUMP_TEXTURE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | hex addresses or WxH extents whose sampled textures are dumped on first upload as texture_<address>_<n>.raw |