From 88cd5bbde5bc77923522845ae3b7fed3e32a9b17 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 11 Oct 2026 14:04:57 +0200 Subject: [PATCH] fix(libSceAgc): accept a hand-built GE_USER_VGPR register offset in sceAgcCreatePrimState EVO Player (PPSA99039), Jelly5 (PPSA99505), Emby5 (PPSA99515) and Nuvio PS5 (PPSA99997) stop on main 0f863c10 with what(): sceAgcCreatePrimState: invalid user configuration register offsets All four build their shader headers by hand (EVO-PLAYER-PS5 evo_agc_shader_header.c, ported from ps5-xash3d-halflife) and store the ge_user_vgpr_en special at 0x25c (GE_USER_VGPR1, value 0) instead of 0x262 (GE_USER_VGPR_EN), as analysed in #3457. They run on the console. OpenAGC's reconstruction of the FW 5.50 sceAgcCreatePrimState (src/game_compat.c at 3c7e8072) copies the four special register pairs verbatim without checking their offsets; KytyPS5's offset checks (src/libs/agc.cpp at 16a7fe68) are EXIT_NOT_IMPLEMENTED guards. The special is now copied at the offset the header stores, not moved to 0x262; the driver reads neither register. GE_CNTL, VGT_SHADER_STAGES_EN and a used VGT_GS_OUT_PRIM_TYPE are still required at their own offsets because the driver reads them there, and the error now names the special and the offset found. With the change all four titles get past sceAgcCreatePrimState and stop in the shader recompiler (GetImageResource dword 3 is not a valid runtime value), as #3457 reported for EVO Player. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit aeaa5080d9595305aa73e74d966f989c88b79839) --- .../prx/libSceAgc/Shader/src/PrimState.cpp | 34 ++++++++++--------- .../prx/libSceAgc/tests/ShaderHelpers.cpp | 26 ++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp index e03b491b2..587ae15a3 100644 --- a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp @@ -11,6 +11,18 @@ #include "prx/libSceAgc/Shader/include/ShaderUtils.hpp" #include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" +namespace { + +void RequireSpecialOffset(const ShaderRegister& reg, std::uint32_t expected, const char* name) { + if (reg.offset != expected) { + char message[160]; + std::snprintf(message, sizeof(message), "sceAgcCreatePrimState: %s special register at offset 0x%x, expected 0x%x", name, static_cast(reg.offset), static_cast(expected)); + throw std::runtime_error(message); + } +} + +} + extern "C" { int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_regs, const Shader* hs, const Shader* gs, std::uint32_t prim_type) { @@ -25,32 +37,22 @@ int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_ throw std::runtime_error(std::string(__func__) + ": invalid shader type or missing special registers"); } (void)GraphicsPrimTypeToGsOut(prim_type); - const auto validStages = [](const Shader* shader) { return shader->specials->vgt_shader_stages_en.offset == ShaderRegs::VGT_SHADER_STAGES_EN; }; - if (!validStages(gs) || (hs != nullptr && !validStages(hs))) { - throw std::runtime_error(std::string(__func__) + ": invalid context register offsets"); - } - const auto normalizeUserVgprEnable = [](ShaderRegister reg) { - if (reg.offset == ShaderRegs::GE_USER_VGPR1 && reg.value == 0) reg.offset = ShaderRegs::GE_USER_VGPR_EN; - return reg; - }; - const auto gsUserVgprEnable = normalizeUserVgprEnable(gs->specials->ge_user_vgpr_en); - const auto hsUserVgprEnable = hs != nullptr ? normalizeUserVgprEnable(hs->specials->ge_user_vgpr_en) : gsUserVgprEnable; - if (gs->specials->ge_cntl.offset != ShaderRegs::GE_CNTL || gsUserVgprEnable.offset != ShaderRegs::GE_USER_VGPR_EN || (hs != nullptr && hsUserVgprEnable.offset != ShaderRegs::GE_USER_VGPR_EN)) { - throw std::runtime_error(std::string(__func__) + ": invalid user configuration register offsets"); - } + RequireSpecialOffset(gs->specials->vgt_shader_stages_en, ShaderRegs::VGT_SHADER_STAGES_EN, "VGT_SHADER_STAGES_EN"); + if (hs != nullptr) RequireSpecialOffset(hs->specials->vgt_shader_stages_en, ShaderRegs::VGT_SHADER_STAGES_EN, "VGT_SHADER_STAGES_EN"); + RequireSpecialOffset(gs->specials->ge_cntl, ShaderRegs::GE_CNTL, "GE_CNTL"); std::array contextValues{gs->specials->vgt_shader_stages_en, gs->specials->vgt_gs_out_prim_type}; if (hs != nullptr) { contextValues[0].value |= hs->specials->vgt_shader_stages_en.value; } if ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_GS_BIT) != 0) { - if (gs->specials->vgt_gs_out_prim_type.offset != ShaderRegs::VGT_GS_OUT_PRIM_TYPE) throw std::runtime_error(std::string(__func__) + ": invalid context register offsets"); + RequireSpecialOffset(gs->specials->vgt_gs_out_prim_type, ShaderRegs::VGT_GS_OUT_PRIM_TYPE, "VGT_GS_OUT_PRIM_TYPE"); } else if (hs != nullptr) { - if (hs->specials->vgt_gs_out_prim_type.offset != ShaderRegs::VGT_GS_OUT_PRIM_TYPE) throw std::runtime_error(std::string(__func__) + ": invalid context register offsets"); + RequireSpecialOffset(hs->specials->vgt_gs_out_prim_type, ShaderRegs::VGT_GS_OUT_PRIM_TYPE, "VGT_GS_OUT_PRIM_TYPE"); contextValues[1] = hs->specials->vgt_gs_out_prim_type; } else { contextValues[1] = {ShaderRegs::VGT_GS_OUT_PRIM_TYPE, GraphicsPrimTypeToGsOut(prim_type)}; } - std::array primitiveValues{gs->specials->ge_cntl, hsUserVgprEnable, ShaderRegister{ShaderRegs::VGT_PRIMITIVE_TYPE, prim_type}}; + std::array primitiveValues{gs->specials->ge_cntl, hs != nullptr ? hs->specials->ge_user_vgpr_en : gs->specials->ge_user_vgpr_en, ShaderRegister{ShaderRegs::VGT_PRIMITIVE_TYPE, prim_type}}; const std::array stages{hs, gs}; const bool patch = prim_type == static_cast(ShaderRegs::PrimitiveType::Patch); if (patch == ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_HS_BIT) != 0)) { diff --git a/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp b/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp index 2dce67750..ce8451e0e 100644 --- a/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp +++ b/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister*, ShaderRegister*, const void*, const Shader*, const Shader*, std::uint32_t); @@ -48,6 +49,30 @@ void OptionalGeometryOutputRegister(ShaderSpecialRegs& special, const Shader& ve special.vgt_shader_stages_en.value = 0; special.vgt_gs_out_prim_type = {VGT_GS_OUT_PRIM_TYPE, 2}; } + +void HandBuiltUserVgprRegister() { + using namespace ShaderRegs; + ShaderSpecialRegs handBuilt{}; + handBuilt.ge_cntl = {GE_CNTL, 0x100u}; + handBuilt.vgt_shader_stages_en = {VGT_SHADER_STAGES_EN, 0x2412010u}; + handBuilt.vgt_gs_out_prim_type = {0x2ceu, 2}; + handBuilt.ge_user_vgpr_en = {0x25cu, 0}; + Shader preRaster{}; + preRaster.type = static_cast(ShaderBinaryType::Gs); + preRaster.specials = &handBuilt; + std::array context{}; + std::array primitive{}; + preparations = 0; + Require(sceAgcCreatePrimState(context.data(), primitive.data(), nullptr, &preRaster, 4) == 0); + Require(context[0].offset == VGT_SHADER_STAGES_EN && context[0].value == 0x2412010u && context[1].offset == VGT_GS_OUT_PRIM_TYPE && context[1].value == static_cast(GsOutputPrimitiveType::Triangles)); + Require(primitive[0].offset == GE_CNTL && primitive[0].value == 0x100u && primitive[1].offset == 0x25cu && primitive[1].value == 0 && primitive[2].offset == VGT_PRIMITIVE_TYPE && primitive[2].value == 4); + Require(preparations == 1 && stageCount == 1 && preparedPrimitive[1].offset == 0x25cu && preparedPrimitive[1].value == 0); + handBuilt.ge_cntl.offset = 0x25cu; + std::string message; + try { static_cast(sceAgcCreatePrimState(context.data(), primitive.data(), nullptr, &preRaster, 4)); } + catch (const std::runtime_error& error) { message = error.what(); } + Require(message.find("GE_CNTL") != std::string::npos && message.find("0x25c") != std::string::npos); +} } extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span stages, std::span context, std::span primitive) { @@ -181,6 +206,7 @@ int main() { Require(sceAgcCreateInterpolantMapping_0100(interpolants.data(), &vertex, nullptr) == 0); Require(preparations == 0 && mappings == 0 && links == 0); OptionalGeometryOutputRegister(special, vertex, hull); + HandBuiltUserVgprRegister(); std::cout << "shader helper preparation tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 04123a3c1..bad8319a2 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -598,3 +598,4 @@ Throughout the project, every function at every stage either **does exactly what - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. +- [sceAgcCreatePrimState](../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well