Merge pull request #5080 from ErenYeager1836/carry/libSceAgc-amm-semaphore

feat(libSceAgc): implement sceAgcSetAmmSemaphoreMemory and sceAgcGetSemaphoreLabel
This commit is contained in:
Adrià 2026-10-13 15:17:12 +02:00 committed by GitHub
commit 5aaa394cff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 244 additions and 10 deletions

View File

@ -33,6 +33,7 @@ add_library(libSceAgc SHARED EXCLUDE_FROM_ALL
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/PacketInfo.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Platform.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/RegisterDefaults.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Semaphore.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/ShaderFusion.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Suspend.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Patch/src/CondExec.cpp
@ -95,6 +96,19 @@ if(BUILD_TESTING)
endif()
endif()
add_test_executable(agc_semaphore_tests EXCLUDE_FROM_ALL tests/Semaphore.cpp)
target_include_directories(agc_semaphore_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_semaphore_tests PRIVATE libSceAgc libc)
configure_windows_unwind(agc_semaphore_tests)
if(BUILD_TESTING)
set_target_properties(agc_semaphore_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
add_test(NAME agc_semaphore_tests COMMAND agc_semaphore_tests)
if(WIN32)
set_tests_properties(agc_semaphore_tests PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_packet_info_tests EXCLUDE_FROM_ALL tests/PacketInfo.cpp)
target_include_directories(agc_packet_info_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_packet_info_tests PRIVATE libSceAgc libc)

View File

@ -0,0 +1,4 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_SEMAPHORE_HPP
#define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_SEMAPHORE_HPP
#endif

View File

@ -0,0 +1,61 @@
#include "prx/libSceAgc/Misc/include/Semaphore.hpp"
#include "prx/libSceAgc/Command/include/Packet.hpp"
#include <cstdint>
#include <cstddef>
#include <cstring>
#include <limits>
#include <mutex>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
namespace {
constexpr std::uint64_t semaphoreMemoryAlignment = 0x4000u;
constexpr std::uint64_t semaphoreLabelBytes = 32u;
constexpr std::uint64_t semaphoreCounterBytes = 8u;
std::mutex semaphoreMutex;
std::uint8_t* semaphoreMemory = nullptr;
std::uint64_t semaphoreBytes = 0;
bool hasOnlyZeroLabelTails(const std::uint8_t* memory, std::uint64_t sizeInBytes) {
for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) {
for (std::uint64_t byte = semaphoreCounterBytes; byte < semaphoreLabelBytes; ++byte) {
if (memory[label + byte] != 0) return false;
}
}
return true;
}
}
extern "C" {
int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes) {
const auto address = reinterpret_cast<std::uintptr_t>(memory);
std::lock_guard lock(semaphoreMutex);
Agc::Command::Require(semaphoreMemory == nullptr, __func__, "the AMM semaphore memory is already registered; the console's error is unknown");
Agc::Command::Require(memory != nullptr && sizeInBytes != 0, __func__, "null or empty AMM semaphore memory; the console's error is unknown");
Agc::Command::Require(((address | sizeInBytes) & (semaphoreMemoryAlignment - 1u)) == 0, __func__, "AMM semaphore memory or size is not 16 KiB aligned; the console's error is unknown");
Agc::Command::Require(sizeInBytes - 1u <= std::numeric_limits<std::uintptr_t>::max() - address, __func__, "AMM semaphore memory wraps the address space");
auto* labels = static_cast<std::uint8_t*>(memory);
Agc::Command::Require(hasOnlyZeroLabelTails(labels, sizeInBytes), __func__, "AMM semaphore labels hold data past their first 8 bytes; whether registration clears it is unknown");
for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) {
std::memset(labels + label, 0, semaphoreCounterBytes);
}
semaphoreMemory = labels;
semaphoreBytes = sizeInBytes;
return 0;
}
int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label) {
std::lock_guard lock(semaphoreMutex);
Agc::Command::Require(semaphoreMemory != nullptr, __func__, "no AMM semaphore memory is registered; the console's error is unknown");
Agc::Command::Require(index < semaphoreBytes / semaphoreLabelBytes, __func__, "the semaphore label lies outside the AMM semaphore memory; the console's error is unknown");
Agc::Command::Require(label != nullptr, __func__, "null semaphore label output; the console's error is unknown");
*label = semaphoreMemory + static_cast<std::uint64_t>(index) * semaphoreLabelBytes;
return 0;
}
}

View File

@ -35,16 +35,6 @@ int APS5_VABI sceAgcGetDefaultCxStateFlat() {
return 0;
}
int APS5_VABI sceAgcGetSemaphoreLabel() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcSetAmmSemaphoreMemory() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcSetSemaphoreMemory() {
NotImplemented_nid_no_patch(__func__);
return 0;

View File

@ -0,0 +1,164 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
#include <limits>
#include <memory>
#include <new>
#include <stdexcept>
#include <vector>
extern "C" int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes);
extern "C" int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label);
namespace {
constexpr std::uint64_t blockBytes = 0x4000u;
constexpr std::uint64_t labelBytes = 32u;
constexpr std::uint64_t counterBytes = 8u;
constexpr std::uint64_t memoryBytes = 2u * blockBytes;
constexpr std::uint64_t storageBytes = 4u * blockBytes;
constexpr std::uint32_t labelCount = static_cast<std::uint32_t>(memoryBytes / labelBytes);
constexpr std::uint8_t guardByte = 0xa5u;
constexpr std::uint8_t counterByte = 0x5au;
struct AlignedRelease {
void operator()(std::uint8_t* memory) const { ::operator delete(memory, std::align_val_t{blockBytes}); }
};
std::unique_ptr<std::uint8_t, AlignedRelease> storage;
void check(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
template <typename TAction>
void expectFailure(TAction action) {
try {
action();
} catch (const std::runtime_error& error) {
check(error.what()[0] != '\0', "empty exception message");
return;
}
throw std::runtime_error("expected an exception");
}
std::uint8_t* memory() {
return storage.get() + blockBytes;
}
std::vector<std::uint8_t> snapshot() {
return {storage.get(), storage.get() + storageBytes};
}
void checkUnchanged(const std::vector<std::uint8_t>& before, const char* message) {
check(std::equal(before.begin(), before.end(), storage.get()), message);
}
void fillStorage() {
std::fill(storage.get(), storage.get() + storageBytes, guardByte);
for (std::uint64_t label = 0; label < memoryBytes; label += labelBytes) {
std::fill(memory() + label, memory() + label + counterBytes, counterByte);
std::fill(memory() + label + counterBytes, memory() + label + labelBytes, std::uint8_t{0});
}
}
void testUnregistered() {
std::array<void*, 3> labels{storage.get(), storage.get(), storage.get()};
expectFailure([&] { sceAgcGetSemaphoreLabel(0, &labels[1]); });
expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); });
check(labels[0] == storage.get() && labels[1] == storage.get() && labels[2] == storage.get(), "a rejected semaphore label changed the output");
}
void testRegistrationRejections() {
const auto before = snapshot();
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, memoryBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), 0); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + 1, memoryBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory() + labelBytes, blockBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes - 1u); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), blockBytes + labelBytes); });
expectFailure([] {
const auto top = std::numeric_limits<std::uintptr_t>::max() & ~static_cast<std::uintptr_t>(blockBytes - 1u);
sceAgcSetAmmSemaphoreMemory(reinterpret_cast<void*>(top), memoryBytes);
});
checkUnchanged(before, "a rejected registration changed memory");
testUnregistered();
}
void testUnsettledClearing() {
for (const std::uint64_t offset : {5u * labelBytes + counterBytes, memoryBytes - 1u}) {
memory()[offset] = 0x77u;
const auto before = snapshot();
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); });
checkUnchanged(before, "a registration with unsettled clearing changed memory");
testUnregistered();
memory()[offset] = 0;
}
}
void testRegistration() {
const auto before = snapshot();
check(sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes) == 0, "registering the AMM semaphore memory failed");
check(std::all_of(memory(), memory() + memoryBytes, [](std::uint8_t byte) { return byte == 0; }), "registration did not clear the semaphore counters");
check(std::equal(before.begin(), before.begin() + blockBytes, storage.get()), "registration wrote before the semaphore memory");
check(std::equal(before.begin() + blockBytes + memoryBytes, before.end(), memory() + memoryBytes), "registration wrote past the semaphore memory");
}
void testLabels() {
for (const std::uint32_t index : {0u, 1u, 7u, labelCount / 2u, labelCount - 1u}) {
std::array<void*, 3> labels{storage.get(), nullptr, storage.get()};
check(sceAgcGetSemaphoreLabel(index, &labels[1]) == 0, "a semaphore label lookup failed");
check(labels[1] == memory() + index * labelBytes, "incorrect semaphore label address");
check(labels[0] == storage.get() && labels[2] == storage.get(), "a semaphore label lookup wrote past its output");
}
}
void testLabelRejections() {
for (const std::uint32_t index : {labelCount, labelCount + 1u, std::numeric_limits<std::uint32_t>::max()}) {
void* label = storage.get();
expectFailure([&] { sceAgcGetSemaphoreLabel(index, &label); });
check(label == storage.get(), "a rejected semaphore label changed the output");
}
expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); });
}
void testRepeatedRegistration() {
std::fill(memory(), memory() + memoryBytes, std::uint8_t{0x3c});
const auto before = snapshot();
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(storage.get(), blockBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); });
checkUnchanged(before, "a repeated registration changed memory");
void* label = nullptr;
check(sceAgcGetSemaphoreLabel(labelCount - 1u, &label) == 0 && label == memory() + memoryBytes - labelBytes, "a repeated registration replaced the semaphore memory");
}
}
int main() {
try {
storage.reset(static_cast<std::uint8_t*>(::operator new(storageBytes, std::align_val_t{blockBytes})));
fillStorage();
testUnregistered();
testRegistrationRejections();
testUnsettledClearing();
testRegistration();
testLabels();
testLabelRejections();
testRepeatedRegistration();
storage.reset();
LibcRunShutdown_nid_postfix();
std::puts("AGC semaphore tests passed");
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
try { LibcRunShutdown_nid_postfix(); }
catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); }
return 1;
}
}

View File

@ -34,6 +34,7 @@
### Functional
- [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, sceAgcGetSemaphoreLabel](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) (libSceAgc) - the `(memory, sizeInBytes)` and `(index, label)` signatures, the 16 KiB alignment of the memory and its size, the single registration, the 32-byte labels and the zeroed first 8 bytes of each are what [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L508-L546) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Agc/AgcExports.AmmSemaphore.cs) agree on; KytyPS5 has neither function. OpenAGC clears the whole memory and sharpemu only those 8 bytes, so a registration whose labels hold data past them throws. The two return different errors for every rejected call (OpenAGC `0x8a6c0002`, `0x8a6c000b`, `0x8a6c0048` and `0x8a6c0049`, sharpemu generic invalid-argument and already-exists errors), so every rejected call throws, including a lookup before registration. Not measured on the console; PPSA02801 imports both (OpenAGC's import analysis), no title is shown calling them.
- [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.
- [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - return null without writing to the buffer for a null command buffer or a stream that is not registered (out-of-range ids included), and throw for a registered stream. The console oracle in prosper measured only that null return, with no stream registered, three argument patterns each and nothing written ([ACB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L48-L56), [DCB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L196-L204)); the packet a registered stream gets is not measured. KytyPS5 has no body for these four, and its `IT_NOP` layout for the DCB active and complete calls differs from the SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/agc_workload_packet.c) derives from the driver, so neither is taken. The `(buf, streamId)`, `(buf, streamId, workloadId)` and `(buf, streamId, workloadIds, workloadCount)` signatures are those of the DCB calls in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L2317-L2384), which says the ACB calls use the same driver builders; no ACB declaration was found. `sceAgcDcbSetWorkloadsActive` checks the stream before it reads the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 ([measured](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L203)) without faulting
- [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