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 <noreply@anthropic.com>
(cherry picked from commit aeaa5080d9595305aa73e74d966f989c88b79839)
This commit is contained in:
David 2026-10-11 14:04:57 +02:00 committed by MarcZX100
parent b33d2e3736
commit 88cd5bbde5
3 changed files with 45 additions and 16 deletions

View File

@ -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<unsigned>(reg.offset), static_cast<unsigned>(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<ShaderRegister, 2> 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<ShaderRegister, 3> primitiveValues{gs->specials->ge_cntl, hsUserVgprEnable, ShaderRegister{ShaderRegs::VGT_PRIMITIVE_TYPE, prim_type}};
std::array<ShaderRegister, 3> 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<const Shader*, 2> stages{hs, gs};
const bool patch = prim_type == static_cast<std::uint32_t>(ShaderRegs::PrimitiveType::Patch);
if (patch == ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_HS_BIT) != 0)) {

View File

@ -7,6 +7,7 @@
#include <array>
#include <span>
#include <stdexcept>
#include <string>
#include <iostream>
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<std::uint8_t>(ShaderBinaryType::Gs);
preRaster.specials = &handBuilt;
std::array<ShaderRegister, 2> context{};
std::array<ShaderRegister, 3> 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<std::uint32_t>(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<void>(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<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> 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';

View File

@ -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