fix(libSceAgc): fuse geometry halves with the back half's SH registers only

The firmware writes only the back half's SH register list into the
caller's memory, and its size query reports eight bytes per back
register for geometry halves too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SNqSvnFwRBz3D76n476yw6
This commit is contained in:
hypermistandClaude Opus 5.5 committed 2026-10-23 19:19:53 +13:00
1 parent 0db47b263d
commit 4629573c16
3 files changed
+5 -15

No files matched your search

@@ -21,7 +21,6 @@ namespace {
using namespace ShaderRegs;
constexpr int GRAPHICS5_ERROR_INVALID_SHADER_HALVES = static_cast<int>(0x8a6c0008u);
constexpr std::uint32_t SPI_SHADER_PGM_CHKSUM_GS = 0x080u;
constexpr std::uint32_t SPI_SHADER_PGM_RSRC1_GS = 0x08Au;
constexpr std::uint32_t SPI_SHADER_PGM_RSRC2_GS = 0x08Bu;
constexpr std::uint32_t SPI_SHADER_PGM_CHKSUM_HS = 0x100u;
@@ -161,11 +160,6 @@ GeometryLayout ComputeGeometryLayout(const char* function, const Shader* front,
if (reg.offset == SPI_SHADER_PGM_RSRC1_GS) reg.value = resources[0];
else if (reg.offset == SPI_SHADER_PGM_RSRC2_GS) reg.value = resources[1];
}
for (std::uint32_t i = 0; i < front->num_sh_registers; ++i) {
const auto& reg = front->sh_registers[i];
if (reg.offset == SPI_SHADER_PGM_CHKSUM_GS || FindHalfRegister(back, reg.offset) != nullptr) continue;
layout.sh.push_back(reg);
}
layout.cx.assign(back->cx_registers, back->cx_registers + back->num_cx_registers);
for (std::uint32_t i = 0; i < front->num_cx_registers; ++i) {
const auto& reg = front->cx_registers[i];
@@ -358,12 +352,7 @@ int APS5_VABI sceAgcFuseShaderHalves_nid_postfix(Shader* fused_result, const Sha
int APS5_VABI sceAgcGetFusedShaderSize_0080(SizeAlign* dst, const Shader* front, const Shader* back) {
if (dst == nullptr || front == nullptr || back == nullptr) APS5_INVALID_ARG_EX;
if (!ValidHalves(front, back)) return GRAPHICS5_ERROR_INVALID_SHADER_HALVES;
if (GeometryHalves(front)) {
const auto layout = ComputeGeometryLayout("sceAgcGetFusedShaderSize_0080", front, back);
dst->m_size = layout.sh.size() * sizeof(ShaderRegister);
dst->m_align = 4;
return 0;
}
if (GeometryHalves(front)) ComputeGeometryLayout("sceAgcGetFusedShaderSize_0080", front, back);
dst->m_size = std::uint64_t{back->num_sh_registers} * sizeof(ShaderRegister);
dst->m_align = 4;
return 0;
@@ -129,7 +129,7 @@ void testFusion(std::size_t misalignment, std::uint32_t terminator = SSetpcS6) {
Halves halves(terminator);
SizeAlign size{};
check(sceAgcGetFusedShaderSize_0080(&size, &halves.front, &halves.back) == 0, "fused size query failed");
check(size.m_size == 7 * sizeof(ShaderRegister) && size.m_align == 4, "fused size is not the fused register list");
check(size.m_size == 6 * sizeof(ShaderRegister) && size.m_align == 4, "fused size is not the back half's register list");
std::vector<std::uint8_t> storage(size.m_size + 512, 0xcd);
auto* scratch = aligned(storage, 256) + misalignment;
Shader fused{};
@@ -149,9 +149,9 @@ void testFusion(std::size_t misalignment, std::uint32_t terminator = SSetpcS6) {
check(frontLo != nullptr && frontLo->value == static_cast<std::uint32_t>(base >> 8u), "fused program address was not patched");
check(find(fused, 0x08Au)->value == ((1u << 24u) | (1u << 6u) | 5u), "RSRC1 register counts were not merged");
check(find(fused, 0x08Bu)->value == ((6u << 1u) | (1u << 7u) | (1u << 15u)), "RSRC2 user SGPRs were not taken from the front half");
check(find(fused, 0x0B2u) != nullptr && find(fused, 0x0B2u)->value == 0x55u, "front-only SH register was lost");
check(find(fused, 0x0B2u) == nullptr, "a front-only SH register was added to the fused list");
check(find(fused, 0x080u) == nullptr, "front program checksum was kept");
check(fused.num_sh_registers == 7 && fused.num_cx_registers == 2, "fused register counts are wrong");
check(fused.num_sh_registers == halves.back.num_sh_registers && fused.num_cx_registers == 2, "fused register counts are wrong");
check(fused.cx_registers[0].offset == 0x29Bu && fused.cx_registers[0].value == 2u && fused.cx_registers[1].offset == 0x2D5u, "context registers were not merged");
check(fused.num_input_semantics == 2 && fused.input_semantics[1].semantic == 2, "front inputs were not kept");
check(fused.num_output_semantics == 1 && fused.output_semantics[0].semantic == 3, "back outputs were not kept");
+1
View File
@@ -38,6 +38,7 @@
- [sceAgcSuspendPointAndCheckStatus](../../../core/libs/prx/libSceAgc/Export.cpp) - this named callable export is listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAgc.c) and was cross-checked against the same module in PS5 firmware 13.60. Its signature and behaviour are unverified; the declaration uses the no-parameter unknown-signature convention and throws through `NotImplemented_nid_no_patch`.
### Functional
- [sceAgcFuseShaderHalves_0200](../../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - for geometry halves the firmware (12.00, read from disassembly, not measured on a console) copies the back half's header, writes only the back half's SH registers into the caller's memory and patches them there, and `sceAgcGetFusedShaderSize_0080` reports eight bytes per back register for both pair types. The SH list here matches that, but the fused program is rebuilt in memory libSceAgc owns: the front code falls through into a copy of the back code, the program addresses point at that copy, and the context registers merge in the front half's registers that the back half lacks, where the firmware keeps the back half's list.
- [sceAgcDebugRaiseException](../../../core/libs/prx/libSceAgc/Misc/src/Debug.cpp) (libSceAgc) - logs the exception id and returns 0, as retail hardware does nothing per [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c); the trap (`ud2`) OpenAGC describes for development hardware is not reproduced. Neither is verified on a console. The `uint32` exception id is [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/6f0cddd4404ec5203e71fea77e75fc373299ba3f/src/libs/agc.cpp)'s claim; OpenAGC declares no arguments, so the logged id may be leftover register contents
- [sceAgcSetAmmSemaphoreMemory](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) - native registration first invokes a kernel ioctl and driver registration; host registration does not reproduce their memory-access checks or error propagation. Null, empty and address-wrapping ranges throw. The full range is cleared after successful registration; duplicate registration, alignment and label-range errors match the native wrapper.
- [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the driver call records only the stream id (1 to 31; it returns 0 for any argument and keeps no stream record), and the DCB calls write the packets [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) writes: an 18-dword (active) or 12-dword (complete) `IT_NOP` packet with the stream id, and for active the 64-bit mask of the workload ids, for complete the workload id and the 64-bit clear mask of that id. Both return null for a null command buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. The layouts and checks come from KytyPS5 alone; the console oracle in [prosper](https://github.com/mattias800/prosper/blob/main/prosper/tests/data/console_oracle/agcbuild.golden.tsv) measures the null return only for unregistered streams, not the packet contents of a registered one. The error codes the console driver returns for an invalid or repeated registration are unknown, so registration never fails here. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed.