diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 521a3f93bd..a7438467df 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -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:$") 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:$") + 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) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index ada06eab1e..2c4ab1c24f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -20,6 +20,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" #include +#include #include #include #include @@ -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 captured; + std::vector> 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("vkDeviceWaitIdle")(context.device), "private image readback wait"); + Buffer host(context, static_cast(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("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("vkCmdCopyImageToBuffer")(commands, texture->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, host.Handle(), static_cast(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("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(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(BytesPerElement(texture->descriptor.format)), mip.height, static_cast(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(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(mip.linearSize * texture->arrayLayers)); + } + } + return; + } std::vector> ranges; { auto& pending = Pending(); @@ -6274,7 +6342,7 @@ void StorageTexture::writeBackLayers(const std::vector& 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 dump; if (dumpSelected && dumps < 8) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp index 5581d46dcc..5777b04e35 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -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(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 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 pixels(static_cast(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); diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index d54b9c8fd1..d09544e984 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -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_
.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_
_.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_
_.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_
_.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_
_.raw |