From dadc23a8c19b0d42b15e6f593257d8a0c2da612e Mon Sep 17 00:00:00 2001 From: lorkydey Date: Mon, 12 Oct 2026 23:12:40 +0200 Subject: [PATCH] fix(libSceNgs2): reject unverified listener transforms --- core/libs/prx/libSceNgs2.native/src/Geom.cpp | 49 ++---------- .../prx/libSceNgs2.native/tests/Ngs2Geom.cpp | 79 +++++++++++-------- docs/dev/debt/libSceNgs2.native.md | 2 +- 3 files changed, 55 insertions(+), 75 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Geom.cpp b/core/libs/prx/libSceNgs2.native/src/Geom.cpp index f6049c096..f8ab93148 100644 --- a/core/libs/prx/libSceNgs2.native/src/Geom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Geom.cpp @@ -1,59 +1,26 @@ -#include #include #include -#include #include "prx/libc/include/General.hpp" #include "Ngs2Internal.hpp" static constexpr float DEFAULT_SOUND_SPEED = 343.0f; -namespace { - -using Vector = std::array; - -Vector ReadVector(const Ngs2GeomVector& value) { - if (!std::isfinite(value.x) || !std::isfinite(value.y) || !std::isfinite(value.z)) APS5_INVALID_ARG_EX; - return {value.x, value.y, value.z}; -} - -double Dot(const Vector& left, const Vector& right) { - return left[0] * right[0] + left[1] * right[1] + left[2] * right[2]; -} - -Vector Unit(Vector value) { - const double length = std::hypot(value[0], value[1], value[2]); - if (!(length > 0.0)) APS5_INVALID_ARG_EX; - for (auto& component : value) component /= length; - return value; -} - -} - int Ngs2GeomCalcListener(const Ngs2GeomListenerParam* param, Ngs2GeomListenerWork* outWork, std::uint32_t flags) { if (param == nullptr) APS5_INVALID_ARG_EX; if (outWork == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - if (flags > 1) throw std::runtime_error("NGS2: unknown listener coordinate flags"); + if (flags != 0) throw std::runtime_error("NGS2: listener coordinate flags are unsupported"); + if (param->position.x != 0.0f || param->position.y != 0.0f || param->position.z != 0.0f) + throw std::runtime_error("NGS2: listener translation is unsupported"); + if (param->orient_front.x != 0.0f || param->orient_front.y != 0.0f || param->orient_front.z != 1.0f || + param->orient_up.x != 0.0f || param->orient_up.y != 1.0f || param->orient_up.z != 0.0f) + throw std::runtime_error("NGS2: non-default listener orientation is unsupported"); + if (!std::isfinite(param->velocity.x) || !std::isfinite(param->velocity.y) || !std::isfinite(param->velocity.z)) APS5_INVALID_ARG_EX; if (!std::isfinite(param->sound_speed) || param->sound_speed <= 0.0f) APS5_INVALID_ARG_EX; - const Vector position = ReadVector(param->position); - ReadVector(param->velocity); - const Vector front = Unit(ReadVector(param->orient_front)); - Vector up = Unit(ReadVector(param->orient_up)); - Vector right = Unit({up[1] * front[2] - up[2] * front[1], up[2] * front[0] - up[0] * front[2], up[0] * front[1] - up[1] * front[0]}); - up = {front[1] * right[2] - front[2] * right[1], front[2] * right[0] - front[0] * right[2], front[0] * right[1] - front[1] * right[0]}; - if (flags == 1) for (auto& component : right) component = -component; - const std::array axes{right, up, front}; Ngs2GeomListenerWork work{}; - for (std::size_t row = 0; row < axes.size(); row++) { - for (std::size_t column = 0; column < 3; column++) work.matrix[row][column] = static_cast(axes[row][column]); - const double translation = -Dot(axes[row], position); - if (std::abs(translation) > std::numeric_limits::max()) APS5_INVALID_ARG_EX; - work.matrix[row][3] = static_cast(translation); - } - work.matrix[3][3] = 1.0f; + for (std::size_t i = 0; i < 4; i++) work.matrix[i][i] = 1.0f; work.velocity = param->velocity; work.sound_speed = param->sound_speed; - work.coordinate = flags; *outWork = work; return SCE_NGS2_OK; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp index 67d472244..91a377793 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp @@ -60,15 +60,22 @@ static Ngs2GeomListenerParam Listener() { static void RequireMatrix(const Ngs2GeomListenerWork& work, const float (&expected)[4][4]) { for (std::size_t row = 0; row < 4; row++) { - for (std::size_t column = 0; column < 4; column++) Require(std::abs(work.matrix[row][column] - expected[row][column]) < 0.00001f); + for (std::size_t column = 0; column < 4; column++) Require(work.matrix[row][column] == expected[row][column]); } } -static void TestListenerTransform() { +static void TestDefaultListener() { auto param = Listener(); + Ngs2GeomListenerWork work; + std::memset(&work, 0x5a, sizeof(work)); + Require(sceNgs2GeomCalcListener(¶m, &work, 0) == SCE_NGS2_OK); + RequireMatrix(work, {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}}); + Require(work.velocity.x == 0.0f && work.velocity.y == 0.0f && work.velocity.z == 0.0f); + Require(work.sound_speed == SoundSpeed && work.coordinate == 0); + Require(work.reserved[0] == 0 && work.reserved[1] == 0 && work.reserved[2] == 0); + param.velocity = {2.0f, -3.0f, 4.0f}; param.sound_speed = 1500.0f; - Ngs2GeomListenerWork work; std::memset(&work, 0x5a, sizeof(work)); Require(sceNgs2GeomCalcListener(¶m, &work, 0) == SCE_NGS2_OK); RequireMatrix(work, {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}}); @@ -76,25 +83,11 @@ static void TestListenerTransform() { Require(work.sound_speed == 1500.0f && work.coordinate == 0); Require(work.reserved[0] == 0 && work.reserved[1] == 0 && work.reserved[2] == 0); - param.position = {3.0f, 4.0f, 5.0f}; + param.position = {-0.0f, -0.0f, -0.0f}; + param.orient_front = {-0.0f, -0.0f, 1.0f}; + param.orient_up = {-0.0f, 1.0f, -0.0f}; Require(sceNgs2GeomCalcListener(¶m, &work, 0) == SCE_NGS2_OK); - RequireMatrix(work, {{1, 0, 0, -3}, {0, 1, 0, -4}, {0, 0, 1, -5}, {0, 0, 0, 1}}); - - param.orient_front = {2.0f, 0.0f, 0.0f}; - param.orient_up = {1.0f, 3.0f, 0.0f}; - Require(sceNgs2GeomCalcListener(¶m, &work, 0) == SCE_NGS2_OK); - RequireMatrix(work, {{0, 0, -1, 5}, {0, 1, 0, -4}, {1, 0, 0, -3}, {0, 0, 0, 1}}); - - param.orient_front = {0.0f, 0.0f, -2.0f}; - param.orient_up = {0.0f, 3.0f, 0.0f}; - Require(sceNgs2GeomCalcListener(¶m, &work, 1) == SCE_NGS2_OK); - RequireMatrix(work, {{1, 0, 0, -3}, {0, 1, 0, -4}, {0, 0, -1, 5}, {0, 0, 0, 1}}); - Require(work.coordinate == 1); - - param.orient_front = {0.0f, 0.0f, std::numeric_limits::max()}; - param.orient_up = {0.0f, std::numeric_limits::min(), 0.0f}; - Require(sceNgs2GeomCalcListener(¶m, &work, 0) == SCE_NGS2_OK); - RequireMatrix(work, {{1, 0, 0, -3}, {0, 1, 0, -4}, {0, 0, 1, -5}, {0, 0, 0, 1}}); + RequireMatrix(work, {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}}); } static void RejectListener(const Ngs2GeomListenerParam* param, std::uint32_t flags) { @@ -112,12 +105,30 @@ static void RejectListener(const Ngs2GeomListenerParam* param, std::uint32_t fla Require(std::memcmp(before, &work, sizeof(work)) == 0); } +static void TestUnsupportedListener() { + auto param = Listener(); + for (const auto flags : {1u, 2u, 0xffffffffu}) RejectListener(¶m, flags); + for (const auto vector : {&Ngs2GeomListenerParam::position, &Ngs2GeomListenerParam::orient_front, &Ngs2GeomListenerParam::orient_up}) { + for (const auto component : {&Ngs2GeomVector::x, &Ngs2GeomVector::y, &Ngs2GeomVector::z}) { + for (const float value : {-1.0f, 0.0f, 1.0f, 2.0f, std::numeric_limits::min(), std::numeric_limits::max()}) { + param = Listener(); + auto& field = (param.*vector).*component; + if (field == value) continue; + field = value; + RejectListener(¶m, 0); + } + param = Listener(); + auto& field = (param.*vector).*component; + field = std::nextafter(field, 2.0f); + RejectListener(¶m, 0); + } + } +} + static void TestInvalidListener() { auto param = Listener(); Require(sceNgs2GeomCalcListener(¶m, nullptr, 0) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); RejectListener(nullptr, 0); - RejectListener(¶m, 2); - RejectListener(¶m, 0xffffffffu); for (const float value : {0.0f, -1.0f, std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN()}) { param.sound_speed = value; RejectListener(¶m, 0); @@ -131,22 +142,24 @@ static void TestInvalidListener() { param = Listener(); param.orient_up = {}; RejectListener(¶m, 0); - param = Listener(); - param.position.x = std::numeric_limits::infinity(); - RejectListener(¶m, 0); - param = Listener(); - param.velocity.z = std::numeric_limits::quiet_NaN(); - RejectListener(¶m, 0); - param = Listener(); - param.orient_front.y = std::numeric_limits::quiet_NaN(); - RejectListener(¶m, 0); + for (const auto vector : {&Ngs2GeomListenerParam::position, &Ngs2GeomListenerParam::orient_front, + &Ngs2GeomListenerParam::orient_up, &Ngs2GeomListenerParam::velocity}) { + for (const auto component : {&Ngs2GeomVector::x, &Ngs2GeomVector::y, &Ngs2GeomVector::z}) { + for (const float value : {std::numeric_limits::infinity(), -std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN()}) { + param = Listener(); + (param.*vector).*component = value; + RejectListener(¶m, 0); + } + } + } } int main() { TestLayouts(); TestResetListenerParam(); TestResetSourceParam(); - TestListenerTransform(); + TestDefaultListener(); + TestUnsupportedListener(); TestInvalidListener(); return 0; } diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 59d150a6f..ae8649422 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -23,4 +23,4 @@ - [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 - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason - [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` stays unimplemented -- [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) builds an orthonormal world-to-listener transform. The row-major matrix, front-axis priority and flag 1 for right-handed input follow the model described by [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/docs/development/dead-cells-ngs2-2026-10-11.md); they have not been measured on PS5 hardware. Velocity stays in world coordinates and positive finite sound speed is preserved. Unknown flags, invalid orientation, nonfinite input and an unrepresentable translation throw without changing the output; console error codes and handling of nonpositive sound speed are unknown. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334. This does not implement source attenuation, panning or Doppler in `sceNgs2GeomApply`. +- [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) supports only a listener at the origin with front +Z, up +Y and flags 0. The identity matrix and preserved finite velocity and positive finite sound speed follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig) for that case, not a PS5 measurement. Other orientations (including non-unit vectors), translations and flags throw without changing the output; the matrix convention, handedness flags and console error codes remain unverified. Nonfinite input and nonpositive sound speed also throw. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334, but its arguments have not been captured here. Source attenuation, panning and Doppler in `sceNgs2GeomApply` remain unimplemented