feat(libSceNgs2.native): implement sceNgs2PanInit and sceNgs2PanGetVolumeMatrix

Pan follows the PS4 firmware 11.00 libSceNgs2.sprx: the speaker layout
from the init angles, then each param's angle and distance into a
volume matrix for the voice. Every param is checked before anything is
written, and angles too large to wrap throw. The pan functions are
exported from the existing Voice.cpp export block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
JAB-dev 2026-10-09 23:26:24 +02:00
parent 9af48b5b02
commit 00e13ecfa7
10 changed files with 268 additions and 23 deletions

View File

@ -3,6 +3,7 @@ function(add_sce_ngs2_library target)
add_library(${target} SHARED EXCLUDE_FROM_ALL
${ngs2Dir}/src/Atrac9.cpp
${ngs2Dir}/src/Custom.cpp
${ngs2Dir}/src/Pan.cpp
${ngs2Dir}/src/Rack.cpp
${ngs2Dir}/src/Render.cpp
${ngs2Dir}/src/System.cpp

View File

@ -548,12 +548,19 @@ struct Ngs2WaveformInfo {
static_assert(sizeof(Ngs2WaveformInfo) == 232);
struct Ngs2PanParam {
std::uint32_t reserved[16];
float angle;
float distance;
float fbw_level;
float lfe_level;
};
static_assert(sizeof(Ngs2PanParam) == 16);
struct Ngs2PanWork {
std::uint32_t reserved[64];
float speaker_angles[8];
float unit_angle;
std::uint32_t num_speakers;
};
static_assert(sizeof(Ngs2PanWork) == 40);
struct Ngs2GeomListenerParam {
std::uint32_t reserved[32];

View File

@ -182,5 +182,8 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param);
void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate);
void Ngs2ProcessLegacyUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate);
void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo, std::uint32_t numBufferInfo);
float Ngs2DefaultLevel(std::uint32_t sourceChannels, std::uint32_t source, std::uint32_t destChannels, std::uint32_t dest);
int Ngs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers);
int Ngs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32_t num_params, uint32_t matrix_format, float* out_volume_matrix);
#endif

View File

@ -0,0 +1,135 @@
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <numbers>
#include <stdexcept>
#include <string>
#include "prx/libc/include/General.hpp"
#include "Ngs2Internal.hpp"
static constexpr float DEGREES = 360.0f;
static constexpr float RADIANS = 2.0f * std::numbers::pi_v<float>;
static constexpr std::uint32_t MAX_PAN_PARAMS = 8;
static constexpr std::uint32_t LFE_CHANNEL = 3;
struct PanLayout {
std::uint32_t inputOrder[7];
std::uint32_t channels[7];
float defaultDegrees[7];
std::uint32_t mixChannels;
};
static constexpr PanLayout STEREO_LAYOUT{{1, 0}, {1, 0}, {90, 270}, 2};
static constexpr PanLayout QUAD_LAYOUT{{1, 3, 2, 0}, {1, 5, 4, 0}, {40, 110, 250, 320}, 6};
static constexpr PanLayout FIVE_LAYOUT{{2, 1, 4, 3, 0}, {2, 1, 5, 4, 0}, {0, 40, 110, 250, 320}, 6};
static constexpr PanLayout SEVEN_LAYOUT{{2, 1, 4, 6, 5, 3, 0}, {2, 1, 5, 7, 6, 4, 0}, {0, 40, 110, 150, 210, 250, 320}, 8};
static const PanLayout* LayoutFor(std::uint32_t numSpeakers) {
switch (numSpeakers) {
case 2: return &STEREO_LAYOUT;
case 4: return &QUAD_LAYOUT;
case 5: return &FIVE_LAYOUT;
case 7: return &SEVEN_LAYOUT;
default: return nullptr;
}
}
static bool IsUnitAngle(float unitAngle) {
return unitAngle == DEGREES || unitAngle == RADIANS;
}
static float EqualPower(float position) {
return std::sin(position * (std::numbers::pi_v<float> / 2.0f));
}
static void AddBetweenSpeakers(const Ngs2PanWork& work, std::uint32_t speaker, float angle, float weight, float* gains) {
const float from = work.speaker_angles[speaker - 1];
const float to = work.speaker_angles[speaker];
if (!(angle >= from) || !(to > angle)) return;
const float position = (angle - from) / (to - from);
gains[speaker - 1] += EqualPower(1.0f - position) * weight;
gains[speaker] += EqualPower(position) * weight;
}
int Ngs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers) {
if (work == nullptr) APS5_INVALID_ARG_EX;
if (speaker_angles != nullptr && !IsUnitAngle(unit_angle)) APS5_INVALID_ARG_EX;
const auto* layout = LayoutFor(num_speakers);
if (layout == nullptr) APS5_INVALID_ARG_EX;
*work = {};
work->num_speakers = num_speakers;
if (speaker_angles == nullptr) {
for (std::uint32_t i = 0; i < num_speakers; i++) work->speaker_angles[i] = layout->defaultDegrees[i];
work->speaker_angles[num_speakers] = layout->defaultDegrees[0] + DEGREES;
work->unit_angle = DEGREES;
if (unit_angle == RADIANS) {
for (std::uint32_t i = 0; i < num_speakers; i++) work->speaker_angles[i] = work->speaker_angles[i] / 180.0f * std::numbers::pi_v<float>;
work->unit_angle = RADIANS;
}
return SCE_NGS2_OK;
}
for (std::uint32_t i = 0; i < num_speakers; i++) {
float angle = speaker_angles[layout->inputOrder[i]];
if (!std::isfinite(angle)) APS5_INVALID_ARG_EX;
while (angle < 0.0f) {
if (angle + unit_angle == angle) throw std::runtime_error("NGS2: pan angles too large to wrap are not implemented");
angle += unit_angle;
}
while (angle >= unit_angle) {
if (!(unit_angle < angle)) APS5_INVALID_ARG_EX;
if (angle - unit_angle == angle) throw std::runtime_error("NGS2: pan angles too large to wrap are not implemented");
angle -= unit_angle;
}
work->speaker_angles[i] = angle;
}
work->speaker_angles[num_speakers] = work->speaker_angles[0] + unit_angle;
work->unit_angle = unit_angle;
return SCE_NGS2_OK;
}
int Ngs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32_t num_params, uint32_t matrix_format, float* out_volume_matrix) {
if (work == nullptr || params == nullptr) APS5_INVALID_ARG_EX;
if (out_volume_matrix == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS;
if (matrix_format != 1 && matrix_format != 2 && matrix_format != 6 && matrix_format != 8) APS5_INVALID_ARG_EX;
const auto* layout = LayoutFor(work->num_speakers);
if (layout == nullptr || !IsUnitAngle(work->unit_angle) || !std::isfinite(work->speaker_angles[0]) || num_params > MAX_PAN_PARAMS) APS5_INVALID_ARG_EX;
for (std::uint32_t p = 0; p < num_params; p++) {
if (!std::isfinite(params[p].angle) || std::isnan(params[p].distance)) APS5_INVALID_ARG_EX;
}
std::memset(out_volume_matrix, 0, static_cast<std::size_t>(num_params) * matrix_format * sizeof(float));
const std::uint32_t numSpeakers = work->num_speakers;
const float unit = work->unit_angle;
const float first = work->speaker_angles[0];
for (std::uint32_t p = 0; p < num_params; p++) {
const auto& param = params[p];
const float distance = std::max(-1.0f, std::min(1.0f, param.distance));
const float towardWeight = std::sin((distance + 1.0f) * 0.5f * std::numbers::pi_v<float> * 0.5f);
const float oppositeWeight = std::sin((1.0f - distance) * 0.5f * std::numbers::pi_v<float> * 0.5f);
float angle = param.angle;
while (angle < first || angle >= first + unit) {
const float before = angle;
if (angle >= first + unit) angle -= unit;
if (angle < first) angle += unit;
if (angle == before) throw std::runtime_error("NGS2: pan angles too large to wrap are not implemented");
}
float opposite = angle + unit * 0.5f;
if (!(opposite < first + unit)) opposite -= unit;
float gains[8] = {};
for (std::uint32_t speaker = 1; speaker <= numSpeakers; speaker++) {
AddBetweenSpeakers(*work, speaker, angle, towardWeight, gains);
AddBetweenSpeakers(*work, speaker, opposite, oppositeWeight, gains);
}
float mix[8] = {};
mix[layout->channels[0]] = gains[0] + gains[numSpeakers];
for (std::uint32_t speaker = 1; speaker < numSpeakers; speaker++) mix[layout->channels[speaker]] = gains[speaker];
for (std::uint32_t channel = 0; channel < 8; channel++) mix[channel] = channel == LFE_CHANNEL ? param.lfe_level : mix[channel] * param.fbw_level;
for (std::uint32_t dest = 0; dest < matrix_format; dest++) {
float& level = out_volume_matrix[static_cast<std::size_t>(dest) * num_params + p];
for (std::uint32_t channel = 0; channel < layout->mixChannels; channel++) level += Ngs2DefaultLevel(layout->mixChannels, channel, matrix_format, dest) * mix[channel];
}
}
return SCE_NGS2_OK;
}

View File

@ -196,7 +196,7 @@ static constexpr Ngs2DefaultRow DEFAULT_MAP[NGS2_MAX_CHANNELS][NGS2_MAX_CHANNELS
{FRONT_LEFT, FRONT_RIGHT, MONO_CENTER, LFE, SURROUND_LEFT, SURROUND_RIGHT, BACK_LEFT, BACK_RIGHT},
};
static float DefaultLevel(std::uint32_t sourceChannels, std::uint32_t source, std::uint32_t destChannels, std::uint32_t dest) {
float Ngs2DefaultLevel(std::uint32_t sourceChannels, std::uint32_t source, std::uint32_t destChannels, std::uint32_t dest) {
const auto& row = DEFAULT_MAP[sourceChannels - 1][source];
switch (destChannels) {
case 1: return row.mono;
@ -212,7 +212,7 @@ static void MixPort(Ngs2Voice& voice, const Ngs2Voice& source, const Ngs2Port& p
const std::size_t outputs = matrix == nullptr ? voice.channels : std::min<std::size_t>(voice.channels, matrix->size() / source.channels);
for (std::size_t dst = 0; dst < outputs; dst++) {
for (std::uint32_t src = 0; src < source.channels; src++) {
const float level = port.volume * (matrix == nullptr ? DefaultLevel(source.channels, src, voice.channels, static_cast<std::uint32_t>(dst)) : (*matrix)[dst * source.channels + src]);
const float level = port.volume * (matrix == nullptr ? Ngs2DefaultLevel(source.channels, src, voice.channels, static_cast<std::uint32_t>(dst)) : (*matrix)[dst * source.channels + src]);
if (level == 0.0f) continue;
for (std::uint32_t i = 0; i < grain; i++) voice.samples[dst * grain + i] += source.samples[src * grain + i] * level;
}

View File

@ -37,25 +37,6 @@ int APS5_VABI sceNgs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param)
return 0;
}
int APS5_VABI sceNgs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32_t num_params, uint32_t matrix_format, float* out_volume_matrix) {
(void)work;
(void)params;
(void)num_params;
(void)matrix_format;
(void)out_volume_matrix;
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceNgs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers) {
(void)work;
(void)speaker_angles;
(void)unit_angle;
(void)num_speakers;
NotImplemented_nid_no_patch(__func__);
return 0;
}
}
#pragma GCC visibility pop

View File

@ -431,6 +431,14 @@ int APS5_VABI sceNgs2VoiceGetStateFlags(uintptr_t voice_handle, uint32_t* state_
return SCE_NGS2_OK;
}
int APS5_VABI sceNgs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers) {
return Ngs2PanInit(work, speaker_angles, unit_angle, num_speakers);
}
int APS5_VABI sceNgs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32_t num_params, uint32_t matrix_format, float* out_volume_matrix) {
return Ngs2PanGetVolumeMatrix(work, params, num_params, matrix_format, out_volume_matrix);
}
}
#pragma GCC visibility pop

View File

@ -1,10 +1,12 @@
#include "Ngs2Test.hpp"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <iterator>
#include <numbers>
#include <stdexcept>
#include <thread>
@ -145,6 +147,110 @@ static void TestPcmBlockEnd() {
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
}
static bool Near(float a, float b) {
return std::fabs(a - b) < 1e-4f;
}
static bool PanThrows(Ngs2PanWork* work, const Ngs2PanParam* params, std::uint32_t numParams, std::uint32_t format) {
float out[64] = {};
try { sceNgs2PanGetVolumeMatrix(work, params, numParams, format, out); } catch (const std::exception&) { return true; }
return false;
}
static bool PanInitThrows(Ngs2PanWork* work, const float* angles, float unitAngle, std::uint32_t numSpeakers) {
try { sceNgs2PanInit(work, angles, unitAngle, numSpeakers); } catch (const std::exception&) { return true; }
return false;
}
struct PanCase {
std::vector<float> angles;
float unitAngle;
std::uint32_t numSpeakers;
std::vector<Ngs2PanParam> params;
std::uint32_t format;
std::vector<float> expected;
};
static void TestPan() {
const float pi = std::numbers::pi_v<float>;
Ngs2PanWork work{};
Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK);
Require(work.num_speakers == 2 && work.unit_angle == 360.0f);
Require(work.speaker_angles[0] == 90.0f && work.speaker_angles[1] == 270.0f && work.speaker_angles[2] == 450.0f);
Require(sceNgs2PanInit(&work, nullptr, 2.0f * pi, 2) == SCE_NGS2_OK && work.unit_angle == 2.0f * pi);
Require(work.speaker_angles[0] == 90.0f / 180.0f * pi && work.speaker_angles[1] == 270.0f / 180.0f * pi && work.speaker_angles[2] == 450.0f);
Require(sceNgs2PanInit(&work, nullptr, 100.0f, 4) == SCE_NGS2_OK && work.unit_angle == 360.0f && work.speaker_angles[4] == 400.0f);
const float frontBack[] = {-30.0f, 30.0f, 0.0f, -110.0f, 470.0f};
Require(sceNgs2PanInit(&work, frontBack, 360.0f, 5) == SCE_NGS2_OK);
const float circle[] = {0.0f, 30.0f, 110.0f, 250.0f, 330.0f, 360.0f};
for (std::uint32_t i = 0; i < 6; i++) Require(work.speaker_angles[i] == circle[i]);
const std::vector<PanCase> cases = {
{{}, 360.0f, 2, {{0, 1, 1, 0}, {45, 1, 1, 0}, {90, 1, 1, 0}, {135, 1, 1, 0}, {180, 1, 1, 0}, {225, 1, 1, 0}, {270, 1, 1, 0}, {315, 1, 1, 0}}, 2,
{0.707106769f, 0.382683456f, 0.0f, 0.382683456f, 0.707106769f, 0.923879504f, 1.0f, 0.923879504f, 0.707106769f, 0.923879504f, 1.0f, 0.923879504f, 0.707106769f, 0.382683456f, 0.0f, 0.382683456f}},
{{}, 360.0f, 2, {{30, 0, 1, 0}, {30, -1, 1, 0}, {30, 0.5f, 0.5f, 0}, {30, 3, 1, 0}, {-30, -0.25f, 1, 0}}, 2,
{0.965925813f, 0.866025448f, 0.39667666f, 0.49999997f, 0.896872759f, 0.965925813f, 0.49999997f, 0.495722443f, 0.866025448f, 0.997858942f}},
{{}, 360.0f, 2, {{60, 1, 1, 0.5f}}, 1, {0.866025388f}},
{{}, 360.0f, 2, {{60, 1, 1, 0.5f}}, 6, {0.258819073f, 0.965925813f, 0.0f, 0.0f, 0.0f, 0.0f}},
{{}, 2.0f * pi, 2, {{0.25f * pi, 1, 1, 0}, {1.5f * pi, 1, 1, 0}}, 2, {0.999965429f, 1.0f, 0.00831161533f, 0.0f}},
{{}, 360.0f, 4, {{100, 1, 1, 0.25f}}, 6, {0.0f, 0.222520918f, 0.0f, 0.25f, 0.0f, 0.974927902f}},
{{}, 360.0f, 4, {{100, 1, 1, 0.25f}}, 2, {0.0f, 0.709984899f}},
{{}, 360.0f, 5, {{70, 1, 0.8f, 0.5f}, {200, 0.5f, 1, 0}}, 6,
{0.0f, 0.0f, 0.625465155f, 0.270598054f, 0.0f, 0.270598054f, 0.5f, 0.0f, 0.0f, 0.782271147f, 0.498791903f, 0.491533577f}},
{{}, 360.0f, 5, {{70, 1, 0.8f, 0.5f}}, 1, {0.691666603f}},
{{}, 360.0f, 7, {{180, 1, 1, 0.25f}}, 8, {0.0f, 0.0f, 0.0f, 0.25f, 0.0f, 0.0f, 0.707106769f, 0.707106769f}},
{{}, 360.0f, 7, {{180, 1, 1, 0.25f}}, 6, {0.0f, 0.0f, 0.0f, 0.25f, 0.707106769f, 0.707106769f}},
{{-pi / 6, pi / 6, 0, -110 * pi / 180, 110 * pi / 180}, 2.0f * pi, 5, {{1.0f, 1, 1, 0}, {-2.5f, 0.2f, 1, 0.1f}}, 6,
{0.0f, 0.0f, 0.859783232f, 0.582614243f, 0.0f, 0.0f, 0.0f, 0.100000001f, 0.0f, 0.753403723f, 0.510659218f, 0.372568965f}},
{{-30, 30, 0, -90, 90, -150, 150}, 360.0f, 7, {{-120, 1, 1, 0}}, 1, {0.707106769f}},
{{-45, 45}, 360.0f, 2, {{10, 1, 1, 0}, {720 + 170, 1, 1, 0}}, 2, {0.57357651f, 0.664795876f, 0.819152057f, 0.747025073f}},
{{-40, 40, -120, 120}, 360.0f, 4, {{-80, 0.75f, 1, 0}}, 8, {0.69351995f, 0.0746578425f, 0.0f, 0.0f, 0.69351995f, 0.180239946f, 0.0f, 0.0f}},
};
for (const auto& panCase : cases) {
Require(sceNgs2PanInit(&work, panCase.angles.empty() ? nullptr : panCase.angles.data(), panCase.unitAngle, panCase.numSpeakers) == SCE_NGS2_OK);
std::vector<float> out(panCase.expected.size(), -1.0f);
Require(sceNgs2PanGetVolumeMatrix(&work, panCase.params.data(), static_cast<std::uint32_t>(panCase.params.size()), panCase.format, out.data()) == SCE_NGS2_OK);
for (std::size_t i = 0; i < out.size(); i++) Require(std::fabs(out[i] - panCase.expected[i]) < 1e-6f);
}
const Ngs2PanParam front{0.0f, 1.0f, 1.0f, 0.0f};
float out[2] = {};
Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK);
Require(sceNgs2PanGetVolumeMatrix(&work, &front, 1, 2, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS);
Require(sceNgs2PanGetVolumeMatrix(&work, &front, 0, 2, out) == SCE_NGS2_OK);
Require(PanThrows(nullptr, &front, 1, 2) && PanThrows(&work, nullptr, 0, 2));
Require(PanThrows(&work, &front, 1, 4) && PanThrows(&work, &front, 1, 3) && !PanThrows(&work, &front, 1, 8));
const Ngs2PanParam many[9] = {};
Require(PanThrows(&work, many, 9, 2) && !PanThrows(&work, many, 8, 2));
const Ngs2PanParam nanAngle{NAN, 1.0f, 1.0f, 0.0f};
Require(PanThrows(&work, &nanAngle, 1, 2));
const Ngs2PanParam laterNan[2] = {front, nanAngle};
float untouched[4] = {-1.0f, -1.0f, -1.0f, -1.0f};
bool rejected = false;
try { sceNgs2PanGetVolumeMatrix(&work, laterNan, 2, 2, untouched); } catch (const std::invalid_argument&) { rejected = true; }
Require(rejected && std::all_of(std::begin(untouched), std::end(untouched), [](float level) { return level == -1.0f; }));
const Ngs2PanParam huge{1e30f, 1.0f, 1.0f, 0.0f};
Require(PanThrows(&work, &huge, 1, 2));
const float hugeSpeaker[] = {1e30f, 30.0f};
const float hugeNegative[] = {-1e30f, 30.0f};
Require(PanInitThrows(&work, hugeSpeaker, 360.0f, 2) && PanInitThrows(&work, hugeNegative, 360.0f, 2));
Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK);
work.speaker_angles[0] = -INFINITY;
Require(PanThrows(&work, &front, 1, 2));
Require(sceNgs2PanInit(&work, nullptr, 360.0f, 2) == SCE_NGS2_OK);
Require(PanInitThrows(nullptr, nullptr, 360.0f, 2));
for (std::uint32_t count : {0u, 1u, 3u, 6u, 8u}) Require(PanInitThrows(&work, nullptr, 360.0f, count));
const float pair[] = {-30.0f, 30.0f};
Require(PanInitThrows(&work, pair, 180.0f, 2) && !PanInitThrows(&work, pair, 2.0f * pi, 2));
const float invalid[] = {0.0f, NAN};
Require(PanInitThrows(&work, invalid, 360.0f, 2));
const float onUnit[] = {720.0f, 30.0f};
const float belowZero[] = {-360.0f, 30.0f};
Require(PanInitThrows(&work, onUnit, 360.0f, 2) && !PanInitThrows(&work, belowZero, 360.0f, 2));
work.unit_angle = 100.0f;
Require(PanThrows(&work, &front, 1, 2));
}
static void TestPitchAndRepeat() {
const auto system = CreateSystem();
const auto master = Mastering(system, 1);
@ -559,6 +665,7 @@ int main() {
Require(std::atexit(RenderAfterStaticTeardown) == 0);
TestErrorsAndInfo();
TestPcmBlockEnd();
TestPan();
TestPitchAndRepeat();
TestSubmixerMatrix();
TestSampleRate();

View File

@ -25,6 +25,8 @@ int APS5_VABI sceNgs2SystemGetUserData(uintptr_t, uintptr_t*);
int APS5_VABI sceNgs2SystemLock(uintptr_t);
int APS5_VABI sceNgs2SystemUnlock(uintptr_t);
int APS5_VABI sceNgs2SystemRender(uintptr_t, const Ngs2RenderBufferInfo*, uint32_t);
int APS5_VABI sceNgs2PanInit(Ngs2PanWork*, const float*, float, uint32_t);
int APS5_VABI sceNgs2PanGetVolumeMatrix(Ngs2PanWork*, const Ngs2PanParam*, uint32_t, uint32_t, float*);
int APS5_VABI sceNgs2RackQueryBufferSize(uint32_t, const Ngs2RackOption*, Ngs2ContextBufferInfo*);
int APS5_VABI sceNgs2RackCreate(uintptr_t, uint32_t, const Ngs2RackOption*, const Ngs2ContextBufferInfo*, uintptr_t*);
int APS5_VABI sceNgs2RackCreateWithAllocator(uintptr_t, uint32_t, const Ngs2RackOption*, const Ngs2BufferAllocator*, uintptr_t*);

View File

@ -229,6 +229,7 @@ Throughout the project, every function at every stage either **does exactly what
- [Resource registration](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws
- [Shader registration](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a shader header is accepted at any address: PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary; whether the console refuses an alignment is unknown
- [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
- [sceRtcGetDosTime, sceRtcSetDosTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date
- [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes; `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does
- [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty