From ef022d53b8f04bb7b2a675efffa9c433b2495f98 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:48:29 -0400 Subject: [PATCH] fix(agc): import GPU mappings on a host thread when the stack is short The GPU-map observer imported a new mapping into Vulkan on the thread that made the mapping, on whatever stack it ran. Guest fibers run on contexts the title allocates in guest memory, with no guard page; Demon's Souls packs its 16 KiB job fiber contexts back to back. A GPU mapping made on a fiber used 10,616 bytes of the fiber's stack on NVIDIA (Add, NoteGpuMapping, the registry walk in GuestAllocationsEnd, the import, the fiber's entry and return), about 7.5 KiB of it for the import, so a fiber that mapped GPU memory deep in a job could overrun its context into the next one. The observer now imports inline when the stack pointer lies inside the thread's stack bounds with at least 256 KiB below it, and otherwise starts a host thread for the import and joins it. On Windows the bounds come from GetCurrentThreadStackLimits, which reads the TIB bounds that libSceFiber sets to the fiber context while a fiber runs; on Linux from pthread_getattr_np, whose bounds a fiber's stack lies outside of. A mapping made while the thread holds the GPU mutex still imports inline: a host thread could wait for that mutex forever. A host thread that cannot be started is reported like any failed import, which is still printed to stderr while the mapping carries on. agc_driver_gpu_map_on_fiber maps GPU memory from a fiber with a 64 KiB context and requires that the mapping use at most 8 KiB of it (10,616 bytes inline, 2,960 on a host thread) and start one host thread, then maps the same memory from the main thread and requires that it start none. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../Execution/include/Driver/Driver.hpp | 1 + .../Execution/src/Driver/Lifecycle.cpp | 83 ++++++++++- .../tests/execution/GpuMapOnFiber.cpp | 132 ++++++++++++++++++ 4 files changed, 212 insertions(+), 8 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e7428c6d5..191b025a1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -315,7 +315,7 @@ if(BUILD_TESTING) endif() if(WIN32) - set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/GuestMemory.cpp PROPERTIES COMPILE_DEFINITIONS _WIN32_WINNT=0x0602) + set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/GuestMemory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Lifecycle.cpp PROPERTIES COMPILE_DEFINITIONS _WIN32_WINNT=0x0602) endif() foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) @@ -1065,6 +1065,8 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources) list(APPEND agcExecutionTests ${agcExecutionTest}) endforeach() +target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber) + target_include_directories(agc_driver_alpha_to_coverage_tests PRIVATE $) target_include_directories(agc_driver_dual_source_blend_tests PRIVATE $) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index b5659313b..ce483328b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -44,6 +44,7 @@ namespace AgcDriver::DriverDetail { class Driver { public: static Driver& Get(); + static std::uint64_t GpuMapImportThreads(); ~Driver(); void Shutdown(); void WaitIdle(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index fa645dd13..0b4390239 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -6,10 +6,64 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include +#include #include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif namespace AgcDriver::DriverDetail { +namespace { + +constexpr std::uintptr_t InlineImportStackBytes = 256 * 1024; + +std::atomic gpuMapImportThreads{0}; + +bool StackHoldsImport() { +#ifdef _WIN32 + ULONG_PTR low = 0; + ULONG_PTR high = 0; + GetCurrentThreadStackLimits(&low, &high); +#else + struct Bounds { + std::uintptr_t low = 0; + std::uintptr_t high = 0; + Bounds() { + pthread_attr_t attributes; + if (pthread_getattr_np(pthread_self(), &attributes) != 0) return; + void* base = nullptr; + std::size_t size = 0; + if (pthread_attr_getstack(&attributes, &base, &size) == 0 && base != nullptr) { + low = reinterpret_cast(base); + high = low + size; + } + pthread_attr_destroy(&attributes); + } + }; + thread_local const Bounds bounds; + const auto low = bounds.low; + const auto high = bounds.high; +#endif + const volatile unsigned char marker = 0; + const auto frame = reinterpret_cast(&marker); + return frame >= low && frame < high && frame - low >= InlineImportStackBytes; +} + +} + +std::uint64_t Driver::GpuMapImportThreads() { + return gpuMapImportThreads.load(std::memory_order_relaxed); +} + Driver& Driver::Get() { static Driver driver; return driver; @@ -64,14 +118,29 @@ Driver::Driver() { throw; } GuestAllocations::GuestAllocationsSetGpuMapObserver_nid_postfix([](const GuestAllocations::Mapped& ranges, std::uint64_t generation) { - try { - const auto current = Driver::Get().device.Load(); - if (current == nullptr || current->ImportGuestMemory(ranges, generation, false)) return; - std::lock_guard lock(GuestMemory::GpuMutex()); - if (Driver::Get().device.Load() == current) current->ImportGuestMemory(ranges, generation, true); - } catch (const std::exception& error) { - std::fprintf(stderr, "[gpu] import of GPU memory at its mapping failed: %s\n", error.what()); + const auto import = [&] { + try { + const auto current = Driver::Get().device.Load(); + if (current == nullptr || current->ImportGuestMemory(ranges, generation, false)) return; + std::lock_guard lock(GuestMemory::GpuMutex()); + if (Driver::Get().device.Load() == current) current->ImportGuestMemory(ranges, generation, true); + } catch (const std::exception& error) { + std::fprintf(stderr, "[gpu] import of GPU memory at its mapping failed: %s\n", error.what()); + } + }; + if (GuestMemory::GpuMutex().HeldByThisThread() || StackHoldsImport()) { + import(); + return; } + std::thread host; + try { + host = std::thread(import); + } catch (const std::system_error& error) { + std::fprintf(stderr, "[gpu] import of GPU memory at its mapping failed: no host thread to run it on: %s\n", error.what()); + return; + } + gpuMapImportThreads.fetch_add(1, std::memory_order_relaxed); + host.join(); }); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp new file mode 100644 index 000000000..dbc7d5310 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp @@ -0,0 +1,132 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include + +using GuestFiberEntry = void (APS5_VABI*)(std::uint64_t, std::uint64_t); + +extern "C" { +std::int32_t APS5_VABI _sceFiberInitializeImpl_nid_postfix(FiberObject*, const char*, GuestFiberEntry, std::uint64_t, void*, std::uint64_t, const void*, std::uint32_t); +std::int32_t APS5_VABI sceFiberFinalize(FiberObject*); +std::int32_t APS5_VABI sceFiberRun_nid_postfix(FiberObject*, std::uint64_t, std::uint64_t*); +std::int32_t APS5_VABI sceFiberReturnToThread(std::uint64_t, std::uint64_t*); +} + +namespace { + +using AgcDriver::DriverDetail::Driver; +using AgcDriver::Graphics::Require; + +constexpr std::size_t ContextBytes = 64 * 1024; +constexpr std::size_t MappingBytes = 4 * 65536; +constexpr std::size_t FiberStackLimit = 8 * 1024; +constexpr std::uint64_t Fill = 0x5aa5c33c5aa5c33cull; + +alignas(16) std::array FiberContext; +struct alignas(8) FiberStorage { + unsigned char bytes[0x100]; +}; +FiberStorage FiberObjectStorage; + +alignas(256) constexpr std::array VertexCode{0x7e000208u, 0x7e020280u, 0x7e040280u, 0x7e0602f2u, 0xf80008cfu, 0x03020100u, 0xbf810000u, 0xbf810000u}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; +alignas(8) Header VertexHeader{}; + +std::uint8_t* Mapping = nullptr; + +void RegisterVertexShader() { + VertexHeader.shader.file_header = 0x34333231u; + VertexHeader.shader.version = 0x18u; + VertexHeader.shader.header_size = sizeof(Header); + VertexHeader.shader.shader_size = static_cast(sizeof(VertexCode)); + VertexHeader.shader.code = VertexCode.data(); + VertexHeader.shader.type = 2; + VertexHeader.shader.user_data = &VertexHeader.userData; + AgcDriverRegisterShader_nid_postfix(&VertexHeader.shader); +} + +void MapForGpu() { + GuestAllocations::Mutation mutation; + mutation.Add(Mapping, MappingBytes, true, true, true); + mutation.NoteGpuMapping(Mapping, MappingBytes); +} + +void APS5_VABI MapOnFiber(std::uint64_t, std::uint64_t) { + MapForGpu(); + sceFiberReturnToThread(0, nullptr); +} + +void ReleaseMapping() { + if (Mapping == nullptr) return; +#ifdef _WIN32 + VirtualFree(Mapping, 0, MEM_RELEASE); +#else + std::free(Mapping); +#endif + Mapping = nullptr; +} + +std::size_t FiberStackUsed() { + std::size_t untouched = 0; + while (untouched < FiberContext.size() && FiberContext[untouched] == Fill) ++untouched; + return (FiberContext.size() - untouched) * sizeof(std::uint64_t); +} + +} + +int main() { + try { + if (!OpenVulkanTestDevice()) return VulkanTestSkipped; + RegisterVertexShader(); +#ifdef _WIN32 + Mapping = static_cast(VirtualAlloc(nullptr, MappingBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + Mapping = static_cast(std::aligned_alloc(65536, MappingBytes)); +#endif + Require(Mapping != nullptr, "GPU mapping on a fiber: cannot allocate the mapped memory"); + std::memset(Mapping, 0, MappingBytes); + FiberContext.fill(Fill); + auto* fiber = reinterpret_cast(&FiberObjectStorage); + Require(_sceFiberInitializeImpl_nid_postfix(fiber, "gpu map", MapOnFiber, 0, FiberContext.data(), ContextBytes, nullptr, 0) == 0, "GPU mapping on a fiber: the fiber was not initialized"); + const auto beforeFiber = Driver::GpuMapImportThreads(); + Require(sceFiberRun_nid_postfix(fiber, 0, nullptr) == 0, "GPU mapping on a fiber: the fiber did not run"); + const auto onFiber = Driver::GpuMapImportThreads() - beforeFiber; + Require(sceFiberFinalize(fiber) == 0, "GPU mapping on a fiber: the fiber was not finalized"); + const auto used = FiberStackUsed(); + GuestAllocations::Mutation().Remove(Mapping); + const auto beforeThread = Driver::GpuMapImportThreads(); + MapForGpu(); + const auto onThread = Driver::GpuMapImportThreads() - beforeThread; + GuestAllocations::Mutation().Remove(Mapping); + AgcDriverShutdown_nid_postfix(); + ReleaseMapping(); + Require(used <= FiberStackLimit, "GPU mapping on a fiber used " + std::to_string(used) + " bytes of the fiber's stack, more than " + std::to_string(FiberStackLimit) + ": the import of the mapped memory ran on it"); + Require(onFiber == 1, "GPU mapping on a fiber started " + std::to_string(onFiber) + " host threads for its import instead of one"); + Require(onThread == 0, "GPU mapping on an ordinary thread started " + std::to_string(onThread) + " host threads for its import instead of importing on its own stack"); + std::cout << "A GPU mapping made on a fiber used " << used << " bytes of its stack\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + ReleaseMapping(); + return 1; + } +}