From c415a17c9f369eee7d6f713dc2f3293283ea0f12 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 14 Oct 2026 04:39:33 -0400 Subject: [PATCH 1/2] feat(audio-propagation): track source-owned configurable path slots --- .../AudioPropagation.hpp | 35 ++++++++ .../prx/libSceAudioPropagation/Export.cpp | 85 ++++++++++++++++--- core/libs/tests/GuestAudioPropagation.cpp | 63 +++++++++++++- docs/dev/debt/libSceAudioPropagation.md | 4 +- 4 files changed, 173 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp index 602d531cfa39..04c66be7d147 100644 --- a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp +++ b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEAUDIOPROPAGATION_AUDIOPROPAGATION_HPP #define CORE_LIBS_PRX_LIBSCEAUDIOPROPAGATION_AUDIOPROPAGATION_HPP +#include #include #include @@ -23,6 +24,39 @@ inline constexpr std::size_t PortalParamsSize = 0x60; inline constexpr std::uint32_t PathEntryId = 0x010107d9; inline constexpr std::size_t PathEntrySize = 0x28; +inline constexpr std::uint32_t MaxPaths = 36; +inline constexpr std::uint32_t MaxPathPoints = 4; +inline constexpr std::uint32_t PathId = 0x010107d3; +inline constexpr std::uint32_t PathPointId = 0x010107d2; +inline constexpr std::uint32_t PathInvalid = 1; +inline constexpr std::uint32_t PathPortal = 2; + +struct Vector3 { + float x; + float y; + float z; + std::uint32_t pad; +}; + +struct PathPoint { + AudioPropagationStructDescriptor desc; + AudioPropagationHandle material; + float gain; + Vector3 position; +}; + +struct AudioPath { + AudioPropagationStructDescriptor desc; + std::array points; + std::uint32_t numPoints; + std::uint32_t flags; + std::uint32_t pad; +}; + +static_assert(sizeof(Vector3) == 0x10); +static_assert(sizeof(PathPoint) == 0x30 && offsetof(PathPoint, position) == 0x1c); +static_assert(sizeof(AudioPath) == 0xe0 && offsetof(AudioPath, numPoints) == 0xd0); + struct Attribute { std::uint32_t id; std::uint32_t reserved0; @@ -78,6 +112,7 @@ std::int32_t APS5_VABI sceAudioPropagationSourceDestroy(AudioPropagationHandle s std::int32_t APS5_VABI sceAudioPropagationSourceSetAttributes(AudioPropagationHandle source, const AudioPropagation::Attribute* attributes, std::uint32_t count); std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPathCount(AudioPropagationHandle source, std::uint32_t* count); std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPath(AudioPropagationHandle source, std::uint32_t index, AudioPropagationHandle* outPath); +std::int32_t APS5_VABI sceAudioPropagationPathGetNumPoints(AudioPropagationHandle path, std::uint32_t* count); std::int32_t APS5_VABI sceAudioPropagationSourceGetRays(AudioPropagationHandle source, void* rays, std::uint32_t* count); std::int32_t APS5_VABI sceAudioPropagationSourceCalculateAudioPaths(AudioPropagationHandle source, const void* rays, std::uint32_t rayCount, std::uint32_t flags, void* paths, std::uint32_t pathCount); std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPaths(AudioPropagationHandle source, const void* entries, std::uint32_t count); diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index c3cdff194d2e..2349e207bc9e 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -1,5 +1,6 @@ #include "AudioPropagation.hpp" +#include #include #include #include @@ -24,18 +25,25 @@ struct SystemState { std::uint64_t reserved; }; -enum class Kind { Room, Portal, Source, Material }; +enum class Kind { Room, Portal, Source, Material, Path }; struct Object { Kind kind; AudioPropagationHandle system; }; +struct PathState { + AudioPath data{}; + float gain = 0.0f; +}; + struct Registry { std::mutex mutex; std::unordered_set systems; std::unordered_map objects; std::unordered_map orphans; + std::unordered_map> sourcePaths; + std::unordered_map paths; AudioPropagationHandle next = 1; }; @@ -98,6 +106,13 @@ void destroyObject(AudioPropagationHandle system, AudioPropagationHandle handle, return; } require(requireObject(state, handle, kind, function).system == system, function); + if (kind == Kind::Source) { + for (const auto path : state.sourcePaths.at(handle)) { + state.paths.erase(path); + state.objects.erase(path); + } + state.sourcePaths.erase(handle); + } state.objects.erase(handle); } @@ -140,9 +155,11 @@ std::int32_t APS5_VABI sceAudioPropagationSystemDestroy(AudioPropagationHandle s auto& state = registry(); std::lock_guard lock(state.mutex); requireSystem(state, system, __func__); + std::erase_if(state.paths, [&](const auto& entry) { return state.objects.at(entry.first).system == system; }); + std::erase_if(state.sourcePaths, [&](const auto& entry) { return state.objects.at(entry.first).system == system; }); std::erase_if(state.objects, [&](const auto& entry) { if (entry.second.system != system) return false; - state.orphans.insert(entry); + if (entry.second.kind != Kind::Path) state.orphans.insert(entry); return true; }); reinterpret_cast(system)->magic = 0; @@ -228,7 +245,30 @@ std::int32_t APS5_VABI sceAudioPropagationPortalSetAttributes(AudioPropagationHa } std::int32_t APS5_VABI sceAudioPropagationSourceCreate(AudioPropagationHandle system, AudioPropagationHandle* outSource) { - createObject(system, Kind::Source, outSource, __func__); + require(outSource != nullptr, __func__); + auto& state = registry(); + std::lock_guard lock(state.mutex); + AudioPropagationHandle source = 0; + createObjectLocked(state, system, Kind::Source, &source, __func__); + std::array handles{}; + try { + for (auto& handle : handles) { + createObjectLocked(state, system, Kind::Path, &handle, __func__); + PathState path{}; + path.data.desc = {PathId, sizeof(AudioPath)}; + path.data.flags = PathInvalid; + state.paths.emplace(handle, path); + } + state.sourcePaths.emplace(source, handles); + } catch (...) { + for (const auto handle : handles) { + state.paths.erase(handle); + state.objects.erase(handle); + } + state.objects.erase(source); + throw; + } + *outSource = source; return 0; } @@ -250,7 +290,7 @@ std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPathCount(AudioPropagati std::lock_guard lock(state.mutex); requireObject(state, source, Kind::Source, __func__); require(count != nullptr, __func__); - *count = 0; + *count = MaxPaths; return 0; } @@ -258,8 +298,18 @@ std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPath(AudioPropagationHan auto& state = registry(); std::lock_guard lock(state.mutex); requireObject(state, source, Kind::Source, __func__); - static_cast(outPath); - throw std::invalid_argument(std::string(__func__) + ": index " + std::to_string(index) + " out of range (0 audio paths)"); + require(outPath != nullptr && index < MaxPaths, __func__); + *outPath = state.sourcePaths.at(source)[index]; + return 0; +} + +std::int32_t APS5_VABI sceAudioPropagationPathGetNumPoints(AudioPropagationHandle path, std::uint32_t* count) { + auto& state = registry(); + std::lock_guard lock(state.mutex); + requireObject(state, path, Kind::Path, __func__); + require(count != nullptr, __func__); + *count = state.paths.at(path).data.numPoints; + return 0; } std::int32_t APS5_VABI sceAudioPropagationSourceGetRays(AudioPropagationHandle source, void* rays, std::uint32_t* count) { @@ -292,10 +342,19 @@ std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPaths(AudioPropagationHa } std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHandle path, const void* data, float gain) { - static_cast(path); - static_cast(data); - static_cast(gain); - NotImplemented_nid_no_patch(__func__); + auto& state = registry(); + std::lock_guard lock(state.mutex); + const auto system = requireObject(state, path, Kind::Path, __func__).system; + requireDescriptor(data != nullptr ? &static_cast(data)->desc : nullptr, PathId, sizeof(AudioPath), __func__); + const auto replacement = *static_cast(data); + require(std::isfinite(gain) && gain >= 0.0f && replacement.numPoints <= MaxPathPoints && (replacement.flags & ~(PathInvalid | PathPortal)) == 0 && replacement.pad == 0, __func__); + for (std::uint32_t index = 0; index < replacement.numPoints; ++index) { + const auto& point = replacement.points[index]; + requireDescriptor(&point.desc, PathPointId, sizeof(PathPoint), __func__); + require(std::isfinite(point.gain) && point.gain >= 0.0f && std::isfinite(point.position.x) && std::isfinite(point.position.y) && std::isfinite(point.position.z) && point.position.pad == 0, __func__); + if (point.material != 0) require(requireObject(state, point.material, Kind::Material, __func__).system == system, __func__); + } + state.paths.at(path) = PathState{replacement, gain}; return 0; } @@ -310,6 +369,12 @@ std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle sy require(requireObject(state, info.source, Kind::Source, __func__).system == system, __func__); require(info.output != nullptr && info.outputSize != 0, __func__); if (info.format != RenderFormat) throw std::runtime_error(std::string(__func__) + ": render format " + std::to_string(info.format) + " is unknown"); + for (const auto handle : state.sourcePaths.at(info.source)) { + const auto& path = state.paths.at(handle); + if (path.gain > 0.0f && path.data.numPoints != 0 && (path.data.flags & PathInvalid) == 0) { + throw std::runtime_error(std::string(__func__) + ": nonempty audio path rendering is not implemented"); + } + } } for (std::uint32_t index = 0; index < count; ++index) std::memset(infos[index].output, 0, infos[index].outputSize); return 0; diff --git a/core/libs/tests/GuestAudioPropagation.cpp b/core/libs/tests/GuestAudioPropagation.cpp index 8930cf679d74..9029925b56c6 100644 --- a/core/libs/tests/GuestAudioPropagation.cpp +++ b/core/libs/tests/GuestAudioPropagation.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace { @@ -114,9 +115,47 @@ int main() { RequireThrows([&] { sceAudioPropagationSourceCreate(system, nullptr); }, "source without output"); Require(sceAudioPropagationSourceSetAttributes(source, portalAttributes, 3) == 0, "source attributes failed"); std::uint32_t count = 7; - Require(sceAudioPropagationSourceGetAudioPathCount(source, &count) == 0 && count == 0, "source reports audio paths"); + Require(sceAudioPropagationSourceGetAudioPathCount(source, &count) == 0 && count == AudioPropagation::MaxPaths, "source path pool size"); AudioPropagationHandle path = 0; - RequireThrows([&] { sceAudioPropagationSourceGetAudioPath(source, 0, &path); }, "audio path without paths"); + Require(sceAudioPropagationSourceGetAudioPath(source, 0, &path) == 0 && path != 0, "path lookup immediately after source creation"); + AudioPropagationHandle repeated = 0; + Require(sceAudioPropagationSourceGetAudioPath(source, 0, &repeated) == 0 && repeated == path, "path handle is unstable"); + AudioPropagationHandle lastPath = 0; + Require(sceAudioPropagationSourceGetAudioPath(source, AudioPropagation::MaxPaths - 1, &lastPath) == 0 && lastPath != path, "last path slot"); + RequireThrows([&] { sceAudioPropagationSourceGetAudioPath(source, AudioPropagation::MaxPaths, &repeated); }, "out of range path slot"); + Require(repeated == path, "rejected path lookup changed the output"); + RequireThrows([&] { sceAudioPropagationSourceGetAudioPath(source, 0, nullptr); }, "null path output"); + Require(sceAudioPropagationPathGetNumPoints(path, &count) == 0 && count == 0, "initial path has points"); + RequireThrows([&] { sceAudioPropagationPathGetNumPoints(source, &count); }, "source used as a path"); + RequireThrows([&] { sceAudioPropagationPathGetNumPoints(path, nullptr); }, "null point-count output"); + AudioPropagation::AudioPath configured{}; + configured.desc = {AudioPropagation::PathId, sizeof(configured)}; + configured.numPoints = 2; + configured.flags = AudioPropagation::PathInvalid; + for (auto& point : configured.points) { + point.desc = {AudioPropagation::PathPointId, sizeof(point)}; + point.gain = 1.0f; + } + configured.points[0].position.x = 3.0f; + Require(sceAudioPropagationSourceSetAudioPath(path, &configured, 1.0f) == 0, "path configuration"); + configured.numPoints = 0; + Require(sceAudioPropagationPathGetNumPoints(path, &count) == 0 && count == 2, "path configuration aliases caller memory"); + configured.numPoints = 2; + auto badPath = configured; + badPath.desc.size--; + RequireThrows([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "wrong path size"); + badPath = configured; + badPath.numPoints = AudioPropagation::MaxPathPoints + 1; + RequireThrows([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "too many path points"); + badPath = configured; + badPath.points[0].material = rooms[0]; + RequireThrows([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "room used as a path material"); + badPath = configured; + badPath.points[0].position.x = std::numeric_limits::infinity(); + RequireThrows([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "nonfinite point position"); + RequireThrows([&] { sceAudioPropagationSourceSetAudioPath(path, &configured, std::numeric_limits::quiet_NaN()); }, "nonfinite path gain"); + RequireThrows([&] { sceAudioPropagationSourceSetAudioPath(path, nullptr, 1.0f); }, "null path data"); + Require(sceAudioPropagationPathGetNumPoints(path, &count) == 0 && count == 2, "rejected update changed path state"); std::uint8_t rays[AudioPropagation::RaySize * 2]{}; count = 7; @@ -139,6 +178,13 @@ int main() { Require(sceAudioPropagationSystemCreate(options, &otherMemory, &otherSystem) == 0, "second system create failed"); AudioPropagationHandle otherSource = 0; Require(sceAudioPropagationSourceCreate(otherSystem, &otherSource) == 0, "second system source create failed"); + AudioPropagationHandle foreignMaterial = 0; + Require(sceAudioPropagationSystemRegisterMaterial(otherSystem, material, &foreignMaterial) == 0, "foreign material create failed"); + badPath = configured; + badPath.points[0].material = foreignMaterial; + RequireThrows([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "material from another system"); + Require(sceAudioPropagationSystemUnregisterMaterial(foreignMaterial) == 0, "foreign material cleanup"); + Require(sceAudioPropagationPathGetNumPoints(path, &count) == 0 && count == 2, "cross-system rejection changed path state"); AudioPropagationHandle secondSource = 0; Require(sceAudioPropagationSourceCreate(system, &secondSource) == 0, "second source create failed"); @@ -200,6 +246,14 @@ int main() { RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 2); }, "render with an unknown format"); Require(untouched(), "render with an unknown format wrote an output"); + reset(); + configured.flags = 0; + Require(sceAudioPropagationSourceSetAudioPath(path, &configured, 1.0f) == 0, "active path configuration"); + RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 2); }, "active path silently rendered"); + Require(untouched(), "unsupported active-path render modified output"); + configured.flags = AudioPropagation::PathInvalid; + Require(sceAudioPropagationSourceSetAudioPath(path, &configured, 1.0f) == 0, "invalidating path"); + Require(sceAudioPropagationSourceDestroy(system, secondSource) == 0, "second source destroy failed"); reset(); RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 2); }, "render of a destroyed source"); @@ -209,13 +263,18 @@ int main() { RequireThrows([&] { sceAudioPropagationSourceDestroy(system + 16, source); }, "source destroy with another system"); Require(sceAudioPropagationPortalDestroy(system, portal) == 0, "portal destroy failed"); Require(sceAudioPropagationSourceDestroy(system, source) == 0, "source destroy failed"); + RequireThrows([&] { sceAudioPropagationPathGetNumPoints(path, &count); }, "path survived source destruction"); + RequireThrows([&] { sceAudioPropagationSourceSetAudioPath(lastPath, &configured, 1.0f); }, "path update after source destruction"); Require(sceAudioPropagationRoomDestroy(system, rooms[0]) == 0, "room destroy failed"); AudioPropagationHandle lateSource = 0; AudioPropagationHandle lateMaterial = 0; Require(sceAudioPropagationSourceCreate(system, &lateSource) == 0, "late source create failed"); + AudioPropagationHandle latePath = 0; + Require(sceAudioPropagationSourceGetAudioPath(lateSource, 0, &latePath) == 0 && latePath != path, "path handle reuse"); Require(sceAudioPropagationSystemRegisterMaterial(system, material, &lateMaterial) == 0, "late material register failed"); Require(sceAudioPropagationSystemDestroy(system) == 0, "system destroy failed"); + RequireThrows([&] { sceAudioPropagationPathGetNumPoints(latePath, &count); }, "path survived system destruction"); RequireThrows([&] { sceAudioPropagationSourceSetAttributes(lateSource, &attribute, 1); }, "source used after its system"); RequireThrows([&] { sceAudioPropagationRoomDestroy(system, lateSource); }, "orphan destroyed as the wrong kind"); RequireThrows([&] { sceAudioPropagationSourceDestroy(system + 16, lateSource); }, "orphan destroyed with another system"); diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index 0785892d9300..36796af56759 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -2,9 +2,9 @@ ### Silent stubs -- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested and sources report no audio paths. Ray results, path calculation and audio paths are accepted only when empty and throw otherwise; `sceAudioPropagationSourceRender` writes silence (zeroes) to each source's output buffer for the given size, without a dry signal. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified. +- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested; each source owns 36 configurable path slots. Ray results and path calculation are accepted only when empty and throw otherwise; active nonempty audio-path rendering throws; `sceAudioPropagationSourceRender` writes silence (zeroes) to each source's output buffer for the given size, without a dry signal. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified. ### Unknown function info -- [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation, NID `tKSmk2JsMAA`) - `(uint64_t path, const void* data, float gain)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs) and SAROS's call, which passes a handle from `sceAudioPropagationSourceGetAudioPath`, path data and gain 1.0. Path updates are unsupported and every call throws without reading or writing caller memory. The no-propagation model yields no path handles; path allocation, ownership, rendering and console error codes remain unknown. +- [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation, NID `tKSmk2JsMAA`) - `(uint64_t path, const void* data, float gain)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs) and SAROS's call, which passes a handle from `sceAudioPropagationSourceGetAudioPath`, path data and gain 1.0. Path updates validate and copy the public 224-byte path and 48-byte point layouts. Each source owns a fixed pool of 36 initially invalid, empty slots, following SharpProspero MaxPaths and SAROS's lookup of slot 0 immediately after source creation. Allocation before calculation and reporting 36 slots through the count query are inferred, not measured on a console. Destroying the source or system invalidates every owned path. Nonempty active rendering remains unimplemented; console error codes remain unknown. - [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation) - signatures and struct descriptors recovered from PPSA21567's calls (#481), not from documentation. The system options layout is unknown (only checked for non-null); the fourth argument of `sceAudioPropagationSourceCalculateAudioPaths` is unknown; `sceAudioPropagationSourceGetAudioPath` is assumed to output an 8-byte path handle. The meaning of the `RenderInfo` word at +0x28 is unknown (PPSA21567 and PPSA21564 pass 2); only 2 is accepted. Both titles pass one `RenderInfo` per call: a count of 0 throws, and larger counts are handled element by element without proof. No error code is known, so invalid input throws instead of returning one From db91769497bb4a8dfd993801e9ed6fb87e7d14f4 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 14 Oct 2026 04:50:09 -0400 Subject: [PATCH 2/2] fix(audio-propagation): preserve mixes with inactive paths across output layouts --- .../prx/libSceAudioPropagation/Export.cpp | 12 ++++++++-- core/libs/tests/GuestAudioPropagation.cpp | 22 +++++++++++-------- docs/dev/debt/libSceAudioPropagation.md | 2 +- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index 2349e207bc9e..8362813e3fb3 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -368,7 +368,16 @@ std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle sy requireDescriptor(&info.desc, RenderInfoId, RenderInfoSize, __func__); require(requireObject(state, info.source, Kind::Source, __func__).system == system, __func__); require(info.output != nullptr && info.outputSize != 0, __func__); - if (info.format != RenderFormat) throw std::runtime_error(std::string(__func__) + ": render format " + std::to_string(info.format) + " is unknown"); + std::uint32_t channels = 0; + switch (info.format) { + case 0: channels = 4; break; + case 1: channels = 9; break; + case 2: channels = 16; break; + case 4: + case 5: channels = 8; break; + default: throw std::runtime_error(std::string(__func__) + ": render format " + std::to_string(info.format) + " is unknown"); + } + require(info.outputSize % (channels * sizeof(float)) == 0, __func__); for (const auto handle : state.sourcePaths.at(info.source)) { const auto& path = state.paths.at(handle); if (path.gain > 0.0f && path.data.numPoints != 0 && (path.data.flags & PathInvalid) == 0) { @@ -376,7 +385,6 @@ std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle sy } } } - for (std::uint32_t index = 0; index < count; ++index) std::memset(infos[index].output, 0, infos[index].outputSize); return 0; } diff --git a/core/libs/tests/GuestAudioPropagation.cpp b/core/libs/tests/GuestAudioPropagation.cpp index 9029925b56c6..59cdff991839 100644 --- a/core/libs/tests/GuestAudioPropagation.cpp +++ b/core/libs/tests/GuestAudioPropagation.cpp @@ -190,12 +190,6 @@ int main() { float outputs[2][64]; const auto fill = [&] { std::memset(outputs, 0xab, sizeof(outputs)); }; - const auto zeroed = [&](int index) { - for (const float sample : outputs[index]) { - if (sample != 0.0f) return false; - } - return true; - }; const auto untouched = [&] { const auto* bytes = reinterpret_cast(outputs); for (std::size_t index = 0; index < sizeof(outputs); ++index) { @@ -211,13 +205,13 @@ int main() { }; reset(); Require(sceAudioPropagationSourceRender(system, infos, 1) == 0, "render failed"); - Require(zeroed(0), "render left the output unwritten"); + Require(untouched(), "empty paths erased the existing mix"); const auto* tail = reinterpret_cast(outputs[1]); Require(tail[0] == 0xab, "render wrote past the output size"); reset(); infos[0].outputSize = sizeof(float) * 16; Require(sceAudioPropagationSourceRender(system, infos, 2) == 0, "render of two sources failed"); - Require(outputs[0][15] == 0.0f && outputs[0][16] != 0.0f && zeroed(1), "render did not write each output for its size"); + Require(untouched(), "empty paths changed the existing mix of multiple sources"); reset(); RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 0); }, "render without infos"); @@ -242,10 +236,20 @@ int main() { infos[0].outputSize = 0; RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 1); }, "render with an empty output"); reset(); - infos[1].format = 1; + infos[1].format = 3; RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 2); }, "render with an unknown format"); Require(untouched(), "render with an unknown format wrote an output"); + for (const auto format : {0u, 1u, 2u, 4u, 5u}) { + reset(); + infos[0].format = format; + infos[0].outputSize = format == 1 ? 9u * sizeof(float) : 16u * sizeof(float); + Require(sceAudioPropagationSourceRender(system, infos, 1) == 0 && untouched(), "empty-path format handling changed the existing mix"); + infos[0].outputSize--; + RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 1); }, "partial output frame accepted"); + Require(untouched(), "rejected partial-frame render wrote output"); + } + reset(); configured.flags = 0; Require(sceAudioPropagationSourceSetAudioPath(path, &configured, 1.0f) == 0, "active path configuration"); diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index 36796af56759..8c790d4b1fe7 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -2,7 +2,7 @@ ### Silent stubs -- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested; each source owns 36 configurable path slots. Ray results and path calculation are accepted only when empty and throw otherwise; active nonempty audio-path rendering throws; `sceAudioPropagationSourceRender` writes silence (zeroes) to each source's output buffer for the given size, without a dry signal. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified. +- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested; each source owns 36 configurable path slots. Ray results and path calculation are accepted only when empty and throw otherwise; active nonempty audio-path rendering throws; `sceAudioPropagationSourceRender` accepts the public float output layouts (4, 9, 16 or 8 channels) for empty/invalid/muted paths and adds no contribution, preserving the caller's existing output mix. It validates whole output frames before returning; any active nonempty path still throws before writing. The additive contract and format numbers follow SharpProspero, not a console measurement. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified. ### Unknown function info