From 4e18def4c166e012172cd1460f8563e9f2a4069f Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:24:35 +0200 Subject: [PATCH] fix(agc): accept unaligned shader headers Driver::RegisterShader required the Shader header at an 8-byte boundary (alignof(Shader)) and read its fields through the pointer. PPSA26344 uses its shader packs in place: its compute shader objects point at headers 4 and 1 bytes past an 8-byte boundary, so the first sceAgcCreateShader of such a header threw "null or misaligned address". The header is now checked for null and readable memory at any address, and its fixed fields and the registers the trace prints are copied out with memcpy. The code address must still be 256-byte aligned, and the header and size checks are unchanged. The console's rule is unknown and is listed in TechnicalDebt. --- .../src/Driver/Shaders/ShaderRegistry.cpp | 39 ++++++++++++------- .../libs/prx/libSceAgcDriver/tests/Submit.cpp | 31 +++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 56 insertions(+), 15 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 8ba86476a..85acc59f5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -163,25 +163,34 @@ std::uint64_t NullPixelProgramAddress() { void Driver::RegisterShader(const Shader* shader) { CheckFailure(); - GuestMemory::CheckRange(shader, sizeof(Shader), alignof(Shader)); - require(shader->file_header == 0x34333231u && shader->version == 0x18u, "invalid shader header"); - require(shader->header_size >= sizeof(Shader), "shader header is smaller than its fixed fields"); - require(shader->shader_size != 0 && (shader->shader_size & 3u) == 0, "invalid shader size"); - GuestMemory::CheckRange(shader, shader->header_size, alignof(Shader)); - const auto* code = const_cast(shader->code); - GuestMemory::CheckRange(code, shader->shader_size, 256); - ShaderSnapshot snapshot{reinterpret_cast(code), reinterpret_cast(shader), shader->type, {}, {}}; - snapshot.code.resize(shader->shader_size / sizeof(std::uint32_t)); - std::memcpy(snapshot.code.data(), code, shader->shader_size); - snapshot.header.resize(shader->header_size); - std::memcpy(snapshot.header.data(), shader, shader->header_size); + GuestMemory::CheckRange(shader, sizeof(Shader), 1); + Shader fields; + std::memcpy(&fields, static_cast(shader), sizeof(Shader)); + require(fields.file_header == 0x34333231u && fields.version == 0x18u, "invalid shader header"); + require(fields.header_size >= sizeof(Shader), "shader header is smaller than its fixed fields"); + require(fields.shader_size != 0 && (fields.shader_size & 3u) == 0, "invalid shader size"); + GuestMemory::CheckRange(shader, fields.header_size, 1); + const auto* code = const_cast(fields.code); + GuestMemory::CheckRange(code, fields.shader_size, 256); + ShaderSnapshot snapshot{reinterpret_cast(code), reinterpret_cast(shader), fields.type, {}, {}}; + snapshot.code.resize(fields.shader_size / sizeof(std::uint32_t)); + std::memcpy(snapshot.code.data(), code, fields.shader_size); + snapshot.header.resize(fields.header_size); + std::memcpy(snapshot.header.data(), static_cast(shader), fields.header_size); static const char* traceRegs = std::getenv("APS5_TRACE_SHADER_REGS"); if (traceRegs != nullptr && (std::string(traceRegs) == "all" || std::strtoull(traceRegs, nullptr, 16) == snapshot.codeAddress)) { - std::fprintf(stderr, "[shader] 0x%llx type %u cx", static_cast(snapshot.codeAddress), shader->type); - for (std::uint32_t i = 0; i < shader->num_cx_registers && shader->cx_registers != nullptr; ++i) std::fprintf(stderr, " %x=%08x", shader->cx_registers[i].offset, shader->cx_registers[i].value); + const auto print = [](const ShaderRegister* registers, std::uint32_t count) { + for (std::uint32_t i = 0; i < count && registers != nullptr; ++i) { + ShaderRegister value; + std::memcpy(&value, static_cast(registers + i), sizeof(value)); + std::fprintf(stderr, " %x=%08x", value.offset, value.value); + } + }; + std::fprintf(stderr, "[shader] 0x%llx type %u cx", static_cast(snapshot.codeAddress), fields.type); + print(fields.cx_registers, fields.num_cx_registers); std::fprintf(stderr, " sh"); - for (std::uint32_t i = 0; i < shader->num_sh_registers && shader->sh_registers != nullptr; ++i) std::fprintf(stderr, " %x=%08x", shader->sh_registers[i].offset, shader->sh_registers[i].value); + print(fields.sh_registers, fields.num_sh_registers); std::fprintf(stderr, "\n"); } std::lock_guard lock(mutex); diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index 1e7f5d2a1..d611db473 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -11,7 +11,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -374,6 +376,34 @@ void testMultiSubmissions() { } } +void testShaderHeaderAlignment() { + alignas(256) static const std::array code{0xbf810000}; + alignas(8) static std::array storage{}; + Shader shader{}; + shader.file_header = 0x34333231; + shader.version = 0x18; + shader.header_size = sizeof(Shader); + shader.shader_size = sizeof(code); + shader.code = code.data(); + const auto at = [](std::size_t offset, const Shader& fields) { + std::memcpy(storage.data() + offset, &fields, sizeof(fields)); + return reinterpret_cast(storage.data() + offset); + }; + for (const std::size_t offset : {0, 4, 1}) AgcDriverRegisterShader_nid_postfix(at(offset, shader)); + const auto refused = [](const std::string& message, const char* reason) { check(message.find(reason) != std::string::npos, message.c_str()); }; + refused(expectFailure([] { AgcDriverRegisterShader_nid_postfix(nullptr); }), "null or misaligned address"); + refused(expectFailure([] { AgcDriverRegisterShader_nid_postfix(reinterpret_cast(0x1001)); }), "not readable"); + Shader misplaced = shader; + misplaced.code = code.data() + 1; + refused(expectFailure([&] { AgcDriverRegisterShader_nid_postfix(at(4, misplaced)); }), "null or misaligned address"); + Shader older = shader; + older.version = 0x17; + refused(expectFailure([&] { AgcDriverRegisterShader_nid_postfix(at(1, older)); }), "invalid shader header"); + Shader truncated = shader; + truncated.header_size = sizeof(Shader) - 4; + refused(expectFailure([&] { AgcDriverRegisterShader_nid_postfix(at(4, truncated)); }), "smaller than its fixed fields"); +} + void testWorkerFailure() { std::array words{0xc0031500, 1, 1, 1, 0x41}; Packet packet{words.data(), static_cast(words.size()), 0, {}}; @@ -470,6 +500,7 @@ int main() { testWaitFreeSubmissionBehindHeldOne(); testWaitFreeSubmissionTheCpuWaitsFor(); testMultiSubmissions(); + testShaderHeaderAlignment(); testWorkerFailure(); check(expectFailure([] { LibcRunShutdown_nid_postfix(); }).find("required shader register") != std::string::npos, "shutdown lost worker failure"); std::puts("AGC driver submit tests passed"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index dbd9ee2f3..dd9d86092 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -165,6 +165,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceShareGetCurrentStatus](../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - validation (feature flag non-zero, status non-null) and the all-zero 16-byte status taken from the [KytyPS5](https://github.com/KytyPS5/KytyPS5) reimplementation, not confirmed on a PS5 title; the meaning of the recording status values is unknown - [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 - [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 - [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes; `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does - [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty