From 67fce999c521984efb7301ebd93340f70f170d9d Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Sun, 27 Sep 2026 08:13:37 +1000 Subject: [PATCH 1/7] fix(libc): support SDK startup without allocator replacements --- core/libs/CMakeLists.txt | 14 +++++++-- core/libs/prx/libc/src/ApplicationHeap.cpp | 32 +++++++++++++++++-- core/libs/tests/ApplicationHeap.cpp | 36 +++++++++++++++++++++- 3 files changed, 77 insertions(+), 5 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index b0572b5bf..2785b4253 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -357,11 +357,21 @@ if(BUILD_TESTING) endif() endif() -add_executable(application_heap_tests EXCLUDE_FROM_ALL tests/ApplicationHeap.cpp prx/libc/src/ApplicationHeap.cpp) +add_executable(application_heap_tests EXCLUDE_FROM_ALL tests/ApplicationHeap.cpp) target_include_directories(application_heap_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) -set_target_properties(application_heap_tests PROPERTIES CXX_EXTENSIONS OFF) target_link_libraries(application_heap_tests PRIVATE libc) configure_windows_unwind(application_heap_tests) +set_target_properties(application_heap_tests PROPERTIES CXX_EXTENSIONS OFF) +if(BUILD_TESTING) + set_target_properties(application_heap_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME application_heap_replacement COMMAND application_heap_tests) + add_test(NAME application_heap_invalid_metadata COMMAND application_heap_tests invalid) + add_test(NAME application_heap_default COMMAND application_heap_tests default) + if(WIN32) + set_tests_properties(application_heap_replacement application_heap_invalid_metadata application_heap_default + PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() add_executable(guest_locale_tests EXCLUDE_FROM_ALL tests/GuestLocale.cpp) target_include_directories(guest_locale_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/core/libs/prx/libc/src/ApplicationHeap.cpp b/core/libs/prx/libc/src/ApplicationHeap.cpp index 81e17250d..6976923ae 100644 --- a/core/libs/prx/libc/src/ApplicationHeap.cpp +++ b/core/libs/prx/libc/src/ApplicationHeap.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/GuestHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -9,6 +10,7 @@ #include #include #include +#include namespace { @@ -28,6 +30,28 @@ Initialize heapFinalize = nullptr; bool heapFinalized = false; thread_local bool heapCallbackActive = false; +void* APS5_VABI defaultAllocate(std::size_t bytes) { return GuestHeap::GuestHeapAllocate_nid_postfix(bytes); } +void APS5_VABI defaultFree(void* pointer) { GuestHeap::GuestHeapFree_nid_postfix(pointer); } +void* APS5_VABI defaultReallocate(void* pointer, std::size_t bytes) { return GuestHeap::GuestHeapReallocate_nid_postfix(pointer, bytes); } +void* APS5_VABI defaultCalloc(std::size_t count, std::size_t bytes) { + if (bytes != 0 && count > std::numeric_limits::max() / bytes) throw std::bad_alloc(); + auto* pointer = defaultAllocate(count * bytes); + std::memset(pointer, 0, count * bytes); + return pointer; +} +void* APS5_VABI defaultAlign(std::size_t alignment, std::size_t bytes) { return GuestHeap::GuestHeapAlign_nid_postfix(alignment, bytes); } +int APS5_VABI defaultPosixAlign(void** pointer, std::size_t alignment, std::size_t bytes) { + if (pointer == nullptr || alignment < sizeof(void*) || (alignment & (alignment - 1)) != 0) return 22; + try { *pointer = defaultAlign(alignment, bytes); return 0; } + catch (const std::bad_alloc&) { return 12; } +} + +std::array defaultApi() { + return {reinterpret_cast(defaultAllocate), reinterpret_cast(defaultFree), + reinterpret_cast(defaultCalloc), reinterpret_cast(defaultReallocate), + reinterpret_cast(defaultAlign), nullptr, reinterpret_cast(defaultPosixAlign)}; +} + class CallbackScope { public: CallbackScope() { @@ -87,8 +111,12 @@ void ApplicationHeapRegister_nid_no_patch(void* const* api) { if (api == nullptr) throw std::invalid_argument("application heap: null allocator API"); std::array replacement; std::memcpy(replacement.data(), api, sizeof(replacement)); - for (std::size_t index = 0; index < 7; ++index) { - if (replacement[index] == nullptr) throw std::invalid_argument("application heap: incomplete allocator API"); + if (std::all_of(replacement.begin(), replacement.end(), [](const void* entry) { return entry == nullptr; })) { + replacement = defaultApi(); + } else { + for (std::size_t index = 0; index < 7; ++index) { + if (replacement[index] == nullptr) throw std::invalid_argument("application heap: incomplete allocator API"); + } } std::lock_guard lock(heapMutex); if (heapFailure) std::rethrow_exception(heapFailure); diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 48009f88e..ccb050782 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -70,7 +70,7 @@ void write(std::array& data, std::size_t offset, TValue value) } -int main(int argc, char**) { +int main(int argc, char** argv) { reject([] { ApplicationHeapAllocate_nid_no_patch(64); }); reject([] { ApplicationHeapRegister_nid_no_patch(nullptr); }); std::array process{}; @@ -92,6 +92,40 @@ int main(int argc, char**) { write(replacement, 0x40, &align); write(replacement, 0x48, &reallocate); write(replacement, 0x50, &posixAlign); + if (argc > 1 && std::strcmp(argv[1], "default") == 0) { + std::array partial{}; + partial[0] = reinterpret_cast(&allocate); + reject([&] { ApplicationHeapRegister_nid_no_patch(partial.data()); }); + std::memset(replacement.data() + 0x20, 0, sizeof(partial)); + ApplicationHeapInitialize_nid_no_patch(process.data()); + require(initializes == 1); + auto* pointer = static_cast(ApplicationHeapCalloc_nid_no_patch(7, 9)); + for (unsigned i = 0; i < 63; ++i) require(pointer[i] == 0); + std::memset(pointer, 0x5a, 63); + pointer = static_cast(ApplicationHeapReallocate_nid_no_patch(pointer, 150)); + for (unsigned i = 0; i < 63; ++i) require(pointer[i] == 0x5a); + pointer = static_cast(ApplicationHeapReallocate_nid_no_patch(pointer, 11)); + for (unsigned i = 0; i < 11; ++i) require(pointer[i] == 0x5a); + require(ApplicationHeapReallocate_nid_no_patch(pointer, 0) == nullptr); + pointer = static_cast(ApplicationHeapReallocate_nid_no_patch(nullptr, 32)); + require(pointer != nullptr); + ApplicationHeapFree_nid_no_patch(pointer); + for (std::size_t alignment : {16, 64, 4096}) { + auto* aligned = ApplicationHeapAlign_nid_no_patch(alignment, 37); + require(reinterpret_cast(aligned) % alignment == 0); + ApplicationHeapFree_nid_no_patch(aligned); + } + void* aligned = nullptr; + require(ApplicationHeapPosixAlign_nid_no_patch(&aligned, 256, 99) == 0); + require(reinterpret_cast(aligned) % 256 == 0); + ApplicationHeapFree_nid_no_patch(aligned); + ApplicationHeapFree_nid_no_patch(nullptr); + reject([] { ApplicationHeapCalloc_nid_no_patch(SIZE_MAX, 2); }); + reject([] { ApplicationHeapAlign_nid_no_patch(3, 16); }); + ApplicationHeapInitialize_nid_no_patch(process.data()); + require(initializes == 1); + return 0; + } if (argc > 1) { write(replacement, 8, std::uint64_t{99}); reject([&] { ApplicationHeapInitialize_nid_no_patch(process.data()); }); From 07930c6726921b82a3164b0bddaa314e170db216 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Sun, 27 Sep 2026 08:31:47 +1000 Subject: [PATCH 2/7] feat(videoout): present linear framebuffer layouts --- core/libs/CMakeLists.txt | 9 +++++ .../Execution/include/DisplayBuffer.hpp | 2 ++ .../Execution/src/DisplayBuffer.cpp | 13 +++++-- .../Execution/src/VulkanDevice.cpp | 5 +++ core/libs/prx/libSceVideoOut/src/Buffer.cpp | 4 +-- core/libs/tests/LinearDisplayBuffer.cpp | 35 +++++++++++++++++++ 6 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 core/libs/tests/LinearDisplayBuffer.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 2785b4253..de0090ebd 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -109,6 +109,15 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(linear_display_buffer_tests tests/LinearDisplayBuffer.cpp) + target_include_directories(linear_display_buffer_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(linear_display_buffer_tests PRIVATE libSceAgcDriver libc) + configure_windows_unwind(linear_display_buffer_tests) + add_test(NAME linear_display_buffer COMMAND linear_display_buffer_tests) + if(WIN32) + set_tests_properties(linear_display_buffer PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_rendering_wait_tests tests/GuestRenderingWait.cpp) target_include_directories(guest_rendering_wait_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_rendering_wait_tests PRIVATE libSceAgcDriver libc) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp index e5c499a66..b7f70c3d8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp @@ -13,6 +13,8 @@ struct DisplayBuffer { std::uint64_t pixelFormat; std::uint32_t width; std::uint32_t height; + std::uint32_t tilingMode = 0; + std::uint32_t pitchInPixel = 0; }; std::size_t DisplayBufferSize(const DisplayBuffer& buffer); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp index e9294fea7..837b13f02 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp @@ -38,14 +38,19 @@ std::size_t DisplayBufferSize(const DisplayBuffer& buffer) { throw std::runtime_error(message); } require(buffer.address != 0 && (buffer.address & 65535u) == 0, "VideoOut: display buffer requires 64 KiB alignment"); - const auto size = static_cast((buffer.width + 127u) / 128u) * ((buffer.height + 127u) / 128u) * 65536u; + require(buffer.tilingMode <= 1, "VideoOut: unsupported display tiling mode"); + require(buffer.tilingMode != 0 || buffer.pitchInPixel == 0, "VideoOut: tiled display pitch is unsupported"); + const auto pitch = buffer.pitchInPixel == 0 ? buffer.width : buffer.pitchInPixel; + require(pitch >= buffer.width && pitch <= 16384, "VideoOut: invalid linear display pitch"); + const auto size = buffer.tilingMode == 1 ? static_cast(pitch) * buffer.height * 4u + : static_cast((buffer.width + 127u) / 128u) * ((buffer.height + 127u) / 128u) * 65536u; require(size <= std::numeric_limits::max() && size <= std::numeric_limits::max() - buffer.address, "VideoOut: display buffer range overflow"); return static_cast(size); } std::vector DecodeDisplayBuffer(const DisplayBuffer& buffer, std::span source) { PerformanceTimer timing("DisplayBuffer.Decode"); - require(source.size() == DisplayBufferSize(buffer), "VideoOut: invalid tiled display buffer size"); + require(source.size() == DisplayBufferSize(buffer), "VideoOut: invalid display buffer size"); std::vector pixels(static_cast(buffer.width) * buffer.height * 4); const auto blocksPerRow = (buffer.width + 127u) / 128u; const bool rgba = baseFormat(buffer.pixelFormat) == PixelFormatR8G8B8A8; @@ -55,7 +60,9 @@ std::vector DecodeDisplayBuffer(const DisplayBuffer& buffer, std::spa timing.Mark("validate_allocate"); for (std::uint32_t y = 0; y < buffer.height; ++y) { for (std::uint32_t x = 0; x < buffer.width; ++x) { - const auto tiled = (static_cast(y / 128u) * blocksPerRow + x / 128u) * 65536u + tileOffset(x, y); + const auto pitch = buffer.pitchInPixel == 0 ? buffer.width : buffer.pitchInPixel; + const auto tiled = buffer.tilingMode == 1 ? (static_cast(y) * pitch + x) * 4u + : (static_cast(y / 128u) * blocksPerRow + x / 128u) * 65536u + tileOffset(x, y); const auto linear = (static_cast(y) * buffer.width + x) * 4; if (tenBit) { std::uint32_t word = 0; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index ddd49fc0a..a3a590e63 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1629,6 +1629,10 @@ FrameDumps& Dumps() { } bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { + if (buffer.tilingMode == 1) { + const auto pixels = ReadDisplayBuffer(buffer); + return present(buffer.width, buffer.height, true, pixels); + } static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; // The display buffer is usually a resident render target whose results are still on the GPU: it // is blitted from that image, which skips the write-back retile, the 33 MB read of guest memory, @@ -1934,6 +1938,7 @@ VulkanDevice::PresentStatistics VulkanDevice::PresentCounts() { bool VulkanDevice::PresentWaitsForSlots(const DisplayBuffer* buffer) const { if (buffer == nullptr || !state->SharedSlotInFlight()) return false; + if (buffer->tilingMode == 1) return true; const auto& dumps = Dumps(); if ((dumps.dumped < dumps.limit && !dumps.cpu) || NoResidentPresent()) return true; VkFilter filter = VK_FILTER_LINEAR; diff --git a/core/libs/prx/libSceVideoOut/src/Buffer.cpp b/core/libs/prx/libSceVideoOut/src/Buffer.cpp index 5c260c1fc..28430fe29 100644 --- a/core/libs/prx/libSceVideoOut/src/Buffer.cpp +++ b/core/libs/prx/libSceVideoOut/src/Buffer.cpp @@ -10,9 +10,9 @@ AgcDriver::DisplayBuffer DescribeVideoOutBuffer(const VideoOutBuffer& buffer, const BufferAttributeGroup& group) { const auto& attribute = group.attribute; if (attribute.reserved0 != 0 || attribute.pad0 != 0 || attribute.reserved1[0] != 0 || attribute.reserved1[1] != 0 || attribute.reserved1[2] != 0) throw std::runtime_error("VideoOut: reserved buffer attribute bits are set"); - if (attribute.tiling_mode != 0 || attribute.pitch_in_pixel != 0 || attribute.aspect_ratio != 0 || attribute.option != 0) throw std::runtime_error("VideoOut: unsupported tiling, pitch, aspect ratio or buffer option"); + if (attribute.tiling_mode > 1 || (attribute.tiling_mode == 0 && attribute.pitch_in_pixel != 0) || attribute.aspect_ratio != 0 || attribute.option != 0) throw std::runtime_error("VideoOut: unsupported tiling, pitch, aspect ratio or buffer option"); if (group.category != VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED || buffer.metadataAddress != 0 || attribute.dcc_control != 0 || attribute.dcc_cb_register_clear_color != 0) throw std::runtime_error("VideoOut: DCC presentation is not implemented"); - const AgcDriver::DisplayBuffer result{buffer.dataAddress, attribute.pixel_format, attribute.width, attribute.height}; + const AgcDriver::DisplayBuffer result{buffer.dataAddress, attribute.pixel_format, attribute.width, attribute.height, attribute.tiling_mode, attribute.pitch_in_pixel}; static_cast(AgcDriverDisplayBufferSize_nid_postfix(result)); return result; } diff --git a/core/libs/tests/LinearDisplayBuffer.cpp b/core/libs/tests/LinearDisplayBuffer.cpp new file mode 100644 index 000000000..ad10b3f42 --- /dev/null +++ b/core/libs/tests/LinearDisplayBuffer.cpp @@ -0,0 +1,35 @@ +#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" +#include +#include +#include + +static void Require(bool condition) { if (!condition) std::abort(); } +template static void Reject(TAction action) { + bool rejected = false; + try { action(); } catch (const std::exception&) { rejected = true; } + Require(rejected); +} + +int main() { + AgcDriver::DisplayBuffer buffer{65536, 0x8000000000000000ull, 2, 2, 1, 3}; + const std::array input{1,2,3,4, 5,6,7,8, 99,99,99,99, + 9,10,11,12, 13,14,15,16, 99,99,99,99}; + const auto source = std::as_bytes(std::span(input)); + Require(AgcDriver::DisplayBufferSize(buffer) == input.size()); + auto pixels = AgcDriver::DecodeDisplayBuffer(buffer, source); + Require(pixels.size() == 16); + for (unsigned i = 0; i < pixels.size(); ++i) Require(std::to_integer(pixels[i]) == i + 1); + buffer.pixelFormat = 0x8000000022000000ull; + pixels = AgcDriver::DecodeDisplayBuffer(buffer, source); + const std::array swapped{3,2,1,4, 7,6,5,8, 11,10,9,12, 15,14,13,16}; + for (unsigned i = 0; i < pixels.size(); ++i) Require(std::to_integer(pixels[i]) == swapped[i]); + Reject([&] { AgcDriver::DecodeDisplayBuffer(buffer, source.first(23)); }); + buffer.pitchInPixel = 1; + Reject([&] { AgcDriver::DisplayBufferSize(buffer); }); + buffer.pitchInPixel = 0; + Require(AgcDriver::DisplayBufferSize(buffer) == 16); + buffer.tilingMode = 2; + Reject([&] { AgcDriver::DisplayBufferSize(buffer); }); + buffer.tilingMode = 0; + Require(AgcDriver::DisplayBufferSize(buffer) == 65536); +} From b25025c6820eb3863f97564a1a827d3cdf3a54bf Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Sun, 27 Sep 2026 08:49:13 +1000 Subject: [PATCH 3/7] fix(agc): support explicit indexed draw commands --- core/libs/CMakeLists.txt | 9 +++ .../prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp | 11 +-- .../prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp | 6 +- .../libSceAgcDriver/Execution/include/Pm4.hpp | 2 +- .../libSceAgcDriver/Execution/src/Driver.cpp | 2 +- .../prx/libSceAgcDriver/Execution/src/Pm4.cpp | 34 ++++++--- core/libs/tests/GuestIndexedDraw.cpp | 74 +++++++++++++++++++ 7 files changed, 115 insertions(+), 23 deletions(-) create mode 100644 core/libs/tests/GuestIndexedDraw.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index de0090ebd..d97236260 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -136,6 +136,15 @@ if(BUILD_TESTING) set_tests_properties(guest_buffer_reuse PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_executable(guest_indexed_draw_tests tests/GuestIndexedDraw.cpp) + target_include_directories(guest_indexed_draw_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_indexed_draw_tests PRIVATE libSceAgc libSceAgcDriver libc) + configure_windows_unwind(guest_indexed_draw_tests) + add_test(NAME guest_indexed_draw COMMAND guest_indexed_draw_tests) + if(WIN32) + set_tests_properties(guest_indexed_draw PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_shader_link_tests tests/GuestShaderLink.cpp) target_include_directories(guest_shader_link_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_shader_link_tests PRIVATE libSceAgc libc) diff --git a/core/libs/prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp b/core/libs/prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp index 8c81f1e8f..d557ba0dc 100644 --- a/core/libs/prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp +++ b/core/libs/prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp @@ -9,13 +9,10 @@ extern "C" { -uint32_t* APS5_VABI sceAgcDcbDrawIndex(CommandBuffer* buf, uint32_t index_count, const volatile void* index_addr, uint64_t modifier) { - (void)buf; - (void)index_count; - (void)index_addr; - (void)modifier; - NotImplemented_nid_no_patch(__func__); - return nullptr; +std::uint32_t* APS5_VABI sceAgcDcbDrawIndex(CommandBuffer* buf, std::uint32_t indexCount, const volatile void* indexAddress, std::uint64_t modifier) { + const auto address = reinterpret_cast(indexAddress); + Agc::Command::CheckAddress(address, 1, __func__); + return Agc::Command::Emit(buf, 0x27u, {indexCount, static_cast(address), static_cast(address >> 32u), indexCount, Agc::Command::DrawInitiator(modifier, true, __func__)}, __func__); } std::uint32_t APS5_VABI sceAgcDcbDrawIndexGetSize() { diff --git a/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp b/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp index 9ff4fb9fd..e7c71b3b6 100644 --- a/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp +++ b/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp @@ -18,12 +18,12 @@ std::uint32_t APS5_VABI sceAgcDcbSetIndexBufferGetSize() { return 0; } -uint32_t* APS5_VABI sceAgcDcbSetIndexCount(CommandBuffer* buf, uint32_t index_count) { - return Agc::Command::Emit(buf, 0x13u, {index_count}, __func__); +std::uint32_t* APS5_VABI sceAgcDcbSetIndexCount(CommandBuffer* buf, std::uint32_t indexCount) { + return Agc::Command::Emit(buf, 0x13u, {indexCount}, __func__); } std::uint32_t APS5_VABI sceAgcDcbSetIndexCountGetSize() { - return 2; + return 8; } std::uint32_t* APS5_VABI sceAgcDcbSetIndexSize(CommandBuffer* buf, std::uint8_t indexSize, std::uint8_t cachePolicy) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp index 9543e5a62..6035dc560 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp @@ -73,7 +73,7 @@ struct DrawArguments { DrawArguments ReadDrawArguments(const DrawParameters::IndirectDraw& indirect, std::uint32_t record); std::uint32_t ReadDrawCount(const DrawParameters::IndirectDraw& indirect); inline bool IndirectDrawOpcode(std::uint32_t opcode) { return opcode == 0x24 || opcode == 0x25 || opcode == 0x2c || opcode == 0x38; } -inline bool DrawOpcode(std::uint32_t opcode) { return opcode == 0x2d || opcode == 0x35 || IndirectDrawOpcode(opcode); } +inline bool DrawOpcode(std::uint32_t opcode) { return opcode == 0x27 || opcode == 0x2d || opcode == 0x35 || IndirectDrawOpcode(opcode); } std::string Name(std::uint32_t header); // A PM4 type-2 packet is a one-dword filler (command-buffer padding); type 3 and type 0 carry a diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp index a2f9e338c..b647e9735 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp @@ -441,7 +441,7 @@ bool NeedsRecordedLabels(std::uint32_t header) { bool PacketLocksItself(std::uint32_t header) { if (header == FlipPacketHeader) return true; switch ((header >> 8u) & 0xffu) { - case 0x15: case 0x16: case 0x2d: case 0x35: case 0x24: case 0x25: case 0x2c: case 0x38: return true; + case 0x15: case 0x16: case 0x27: case 0x2d: case 0x35: case 0x24: case 0x25: case 0x2c: case 0x38: return true; default: return false; } } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index 21dd47ae2..a89bc0eca 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -137,14 +137,14 @@ std::string_view UnsupportedReason(std::uint32_t header) { } } switch (opcode) { - case 0x11: case 0x12: case 0x13: case 0x15: case 0x16: case 0x26: + case 0x11: case 0x12: case 0x13: case 0x15: case 0x16: case 0x26: case 0x27: case 0x2a: case 0x2d: case 0x2f: case 0x35: case 0x37: case 0x40: case 0x42: case 0x46: case 0x50: case 0x58: case 0x63: case 0x64: case 0x69: case 0x76: case 0x79: case 0x7a: case 0x81: case 0x83: case 0x9f: return {}; case 0x24: case 0x25: case 0x2c: case 0x38: if (IndirectDrawsDisabled()) return "graphics draw, shader stages and guest render-target materialization are not implemented"; return {}; - case 0x27: case 0x3a: case 0x8d: + case 0x3a: case 0x8d: return "graphics draw, shader stages and guest render-target materialization are not implemented"; case 0x20: return "GPU query predication is not implemented"; case 0x22: return "conditional command execution and conditional flip reservation are not implemented"; @@ -219,6 +219,13 @@ void Validate(std::span packet, std::uint32_t queue) { case 0x12: graphics(); size(2); require((packet[1] & ~0xfu) == 0, "unsupported CLEAR_STATE payload bits"); break; case 0x13: case 0x2f: graphics(); size(2); break; case 0x26: graphics(); size(3); break; + case 0x27: + graphics(); + size(6); + require(packet[3] <= 0xffffu, "unsupported index address bits"); + require(packet[4] <= packet[1], "index count exceeds maximum index size"); + require((packet[5] & ~0x20u) == 0, "unsupported indexed draw flags"); + break; case 0x2a: graphics(); size(2); require(packet[1] <= 3, "unsupported index-type modifiers"); break; case 0x2d: graphics(); @@ -470,7 +477,7 @@ bool UsesGpuCacheBarrier(std::span packet) { bool AccessesMemory(std::uint32_t header) { switch ((header >> 8u) & 0xffu) { - case 0x3c: case 0x93: case 0x49: case 0x16: case 0x2d: case 0x35: case 0x24: case 0x25: case 0x2c: case 0x38: case 0x37: case 0x40: case 0x50: case 0x63: case 0x64: case 0x83: case 0x9f: return true; + case 0x27: case 0x3c: case 0x93: case 0x49: case 0x16: case 0x2d: case 0x35: case 0x24: case 0x25: case 0x2c: case 0x38: case 0x37: case 0x40: case 0x50: case 0x63: case 0x64: case 0x83: case 0x9f: return true; default: return false; } } @@ -578,16 +585,21 @@ DrawParameters ResolveDraw(std::span packet, const QueueSta require(packet[1] == 0 || firstVertex <= std::numeric_limits::max() - (packet[1] - 1u), "auto draw vertex range overflow"); return {0, packet[1], 0, queue.instanceCount, packet[2] & 0x20u, false, firstVertex, 0}; } - require(((packet[0] >> 8u) & 0xffu) == 0x35, "expected DRAW_INDEX_OFFSET_2 packet"); - const auto indexSize = indexSizeOf(queue); - const auto offset = static_cast(packet[2]) * indexSize; - require(offset <= std::numeric_limits::max() - queue.indexBase, "index address overflow"); - const auto address = queue.indexBase + offset; - const auto bytes = static_cast(packet[3]) * indexSize; + const bool explicitAddress = ((packet[0] >> 8u) & 0xffu) == 0x27; + require(explicitAddress || ((packet[0] >> 8u) & 0xffu) == 0x35, "expected indexed draw packet"); + require(queue.indexType <= 2, "unsupported index type"); + const std::uint32_t indexSize = queue.indexType == 0 ? 2 : queue.indexType == 1 ? 4 : 1; + const auto base = explicitAddress ? address(packet[2], packet[3]) : queue.indexBase; + const auto count = explicitAddress ? packet[4] : packet[3]; + require(base != 0 && base % indexSize == 0, "null or misaligned index base"); + const auto offset = explicitAddress ? 0 : static_cast(packet[2]) * indexSize; + require(offset <= std::numeric_limits::max() - base, "index address overflow"); + const auto address = base + offset; + const auto bytes = static_cast(count) * indexSize; require(bytes <= std::numeric_limits::max(), "index range size overflow"); GuestMemory::CheckRange(reinterpret_cast(address), static_cast(bytes), indexSize); - APS5_LOG_OUT_DEBUG("ResolveDraw context targetMask=0x%x shaderMask=0x%x indexCount=%u indexType=%u instances=%u", queue.context.contains(0x8e) ? queue.context.at(0x8e) : 0u, queue.context.contains(0x8f) ? queue.context.at(0x8f) : 0u, packet[3], queue.indexType, queue.instanceCount); - return {address, packet[3], indexSize, queue.instanceCount, packet[4]}; + APS5_LOG_OUT_DEBUG("ResolveDraw context targetMask=0x%x shaderMask=0x%x indexCount=%u indexType=%u instances=%u", queue.context.contains(0x8e) ? queue.context.at(0x8e) : 0u, queue.context.contains(0x8f) ? queue.context.at(0x8f) : 0u, count, queue.indexType, queue.instanceCount); + return {address, count, indexSize, queue.instanceCount, packet.back()}; } void Execute(std::span packet, QueueState& queue) { diff --git a/core/libs/tests/GuestIndexedDraw.cpp b/core/libs/tests/GuestIndexedDraw.cpp new file mode 100644 index 000000000..3c6ed23b0 --- /dev/null +++ b/core/libs/tests/GuestIndexedDraw.cpp @@ -0,0 +1,74 @@ +#include "SceTypes.hpp" +#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" +#include +#include +#include + +extern "C" { +std::uint32_t* APS5_VABI sceAgcDcbSetIndexCount(CommandBuffer*, std::uint32_t); +std::uint32_t APS5_VABI sceAgcDcbSetIndexCountGetSize(); +std::uint32_t* APS5_VABI sceAgcDcbDrawIndex(CommandBuffer*, std::uint32_t, const volatile void*, std::uint64_t); +std::uint32_t APS5_VABI sceAgcDcbDrawIndexGetSize(); +} +static void Require(bool condition) { if (!condition) std::abort(); } +template static void Reject(TAction action) { + bool rejected = false; + try { action(); } catch (const std::exception&) { rejected = true; } + Require(rejected); +} +int main() { + std::array words{}; + CommandBuffer buffer{words.data(), words.data()+words.size(), words.data(), words.data()+words.size(), nullptr, nullptr, 0}; + auto* sizePacket = sceAgcDcbSetIndexCount(&buffer, 3); + Require(sizePacket == words.data() && words[0] == 0xc0001300u && words[1] == 3); + Require(sceAgcDcbSetIndexCountGetSize() == 8); + AgcDriver::QueueState queue; + AgcDriver::Pm4::Validate({sizePacket,2},0); + AgcDriver::Pm4::Execute({sizePacket,2},queue); + Require(queue.indexBufferSize == 3); + alignas(4) std::array indices{2,0,1}; + auto* draw = sceAgcDcbDrawIndex(&buffer,3,indices.data(),0); + Require(draw == words.data()+2 && draw[0] == 0xc0042700u && draw[1] == 3 && draw[4] == 3 && draw[5] == 0); + Require(sceAgcDcbDrawIndexGetSize() == 24); + Require(AgcDriver::Pm4::AccessesMemory(draw[0])); + Require(AgcDriver::Pm4::DrawOpcode((draw[0] >> 8u) & 0xffu)); + queue.indexBase = 1; + for (unsigned type=0;type<3;++type) { + queue.indexType=type; + const auto result=AgcDriver::Pm4::ResolveDraw({draw,6},queue); + Require(result.indexAddress == reinterpret_cast(indices.data())); + Require(result.indexCount == 3 && result.indexSize == (type==0?2:type==1?4:1) && result.indexed); + } + Require(queue.indexBase == 1); + queue.indexType = 1; + queue.indexBase = reinterpret_cast(indices.data()); + const std::array offsetDraw{0xc0033500u,2,1,2,0}; + const auto offsetResult = AgcDriver::Pm4::ResolveDraw(offsetDraw,queue); + Require(offsetResult.indexAddress == queue.indexBase+4 && offsetResult.indexCount == 2); + draw[5] = 0x20; + Require(AgcDriver::Pm4::ResolveDraw({draw,6},queue).flags == 0x20); + draw[5] = 0; + draw[1] = 0; draw[4] = 0; + Require(AgcDriver::Pm4::ResolveDraw({draw,6},queue).indexCount == 0); + draw[1] = 3; draw[4] = 3; + draw[4]=4; + Reject([&]{AgcDriver::Pm4::Validate({draw,6},0);}); + draw[4]=3; + draw[5]=2; + Reject([&]{AgcDriver::Pm4::Validate({draw,6},0);}); + draw[5]=0; + Reject([&]{AgcDriver::Pm4::Validate({draw,5},0);}); + Reject([&]{AgcDriver::Pm4::Validate({draw,6},1);}); + draw[3]=0x10000; + Reject([&]{AgcDriver::Pm4::Validate({draw,6},0);}); + draw[2]=1; draw[3]=0; queue.indexType=1; + Reject([&]{AgcDriver::Pm4::ResolveDraw({draw,6},queue);}); + auto* saved=buffer.cursor_up; + Reject([&]{sceAgcDcbDrawIndex(&buffer,3,nullptr,0);}); + Reject([&]{sceAgcDcbDrawIndex(&buffer,3,indices.data(),1ull<<40);}); + Require(buffer.cursor_up==saved); + buffer.cursor_down=buffer.cursor_up+5; + Reject([&]{sceAgcDcbDrawIndex(&buffer,3,indices.data(),0);}); + Require(buffer.cursor_up==saved); + return 0; +} From fc26cff30ff132d862e635722da16617b3c89183 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Sun, 27 Sep 2026 09:00:03 +1000 Subject: [PATCH 4/7] test(shader): cover decoded buffer address operands --- core/libs/CMakeLists.txt | 8 +++++ core/libs/tests/ShaderBufferAddress.cpp | 42 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 core/libs/tests/ShaderBufferAddress.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index d97236260..3374519ba 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -137,6 +137,14 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() add_executable(guest_indexed_draw_tests tests/GuestIndexedDraw.cpp) + add_executable(shader_buffer_address_tests tests/ShaderBufferAddress.cpp) + target_link_libraries(shader_buffer_address_tests PRIVATE shader_recompiler_agc_driver libc) + configure_windows_unwind(shader_buffer_address_tests) + add_test(NAME shader_buffer_address COMMAND shader_buffer_address_tests) + if(WIN32) + set_tests_properties(shader_buffer_address PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() target_include_directories(guest_indexed_draw_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_indexed_draw_tests PRIVATE libSceAgc libSceAgcDriver libc) configure_windows_unwind(guest_indexed_draw_tests) diff --git a/core/libs/tests/ShaderBufferAddress.cpp b/core/libs/tests/ShaderBufferAddress.cpp new file mode 100644 index 000000000..8062fb67d --- /dev/null +++ b/core/libs/tests/ShaderBufferAddress.cpp @@ -0,0 +1,42 @@ +#include "Translation/TranslationContext.hpp" +#include "RdnaDecoder/RdnaMemoryOpDecoder.hpp" +#include +#include + +using namespace ShaderRecompiler; +static void Require(bool value) { if (!value) throw std::runtime_error("buffer address operand regression"); } +static void CheckRegister(IrValue* value, IrOpcode opcode, unsigned index) { + Require(value->Opcode() == opcode); + Require(value->Argument(0)->Register().index == index); +} +static void Check(bool typed, unsigned opcode, IrOpcode expected, bool idxen, bool offen, bool immediate) { + const std::array code{ + (typed ? (0x3au<<26)|(opcode<<16)|(4u<<19) : (0x38u<<26)|(opcode<<18)) + | (unsigned(idxen)<<13)|(unsigned(offen)<<12)|12u, + ((immediate?128u:20u)<<24)|(2u<<16)|(4u<<8)|10u + }; + const auto instruction = typed ? DecodeRdnaMtbuf(0,code,0) : DecodeRdnaMubuf(0,code,0); + IrProgram program; + auto& block=program.CreateBlock(); + program.SetEntryBlock(block); + TranslationContext context(program,block,256); + context.TranslateInstruction(instruction); + IrValue* memory=nullptr; + for(auto* value:block.Instructions()) if(value->Opcode()==expected) memory=value; + Require(memory!=nullptr); + if(idxen) CheckRegister(memory->Argument(1),IrOpcode::GetVectorRegister,10); + else Require(memory->Argument(1)->HasImmediate() && memory->Argument(1)->ImmediateU32()==0); + if(offen) CheckRegister(memory->Argument(2),IrOpcode::GetVectorRegister,idxen?11:10); + else Require(memory->Argument(2)->HasImmediate() && memory->Argument(2)->ImmediateU32()==0); + if(immediate) Require(memory->Argument(3)->HasImmediate() && memory->Argument(3)->ImmediateU32()==0); + else CheckRegister(memory->Argument(3),IrOpcode::GetScalarRegister,20); +} +int main() { + for(bool idxen:{false,true}) for(bool offen:{false,true}) for(bool immediate:{false,true}) { + Check(false,0x0c,IrOpcode::LoadBufferU32,idxen,offen,immediate); + Check(false,0x1c,IrOpcode::StoreBufferU32,idxen,offen,immediate); + Check(false,0x32,IrOpcode::BufferAtomicIAdd32,idxen,offen,immediate); + Check(true,0,IrOpcode::LoadBufferU32,idxen,offen,immediate); + Check(true,4,IrOpcode::StoreBufferU32,idxen,offen,immediate); + } +} From e1e3a122270ed575a4588af412c5b24a9b8267d4 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Mon, 28 Sep 2026 09:07:34 +1000 Subject: [PATCH 5/7] fix(build): restrict POSIX exception runtime test to Unix --- core/libs/CMakeLists.txt | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 3374519ba..63bd99a00 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -372,14 +372,11 @@ if(BUILD_TESTING) set_tests_properties(guest_strings PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - add_executable(exception_runtime_tests tests/ExceptionRuntime.cpp) - target_include_directories(exception_runtime_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(exception_runtime_tests PRIVATE libc) - configure_windows_unwind(exception_runtime_tests) - add_test(NAME exception_runtime COMMAND exception_runtime_tests) - if(WIN32) - set_tests_properties(exception_runtime PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") + if(UNIX) + add_executable(exception_runtime_tests tests/ExceptionRuntime.cpp) + target_include_directories(exception_runtime_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(exception_runtime_tests PRIVATE libc) + add_test(NAME exception_runtime COMMAND exception_runtime_tests) endif() endif() From 9814c93f757a6cc1194ecd9d23ec115dc5078681 Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Mon, 28 Sep 2026 09:55:00 +1000 Subject: [PATCH 6/7] fix(agc): avoid exception-driven render target format probing --- core/libs/CMakeLists.txt | 9 +++++++++ .../Graphics/include/TextureFormat.hpp | 2 ++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 14 +------------- .../Graphics/src/TextureFormat.cpp | 7 +++++++ core/libs/tests/TextureFormatLookup.cpp | 15 +++++++++++++++ 5 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 core/libs/tests/TextureFormatLookup.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 63bd99a00..cb33a343f 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -109,6 +109,15 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(texture_format_lookup_tests tests/TextureFormatLookup.cpp) + target_include_directories(texture_format_lookup_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include) + target_link_libraries(texture_format_lookup_tests PRIVATE libSceAgcDriver libc) + configure_windows_unwind(texture_format_lookup_tests) + add_test(NAME texture_format_lookup COMMAND texture_format_lookup_tests) + if(WIN32) + set_tests_properties(texture_format_lookup PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(linear_display_buffer_tests tests/LinearDisplayBuffer.cpp) target_include_directories(linear_display_buffer_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(linear_display_buffer_tests PRIVATE libSceAgcDriver libc) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp index a631ca03e..3f2d1f0d8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp @@ -6,10 +6,12 @@ #endif #include #include +#include namespace AgcDriver::Graphics { VkFormat ResolveTextureFormat(std::uint32_t guestFormat); +std::optional FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes); std::uint32_t BytesPerElement(std::uint32_t guestFormat); bool IsBlockCompressed(std::uint32_t guestFormat); std::uint32_t BlockWidth(std::uint32_t guestFormat); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index ed00f454a..9f008e3aa 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -34,19 +34,7 @@ namespace { // A guest texture format code with the same Vulkan format and texel size as a color buffer. std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) { - static std::mutex mutex; - static std::map, std::uint32_t> known; - std::lock_guard lock(mutex); - if (const auto found = known.find({format, elementBytes}); found != known.end()) return found->second; - for (std::uint32_t code = 0; code < 256; ++code) { - try { - if (!IsBlockCompressed(code) && ResolveTextureFormat(code) == format && BytesPerElement(code) == elementBytes) { - known.emplace(std::make_pair(format, elementBytes), code); - return code; - } - } catch (const std::exception&) { - } - } + if (const auto guest = FindGuestTextureFormat(format, elementBytes)) return *guest; throw std::runtime_error("AGC graphics: no guest texture format matches the color buffer format " + std::to_string(static_cast(format))); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp index 06c67ae59..83b6bfbec 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -108,6 +108,13 @@ VkFormat ResolveTextureFormat(std::uint32_t guestFormat) { return findFormatEntry(guestFormat).vkFormat; } +std::optional FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes) { + for (const auto& entry : kFormatLookup) { + if (!entry.blockCompressed && entry.vkFormat == format && entry.bytesPerElement == elementBytes) return entry.guestFormat; + } + return std::nullopt; +} + std::uint32_t BytesPerElement(std::uint32_t guestFormat) { return findFormatEntry(guestFormat).bytesPerElement; } diff --git a/core/libs/tests/TextureFormatLookup.cpp b/core/libs/tests/TextureFormatLookup.cpp new file mode 100644 index 000000000..fd8901eb5 --- /dev/null +++ b/core/libs/tests/TextureFormatLookup.cpp @@ -0,0 +1,15 @@ +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include + +int main() { + using namespace AgcDriver::Graphics; + for (auto format : {VK_FORMAT_R8_UNORM, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R16G16B16A16_SFLOAT}) { + const unsigned bytes = format == VK_FORMAT_R8_UNORM ? 1 : format == VK_FORMAT_R8G8B8A8_UNORM ? 4 : 8; + const auto guest = FindGuestTextureFormat(format, bytes); + if (!guest || ResolveTextureFormat(*guest) != format || BytesPerElement(*guest) != bytes) std::abort(); + if (FindGuestTextureFormat(format, bytes + 1)) std::abort(); + } + if (FindGuestTextureFormat(VK_FORMAT_B8G8R8A8_UNORM, 4)) std::abort(); + if (FindGuestTextureFormat(VK_FORMAT_UNDEFINED, 4)) std::abort(); + if (FindGuestTextureFormat(VK_FORMAT_BC1_RGBA_UNORM_BLOCK, 8)) std::abort(); +} From e424b6b7bfd625eeeb9e3146c631b0a536806f1a Mon Sep 17 00:00:00 2001 From: SamG-Coder <13449963+SamG-Coder@users.noreply.github.com> Date: Mon, 28 Sep 2026 09:55:01 +1000 Subject: [PATCH 7/7] fix(agc): preserve recorder buffers through Windows thread teardown --- core/libs/CMakeLists.txt | 10 +++++ .../libSceAgcDriver/Graphics/src/Recorder.cpp | 44 ++++++++++--------- .../libs/prx/libc/include/HostThreadLocal.hpp | 28 ++++++++++++ core/libs/tests/HostThreadLocal.cpp | 24 ++++++++++ core/libs/tests/HostThreadLocalLibrary.cpp | 31 +++++++++++++ 5 files changed, 117 insertions(+), 20 deletions(-) create mode 100644 core/libs/prx/libc/include/HostThreadLocal.hpp create mode 100644 core/libs/tests/HostThreadLocal.cpp create mode 100644 core/libs/tests/HostThreadLocalLibrary.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index cb33a343f..762c9b1d7 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -109,6 +109,16 @@ endforeach() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_library(host_thread_local_test_library SHARED tests/HostThreadLocalLibrary.cpp) + target_include_directories(host_thread_local_test_library PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + add_executable(host_thread_local_tests tests/HostThreadLocal.cpp) + target_link_libraries(host_thread_local_tests PRIVATE host_thread_local_test_library) + if(MINGW) + target_compile_options(host_thread_local_test_library PRIVATE -fasynchronous-unwind-tables) + target_compile_options(host_thread_local_tests PRIVATE -fasynchronous-unwind-tables) + endif() + add_test(NAME host_thread_local COMMAND host_thread_local_tests) + set_tests_properties(host_thread_local PROPERTIES TIMEOUT 15) add_executable(texture_format_lookup_tests tests/TextureFormatLookup.cpp) target_include_directories(texture_format_lookup_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include) target_link_libraries(texture_format_lookup_tests PRIVATE libSceAgcDriver libc) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 8c11f3ccd..d9184240f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1,3 +1,4 @@ +#include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" @@ -276,7 +277,8 @@ struct DeferredBatch { std::vector> kept; std::vector> completions; }; -thread_local std::vector deferredBatches; +struct DeferredBatchesTag {}; +auto& DeferredBatches() { return HostThreadLocal, DeferredBatchesTag>(); } std::atomic deferredPending{0}; // APS5_PROFILE_DRAW, for the [recorder] line: batches and objects released after an unlock and the // time that took, on the release thread and inline (on the unlocking thread: the kill switch, a @@ -392,11 +394,11 @@ void JoinReleaseThread() { // The unlock hook: hands what this thread deferred to the release thread, or destroys it here // (APS5_RELEASE_ON_UNLOCK=1, the queue is full or stopping, or the hand-off cannot be made). void ReleaseDeferredKeeps() { - if (deferredBatches.empty()) return; + if (DeferredBatches().empty()) return; // Taken off the thread's list first: a destructor that took and released the mutex (none is // known to) would re-enter here and must find nothing. - auto releasing = std::move(deferredBatches); - deferredBatches.clear(); + auto releasing = std::move(DeferredBatches()); + DeferredBatches().clear(); if (ReleaseOnUnlock()) { DestroyDeferred(std::move(releasing), false); return; @@ -475,10 +477,12 @@ Recorder* labelTableOwner = nullptr; // The ranges of the calling worker's queued labels (NoteQueuedLabel, cleared by // ForgetQueuedLabels): the flush hook, on the same thread, has them recorded before an access that // overlaps one, through the function the driver installed. -thread_local std::vector> queuedLabelRanges; +struct QueuedLabelRangesTag {}; +auto& QueuedLabelRanges() { return HostThreadLocal>, QueuedLabelRangesTag>(); } std::atomic queuedLabelRecorder{nullptr}; // The dwords (with their stamps) this thread noted since its last CloseLabelGroup (see Recorder::CloseLabelGroup). -thread_local std::vector> labelGroupDwords; +struct LabelGroupDwordsTag {}; +auto& LabelGroupDwords() { return HostThreadLocal>, LabelGroupDwordsTag>(); } // Late-rule counters (Recorder::LateCounts), per thread: lookups run on the waiting worker. thread_local Recorder::LateStatistics lateCounts{}; @@ -548,7 +552,7 @@ bool SeparateQueuedLabels() { bool QueuedLabelOverlaps(std::uint64_t address, std::size_t bytes) { const auto end = address + bytes; - for (const auto& [begin, finish] : queuedLabelRanges) { + for (const auto& [begin, finish] : QueuedLabelRanges()) { if (address < finish && begin < end) return true; } return false; @@ -943,7 +947,7 @@ void FlushForAccess(std::uint64_t address, std::size_t bytes) { // would reap nested in the reap in progress, landing later batches' write-backs before the // rest of this one's, and this queue's queued label follows every batch of its own in flight // anyway, so the write-back landing first is the order the hardware gives. - if (!queuedLabelRanges.empty() && QueuedLabelOverlaps(address, bytes)) { + if (!QueuedLabelRanges().empty() && QueuedLabelOverlaps(address, bytes)) { if (completionDepth != 0 && HookCompletionGuard()) { queuedLabelHookInCompletion.fetch_add(1, std::memory_order_relaxed); } else if (auto* record = queuedLabelRecorder.load(std::memory_order_acquire); record != nullptr) { @@ -1074,9 +1078,9 @@ Recorder::~Recorder() { // other threads (an unlock hook that found the queue stopping or full) are waited for before // the caller destroys the device. The count is global: a newer recorder's batches (device // replacement) are waited for too, which only prolongs the spin. - if (!deferredBatches.empty()) { - auto own = std::move(deferredBatches); - deferredBatches.clear(); + if (!DeferredBatches().empty()) { + auto own = std::move(DeferredBatches()); + DeferredBatches().clear(); DestroyDeferred(std::move(own), false); } JoinReleaseThread(); @@ -1160,9 +1164,9 @@ Recorder::LateStatistics Recorder::LateCounts() { } void Recorder::CloseLabelGroup(std::uint64_t trackerGeneration) { - if (labelGroupDwords.empty()) return; - auto group = std::move(labelGroupDwords); - labelGroupDwords.clear(); + if (LabelGroupDwords().empty()) return; + auto group = std::move(LabelGroupDwords()); + LabelGroupDwords().clear(); if (trackerGeneration == 0) return; std::lock_guard tableLock(labelTableMutex); if (labelTableOwner == nullptr) return; @@ -1183,7 +1187,7 @@ void Recorder::NoteQueuedLabel(std::uint64_t address, std::span if (bytes.size() < 4 || address % 4 != 0) return; // The range before the table: the hook covers the group whether or not a table exists to // enter it in (no owner: the callback records it outright, by a CPU store when need be). - queuedLabelRanges.emplace_back(address, address + bytes.size()); + QueuedLabelRanges().emplace_back(address, address + bytes.size()); queuedLabelsNoted.fetch_add(1, std::memory_order_relaxed); std::lock_guard tableLock(labelTableMutex); if (labelTableOwner == nullptr) return; @@ -1199,9 +1203,9 @@ void Recorder::NoteQueuedLabel(std::uint64_t address, std::span } void Recorder::ForgetQueuedLabels() { - if (queuedLabelRanges.empty()) return; - auto ranges = std::move(queuedLabelRanges); - queuedLabelRanges.clear(); + if (QueuedLabelRanges().empty()) return; + auto ranges = std::move(QueuedLabelRanges()); + QueuedLabelRanges().clear(); std::lock_guard tableLock(labelTableMutex); if (labelTableOwner == nullptr) return; auto& recorded = labelTableOwner->labels; @@ -2124,7 +2128,7 @@ void Recorder::noteLabelOn(Batch& batch, std::uint64_t address, std::span batch, bool wait, int source) { // destroys the batch's objects here, under the mutex, counts nothing the destructor would // wait for and must not escape (the rest of finish() releases the fence and command buffer). try { - deferredBatches.push_back({std::move(batch->kept), std::move(batch->completions)}); + DeferredBatches().push_back({std::move(batch->kept), std::move(batch->completions)}); deferredPending.fetch_add(1, std::memory_order_acq_rel); } catch (...) { } diff --git a/core/libs/prx/libc/include/HostThreadLocal.hpp b/core/libs/prx/libc/include/HostThreadLocal.hpp new file mode 100644 index 000000000..076459cc9 --- /dev/null +++ b/core/libs/prx/libc/include/HostThreadLocal.hpp @@ -0,0 +1,28 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_HOSTTHREADLOCAL_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_HOSTTHREADLOCAL_HPP + +#include +#include +#ifdef _WIN32 +#include +#endif + +template +TValue& HostThreadLocal() { +#ifdef _WIN32 + static const DWORD slot = FlsAlloc(+[](void* value) { delete static_cast(value); }); + if (slot == FLS_OUT_OF_INDEXES) throw std::runtime_error("host thread-local slot allocation failed"); + auto* value = static_cast(FlsGetValue(slot)); + if (value == nullptr) { + auto created = std::make_unique(); + if (!FlsSetValue(slot, created.get())) throw std::runtime_error("host thread-local value registration failed"); + value = created.release(); + } + return *value; +#else + static thread_local TValue value; + return value; +#endif +} + +#endif diff --git a/core/libs/tests/HostThreadLocal.cpp b/core/libs/tests/HostThreadLocal.cpp new file mode 100644 index 000000000..4fedf466a --- /dev/null +++ b/core/libs/tests/HostThreadLocal.cpp @@ -0,0 +1,24 @@ +#include +#include +#ifdef _WIN32 +#include +#endif + +extern "C" void TouchHostThreadLocal(); +extern "C" unsigned DestroyedHostThreadLocals(); + +int main() { + for (unsigned i = 0; i < 64; ++i) { + std::thread worker([] { TouchHostThreadLocal(); TouchHostThreadLocal(); }); + worker.join(); + if (DestroyedHostThreadLocals() != (i + 1) * 2) std::abort(); + } +#ifdef _WIN32 + for (unsigned i = 0; i < 64; ++i) { + const auto worker = CreateThread(nullptr, 0, +[](void*) -> DWORD { TouchHostThreadLocal(); return 0; }, nullptr, 0, nullptr); + if (!worker || WaitForSingleObject(worker, 5000) != WAIT_OBJECT_0) std::abort(); + CloseHandle(worker); + if (DestroyedHostThreadLocals() != 128 + (i + 1) * 2) std::abort(); + } +#endif +} diff --git a/core/libs/tests/HostThreadLocalLibrary.cpp b/core/libs/tests/HostThreadLocalLibrary.cpp new file mode 100644 index 000000000..9f11e94d2 --- /dev/null +++ b/core/libs/tests/HostThreadLocalLibrary.cpp @@ -0,0 +1,31 @@ +#include "prx/libc/include/HostThreadLocal.hpp" +#include +#include +#include + +namespace { +std::atomic destroyed{0}; +struct Value { + std::vector data = std::vector(256, 42); + ~Value() { + for (auto item : data) if (item != 42) std::abort(); + ++destroyed; + } +}; +struct First {}; +struct Second {}; +} + +#ifdef _WIN32 +#define TEST_EXPORT __declspec(dllexport) +#else +#define TEST_EXPORT +#endif + +extern "C" TEST_EXPORT void TouchHostThreadLocal() { + auto& first = HostThreadLocal(); + auto& second = HostThreadLocal(); + if (&first == &second || &first != &HostThreadLocal()) std::abort(); +} + +extern "C" TEST_EXPORT unsigned DestroyedHostThreadLocals() { return destroyed.load(); }