fix(audioout2): match measured MAIN port format validation

Measured on a PS5 (firmware 13.60) through dlopen of libSceAudioOut.sprx:
a MAIN (type 0) port accepts 1, 2, 8 or 12 channels (bits 8-11) with
sample type 0 or 1 (bits 0-6). Bit 7 without 8 channels returns
0x80268001, any other unsupported format returns 0x8026800E, and both
write UINT64_MAX to the output handle. Bits 12-31 do not affect the
result. Other port types keep their current validation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
hugodearaujo 2026-10-08 17:41:02 -03:00
parent 4b6ab6bb73
commit b7b281678b
3 changed files with 68 additions and 31 deletions

View File

@ -28,6 +28,9 @@ static constexpr std::uint32_t FORMAT_CHANNELS_SHIFT = 8;
static constexpr std::uint32_t FORMAT_CHANNELS_MASK = 0xFu;
static constexpr std::uint32_t FORMAT_TYPE_MASK = 0x7Fu;
static constexpr std::uint32_t FORMAT_FIELDS_MASK = 0xFFFu;
static constexpr std::uint16_t PORT_TYPE_MAIN = 0;
static constexpr int PORT_ERROR_FORMAT_FLAG_WITHOUT_8_CHANNELS = static_cast<int>(0x80268001);
static constexpr int PORT_ERROR_UNSUPPORTED_FORMAT = static_cast<int>(0x8026800E);
static std::mutex g_portsLock;
// Grows on demand: the title opens its bed ports plus max_object_ports object ports at once.
@ -158,6 +161,19 @@ extern "C" {
int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut2PortParam* params, AudioOut2PortHandle* port) {
if (!ctx) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE;
if (!params || !port) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT;
const auto channels = (params->data_format >> FORMAT_CHANNELS_SHIFT) & FORMAT_CHANNELS_MASK;
const auto sampleType = params->data_format & FORMAT_TYPE_MASK;
const auto* fold = StereoFoldFor(channels);
if (params->port_type == PORT_TYPE_MAIN) {
if ((params->data_format & 0x80u) != 0 && channels != 8) {
*port = static_cast<AudioOut2PortHandle>(-1);
return PORT_ERROR_FORMAT_FLAG_WITHOUT_8_CHANNELS;
}
if (fold == nullptr || channels == 6 || sampleType > 1) {
*port = static_cast<AudioOut2PortHandle>(-1);
return PORT_ERROR_UNSUPPORTED_FORMAT;
}
}
std::lock_guard lock(g_portsLock);
std::size_t index = 0;
while (index < g_ports.size() && g_ports[index].used) index++;
@ -171,10 +187,9 @@ int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut2
entry.dataFormat = params->data_format;
entry.samplingFreq = params->sampling_freq;
entry.flags = params->flags;
entry.channels = (params->data_format >> FORMAT_CHANNELS_SHIFT) & FORMAT_CHANNELS_MASK;
const auto sampleType = params->data_format & FORMAT_TYPE_MASK;
entry.fold = StereoFoldFor(entry.channels);
if (entry.fold == nullptr || sampleType > 1 || (params->data_format & ~FORMAT_FIELDS_MASK) != 0) {
entry.channels = channels;
entry.fold = fold;
if (entry.fold == nullptr || sampleType > 1 || (params->port_type != PORT_TYPE_MAIN && (params->data_format & ~FORMAT_FIELDS_MASK) != 0)) {
entry = AudioOut2Port{};
throw std::runtime_error("sceAudioOut2PortCreate: data format 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%x", params->data_format); return std::string(text); }() + " is not implemented");
}

View File

@ -9,7 +9,6 @@
#include <iterator>
#include <limits>
#include <random>
#include <stdexcept>
#include <string>
#include <vector>
@ -33,16 +32,6 @@ static void SetEnvironment(const char* name, const std::string& value) {
#endif
}
template<typename TFunction>
static bool ThrowsRuntimeError(TFunction function) {
try {
function();
} catch (const std::runtime_error&) {
return true;
}
return false;
}
namespace {
constexpr std::uint32_t grain = 256;
@ -65,7 +54,6 @@ struct Layout {
const Layout mono{1, {1.0f}, {1.0f}};
const Layout stereo{2, {1.0f, 0.0f}, {0.0f, 1.0f}};
const Layout surround51{6, {1.0f, 0.0f, fold, 0.0f, fold, 0.0f}, {0.0f, 1.0f, fold, 0.0f, 0.0f, fold}};
const Layout surround71{8, {1.0f, 0.0f, fold, 0.0f, fold, 0.0f, fold, 0.0f}, {0.0f, 1.0f, fold, 0.0f, 0.0f, fold, 0.0f, fold}};
const Layout surround714{12, {1.0f, 0.0f, fold, 0.0f, fold, 0.0f, fold, 0.0f, fold, 0.0f, fold * fold, 0.0f},
{0.0f, 1.0f, fold, 0.0f, 0.0f, fold, 0.0f, fold, 0.0f, fold, 0.0f, fold * fold}};
@ -216,20 +204,49 @@ void TestDroppedLfe() {
RequireFold(Play(Format(surround714.channels, formatFloat), data.data(), volume), surround714, expected, volume, 1.0f);
}
void TestRejectedLayouts() {
void TestMeasuredMainPortFormats() {
struct Case {
std::uint32_t format;
std::uint32_t result;
};
constexpr Case cases[] = {
{0x000, 0x8026800E}, {0x001, 0x8026800E}, {0x080, 0x80268001},
{0x100, 0}, {0x101, 0}, {0x102, 0x8026800E}, {0x17F, 0x8026800E},
{0x180, 0x80268001}, {0x181, 0x80268001}, {0x1FF, 0x80268001},
{0x200, 0}, {0x201, 0}, {0x202, 0x8026800E}, {0x27F, 0x8026800E},
{0x280, 0x80268001}, {0x281, 0x80268001}, {0x2FF, 0x80268001},
{0x300, 0x8026800E}, {0x301, 0x8026800E}, {0x380, 0x80268001},
{0x400, 0x8026800E}, {0x401, 0x8026800E}, {0x500, 0x8026800E},
{0x600, 0x8026800E}, {0x601, 0x8026800E}, {0x680, 0x80268001},
{0x700, 0x8026800E}, {0x701, 0x8026800E}, {0x780, 0x80268001},
{0x800, 0}, {0x801, 0}, {0x802, 0x8026800E}, {0x87F, 0x8026800E},
{0x880, 0}, {0x881, 0}, {0x882, 0x8026800E}, {0x8FF, 0x8026800E},
{0x900, 0x8026800E}, {0x901, 0x8026800E}, {0x980, 0x80268001},
{0xA00, 0x8026800E}, {0xB00, 0x8026800E}, {0xC00, 0}, {0xC01, 0},
{0xC02, 0x8026800E}, {0xC7F, 0x8026800E}, {0xC80, 0x80268001},
{0xC81, 0x80268001}, {0xD00, 0x8026800E}, {0xE00, 0x8026800E},
{0xF00, 0x8026800E}, {0xFFF, 0x80268001},
{0xFFFFF100, 0}, {0xFFFFF201, 0}, {0xFFFFF800, 0}, {0xFFFFFC01, 0},
{0xFFFFF600, 0x8026800E}, {0xFFFFF202, 0x8026800E}, {0xFFFFF280, 0x80268001},
};
const auto context = CreateContext();
for (const std::uint32_t channels : {0u, 3u, 4u, 5u, 7u, 9u, 10u, 11u, 13u, 15u}) {
AudioOut2PortHandle port = 0;
Require(ThrowsRuntimeError([&] { CreatePort(context, Format(channels, formatFloat), &port); }));
Require(ThrowsRuntimeError([&] { CreatePort(context, Format(channels, formatS16), &port); }));
for (const auto& test : cases) {
AudioOut2PortHandle port = 0xAAAAAAAAAAAAAAAAull;
Require(static_cast<std::uint32_t>(CreatePort(context, test.format, &port)) == test.result);
if (test.result == 0) {
Require(port != 0 && port != static_cast<AudioOut2PortHandle>(-1));
Require(sceAudioOut2PortDestroy(port) == 0);
} else {
Require(port == static_cast<AudioOut2PortHandle>(-1));
}
}
for (const std::uint32_t format : {0x100u, 0x201u, 0x800u, 0xC01u}) {
for (std::uint32_t bit = 12; bit < 32; bit++) {
AudioOut2PortHandle port = 0;
Require(CreatePort(context, format | (1u << bit), &port) == 0);
Require(sceAudioOut2PortDestroy(port) == 0);
}
}
AudioOut2PortHandle port = 0;
Require(ThrowsRuntimeError([&] { CreatePort(context, Format(12, 2), &port); }));
Require(ThrowsRuntimeError([&] { CreatePort(context, Format(12, formatFloat) | 0x1000, &port); }));
Require(ThrowsRuntimeError([&] { CreatePort(context, Format(12, formatFloat) | 0x10000, &port); }));
Require(CreatePort(context, Format(12, formatFloat), &port) == 0);
Require(port != 0);
Require(sceAudioOut2PortDestroy(port) == 0);
Require(sceAudioOut2ContextDestroy(context) == 0);
}
@ -237,7 +254,7 @@ void TestRejectedLayouts() {
int main() {
SetEnvironment("SDL_AUDIODRIVER", "disk");
for (const Layout* layout : {&mono, &stereo, &surround51, &surround71, &surround714}) {
for (const Layout* layout : {&mono, &stereo, &surround71, &surround714}) {
TestFloatLayout(*layout);
TestS16Layout(*layout);
}
@ -246,8 +263,13 @@ int main() {
const auto volume = Volume(8);
RequireFold(Play(Format(8, formatFloat) | 0x80, data.data(), volume), surround71, data, volume, 1.0f);
}
{
const auto data = FloatGrain(2);
const auto volume = Volume(2);
RequireFold(Play(0xFFFFF200u, data.data(), volume), stereo, data, volume, 1.0f);
}
TestHeightChannels();
TestDroppedLfe();
TestRejectedLayouts();
TestMeasuredMainPortFormats();
return 0;
}

View File

@ -143,7 +143,7 @@ Throughout the project, every function at every stage either **does exactly what
- [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - effect of a negative mix level unknown; only 0 to 32768 (0 dB) is accepted
- [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - no source applies the level (shadPS4 only stores it); it is assumed to be a linear gain on the port's own output, and the -9 dB default (11626) is applied to every pad speaker port, including those of titles that never call the function
- [sceAudioOutGetLastOutputTime](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - signature and behaviour taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [fpPS4](https://github.com/red-prig/fpPS4) reimplementations of the PS4 library, not confirmed on a PS5 title: the time is the process time at which `sceAudioOutOutput` or `sceAudioOutOutputs` accepted the port's last block, not the time the block reached the device
- [sceAudioOut2PortCreate](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - `data_format` is read as the channel count (bits 8-11: only 1, 2, 6, 8 or 12) and sample type (bits 0-6: 0 float, 1 int16); bit 7 and the port `flags` are not decoded, and bits 12-31 throw
- [sceAudioOut2PortCreate](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - MAIN (type 0) format validation follows PS5 firmware 13.60 measurements: bits 8-11 accept 1, 2, 8 or 12, bits 0-6 accept 0 or 1, bit 7 requires 8 channels, and bits 12-31 do not affect validation. The sample types are still read as float and int16; bit 7's layout, the PCM effect of bits 12-31 and the port `flags` are unverified. Other port types retain unverified format validation, including 6 channels and rejection of bits 12-31
- [sceAudioOut2PortCreate](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - 12 channels are assumed to be L R C LFE Ls Rs Lb Rb Ltf Rtf Ltb Rtb as in KytyPS5's 12-to-8 fold, and 6 and 8 channels to be its first 6 and 8; no title or SDK header confirms the order
- [sceAudioOut2PortCreate](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - the stereo fold is assumed, not the console's downmix: C at -3 dB into both sides, Ls/Rs, Lb/Rb and Ltf/Rtf at -3 dB and Ltb/Rtb at -6 dB into their own side, LFE dropped
- [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; only 0x8A810001 (not initialized) is returned