diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 1927c61f6fac..951a7c85ed52 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1093,6 +1093,7 @@ add_custom_command( add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL tests/Recorder.cpp + tests/HostImportLifetime.cpp tests/ResidentPresent.cpp ${agcTriangleHeaders} ${agcSampleLodShaderHeader} @@ -1273,6 +1274,22 @@ if(BUILD_TESTING) set_tests_properties(agc_pending_key_stores agc_shadowed_key_stores agc_sample_dumps agc_draw_descriptors PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_host_import_lifetime COMMAND agc_driver_recorder_tests --host-import-lifetime-only) + add_test(NAME agc_driver_host_import_mutation_race COMMAND agc_driver_recorder_tests --host-import-mutation-race-only) + set_tests_properties(agc_driver_host_import_lifetime agc_driver_host_import_mutation_race PROPERTIES + ENVIRONMENT "APS5_PIN_WAIT_MS=2000" SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_driver_unit_shadow COMMAND agc_driver_recorder_tests --unit-shadow-only) + add_test(NAME agc_driver_unit_shadow_budget COMMAND agc_driver_recorder_tests --unit-shadow-only) + set_tests_properties(agc_driver_unit_shadow PROPERTIES + ENVIRONMENT "APS5_PIN_WAIT_MS=2000" SKIP_RETURN_CODE 77 TIMEOUT 30) + set_tests_properties(agc_driver_unit_shadow_budget PROPERTIES + ENVIRONMENT "APS5_PIN_WAIT_MS=2000;APS5_UNIT_SHADOW_MIB=8" SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_host_import_lifetime agc_driver_host_import_mutation_race agc_driver_unit_shadow agc_driver_unit_shadow_budget PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + set_property(TEST agc_driver_host_import_lifetime APPEND PROPERTY ENVIRONMENT_MODIFICATION + "APS5_RELEASE_UNDER_LOCK=unset:" "APS5_RELEASE_ON_UNLOCK=unset:") endif() add_test_executable(agc_recorder_kept_bytes_tests EXCLUDE_FROM_ALL tests/RecorderKeptBytes.cpp) @@ -1635,6 +1652,8 @@ target_link_libraries(agc_driver_interpolation_modes_tests PRIVATE shader_recomp target_include_directories(agc_driver_prepared_unregistered_graphics_tests PRIVATE $) +target_link_libraries(agc_driver_guest_memory_lifetime_tests PRIVATE libkernel) + target_include_directories(agc_driver_prepared_shaders_tests PRIVATE $ ) @@ -1849,6 +1868,48 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_uint8x4_color_target_no_resident PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + set(agcGuestMemoryLifetimeTests agc_driver_guest_memory_lifetime) + foreach(mode IN ITEMS copy direct locked heap heap_uncached) + set(testName agc_driver_guest_memory_lifetime_${mode}) + if(mode STREQUAL "heap_uncached") + add_test(NAME ${testName} COMMAND agc_driver_guest_memory_lifetime_tests heap) + else() + add_test(NAME ${testName} COMMAND agc_driver_guest_memory_lifetime_tests ${mode}) + endif() + list(APPEND agcGuestMemoryLifetimeTests ${testName}) + endforeach() + set_tests_properties(${agcGuestMemoryLifetimeTests} PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIN_WAIT_MS=1000;APS5_HEAP_CACHE_MIB=4" + SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_driver_guest_memory_lifetime_heap_uncached PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIN_WAIT_MS=1000;APS5_HEAP_CACHE_MIB=0") + if(WIN32) + set_tests_properties(${agcGuestMemoryLifetimeTests} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND Python3_Interpreter_FOUND) + add_library(agc_guest_memory_lifetime_fixture SHARED tests/GuestMemoryLifetimeFixture.cpp) + target_include_directories(agc_guest_memory_lifetime_fixture PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include + ${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers/include) + target_link_libraries(agc_guest_memory_lifetime_fixture PRIVATE libSceAgcDriver SDL2-static libc) + add_dependencies(agc_guest_memory_lifetime_fixture relinker libkernel) + set_target_properties(agc_guest_memory_lifetime_fixture PROPERTIES + OUTPUT_NAME GuestMemoryFixture SUFFIX ".prx" CXX_EXTENSIONS OFF + BUILD_WITH_INSTALL_RPATH TRUE INSTALL_RPATH "$ORIGIN") + add_test(NAME agc_driver_guest_memory_lifetime_converted COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_guest_memory_lifetime.py + ${CMAKE_SOURCE_DIR}/core/relinker/relinker/tests + $ + $ + $ + $ + $) + set_tests_properties(agc_driver_guest_memory_lifetime_converted PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIN_WAIT_MS=1000" SKIP_RETURN_CODE 77 TIMEOUT 30) + endif() set_tests_properties(agc_driver_flat_store PROPERTIES RUN_SERIAL TRUE) set_target_properties(agc_driver_flat_apertures_tests PROPERTIES EXCLUDE_FROM_ALL OFF) foreach(flatAperturesPart RANGE 13) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index a44b59cb2622..a836b30afa27 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1036,7 +1036,30 @@ std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std: return bytes; } +struct ImportConstruction { + ImportConstruction(const Context& context, std::uint64_t base, std::uint64_t bytes) + : device(context.device), destroyBuffer(context.Function("vkDestroyBuffer")), freeMemory(context.Function("vkFreeMemory")), entry{base, bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0} {} + ImportConstruction(const ImportConstruction&) = delete; + ImportConstruction& operator=(const ImportConstruction&) = delete; + ~ImportConstruction() { + if (!armed) return; + if (entry.buffer != VK_NULL_HANDLE) destroyBuffer(device, entry.buffer, nullptr); + if (entry.memory != VK_NULL_HANDLE) freeMemory(device, entry.memory, nullptr); +#ifdef _WIN32 + GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias); +#endif + } + VkDevice device; + PFN_vkDestroyBuffer destroyBuffer; + PFN_vkFreeMemory freeMemory; + HostImport entry; + bool armed = true; +}; + +void ensurePinWaiter(); + const HostImport* importAllocation(const Context& context, HostImports& state, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end = 0, bool evict = true) { + ensurePinWaiter(); const bool fresh = !importsStale(context, state); if (fresh && end != 0) { if (const auto* current = findImport(state, address, end)) return current; @@ -1113,7 +1136,8 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import it = next; } if (state.liveBytes + bytes > budget) retireLeastRecent(context, state, lease, state.liveBytes + bytes - budget); - HostImport entry{base, bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0}; + ImportConstruction owner(context, base, bytes); + auto& entry = owner.entry; entry.range = run.ranges.front(); entry.ranges = std::move(run.ranges); #ifdef _WIN32 @@ -1208,9 +1232,6 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import #endif entry.unwatched = step == nullptr && !(entry.dmaBuf ? watchedDmaBuf : watchedHostPointer); if (step != nullptr) { -#ifdef _WIN32 - GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias); -#endif failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx failed at %s (%d); falling back to copies\n", static_cast(base), static_cast(bytes), step, static_cast(result)); #ifdef _WIN32 @@ -1244,7 +1265,9 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)%s\n", static_cast(base), static_cast(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0, joined ? ", joined at bind" : ""); entry.joined = joined; entry.lastUse = ++state.useCounter; - return &state.imports.emplace(base, entry).first->second; + const auto inserted = state.imports.emplace(base, entry); + owner.armed = false; + return &inserted.first->second; } // The readable registered range containing [begin, end), or null. @@ -2078,7 +2101,7 @@ SnapshotStats& Snapshots() { return stats; } -bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) noexcept { +bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) { const auto start = std::chrono::steady_clock::now(); bool synced = false; bool drained = false; @@ -2098,19 +2121,82 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) noexcept state.stats.contentionMs += std::chrono::duration(std::chrono::steady_clock::now() - start).count(); return true; } + if (Recorder::InCompletion()) throw std::runtime_error("guest allocation mutation cannot wait for an active GPU completion"); if (GuestMemory::GpuMutex().HeldByThisThread()) { - try { - auto* recorder = Recorder::Active(); + auto* recorder = Recorder::Active(); + if (recorder != nullptr && !recorder->Idle()) { + Recorder::CountSync(4); + recorder->Sync(); + synced = true; + drained = true; + } + if (recorder != nullptr) recorder->FlushDeferredReleases(); + const auto end = static_cast(address) + bytes; + auto& imports = Imports(); + if (releaseParkedImports(imports, address, end)) retiredImport = true; + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + { + std::lock_guard importsLock(imports.mutex); + for (auto it = imports.imports.begin(); it != imports.imports.end();) { + const auto base = it->first; + if (base >= end || address >= base + it->second.bytes) { + ++it; + continue; + } + const auto next = std::next(it); + retireImport(imports.device, imports, it, lease, false); + it = next; + retiredImport = true; + } + } + if (retiredImport && recorder != nullptr && !recorder->Idle()) { + Recorder::CountSync(4); + recorder->Sync(); + synced = true; + drained = true; + } + if (retiredImport && recorder != nullptr) recorder->FlushDeferredReleases(); + } else { + std::lock_guard lock(GuestMemory::GpuMutex()); + auto& state = Leases(); + auto* recorder = Recorder::Active(); + const auto target = state.newestLeaseSerial; + if (recorder != nullptr && recorder == state.leaseRecorder && target > state.finishedLeaseSerial) { + Recorder::CountSync(4); + recorder->Submit(); + recorder->FinishUpTo(target); + state.finishedLeaseSerial = target; + synced = true; + } else if (recorder != nullptr && !recorder->Idle()) { + Recorder::CountSync(4); + recorder->Sync(); + synced = true; + drained = true; + } else { + std::this_thread::yield(); + } + if (recorder != nullptr) recorder->FlushDeferredReleases(); + + const auto end = static_cast(address) + bytes; + auto& imports = Imports(); + if (releaseParkedImports(imports, address, end)) retiredImport = true; + bool overlapsImport = false; + { + std::lock_guard importsLock(imports.mutex); + for (const auto& [base, entry] : imports.imports) { + if (base < end && address < base + entry.bytes) { + overlapsImport = true; + break; + } + } + } + if (overlapsImport) { if (recorder != nullptr && !recorder->Idle()) { Recorder::CountSync(4); recorder->Sync(); synced = true; drained = true; } - if (recorder != nullptr) recorder->FlushDeferredReleases(); - const auto end = static_cast(address) + bytes; - auto& imports = Imports(); - if (releaseParkedImports(imports, address, end)) retiredImport = true; const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); { std::lock_guard importsLock(imports.mutex); @@ -2132,79 +2218,6 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) noexcept synced = true; drained = true; } - if (retiredImport && recorder != nullptr) recorder->FlushDeferredReleases(); - } catch (const std::exception& error) { - std::fprintf(stderr, "[gpu] lease wait failed: %s\n", error.what()); - } - } else { - try { - std::lock_guard lock(GuestMemory::GpuMutex()); - auto& state = Leases(); - auto* recorder = Recorder::Active(); - const auto target = state.newestLeaseSerial; - if (recorder != nullptr && recorder == state.leaseRecorder && target > state.finishedLeaseSerial) { - // The open batch may be the target; FinishUpTo waits front to back up to it only. - Recorder::CountSync(4); - recorder->Submit(); - recorder->FinishUpTo(target); - state.finishedLeaseSerial = target; - synced = true; - } else if (recorder != nullptr && !recorder->Idle()) { - Recorder::CountSync(4); - recorder->Sync(); - synced = true; - drained = true; - } else { - // Nothing recorded holds the lease: the holder is a lease-holding object that has - // yet to record (a test's, or a build whose worker gave up the lock); give it time. - std::this_thread::yield(); - } - if (recorder != nullptr) recorder->FlushDeferredReleases(); - - const auto end = static_cast(address) + bytes; - auto& imports = Imports(); - if (releaseParkedImports(imports, address, end)) retiredImport = true; - bool overlapsImport = false; - { - std::lock_guard importsLock(imports.mutex); - for (const auto& [base, entry] : imports.imports) { - if (base < end && address < base + entry.bytes) { - overlapsImport = true; - break; - } - } - } - if (overlapsImport) { - if (recorder != nullptr && !recorder->Idle()) { - Recorder::CountSync(4); - recorder->Sync(); - synced = true; - drained = true; - } - const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); - { - std::lock_guard importsLock(imports.mutex); - for (auto it = imports.imports.begin(); it != imports.imports.end();) { - const auto base = it->first; - if (base >= end || address >= base + it->second.bytes) { - ++it; - continue; - } - const auto next = std::next(it); - retireImport(imports.device, imports, it, lease, false); - it = next; - retiredImport = true; - } - } - if (retiredImport && recorder != nullptr && !recorder->Idle()) { - Recorder::CountSync(4); - recorder->Sync(); - synced = true; - drained = true; - } - } - } catch (const std::exception& error) { - std::fprintf(stderr, "[gpu] lease wait failed: %s\n", error.what()); } } auto& state = Leases(); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeFixture.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeFixture.cpp new file mode 100644 index 000000000000..465575f5dd78 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeFixture.cpp @@ -0,0 +1,82 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/tests/execution/VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include +#include +#include +#include +#include +#include + +namespace { +std::unique_ptr device; +constexpr std::array Pattern{0x12345678u, 0x89abcdefu, 0x24681357u, 0xfedcba98u}; +} + +extern "C" int APS5_VABI Begin_nid_no_patch() { + try { + device = OpenVulkanTestDevice(); + if (!device || !GuestMemoryLifetimeHasHostImports(device->Device())) { + device.reset(); + std::fflush(stdout); + std::exit(VulkanTestSkipped); + } + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "converted guest Vulkan initialization failed: %s\n", error.what()); + std::exit(1); + } +} + +extern "C" int APS5_VABI Fill_nid_no_patch(void* pointer, int gpu) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(pointer); + if (!range.readable || !range.writable || range.gpu != (gpu != 0)) { + std::fputs("converted guest mapping does not match its requested CPU and GPU access\n", stderr); + return 1; + } + } + if (!device->FillBuffer(reinterpret_cast(pointer), 4096, Pattern)) return 1; + AgcDriver::Graphics::Context probe{}; + probe.device = device->Device(); + probe.hostImportAlignment = 1; + return !AgcDriver::Graphics::HostImportCovers(probe, reinterpret_cast(pointer), 65536); +} + +extern "C" int APS5_VABI Retired_nid_no_patch(const void* pointer) { + AgcDriver::Graphics::Context probe{}; + probe.device = device->Device(); + probe.hostImportAlignment = 1; + const bool imported = AgcDriver::Graphics::HostImportCovers(probe, reinterpret_cast(pointer), 65536); + if (imported) std::fputs("converted guest observed a stale Vulkan host import\n", stderr); + return imported; +} + +extern "C" int APS5_VABI Verify_nid_no_patch(const void* pointer, int zero) { + const auto* bytes = static_cast(pointer); + for (std::size_t offset = 0; offset < 4096; ++offset) { + const auto expected = zero ? std::uint8_t{0} : reinterpret_cast(Pattern.data())[offset % sizeof(Pattern)]; + if (bytes[offset] != expected) { + std::fprintf(stderr, "converted guest memory mismatch at byte %zu: expected %u, observed %u\n", + offset, unsigned(expected), unsigned(bytes[offset])); + return 1; + } + } + return 0; +} + +extern "C" [[noreturn]] void APS5_VABI Finish_nid_no_patch(int result) { + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device->WaitIdle(); + } + device.reset(); + std::printf("converted guest memory/Vulkan lifetime result=%d\n", result); + std::fflush(stdout); + std::exit(result); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp b/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp new file mode 100644 index 000000000000..863b8ca67f5e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp @@ -0,0 +1,34 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_GUESTMEMORYLIFETIMESUPPORT_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_GUESTMEMORYLIFETIMESUPPORT_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +static inline bool GuestMemoryLifetimeHasHostImports(VkDevice device) { + if (std::getenv("APS5_NO_HOST_IMPORT") != nullptr) { + std::puts("skipped, host imports are disabled by APS5_NO_HOST_IMPORT"); + return false; + } +#ifdef _WIN32 + constexpr const char* libraryName = "vulkan-1.dll"; +#else + constexpr const char* libraryName = "libvulkan.so.1"; +#endif + const std::unique_ptr library(SDL_LoadObject(libraryName), &SDL_UnloadObject); + if (!library) throw std::runtime_error(std::string("cannot load Vulkan for the host import capability query: ") + SDL_GetError()); + const auto deviceProc = reinterpret_cast(SDL_LoadFunction(library.get(), "vkGetDeviceProcAddr")); + if (deviceProc == nullptr) throw std::runtime_error(std::string("cannot resolve vkGetDeviceProcAddr for the host import capability query: ") + SDL_GetError()); + if (deviceProc(device, "vkGetMemoryHostPointerPropertiesEXT") == nullptr) { + std::puts("skipped, the device has no VK_EXT_external_memory_host"); + return false; + } + return true; +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp new file mode 100644 index 000000000000..c507b67de3e8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp @@ -0,0 +1,803 @@ +#include "HostImportLifetime.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; +using AgcDriver::GuestMemory::GpuMutex; + +struct ImportState { + VkBuffer buffer; + VkDeviceMemory memory; + std::uint64_t address; + std::size_t bytes; + std::atomic destroyed{false}; + std::atomic freed{false}; + std::atomic frees{0}; + std::atomic outOfOrder{false}; + std::atomic inaccessible{false}; + std::mutex mutex; + std::condition_variable wake; + bool blockFree = false; + bool freeing = false; + bool allowFree = false; + std::thread::id freeingThread; +}; + +struct Tracking { + PFN_vkGetDeviceProcAddr resolve; + VkDevice device; + std::mutex mutex; + std::map> imports; + std::atomic failAddressLookup{false}; + std::atomic failQueueSubmit{false}; + std::atomic rejectedSubmissions{0}; + std::atomic acceptedSubmissions{0}; + std::vector rejectedFences; + std::shared_ptr lastAllocated; + + void CleanupRejectedFences() { + std::vector fences; + { + std::lock_guard lock(mutex); + fences.swap(rejectedFences); + } + const auto destroy = reinterpret_cast(resolve(device, "vkDestroyFence")); + for (const auto fence : fences) destroy(device, fence, nullptr); + } + + std::shared_ptr Add(const HostImport& import) { + auto state = std::make_shared(); + state->buffer = import.buffer; + state->memory = import.memory; + state->address = import.base; + state->bytes = import.bytes; + std::lock_guard lock(mutex); + imports[import.memory] = state; + return state; + } +}; + +Tracking* tracking = nullptr; + +VKAPI_ATTR void VKAPI_CALL destroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* allocator) { + const auto original = reinterpret_cast(tracking->resolve(device, "vkDestroyBuffer")); + original(device, buffer, allocator); + std::lock_guard lock(tracking->mutex); + for (const auto& [memory, state] : tracking->imports) { + if (state->buffer == buffer && !state->freed.load()) state->destroyed.store(true); + } +} + +VKAPI_ATTR void VKAPI_CALL freeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* allocator) { + std::shared_ptr state; + { + std::lock_guard lock(tracking->mutex); + const auto found = tracking->imports.find(memory); + if (found != tracking->imports.end()) state = found->second; + } + if (state != nullptr) { + if (!state->destroyed.load()) state->outOfOrder.store(true); + if (!RegisteredReadableCovers(state->address, state->bytes) || !AgcDriver::GuestMemory::Accessible(reinterpret_cast(state->address), state->bytes, false)) state->inaccessible.store(true); + std::unique_lock lock(state->mutex); + if (state->blockFree) { + state->freeing = true; + state->freeingThread = std::this_thread::get_id(); + state->wake.notify_all(); + if (!state->wake.wait_for(lock, std::chrono::seconds(2), [&] { return state->allowFree; })) state->outOfOrder.store(true); + } + } + const auto original = reinterpret_cast(tracking->resolve(device, "vkFreeMemory")); + original(device, memory, allocator); + if (state != nullptr) { + if (!RegisteredReadableCovers(state->address, state->bytes) || !AgcDriver::GuestMemory::Accessible(reinterpret_cast(state->address), state->bytes, false)) state->inaccessible.store(true); + state->frees.fetch_add(1); + state->freed.store(true); + } +} + +VKAPI_ATTR VkResult VKAPI_CALL allocateMemory(VkDevice device, const VkMemoryAllocateInfo* allocation, const VkAllocationCallbacks* allocator, VkDeviceMemory* memory) { + const auto original = reinterpret_cast(tracking->resolve(device, "vkAllocateMemory")); + const auto result = original(device, allocation, allocator, memory); + if (result != VK_SUCCESS) return result; + for (auto* next = static_cast(allocation->pNext); next != nullptr; next = next->pNext) { + if (next->sType != VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT) continue; + const auto* host = reinterpret_cast(next); + auto state = std::make_shared(); + state->buffer = VK_NULL_HANDLE; + state->memory = *memory; + state->address = reinterpret_cast(host->pHostPointer); + state->bytes = static_cast(allocation->allocationSize); + std::lock_guard lock(tracking->mutex); + tracking->imports[*memory] = state; + tracking->lastAllocated = state; + } + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL bindBufferMemory(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize offset) { + { + std::lock_guard lock(tracking->mutex); + if (const auto found = tracking->imports.find(memory); found != tracking->imports.end()) found->second->buffer = buffer; + } + const auto original = reinterpret_cast(tracking->resolve(device, "vkBindBufferMemory")); + return original(device, buffer, memory, offset); +} + +VKAPI_ATTR void VKAPI_CALL destroyFence(VkDevice device, VkFence fence, const VkAllocationCallbacks* allocator) { + const auto original = reinterpret_cast(tracking->resolve(device, "vkDestroyFence")); + original(device, fence, allocator); + std::lock_guard lock(tracking->mutex); + std::erase(tracking->rejectedFences, fence); +} + +VKAPI_ATTR VkResult VKAPI_CALL queueSubmit(VkQueue queue, std::uint32_t count, const VkSubmitInfo* submissions, VkFence fence) { + if (tracking->failQueueSubmit.exchange(false)) { + std::lock_guard lock(tracking->mutex); + tracking->rejectedFences.push_back(fence); + tracking->rejectedSubmissions.fetch_add(1); + return VK_ERROR_OUT_OF_HOST_MEMORY; + } + const auto original = reinterpret_cast(tracking->resolve(tracking->device, "vkQueueSubmit")); + const auto result = original(queue, count, submissions, fence); + if (result == VK_SUCCESS) { + std::lock_guard lock(tracking->mutex); + std::erase(tracking->rejectedFences, fence); + tracking->acceptedSubmissions.fetch_add(1); + } + return result; +} + +PFN_vkVoidFunction VKAPI_CALL deviceProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkDestroyFence") == 0) return reinterpret_cast(destroyFence); + if (std::strcmp(name, "vkQueueSubmit") == 0) return reinterpret_cast(queueSubmit); + if (std::strcmp(name, "vkDestroyBuffer") == 0) return reinterpret_cast(destroyBuffer); + if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast(freeMemory); + if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast(allocateMemory); + if (std::strcmp(name, "vkBindBufferMemory") == 0) return reinterpret_cast(bindBufferMemory); + if (std::strcmp(name, "vkGetBufferDeviceAddressKHR") == 0 && tracking->failAddressLookup.exchange(false)) return nullptr; + return tracking->resolve(device, name); +} + +void requireFreed(const std::shared_ptr& state) { + Require(state->freed.load(), "guest mutation reached host apply before vkFreeMemory completed"); + Require(state->destroyed.load() && !state->outOfOrder.load() && state->frees.load() == 1, "host import Vulkan objects were destroyed out of order or more than once"); + Require(!state->inaccessible.load(), "guest pages became inaccessible before vkFreeMemory returned"); +} + +class Fixture { +public: + explicit Fixture(const Context& original) : context(original), calls{original.deviceProc, original.device} { + Require(tracking == nullptr, "host import tracking is already active"); + tracking = &calls; + context.deviceProc = deviceProc; + context.functions = nullptr; + std::lock_guard gpu(GpuMutex()); + recorder = std::make_unique(context); + recorder->Activate(); + } + + ~Fixture() { + std::lock_guard gpu(GpuMutex()); + recorder.reset(); + calls.CleanupRejectedFences(); + ClearHostImports(context.device); + context.bufferPool.reset(); + tracking = nullptr; + } + + std::shared_ptr Import(std::uint64_t address, std::size_t bytes) { + std::lock_guard gpu(GpuMutex()); + const auto* imported = HostImportFor(context, address, bytes); + Require(imported != nullptr, "host pointer import was refused for the lifetime test"); + return calls.Add(*imported); + } + + void Cleanup() { + std::lock_guard gpu(GpuMutex()); + recorder->Sync(); + ClearHostImports(context.device); + } + + Context context; + Tracking calls; + std::unique_ptr recorder; +}; + +class Allocation { +public: + Allocation(Fixture& fixture, std::size_t units = 1) : fixture(fixture) { + alignment = static_cast(std::max(65536, fixture.context.hostImportAlignment)); + bytes = alignment * units; + data = ::operator new(bytes, std::align_val_t{alignment}); + std::memset(data, 0x37, bytes); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + + ~Allocation() { + try { + if (registered) { + fixture.Cleanup(); + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } + Release(); + } catch (const std::exception& error) { + std::fprintf(stderr, "host import test cleanup failed: %s\n", error.what()); + std::terminate(); + } + } + + std::uint64_t Address() const { return reinterpret_cast(data); } + + void Release() { + if (released) return; + ::operator delete(data, std::align_val_t{alignment}); + released = true; + } + + Fixture& fixture; + void* data = nullptr; + std::size_t alignment = 0; + std::size_t bytes = 0; + bool registered = true; + bool released = false; +}; + +void descriptorFirstImport(Fixture& fixture) { + Require(fixture.calls.lastAllocated == nullptr, "descriptor-only lifetime test did not run before the first host import"); + Allocation allocation(fixture); + std::shared_ptr imported; + { + std::lock_guard gpu(GpuMutex()); + GuestBufferMemory memory(fixture.context); + memory.AddReadable(allocation.Address(), allocation.bytes); + memory.Upload(false); + imported = fixture.calls.lastAllocated; + Require(imported != nullptr && imported->address == allocation.Address(), "descriptor-only upload did not import its allocation"); + std::uint32_t adjustment = 0; + const auto descriptor = memory.Descriptor(allocation.Address(), allocation.bytes, adjustment); + Require(descriptor.buffer == imported->buffer && descriptor.offset == 0 && adjustment == 0, "descriptor-only upload did not bind the tracked import"); + } + Require(HostImportCovers(fixture.context, allocation.Address(), allocation.bytes), "descriptor-only import did not persist after its build ended"); + { + GuestAllocations::Mutation mutation; + mutation.Unmap(allocation.data, allocation.bytes, [&](const void*, std::size_t, const void*, bool) { + requireFreed(imported); + allocation.Release(); + }); + allocation.registered = false; + } + Require(!HostImportCovers(fixture.context, allocation.Address(), allocation.bytes), "descriptor-only import survived allocation removal"); +} + +class PendingRetirement { +public: + explicit PendingRetirement(Fixture& fixture) : fixture(fixture), first(fixture) { + std::lock_guard gpu(GpuMutex()); + build = std::make_unique(fixture.context); + build->AcquireRegistered(); + build->UploadPrepare(true); + retry = std::make_unique(fixture.context); + retry->AcquireRegistered(); + retry->UploadPrepare(true); + allocation = std::make_unique(fixture); + imported = fixture.Import(allocation->Address(), allocation->bytes); + result = std::make_unique(fixture.context, allocation->bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const auto commands = fixture.recorder->Commands(); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(fixture.context, commands, imported->buffer, 0, result->Handle(), 0, allocation->bytes); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + fixture.recorder->NotePendingRead(allocation->Address(), allocation->bytes, Recorder::ReadKind::AddressBased); + fixture.recorder->Submit(); + Check(fixture.context.Function("vkQueueWaitIdle")(fixture.context.queue), "vkQueueWaitIdle retirement fixture"); + generation = std::make_unique(fixture); + Require(!fixture.recorder->Idle(), "retirement fixture has no submitted batch"); + } + + ~PendingRetirement() { + if (imported != nullptr) { + { + std::lock_guard lock(imported->mutex); + imported->allowFree = true; + } + imported->wake.notify_all(); + } + build.reset(); + retry.reset(); + fixture.Cleanup(); + } + + void VerifyCopy() { + result->Invalidate(); + const auto bytes = result->Bytes(); + Require(std::all_of(bytes.begin(), bytes.end(), [](std::byte value) { return value == std::byte{0x37}; }), "retirement lost the submitted import read"); + } + + Fixture& fixture; + Allocation first; + std::unique_ptr result; + std::unique_ptr allocation; + std::unique_ptr generation; + std::unique_ptr build; + std::unique_ptr retry; + std::shared_ptr imported; +}; + +void deferredRetirement(Fixture& fixture) { + PendingRetirement pending(fixture); + pending.imported->blockFree = true; + { + std::lock_guard gpu(GpuMutex()); + pending.build->UploadFinish(true); + Require(!HostImportCovers(fixture.context, pending.allocation->Address(), pending.allocation->bytes), "deferred retirement left its import cached"); + Require(!pending.imported->freed.load(), "retirement did not keep its import through the pending batch"); + fixture.recorder->Sync(); + fixture.recorder->FlushDeferredReleases(); + } + bool entered = false; + bool onReleaseThread = false; + { + std::unique_lock lock(pending.imported->mutex); + entered = pending.imported->wake.wait_for(lock, std::chrono::seconds(2), [&] { return pending.imported->freeing; }); + onReleaseThread = pending.imported->freeingThread != std::this_thread::get_id(); + } + std::atomic applied{false}; + const auto waitsBefore = LeaseCounters().contentionWaits; + std::promise starting; + auto started = starting.get_future(); + auto mutation = std::async(std::launch::async, [&] { + starting.set_value(); + GuestAllocations::Mutation mutation; + mutation.Unmap(pending.allocation->data, pending.allocation->bytes, [&](const void*, std::size_t, const void*, bool) { + requireFreed(pending.imported); + applied.store(true); + pending.allocation->Release(); + }); + pending.allocation->registered = false; + }); + started.wait(); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (LeaseCounters().contentionWaits == waitsBefore && mutation.wait_for(std::chrono::milliseconds(0)) == std::future_status::timeout && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + const bool reachedWaiter = LeaseCounters().contentionWaits > waitsBefore; + const bool blocked = mutation.wait_for(std::chrono::milliseconds(0)) == std::future_status::timeout; + const bool premature = applied.load(); + const bool registered = RegisteredReadableCovers(pending.allocation->Address(), pending.allocation->bytes); + { + std::lock_guard lock(pending.imported->mutex); + pending.imported->allowFree = true; + } + pending.imported->wake.notify_all(); + mutation.get(); + Require(entered && onReleaseThread, "retired import destruction did not run on the recorder release thread"); + Require(reachedWaiter && blocked && !premature && registered, "guest mutation passed a retired import whose Vulkan free was still running"); + Require(applied.load(), "guest mutation did not resume after deferred Vulkan free"); + requireFreed(pending.imported); + pending.VerifyCopy(); +} + +void idleUnmap(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + { + GuestAllocations::Mutation mutation; + mutation.Unmap(allocation.data, allocation.bytes, [&](const void*, std::size_t, const void*, bool) { + requireFreed(imported); + allocation.Release(); + }); + allocation.registered = false; + } + Require(!HostImportCovers(fixture.context, allocation.Address(), allocation.bytes), "an unmapped range still has a live host import"); +} + +void pendingRead(Fixture& fixture, bool submitted, bool locked) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + Buffer result(fixture.context, allocation.bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + std::atomic completed{false}; + struct Finish { + Fixture& fixture; + ~Finish() { fixture.Cleanup(); } + } finish{fixture}; + std::unique_lock gpu(GpuMutex()); + const auto commands = fixture.recorder->Commands(); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(fixture.context, commands, imported->buffer, 0, result.Handle(), 0, allocation.bytes); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + fixture.recorder->NotePendingRead(allocation.Address(), allocation.bytes, Recorder::ReadKind::AddressBased); + fixture.recorder->Keep(std::make_shared(GuestAllocations::GuestAllocationsAcquire_nid_postfix())); + fixture.recorder->OnComplete([&] { completed.store(true); }); + if (submitted) fixture.recorder->Submit(); + if (!locked) gpu.unlock(); + { + GuestAllocations::Mutation mutation; + mutation.Unmap(allocation.data, allocation.bytes, [&](const void*, std::size_t, const void*, bool) { + requireFreed(imported); + Require(completed.load(), "guest unmap passed a pending GPU read"); + result.Invalidate(); + const auto copied = result.Bytes(); + Require(std::all_of(copied.begin(), copied.end(), [](std::byte value) { return value == std::byte{0x37}; }), "GPU read lost the source contents before guest unmap"); + allocation.Release(); + }); + allocation.registered = false; + } +} + +void productionWaiterFailure(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + Buffer result(fixture.context, allocation.bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + bool completed = false; + struct Finish { + Fixture& fixture; + ~Finish() { fixture.Cleanup(); } + } finish{fixture}; + const auto recordCopy = [&] { + const auto commands = fixture.recorder->Commands(); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(fixture.context, commands, imported->buffer, 0, result.Handle(), 0, allocation.bytes); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + fixture.recorder->NotePendingRead(allocation.Address(), allocation.bytes, Recorder::ReadKind::AddressBased); + fixture.recorder->Keep(std::make_shared(GuestAllocations::GuestAllocationsAcquire_nid_postfix())); + }; + { + std::lock_guard gpu(GpuMutex()); + recordCopy(); + } + const auto rejectedBefore = fixture.calls.rejectedSubmissions.load(); + const auto acceptedBefore = fixture.calls.acceptedSubmissions.load(); + fixture.calls.failQueueSubmit.store(true); + bool applied = false; + std::string failure; + { + GuestAllocations::Mutation mutation; + try { + mutation.Protect(allocation.data, allocation.bytes, true, true, true, [&] { applied = true; }); + } catch (const std::runtime_error& error) { + failure = error.what(); + } + const auto range = mutation.Find(allocation.data); + Require(range.readable && range.writable && range.gpu, "failed production waiter changed the guest registration"); + } + Require(!fixture.calls.failQueueSubmit.load() && fixture.calls.rejectedSubmissions.load() == rejectedBefore + 1, "production pin waiter did not reach the injected queue submission failure"); + Require(fixture.calls.acceptedSubmissions.load() == acceptedBefore, "failed production waiter accepted GPU work before reporting failure"); + Require(failure == "AGC graphics: vkQueueSubmit recorder: Vulkan result -1" && !applied, "production pin waiter did not propagate the original Vulkan error before host apply"); + Require(!imported->destroyed.load() && !imported->freed.load(), "failed production waiter retired its host import"); + Require(HostImportCovers(fixture.context, allocation.Address(), allocation.bytes), "failed production waiter removed the live import"); + { + std::lock_guard gpu(GpuMutex()); + recordCopy(); + fixture.recorder->OnComplete([&] { completed = true; }); + } + { + GuestAllocations::Mutation mutation; + mutation.Unmap(allocation.data, allocation.bytes, [&](const void*, std::size_t, const void*, bool) { + requireFreed(imported); + Require(completed, "production waiter retry passed a pending GPU read"); + result.Invalidate(); + const auto copied = result.Bytes(); + Require(std::all_of(copied.begin(), copied.end(), [](std::byte value) { return value == std::byte{0x37}; }), "production waiter retry lost its real GPU copy"); + allocation.Release(); + }); + allocation.registered = false; + } + Require(fixture.calls.acceptedSubmissions.load() == acceptedBefore + 1, "production waiter retry did not submit exactly its new copy"); + requireFreed(imported); +} + +void partialUnmap(Fixture& fixture) { + Allocation allocation(fixture, 3); + Allocation unrelated(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + const auto other = fixture.Import(unrelated.Address(), unrelated.bytes); + auto* middle = static_cast(allocation.data) + allocation.alignment; + { + GuestAllocations::Mutation mutation; + mutation.Unmap(middle, allocation.alignment, [&](const void* piece, std::size_t bytes, const void* original, bool last) { + requireFreed(imported); + Require(!other->freed.load(), "partial unmap retired a disjoint host import"); + Require(piece == middle && bytes == allocation.alignment && original == allocation.data && !last, "partial unmap changed the wrong guest extent"); + }); + } + Require(!RegisteredReadableCovers(reinterpret_cast(middle), allocation.alignment), "the middle of a partial unmap remains registered"); + const auto low = fixture.Import(allocation.Address(), allocation.alignment); + const auto high = fixture.Import(allocation.Address() + 2 * allocation.alignment, allocation.alignment); + Require(!low->freed.load() && !high->freed.load() && !other->freed.load(), "remaining partial mappings could not be imported"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(allocation.data); + allocation.registered = false; + } + requireFreed(low); + requireFreed(high); + Require(!other->freed.load(), "removing a split allocation retired a disjoint import"); +} + +void protectRollback(Fixture& fixture) { + Allocation allocation(fixture); + auto imported = fixture.Import(allocation.Address(), allocation.bytes); + bool called = false; + try { + GuestAllocations::Mutation mutation; + mutation.Protect(allocation.data, allocation.bytes, true, false, true, [&] { + requireFreed(imported); + called = true; + throw std::runtime_error("expected host protection failure"); + }); + throw std::runtime_error("host protection failure was ignored"); + } catch (const std::runtime_error& error) { + Require(called && std::string(error.what()) == "expected host protection failure", "unexpected protection failure"); + } + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(allocation.data); + Require(range.readable && range.writable && range.gpu, "failed host protection changed the guest registration"); + } + imported = fixture.Import(allocation.Address(), allocation.bytes); + { + GuestAllocations::Mutation mutation; + mutation.Protect(allocation.data, allocation.bytes, true, false, true, [&] { requireFreed(imported); }); + } + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(allocation.data); + Require(range.readable && !range.writable && range.gpu, "successful protection did not update the guest registration"); + mutation.Protect(allocation.data, allocation.bytes, true, true, true, [] {}); + } + imported = fixture.Import(allocation.Address(), allocation.bytes); + { + GuestAllocations::Mutation mutation; + mutation.Remove(allocation.data); + allocation.registered = false; + } + requireFreed(imported); +} + +void blockedFree(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + imported->blockFree = true; + std::atomic applied{false}; + auto mutation = std::async(std::launch::async, [&] { + GuestAllocations::Mutation mutation; + mutation.Unmap(allocation.data, allocation.bytes, [&](const void*, std::size_t, const void*, bool) { + requireFreed(imported); + applied.store(true); + allocation.Release(); + }); + allocation.registered = false; + }); + bool entered = false; + { + std::unique_lock lock(imported->mutex); + entered = imported->wake.wait_for(lock, std::chrono::seconds(2), [&] { return imported->freeing; }); + } + const bool premature = applied.load(); + const bool stillRegistered = RegisteredReadableCovers(allocation.Address(), allocation.bytes); + { + std::lock_guard lock(imported->mutex); + imported->allowFree = true; + } + imported->wake.notify_all(); + mutation.get(); + Require(entered, "guest unmap never reached Vulkan import destruction"); + Require(!premature && stillRegistered, "guest unmap released its registration while vkFreeMemory was blocked"); + Require(applied.load(), "guest unmap did not resume after vkFreeMemory returned"); +} + +void failedConstruction(Fixture& fixture) { + Allocation allocation(fixture); + fixture.calls.failAddressLookup.store(true); + bool failed = false; + try { + fixture.Import(allocation.Address(), allocation.bytes); + } catch (const std::runtime_error& error) { + failed = std::string(error.what()).find("vkGetBufferDeviceAddressKHR") != std::string::npos; + } + Require(failed && !fixture.calls.failAddressLookup.load(), "import construction did not reach the injected Vulkan resolver failure"); + const auto allocated = fixture.calls.lastAllocated; + Require(allocated != nullptr && allocated->address == allocation.Address(), "resolver failure happened before host memory was imported"); + if (!allocated->freed.load()) { + destroyBuffer(fixture.context.device, allocated->buffer, nullptr); + freeMemory(fixture.context.device, allocated->memory, nullptr); + throw std::runtime_error("failed host import construction leaked its Vulkan memory"); + } + requireFreed(allocated); + Require(!HostImportCovers(fixture.context, allocation.Address(), allocation.bytes), "failed host import construction remained cached"); + const auto retried = fixture.Import(allocation.Address(), allocation.bytes); + { + GuestAllocations::Mutation mutation; + mutation.Remove(allocation.data); + allocation.registered = false; + } + requireFreed(retried); + Require(allocated->frees.load() == 1, "retry destroyed the failed import a second time"); +} + +void mutationDuringCompletion(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + Buffer result(fixture.context, allocation.bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + bool rejected = false; + bool applied = false; + bool laterCompletion = false; + bool copied = false; + std::lock_guard gpu(GpuMutex()); + const auto commands = fixture.recorder->Commands(); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(fixture.context, commands, imported->buffer, 0, result.Handle(), 0, allocation.bytes); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + fixture.recorder->OnComplete([&] { + try { + GuestAllocations::Mutation mutation; + mutation.Protect(allocation.data, allocation.bytes, false, false, false, [&] { applied = true; }); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).find("active GPU completion") != std::string::npos; + } + }); + fixture.recorder->OnComplete([&] { + laterCompletion = true; + result.Invalidate(); + const auto bytes = result.Bytes(); + copied = std::all_of(bytes.begin(), bytes.end(), [](std::byte value) { return value == std::byte{0x37}; }); + }); + fixture.recorder->Sync(); + Require(rejected && !applied, "an active completion allowed guest memory mutation before later completions"); + Require(laterCompletion && copied, "rejecting a completion mutation lost later GPU results"); + Require(!imported->freed.load(), "rejecting a completion mutation retired its host import"); + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(allocation.data); + Require(range.readable && range.writable && range.gpu, "rejecting a completion mutation changed its guest range"); + mutation.Remove(allocation.data); + allocation.registered = false; + } + requireFreed(imported); +} + +void completedLeaseUnderLock(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + std::lock_guard gpu(GpuMutex()); + fixture.recorder->Keep(std::make_shared(GuestAllocations::GuestAllocationsAcquire_nid_postfix())); + fixture.recorder->Sync(); + Require(fixture.recorder->Idle(), "completed lease test recorder is not idle"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(allocation.data); + allocation.registered = false; + } + requireFreed(imported); +} + + +struct MutationRace { + Fixture* fixture; + Allocation* allocation; + std::shared_ptr replacement; + unsigned calls = 0; +}; + +MutationRace* mutationRace = nullptr; + +bool raceWaiter(std::uintptr_t address, std::size_t bytes) { + auto& race = *mutationRace; + Require(address == race.allocation->Address() && bytes == race.allocation->bytes, "pin waiter received the wrong mutation extent"); + std::lock_guard gpu(GpuMutex()); + race.fixture->Cleanup(); + if (++race.calls == 1) race.replacement = race.fixture->Import(address, bytes); + return true; +} + +bool failingWaiter(std::uintptr_t, std::size_t) { + throw std::runtime_error("expected pin waiter failure"); +} + +void waiterFailure(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + GuestAllocations::GuestAllocationsSetPinWaiter_nid_postfix(&failingWaiter); + bool failed = false; + bool excluded = false; + std::future reader; + { + GuestAllocations::Mutation mutation; + try { + mutation.Protect(allocation.data, allocation.bytes, false, false, false, [] { throw std::runtime_error("host apply ran after a pin waiter failure"); }); + } catch (const std::runtime_error& error) { + failed = std::string(error.what()) == "expected pin waiter failure"; + } + std::promise starting; + auto ready = starting.get_future(); + reader = std::async(std::launch::async, [&] { + starting.set_value(); + static_cast(GuestAllocations::GuestAllocationsAcquire_nid_postfix()); + }); + ready.wait(); + excluded = reader.wait_for(std::chrono::milliseconds(20)) == std::future_status::timeout; + const auto range = mutation.Find(allocation.data); + Require(range.readable && range.writable && range.gpu, "a pin waiter failure changed the registered permissions"); + } + reader.get(); + Require(failed, "a pin waiter error did not propagate"); + Require(excluded, "a failed pin waiter did not restore the registry lock"); + Require(!imported->freed.load(), "a failed pin waiter retired an import"); +} + +void reimportBeforeRelock(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + MutationRace race{&fixture, &allocation}; + mutationRace = ∽̱ + GuestAllocations::GuestAllocationsSetPinWaiter_nid_postfix(&raceWaiter); + { + GuestAllocations::Mutation mutation; + mutation.Unmap(allocation.data, allocation.bytes, [&](const void*, std::size_t, const void*, bool) { + Require(race.calls == 2 && race.replacement != nullptr, "guest mutation did not recheck an import acquired before registry relock"); + requireFreed(imported); + requireFreed(race.replacement); + allocation.Release(); + }); + allocation.registered = false; + } + mutationRace = nullptr; +} + +} + +int RunHostImportLifetimeTests(const Context& context) { + if (context.hostImportAlignment == 0) { + std::puts("skipped, the device has no VK_EXT_external_memory_host"); + return 77; + } + Fixture fixture(context); + descriptorFirstImport(fixture); + idleUnmap(fixture); + pendingRead(fixture, false, false); + pendingRead(fixture, true, false); + pendingRead(fixture, false, true); + pendingRead(fixture, true, true); + partialUnmap(fixture); + protectRollback(fixture); + completedLeaseUnderLock(fixture); + mutationDuringCompletion(fixture); + blockedFree(fixture); + failedConstruction(fixture); + productionWaiterFailure(fixture); + deferredRetirement(fixture); + std::puts("host import lifetime tests passed"); + return 0; +} + +int RunHostImportMutationRaceTests(const Context& context) { + if (context.hostImportAlignment == 0) { + std::puts("skipped, the device has no VK_EXT_external_memory_host"); + return 77; + } + Fixture fixture(context); + waiterFailure(fixture); + reimportBeforeRelock(fixture); + std::puts("host import mutation race tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.hpp b/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.hpp new file mode 100644 index 000000000000..9e28ea9da5e1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.hpp @@ -0,0 +1,10 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_HOSTIMPORTLIFETIME_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_HOSTIMPORTLIFETIME_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" + +int RunHostImportLifetimeTests(const AgcDriver::Graphics::Context& context); + +int RunHostImportMutationRaceTests(const AgcDriver::Graphics::Context& context); + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 64c2075bf578..b7adb1d449a7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -29,6 +29,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "ResidentPresent.hpp" #include "ImageTableAbi.hpp" +#include "HostImportLifetime.hpp" #include "SampleLod_spv.h" #include "BufferUpdate_spv.h" #include "SampleArray_spv.h" @@ -8300,6 +8301,8 @@ int main(int argc, char** argv) { return 77; } Device& device = *created; + if (argc == 2 && std::string_view(argv[1]) == "--host-import-lifetime-only") return RunHostImportLifetimeTests(device.GetContext()); + if (argc == 2 && std::string_view(argv[1]) == "--host-import-mutation-race-only") return RunHostImportMutationRaceTests(device.GetContext()); if (argc == 2 && std::string_view(argv[1]) == "--import-watch-only") { importWatchTests(device); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp new file mode 100644 index 000000000000..9e9e22d81439 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp @@ -0,0 +1,259 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceAgcDriver/tests/execution/VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; +int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept; +int APS5_VABI mprotect_nid_postfix(void*, std::size_t, int) noexcept; +void* APS5_VABI memalign_nid_postfix(std::size_t, std::size_t); +void* APS5_VABI realloc_nid_postfix(void*, std::size_t); +void APS5_VABI free_nid_postfix(void*); +int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); +int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); +int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); +int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); +} + +namespace { + +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t FillBytes = 4096; +constexpr std::array Pattern{0x12345678u, 0x89abcdefu, 0x24681357u, 0xfedcba98u}; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +std::uint64_t Address(const void* pointer) { + return reinterpret_cast(pointer); +} + +bool Imported(const AgcDriver::VulkanDevice& device, const void* pointer, std::size_t bytes = BlockBytes) { + AgcDriver::Graphics::Context probe{}; + probe.device = device.Device(); + probe.hostImportAlignment = 1; + return AgcDriver::Graphics::HostImportCovers(probe, Address(pointer), bytes); +} + +void* Map(std::size_t bytes = BlockBytes) { + auto* result = mmap_nid_postfix(nullptr, bytes, 3, 0x1002, -1, 0); + Require(result != reinterpret_cast(~std::uintptr_t{0}), "guest mmap failed"); + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(result); + Require(range.readable && range.writable && !range.gpu, "guest mmap did not register CPU-only read/write access"); + } + std::memset(result, 0, bytes); + return result; +} + +void CheckPattern(const void* pointer) { + const auto* data = static_cast(pointer); + for (std::size_t offset = 0; offset < FillBytes; offset += sizeof(Pattern)) + Require(std::memcmp(data + offset, Pattern.data(), sizeof(Pattern)) == 0, "guest mutation lost the pending GPU fill"); +} + +void Fill(AgcDriver::VulkanDevice& device, void* pointer) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Require(device.FillBuffer(Address(pointer), FillBytes, Pattern), "Vulkan device did not import the guest allocation"); + Require(Imported(device, pointer), "GPU fill did not create a host import"); +} + +void CheckMappings(AgcDriver::VulkanDevice& device) { + auto* mapping = Map(); + Fill(device, mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 3) == 0, "guest writable protection refresh failed"); + Require(!Imported(device, mapping), "host import survived guest writable protection refresh"); + CheckPattern(mapping); + Fill(device, mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 1) == 0, "guest read-only protection failed"); + Require(!Imported(device, mapping), "host import survived guest protection change"); + CheckPattern(mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 0) == 0, "guest inaccessible protection failed"); + Require(mprotect_nid_postfix(mapping, BlockBytes, 3) == 0, "guest writable protection restore failed"); + CheckPattern(mapping); + Fill(device, mapping); + void* replaced = mapping; + Require(sceKernelMapFlexibleMemory(&replaced, BlockBytes, 0x33, 0x10) == 0 && replaced == mapping, "guest fixed mapping replacement failed"); + Require(!Imported(device, mapping), "host import survived guest fixed mapping replacement"); + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(mapping); + Require(range.readable && range.writable && range.gpu, "guest fixed mapping did not register its requested CPU and GPU access"); + } + const auto* data = static_cast(mapping); + for (std::size_t offset = 0; offset < BlockBytes; ++offset) + Require(data[offset] == 0, "GPU work reached replacement guest pages"); + Fill(device, mapping); + Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "guest munmap failed"); + Require(!Imported(device, mapping), "host import survived guest munmap"); + + mapping = Map(3 * BlockBytes); + Fill(device, mapping); + auto* middle = static_cast(mapping) + BlockBytes; + Require(mprotect_nid_postfix(middle, BlockBytes, 1) == 0, "guest partial protection failed"); + Require(!Imported(device, mapping), "whole host import survived partial protection change"); + CheckPattern(mapping); + Fill(device, mapping); + Require(munmap_nid_postfix(middle, BlockBytes) == 0, "guest middle unmap failed"); + Require(Imported(device, mapping), "unrelated prefix import was retired by middle unmap"); + Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "guest prefix unmap failed"); + Require(!Imported(device, mapping), "prefix host import survived guest munmap"); + Require(munmap_nid_postfix(middle + BlockBytes, BlockBytes) == 0, "guest suffix unmap failed"); +} + +void CheckHeap(AgcDriver::VulkanDevice& device) { + const std::array api{}; + ApplicationHeapRegister_nid_no_patch(api.data()); + auto* allocation = memalign_nid_postfix(BlockBytes, BlockBytes); + Require(allocation != nullptr, "guest aligned allocation failed"); + std::memset(allocation, 0, BlockBytes); + Fill(device, allocation); + auto* replacement = realloc_nid_postfix(allocation, 2 * BlockBytes); + Require(replacement != nullptr, "guest realloc failed"); + Require(!Imported(device, allocation), "host import survived guest realloc"); + CheckPattern(replacement); + free_nid_postfix(replacement); + for (int iteration = 0; iteration < 3; ++iteration) { + allocation = memalign_nid_postfix(BlockBytes, BlockBytes); + Require(allocation != nullptr, "guest heap reuse allocation failed"); + std::memset(allocation, 0, BlockBytes); + Fill(device, allocation); + free_nid_postfix(allocation); + Require(!Imported(device, allocation), "host import survived guest free or heap cache eviction"); + } +} + +void CheckLockedMutation(AgcDriver::VulkanDevice& device) { + auto* mapping = Map(); + { + std::lock_guard outer(AgcDriver::GuestMemory::GpuMutex()); + Fill(device, mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 3) == 0, "writable protection refresh under the GPU lock did not finish"); + Require(!Imported(device, mapping), "locked protection refresh retained the import"); + CheckPattern(mapping); + Fill(device, mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 1) == 0, "protection change under the GPU lock did not finish"); + Require(!Imported(device, mapping), "locked protection change retained the import"); + CheckPattern(mapping); + Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "unmap under the GPU lock did not finish"); + } + + mapping = Map(); + { + std::lock_guard outer(AgcDriver::GuestMemory::GpuMutex()); + auto lease = std::make_shared(GuestAllocations::GuestAllocationsAcquire_nid_postfix()); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "Vulkan device has no active recorder"); + recorder->Keep(std::move(lease)); + Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "completed batch kept its lease until the outer GPU lock was released"); + } +} + +void CheckCopy(AgcDriver::VulkanDevice& device) { + auto* source = Map(); + auto* destination = Map(); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Fill(device, source); + const auto copied = device.CopyBuffer(Address(destination), Address(source), FillBytes, 0, BlockBytes, 0, 0, 0, [](std::span, std::uint64_t) {}); + Require(copied.path == 1, "guest source was not read by a Vulkan host-import transfer"); + Require(Imported(device, source) && Imported(device, destination), "GPU copy did not import both guest ranges"); + } + Require(mprotect_nid_postfix(source, BlockBytes, 3) == 0, "copied source protection refresh failed"); + Require(!Imported(device, source), "GPU copy source remained imported after guest protection refresh"); + CheckPattern(destination); + Require(Imported(device, destination), "source retirement unnecessarily retired the copy destination"); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Fill(device, source); + const auto copied = device.CopyBuffer(Address(destination), Address(source), FillBytes, 0, BlockBytes, 0, 0, 0, [](std::span, std::uint64_t) {}); + Require(copied.path == 1, "second source transfer did not use host imports"); + } + Require(munmap_nid_postfix(source, BlockBytes) == 0, "copied source unmap failed"); + Require(!Imported(device, source), "GPU copy source remained imported after guest unmap"); + CheckPattern(destination); + Require(Imported(device, destination), "source unmap retired the unrelated copy destination"); + Require(munmap_nid_postfix(destination, BlockBytes) == 0, "copy destination unmap failed"); + Require(!Imported(device, destination), "GPU copy destination remained imported after guest unmap"); +} + +void CheckDirectRelease(AgcDriver::VulkanDevice& device) { + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, BlockBytes, BlockBytes, 0, &physical) == 0, "guest direct allocation failed"); + void* first = nullptr; + void* second = nullptr; + Require(sceKernelMapDirectMemory(&first, BlockBytes, 0x33, 0, physical, BlockBytes) == 0, "first direct view mapping failed"); + Require(sceKernelMapDirectMemory(&second, BlockBytes, 0x33, 0, physical, BlockBytes) == 0 && second != first, "second direct view mapping failed"); + { + GuestAllocations::Mutation mutation; + const auto firstRange = mutation.Find(first); + const auto secondRange = mutation.Find(second); + Require(firstRange.readable && firstRange.writable && firstRange.gpu && secondRange.readable && secondRange.writable && secondRange.gpu, "direct views did not register their requested CPU and GPU access"); + } + std::memset(first, 0, BlockBytes); + static_cast(first)[BlockBytes - 1] = 0x5a; + Require(static_cast(second)[BlockBytes - 1] == 0x5a, "direct views do not share their physical backing"); + auto* destination = Map(); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Fill(device, first); + Require(device.FillBuffer(Address(second) + FillBytes, FillBytes, Pattern), "second direct view was not imported for its GPU fill"); + const auto firstCopy = device.CopyBuffer(Address(destination), Address(first), FillBytes, 0, BlockBytes, 0, 0, 0, [](std::span, std::uint64_t) {}); + const auto secondCopy = device.CopyBuffer(Address(destination) + FillBytes, Address(second) + FillBytes, FillBytes, 0, BlockBytes, 0, 0, 0, [](std::span, std::uint64_t) {}); + Require(firstCopy.path == 1 && secondCopy.path == 1, "direct views were not read through GPU import transfers"); + Require(Imported(device, first) && Imported(device, second), "both direct view imports were not retained before release"); + } + Require(sceKernelReleaseDirectMemory(physical, BlockBytes) == 0, "guest direct release failed"); + Require(!Imported(device, first) && !Imported(device, second), "direct release retained an imported virtual view"); + Require(!AgcDriver::Graphics::RegisteredReadableCovers(Address(first), BlockBytes) && !AgcDriver::Graphics::RegisteredReadableCovers(Address(second), BlockBytes), "direct release left a virtual view registered"); + CheckPattern(destination); + CheckPattern(static_cast(destination) + FillBytes); + Require(Imported(device, destination), "direct release retired the unrelated copy destination"); + Require(munmap_nid_postfix(destination, BlockBytes) == 0, "direct release copy destination unmap failed"); + Require(!Imported(device, destination), "direct release copy destination remained imported after unmap"); +} + +} + +int main(int argc, char** argv) { + try { + Require(argc <= 2, "expected mappings, heap, locked, copy or direct test mode"); + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!GuestMemoryLifetimeHasHostImports(device->Device())) return VulkanTestSkipped; + const std::string_view mode = argc == 2 ? argv[1] : "mappings"; + if (mode == "mappings") CheckMappings(*device); + else if (mode == "heap") CheckHeap(*device); + else if (mode == "locked") CheckLockedMutation(*device); + else if (mode == "copy") CheckCopy(*device); + else if (mode == "direct") CheckDirectRelease(*device); + else throw std::runtime_error("unknown guest memory lifetime test mode"); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device->WaitIdle(); + } + device.reset(); + std::puts("guest memory host import lifetime passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "guest memory host import lifetime: %s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/test_guest_memory_lifetime.py b/core/libs/prx/libSceAgcDriver/tests/test_guest_memory_lifetime.py new file mode 100644 index 000000000000..e5cc5d7467bf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/test_guest_memory_lifetime.py @@ -0,0 +1,143 @@ +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +sys.path.insert(0, sys.argv[1]) +from test_guest_symbol_names import importing_executable, string_table, DYNAMIC, STRINGS, SYMBOLS, RELOCATIONS, GOT + +NAMES = ['Begin_nid_no_patch', 'mmap_nid_postfix', 'Fill_nid_no_patch', 'mprotect_nid_postfix', + 'Retired_nid_no_patch', 'Verify_nid_no_patch', 'sceKernelMapFlexibleMemory', + 'munmap_nid_postfix', 'Finish_nid_no_patch'] + + +def fixture(): + image = importing_executable(NAMES) + struct.pack_into('lock.owns_lock()) { state->lock.unlock(); - if (waiter != nullptr) progressed = waiter(address, bytes); - else std::this_thread::yield(); + try { + if (waiter != nullptr) progressed = waiter(address, bytes); + else std::this_thread::yield(); + } catch (...) { + state->lock.lock(); + throw; + } state->lock.lock(); } else { std::this_thread::yield();