From c4faf55fc4faf74e49804661d21f8a28b84ae87f Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:19:24 -0400 Subject: [PATCH] test(agc): cover unlocked host import retirement during mapping --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 130 ++++++++++++++++++ .../tests/execution/HostImportAtMap.cpp | 13 +- 3 files changed, 141 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 5749f1e6e..86cb782d1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -814,6 +814,8 @@ if(BUILD_TESTING) set_tests_properties(agc_host_import_unmap PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_parked_import_unmap COMMAND agc_driver_recorder_tests --parked-import-unmap-only) set_tests_properties(agc_parked_import_unmap PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") + add_test(NAME agc_host_import_retirement COMMAND agc_driver_recorder_tests --host-import-retirement-only) + set_tests_properties(agc_host_import_retirement PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_unit_shadow COMMAND agc_driver_recorder_tests --unit-shadow-only) set_tests_properties(agc_unit_shadow PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") add_test(NAME agc_descriptor_pool_recycling COMMAND agc_driver_recorder_tests --descriptor-pool-only) @@ -825,7 +827,7 @@ if(BUILD_TESTING) add_test(NAME agc_draw_inputs_in_place COMMAND agc_driver_recorder_tests --draw-inputs-in-place-only) set_tests_properties(agc_draw_inputs_in_place PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access agc_draw_inputs_in_place PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 938b00102..a846bea31 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -141,6 +141,9 @@ public: selectedRank = rank; } Require(context.physical != VK_NULL_HANDLE, "no Vulkan 1.1 device"); + VkPhysicalDeviceProperties selected{}; + physicalProperties(context.physical, &selected); + std::cout << "Vulkan device: " << selected.deviceName << '\n'; const auto extensions = function("vkEnumerateDeviceExtensionProperties"); Check(extensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); std::vector available(count); @@ -948,6 +951,129 @@ void remappedImportTests(const Device& device) { HostImportFor(context, address, bytes); } +void hostImportRetirementTests(const Device& device) { + const auto& context = device.GetContext(); + Require(context.hostImportAlignment != 0 && context.hostImportAlignment <= 65536, "host import retirement requires host imports aligned within 64 KiB"); + constexpr std::size_t UnitBytes = 65536; + constexpr std::size_t AllocationBytes = 4 * UnitBytes; + constexpr std::size_t CopyBytes = 256; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, AllocationBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(UnitBytes, AllocationBytes); +#endif + Require(block != nullptr, "cannot allocate the host import retirement block"); + const auto release = [](void* pointer) { +#ifdef _WIN32 + VirtualFree(pointer, 0, MEM_RELEASE); +#else + std::free(pointer); +#endif + }; + std::unique_ptr allocation(block, release); + auto* memory = static_cast(block); + const auto address = reinterpret_cast(block); + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(context); + Recorder recorder(context); + recorder.Activate(); + Buffer readback(context, CopyBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + std::array registered{}; + const auto cleanup = [&] { + if (!gpu.owns_lock()) gpu.lock(); + recorder.Sync(); + recorder.FlushDeferredReleases(); + ClearHostImports(context.device); + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < registered.size(); ++index) { + if (!registered[index]) continue; + mutation.Remove(memory + index * UnitBytes); + registered[index] = false; + } + SetHostImportWindow(0); + }; + const auto add = [&](std::size_t index) { + GuestAllocations::Mutation mutation; + mutation.Add(memory + index * UnitBytes, UnitBytes, true, true, true); + registered[index] = true; + }; + const auto mapped = [&](std::size_t index) { + GuestAllocations::Mapped result; + for (const auto& range : GuestAllocations::GuestAllocationsAcquire_nid_postfix()) { + if (range->address == address + index * UnitBytes) result.push_back(range); + } + Require(result.size() == 1, "host import retirement lost its registered range"); + return result; + }; + bool deferredEveryRetirement = true; + bool preservedEveryRecorder = true; + try { + const std::array shapes{"same-base growth", "earlier-base overlap", "predecessor split"}; + for (std::size_t shape = 0; shape < shapes.size(); ++shape) { + SetHostImportWindow(0); + Require(ImportMappedRanges(context, {}, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true), "host import retirement could not adopt the device"); + const std::size_t first = shape == 1 ? 1 : 0; + add(first); + if (shape == 2) add(1); + for (std::size_t index = 0; index < AllocationBytes; ++index) memory[index] = static_cast((index * 37u + shape * 19u + 11u) & 0xffu); + const auto initial = mapped(first); + const auto initialGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + gpu.unlock(); + const bool fresh = ImportMappedRanges(context, initial, initialGeneration, false); + const bool unchanged = ImportMappedRanges(context, initial, initialGeneration, false); + gpu.lock(); + Require(fresh && unchanged, "host import retirement blocked a fresh or unchanged fast import"); + const auto sourceAddress = address + (shape == 2 ? UnitBytes : first * UnitBytes); + const auto* imported = HostImportFor(context, sourceAddress, CopyBytes); + Require(imported != nullptr, "host import retirement could not import its source"); + const auto source = imported->buffer; + const auto sourceOffset = sourceAddress - imported->base; + std::array expected{}; + std::memcpy(expected.data(), reinterpret_cast(sourceAddress), CopyBytes); + recorder.Commands(); + recorder.Submit(); + device.WaitQueue(); + Require(!recorder.Recording() && recorder.InFlightBatches() == 1, "host import retirement requires a submitted batch without an open batch"); + std::size_t changed = 0; + if (shape == 0) { + changed = 1; + add(changed); + } else if (shape == 1) { + add(changed); + } else { + SetHostImportWindow(UnitBytes); + add(3); + changed = 1; + } + const auto ranges = mapped(changed); + const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + gpu.unlock(); + const bool deferred = !ImportMappedRanges(context, ranges, generation, false); + gpu.lock(); + const bool preserved = !recorder.Recording() && recorder.InFlightBatches() == 1; + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(context, commands, source, sourceOffset, readback.Handle(), 0, CopyBytes); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + Require(ImportMappedRanges(context, ranges, generation, true), "host import retirement refused its retry under the GPU lock"); + Require(HostImportFor(context, sourceAddress, CopyBytes) != nullptr, "host import retirement lost the replacement import"); + recorder.Sync(); + readback.Invalidate(); + Require(std::equal(expected.begin(), expected.end(), readback.Bytes().begin()), "host import retirement changed bytes copied from the retired import"); + std::cout << "host import retirement: " << shapes[shape] << " GPU readback verified; deferred=" << deferred << "; recorder preserved=" << preserved << '\n'; + deferredEveryRetirement = deferredEveryRetirement && deferred; + preservedEveryRecorder = preservedEveryRecorder && preserved; + cleanup(); + } + } catch (...) { + cleanup(); + throw; + } + Require(deferredEveryRetirement, "unlocked mapping retired an import instead of requesting the GPU lock"); + Require(preservedEveryRecorder, "unlocked mapping changed recorder batches"); + std::cout << "Host import retirement locking and GPU readback tests passed\n"; +} + void movedMetadataTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0) { @@ -5447,6 +5573,10 @@ int main(int argc, char** argv) { return 77; } Device& device = *created; + if (argc == 2 && std::string_view(argv[1]) == "--host-import-retirement-only") { + hostImportRetirementTests(device); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--pipeline-library-only") { pipelineLibraryTests(device); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/HostImportAtMap.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportAtMap.cpp index c337c09d8..00a4f23f9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/HostImportAtMap.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/HostImportAtMap.cpp @@ -25,16 +25,18 @@ using AgcDriver::Graphics::Require; constexpr std::size_t BlockBytes = 65536; constexpr std::size_t AllocationBytes = BlockBytes * 4; +constexpr std::size_t ReservationBytes = AllocationBytes + BlockBytes * 2; class GuestAllocation { public: GuestAllocation() { #ifdef _WIN32 - memory = static_cast(VirtualAlloc(nullptr, AllocationBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + allocation = static_cast(VirtualAlloc(nullptr, ReservationBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); #else - memory = static_cast(std::aligned_alloc(BlockBytes, AllocationBytes)); + allocation = static_cast(std::aligned_alloc(BlockBytes, ReservationBytes)); #endif - Require(memory != nullptr, "host import at map: cannot allocate the guest memory"); + Require(allocation != nullptr, "host import at map: cannot allocate the guest memory"); + memory = allocation + BlockBytes; std::memset(memory, 0, AllocationBytes); Register(); } @@ -42,9 +44,9 @@ public: ~GuestAllocation() { if (registered) Unregister(); #ifdef _WIN32 - VirtualFree(memory, 0, MEM_RELEASE); + VirtualFree(allocation, 0, MEM_RELEASE); #else - std::free(memory); + std::free(allocation); #endif } @@ -73,6 +75,7 @@ public: } private: + std::uint8_t* allocation = nullptr; std::uint8_t* memory = nullptr; bool registered = false; };