diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 5a87210c8..3ec74d28b 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -64,5 +64,6 @@ add_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL Execution/src/GuestMemory.cpp ) target_include_directories(agc_driver_graphics_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include) +target_link_libraries(agc_driver_graphics_tests PRIVATE libc) set_target_properties(agc_driver_graphics_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_graphics_tests) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp index a19a03bb3..a6d73085d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp @@ -15,7 +15,7 @@ using Registers = std::map; struct QueueState { Registers shader; Registers context; - Registers userConfig; + Registers userConfig{{0x24b, 0}}; std::optional savedContext; std::array constantRam{}; std::uint64_t indexBase = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/README.md b/core/libs/prx/libSceAgcDriver/Graphics/README.md index acb9bd88a..15c518b5e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/README.md +++ b/core/libs/prx/libSceAgcDriver/Graphics/README.md @@ -24,6 +24,8 @@ Missing required registers and unsupported active state raise exceptions. Unsupported resources are never replaced with empty buffers or dummy images. The renderer never substitutes a clear for a failed draw. +Primitive restart uses the PS5 user-config register `GE_MULTI_PRIM_IB_RESET_EN` at `0x24B`, not the legacy context register at `0x2A5`. Queue creation/reset initializes it to zero; context-only clear preserves it. This matches `UserConfig::m_primitive_reset_control` and `HwUcSetMultiPrimIbReset` in `raw/source/KytyPS5-main/src/graphics/guest_gpu`, and the user-config bank of the project's AGC register defaults. Missing entries still fail at read time. Legacy context stream-output registers are not required for the supported primitive-generation stage; transform-feedback SPIR-V remains unsupported. + The target's existing contents are uploaded before rendering. A render pass uses LOAD/STORE, and the finished image and writable shader buffers are copied back to guest memory after the GPU fence completes. Queue cleanup on an exceptional completion path must finish before Vulkan objects used by the queue can be destroyed; cleanup does not retry the draw. This path deliberately creates transient resources per draw. It provides no resource or pipeline cache. It does not establish a tiled VideoOut rendering path: VideoOut currently expects tiled display memory, while this renderer accepts linear color surfaces only. Tiled rendering, textures/samplers, depth/stencil, additional PM4 draw variants and presentation extension compatibility remain separate work. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 282c0d154..f4810f8d2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -4,13 +4,18 @@ #include #include #include +#include namespace AgcDriver::Graphics { namespace { -std::uint32_t read(const Registers& registers, std::uint32_t offset) { +std::uint32_t read(const Registers& registers, std::uint32_t offset, const char* bank = "context") { const auto it = registers.find(offset); - Require(it != registers.end(), "missing register at DWORD " + std::to_string(offset)); + if (it == registers.end()) { + std::ostringstream message; + message << "missing register in " << bank << " bank at DWORD 0x" << std::hex << offset << " (" << std::dec << offset << ')'; + throw std::runtime_error("AGC graphics: " + message.str()); + } return it->second; } @@ -20,8 +25,8 @@ float readFloat(const Registers& registers, std::uint32_t offset) { return value; } -void zero(const Registers& registers, std::uint32_t offset, std::uint32_t mask, const char* name) { - Require((read(registers, offset) & mask) == 0, std::string(name) + " is unsupported"); +void zero(const Registers& registers, std::uint32_t offset, std::uint32_t mask, const char* name, const char* bank = "context") { + Require((read(registers, offset, bank) & mask) == 0, std::string(name) + " is unsupported"); } VkBlendFactor blendFactor(std::uint32_t value) { @@ -78,16 +83,14 @@ void intersect(VkRect2D& result, const Registers& registers, std::uint32_t offse State DecodeState(const QueueState& queue) { const auto& cx = queue.context; State result{}; - const auto primitive = read(queue.userConfig, 0x242); + const auto primitive = read(queue.userConfig, 0x242, "user-config"); switch (primitive) { case 4: result.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; break; case 5: result.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN; break; case 6: result.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; break; default: throw std::runtime_error("AGC graphics: unsupported primitive type " + std::to_string(primitive)); } - zero(cx, 0x2a5, ~0u, "primitive restart"); - zero(cx, 0x2e5, ~0u, "stream output"); - zero(cx, 0x2e6, ~0u, "stream output buffers"); + zero(queue.userConfig, 0x24b, ~0u, "primitive restart (GE_MULTI_PRIM_IB_RESET_EN)", "user-config"); Require(read(cx, 0x2d5) == 0x2000u, "only a wave64 primitive-generation vertex stage without tessellation or geometry amplification is supported"); zero(cx, 0x1b6, 0x8000u, "wave32 fragment shaders"); zero(cx, 0x207, ~0u, "clip distances, layer, viewport or auxiliary vertex exports"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c53022d7d..c1b6d488d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -14,7 +14,7 @@ AgcDriver::QueueState makeState() { AgcDriver::QueueState queue; queue.userConfig[0x242] = 4; queue.context = { - {0x2a5, 0}, {0x2e5, 0}, {0x2e6, 0}, {0x2d5, 0x2000}, + {0x2d5, 0x2000}, {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, @@ -55,11 +55,25 @@ void expectFailure(TAction action, std::string_view reason) { } void stateTests() { + AgcDriver::QueueState initial; + Require(initial.userConfig.at(0x24b) == 0, "primitive restart must be disabled in initial queue state"); + initial.userConfig[0x24b] = 1; + initial.ClearContext(); + Require(initial.userConfig.at(0x24b) == 1, "context clear must preserve user configuration"); + initial = AgcDriver::QueueState{}; + Require(initial.userConfig.at(0x24b) == 0, "queue reset must disable primitive restart"); auto queue = makeState(); auto state = AgcDriver::Graphics::DecodeState(queue); Require(state.color.address == reinterpret_cast(colorMemory.data()) && state.color.bytes == colorMemory.size(), "render-target address or size changed"); Require(state.viewport.y == 4 && state.viewport.height == -4, "negative viewport height was lost"); Require(state.color.format == VK_FORMAT_R8G8B8A8_UNORM, "RGBA format changed"); + queue.userConfig[0x24b] = 1; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "GE_MULTI_PRIM_IB_RESET_EN"); + queue = makeState(); + queue.userConfig.erase(0x24b); + queue.context[0x2a5] = 0; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "user-config bank at DWORD 0x24b"); + queue = makeState(); queue.context[0x90] = 0x80010003; queue.context[0x91] = 0x30020; state = AgcDriver::Graphics::DecodeState(queue);