From cd1f76c8315424bc8317fe6ad0b80b2c05406212 Mon Sep 17 00:00:00 2001 From: EdouardSence <103503538+EdouardSence@users.noreply.github.com> Date: Thu, 8 Oct 2026 23:05:20 +0200 Subject: [PATCH] fix(agc): accept bound copies of a registered shader header that differ only in user_data Co-Authored-By: Claude Opus 5.5 --- .../src/Driver/Shaders/ShaderRegistry.cpp | 19 ++++++++++++++++--- .../tests/execution/PreparedShaders.cpp | 11 +++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 585355b3b..06e22c8ba 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -12,6 +12,7 @@ #include "CompiledVariant.hpp" #include "Optimization/ResourceProgram.hpp" #include +#include #include #include #include @@ -161,6 +162,18 @@ void ShaderPreparationTransaction::Commit() { committed = true; } +bool SameHeader(const ShaderSnapshot& snapshot, const Shader* shader) { + if (snapshot.headerAddress == reinterpret_cast(shader)) return true; + if (snapshot.header.size() < sizeof(Shader)) return false; + GuestMemory::CheckRange(shader, sizeof(Shader), 1); + Shader used; + Shader registered; + std::memcpy(&used, static_cast(shader), sizeof(Shader)); + std::memcpy(®istered, snapshot.header.data(), sizeof(Shader)); + used.user_data = registered.user_data; + return std::memcmp(&used, ®istered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) == 0; +} + void PublishRegisteredShader(std::shared_ptr& registry, const std::shared_ptr& snapshot) { ShaderPreparationTransaction transaction; if (registry != nullptr) { @@ -514,7 +527,7 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std require(registry != nullptr && registry->contains(address), "graphics ABI refers to an unregistered shader"); if (owner == nullptr) owner = registry->at(address); const auto& snapshot = *registry->at(address); - require(snapshot.headerAddress == reinterpret_cast(shader), "graphics ABI refers to a replaced shader header"); + require(SameHeader(snapshot, shader), "graphics ABI refers to a replaced shader header"); require(snapshot.registeredState != nullptr, "registered shader state is missing"); const auto& registered = *snapshot.registeredState; for (const auto& [offset, value] : registered.shader) state.shader.insert_or_assign(offset, value); @@ -579,7 +592,7 @@ void Driver::ResolveShaderAbi(const Shader* shader, std::span(const_cast(shader->code)); require(shaders != nullptr && shaders->contains(address), "static ABI refers to an unregistered shader"); snapshot = shaders->at(address); - require(snapshot->headerAddress == reinterpret_cast(shader), "static ABI refers to a replaced shader header"); + require(SameHeader(*snapshot, shader), "static ABI refers to a replaced shader header"); } require(snapshot->registeredState != nullptr, "registered shader state is missing"); QueueState state{}; @@ -641,7 +654,7 @@ void Driver::ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std:: const auto address = reinterpret_cast(const_cast(shader->code)); require(shaders != nullptr && shaders->contains(address), "rectangle ABI refers to an unregistered shader"); const auto snapshot = shaders->at(address); - require(snapshot->headerAddress == reinterpret_cast(shader), "rectangle ABI refers to a replaced shader header"); + require(SameHeader(*snapshot, shader), "rectangle ABI refers to a replaced shader header"); return snapshot; }; front = lookup(vertex); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 883ce2786..316e88907 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -297,6 +299,15 @@ void Registration(bool indirect) { })); } for (auto& registration : registrations) registration.get(); + Shader copy = header.shader; + copy.user_data = reinterpret_cast(©); + AgcDriverResolveShaderAbi_nid_postfix(©, {}, {}); + alignas(Shader) std::array padded{}; + std::memcpy(padded.data(), ©, sizeof(Shader)); + padded.back() = std::byte{0x7d}; + AgcDriverResolveShaderAbi_nid_postfix(reinterpret_cast(padded.data()), {}, {}); + copy.target ^= 1u; + ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(©, {}, {}); }, "replaced shader header"); header.registers[1].value |= 0x100u; ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "invalid registered program address"); header.registers[1].value &= 0xffu; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4fbe5130e..941c7205d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -245,6 +245,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes) - [Resource registration](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws - [Shader registration](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a shader header is accepted at any address: PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary; whether the console refuses an alignment is unknown +- [Shader header copies](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a header bound for a draw or dispatch at another address than the registered one is accepted when its fixed fields match the registered header except `user_data`; the registered header's metadata is used. PPSA21567 binds such a copy of a vertex shader header with `user_data` set to null. Whether the console reads the copy's `user_data` is unknown - [sceNgs2VoiceControl sampler filter](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason - [sceRtcGetDosTime, sceRtcSetDosTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date