feat(libSceAudio3d): implement sceAudio3dObjectReserve and sceAudio3dObjectUnreserve

This commit is contained in:
Adrià Franch 2026-10-06 10:12:08 +02:00
parent def8119927
commit c3871dcb99
3 changed files with 64 additions and 1 deletions

View File

@ -4,6 +4,7 @@
#include <cstring>
#include <deque>
#include <mutex>
#include <set>
#include <stdexcept>
#include <string>
#include <thread>
@ -13,12 +14,14 @@
namespace {
constexpr int AUDIO3D_ERROR_INVALID_PORT = static_cast<int>(0x80EA0002);
constexpr int AUDIO3D_ERROR_INVALID_OBJECT = static_cast<int>(0x80EA0003);
constexpr int AUDIO3D_ERROR_INVALID_PARAMETER = static_cast<int>(0x80EA0004);
constexpr int AUDIO3D_ERROR_OUT_OF_RESOURCES = static_cast<int>(0x80EA0006);
constexpr int AUDIO3D_ERROR_NOT_READY = static_cast<int>(0x80EA0007);
constexpr int AUDIO3D_USER_ID_SYSTEM = 0xFF;
constexpr std::uint32_t AUDIO3D_PORT_ID = 0;
constexpr std::uint32_t AUDIO3D_OBJECT_INVALID = 0xFFFFFFFF;
constexpr std::uint32_t AUDIO3D_RATE_48000 = 0;
constexpr std::uint32_t AUDIO3D_SAMPLE_RATE = 48000;
constexpr std::uint32_t AUDIO3D_BUFFER_ADVANCE_AND_PUSH = 2;
@ -36,6 +39,9 @@ struct Audio3dPort {
Clock::duration frame_duration{};
std::uint32_t advanced = 0;
std::deque<Clock::time_point> playing;
std::uint32_t max_objects = 0;
std::uint32_t next_object_id = 0;
std::set<std::uint32_t> objects;
};
std::mutex g_mutex;
@ -136,6 +142,7 @@ int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* param
g_port = Audio3dPort{};
g_port.open = true;
g_port.queue_depth = parameters->queue_depth;
g_port.max_objects = parameters->max_objects;
g_port.frame_duration = std::chrono::duration_cast<Clock::duration>(
std::chrono::nanoseconds(std::uint64_t{parameters->granularity} * 1000000000u / AUDIO3D_SAMPLE_RATE));
*id = AUDIO3D_PORT_ID;
@ -179,4 +186,25 @@ int APS5_VABI sceAudio3dPortSetAttribute(uint32_t port_id, uint32_t attribute_id
}
}
int APS5_VABI sceAudio3dObjectReserve(uint32_t port_id, uint32_t* object_id) {
if (object_id == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER;
*object_id = AUDIO3D_OBJECT_INVALID;
std::lock_guard lock(g_mutex);
if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT;
if (g_port.objects.size() >= g_port.max_objects) return AUDIO3D_ERROR_OUT_OF_RESOURCES;
do {
++g_port.next_object_id;
} while (g_port.next_object_id == 0 || g_port.next_object_id == AUDIO3D_OBJECT_INVALID || g_port.objects.contains(g_port.next_object_id));
g_port.objects.insert(g_port.next_object_id);
*object_id = g_port.next_object_id;
return 0;
}
int APS5_VABI sceAudio3dObjectUnreserve(uint32_t port_id, uint32_t object_id) {
std::lock_guard lock(g_mutex);
if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT;
if (g_port.objects.erase(object_id) == 0) return AUDIO3D_ERROR_INVALID_OBJECT;
return 0;
}
}

View File

@ -16,15 +16,19 @@ int APS5_VABI sceAudio3dPortSetAttribute(std::uint32_t port_id, std::uint32_t at
int APS5_VABI sceAudio3dPortGetQueueLevel(std::uint32_t port_id, std::uint32_t* queue_level, std::uint32_t* queue_available);
int APS5_VABI sceAudio3dPortAdvance(std::uint32_t port_id);
int APS5_VABI sceAudio3dPortPush(std::uint32_t port_id, std::uint32_t blocking);
int APS5_VABI sceAudio3dObjectReserve(std::uint32_t port_id, std::uint32_t* object_id);
int APS5_VABI sceAudio3dObjectUnreserve(std::uint32_t port_id, std::uint32_t object_id);
}
namespace {
constexpr int INVALID_PORT = static_cast<int>(0x80EA0002);
constexpr int INVALID_OBJECT = static_cast<int>(0x80EA0003);
constexpr int INVALID_PARAMETER = static_cast<int>(0x80EA0004);
constexpr int OUT_OF_RESOURCES = static_cast<int>(0x80EA0006);
constexpr int NOT_READY = static_cast<int>(0x80EA0007);
constexpr int SYSTEM_USER = 0xFF;
constexpr std::uint32_t OBJECT_INVALID = 0xFFFFFFFF;
void Require(bool value, const char* message) {
if (value) return;
@ -140,6 +144,35 @@ void CheckAttributes() {
RequireThrows([&] { sceAudio3dPortSetAttribute(0, 0x10001, &value, 8); }, "wrong attribute size must throw");
}
void CheckObjects() {
std::uint32_t object = 7;
Require(sceAudio3dObjectReserve(0, nullptr) == INVALID_PARAMETER, "null object id");
Require(sceAudio3dObjectReserve(0, &object) == INVALID_PORT && object == OBJECT_INVALID, "reserve needs an open port");
Require(sceAudio3dObjectUnreserve(0, 1) == INVALID_PORT, "unreserve needs an open port");
Audio3dOpenParameters parameters = Defaults();
parameters.max_objects = 2;
std::uint32_t id = 7;
Require(Open(parameters, &id) == 0 && id == 0, "open");
object = 7;
Require(sceAudio3dObjectReserve(1, &object) == INVALID_PORT && object == OBJECT_INVALID, "reserve on port 1");
Require(sceAudio3dObjectReserve(0, &object) == 0 && object == 1, "first object is 1");
Require(sceAudio3dObjectReserve(0, &object) == 0 && object == 2, "second object is 2");
Require(sceAudio3dObjectReserve(0, &object) == OUT_OF_RESOURCES && object == OBJECT_INVALID, "max_objects is the limit");
Require(sceAudio3dObjectUnreserve(1, 1) == INVALID_PORT, "unreserve on port 1");
Require(sceAudio3dObjectUnreserve(0, 3) == INVALID_OBJECT, "unreserve of an unknown object");
Require(sceAudio3dObjectUnreserve(0, OBJECT_INVALID) == INVALID_OBJECT, "unreserve of the invalid object");
Require(sceAudio3dObjectUnreserve(0, 1) == 0, "unreserve");
Require(sceAudio3dObjectUnreserve(0, 1) == INVALID_OBJECT, "double unreserve");
Require(sceAudio3dObjectReserve(0, &object) == 0 && object == 3, "ids are not reused while the port is open");
Require(sceAudio3dObjectReserve(0, &object) == OUT_OF_RESOURCES, "full again");
Require(sceAudio3dPortClose(0) == 0, "close");
Require(Open(parameters, &id) == 0 && id == 0, "reopen");
Require(sceAudio3dObjectUnreserve(0, 2) == INVALID_OBJECT, "close drops the objects");
Require(sceAudio3dObjectReserve(0, &object) == 0 && object == 1, "ids restart on a new port");
Require(sceAudio3dObjectReserve(0, &object) == 0 && object == 2, "a new port has the full limit");
Require(sceAudio3dPortClose(0) == 0, "close");
}
void CheckQueue() {
using namespace std::chrono;
constexpr std::uint32_t granularity = 0x1800;
@ -201,6 +234,7 @@ int main() {
CheckBeforeInitialize();
CheckOpenParameters();
CheckOpenClose();
CheckObjects();
CheckQueue();
CheckTerminate();
}

View File

@ -22,7 +22,7 @@ Throughout the project, every function at every stage either **does exactly what
- [libSceSystemService](../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick`, `sceSystemServiceReportAbnormalTermination`, `sceSystemServiceDisableMusicPlayer` and `sceSystemServiceReenableMusicPlayer` return success and do nothing: the host has no auto power-off timer to reset, no system crash reporter to notify and no system music player to pause or resume
- [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) validates the priority and CPU affinity that the open param requests for the VideoOut service thread but does not apply them: the port's present and vblank threads are host threads, and guest priorities and affinities do not reach host scheduling
- Without `VK_KHR_fragment_shader_barycentric` the shader recompiler [does not load the barycentric VGPRs](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp): [`v_interp_p1_f32` is dropped and `v_interp_p2_f32` reads the attribute Vulkan interpolated](../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp), so pixel shaders that use the barycentrics for anything other than `v_interp` get wrong values.
- [libSceAudio3d](../../core/libs/prx/libSceAudio3d/Export.cpp) - the port produces no sound: the library has no bed or object exports to give it audio, so `sceAudio3dPortPush` only paces the queue at `granularity` samples per frame at 48 kHz. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them
- [libSceAudio3d](../../core/libs/prx/libSceAudio3d/Export.cpp) - the port produces no sound: objects can be reserved but the library has no bed exports or `sceAudio3dObjectSetAttributes` to give them audio, so `sceAudio3dPortPush` only paces the queue at `granularity` samples per frame at 48 kHz. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them
- [libSceAvPlayer](../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement.
- [libSceNpTrophy2](../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as no trophy is ever unlocked, and `sceNpTrophy2UnregisterUnlockCallback` only returns success.
- [sceAvPlayerSetTrickSpeed](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time.
@ -47,6 +47,7 @@ Throughout the project, every function at every stage either **does exactly what
- [s_setkill](../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified
- [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - accepted format assumed to be the ISO 8601 subset `YYYY-MM-DDTHH:MM:SS[.ffffff]` (T/t/space separator, optional fractional seconds) with the sibling RFC 3339 zone suffix (`Z` or `±HH:MM`), plus the RFC 2822 shape `Ddd, DD Mon YYYY HH:MM:SS[ GMT| ±HHMM]` and the asctime shape `Ddd Mon DD HH:MM:SS YYYY` (day zero- or space-padded) that shadPS4 parses at fixed positions; the weekday name is not checked against the date, a `±HHMM` offset is subtracted to get UTC like the RFC 3339 path (shadPS4 adds it), a string without a zone is taken as UTC, and anything outside these shapes throws instead of returning a parse error, unverified against hardware
- [sceAudio3dPortOpen](../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - buffer modes 0 and 1 (also selected by the 0x10 and 0x18 parameter sizes) and 3 beds depend on an SDK version check in the module ([prosper](https://github.com/mattias800/prosper)) and throw. A repeated `sceAudio3dInitialize` (`NOT_READY` in shadPS4, 0 in prosper) and `sceAudio3dPortAdvance` on a full queue (`NOT_READY` in shadPS4, unhandled in KytyPS5) throw too
- [sceAudio3dObjectReserve](../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF
- [AMPR WriteAddressFromCounterPairOnCompletion](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order
- [AMPR command sizes](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_AMPR_ERROR_BUFFER_FULL` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes
- [AMPR counter access](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and checks of `WaitOnCounter_04_00`, `WriteCounter_04_00`, `ConstructNop` and `ConstructMarker` taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands counter commands to the console and does not show their effect. The counters are taken as 32 bits each: the 8-byte access of counter n spans n (low) and n + 1 (high), the 2- and 1-byte accesses select a half or a byte of the counter by its offset, waits compare at the access width (signed and wrapped compares included), the AND mask applies to both sides and the atomic writes wrap within the access. The `ConstructNop` type is not recorded