fix(avplayer): read the PS5 SceAvPlayerInitDataEx layout

PS5 titles pass a 0x230-byte SceAvPlayerInitDataEx: auto start is the
byte after debug_level at 0x74, nine 0x30-byte thread parameter blocks
fill 0x78-0x228 and the output video frame buffer count is at 0x228.
sceAvPlayerInitEx read both fields at their PS4 offsets: auto start at
0xa8 is the video decoder thread priority and the buffer count at 0xa4
lies in the audio decoder block. PPSA25063 (Poppy Playtime: Chapter 4)
sets every thread priority to 700, so its intro video started on its
own and stopped at once; it writes auto start 0 and starts playback
itself.

With this_size 0x230 or more both fields are read at the PS5 offsets;
smaller structures are read as before. The layout follows KytyPS5,
prosper, SharpProspero and sharpemu and is recorded in TechnicalDebt.

guest_avplayer builds a 0x230-byte structure with auto start 0 and
nonzero thread parameters, then with auto start 1, and checks when
playback starts and how many video buffers are allocated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Angel 2026-10-08 20:33:30 +02:00
parent 745b775e2d
commit b6f42c75d8
3 changed files with 57 additions and 2 deletions

View File

@ -2,7 +2,9 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <string_view>
#include "SceTypes.hpp"
#include "prx/libSceAvPlayer/include/AvPlayer.hpp"
@ -19,6 +21,10 @@ std::uint32_t VideoBufferCount(std::int32_t requested) {
return static_cast<std::uint32_t>(std::clamp(requested, 2, 16));
}
constexpr std::size_t Ps5InitDataExSize = 0x230;
constexpr std::size_t Ps5AutoStartOffset = 0x74;
constexpr std::size_t Ps5VideoFrameBuffersOffset = 0x228;
}
#pragma GCC visibility push(default)
@ -97,8 +103,14 @@ int APS5_VABI sceAvPlayerInitEx(const AvPlayerInitDataEx* init_ex, AvPlayerInter
init.file_replacement = init_ex->file_replacement;
init.event_replacement = init_ex->event_replacement;
init.debug_level = init_ex->debug_level;
init.num_output_video_framebuffers = init_ex->num_output_video_framebuffers;
init.auto_start = init_ex->auto_start;
if (init_ex->this_size >= Ps5InitDataExSize) {
const auto* bytes = reinterpret_cast<const std::uint8_t*>(init_ex);
std::memcpy(&init.num_output_video_framebuffers, bytes + Ps5VideoFrameBuffersOffset, sizeof(init.num_output_video_framebuffers));
init.auto_start = bytes[Ps5AutoStartOffset] != 0;
} else {
init.num_output_video_framebuffers = init_ex->num_output_video_framebuffers;
init.auto_start = init_ex->auto_start;
}
init.default_language = init_ex->default_language;
*handle = new Player(init, VideoBufferCount(init.num_output_video_framebuffers));
return SCE_OK;

View File

@ -2,6 +2,7 @@
#include <array>
#include <chrono>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
@ -993,6 +994,46 @@ void TestHandedOutFramesStayIntact() {
Check(sceAvPlayerClose(player) == 0, "close failed");
}
void TestPs5ExtendedInitLayout() {
constexpr std::size_t Ps5Size = 0x230;
constexpr std::size_t Ps5AutoStart = 0x74;
constexpr std::size_t Ps5ThreadParameters = 0x78;
constexpr std::size_t Ps5VideoFrameBuffers = 0x228;
constexpr std::int32_t Buffers = 3;
constexpr int DecodeAheadBuffers = 4;
static_assert(offsetof(AvPlayerInitDataEx, audio_decoder_priority) == Ps5AutoStart);
const auto run = [&](bool autoStart) {
Events events;
const AvPlayerInitData common = InitData(&events);
AvPlayerInitDataEx head{};
head.this_size = Ps5Size;
head.memory_replacement = common.memory_replacement;
head.event_replacement = common.event_replacement;
head.default_language = common.default_language;
alignas(AvPlayerInitDataEx) std::array<std::uint8_t, Ps5Size> raw{};
std::memcpy(raw.data(), &head, Ps5AutoStart);
raw[Ps5AutoStart] = autoStart ? 1 : 0;
if (!autoStart) std::fill(raw.begin() + static_cast<std::ptrdiff_t>(Ps5ThreadParameters), raw.end(), std::uint8_t{1});
std::memcpy(raw.data() + Ps5VideoFrameBuffers, &Buffers, sizeof(Buffers));
AvPlayerInternal* player = nullptr;
Check(sceAvPlayerInitEx(reinterpret_cast<const AvPlayerInitDataEx*>(raw.data()), &player) == 0 && player != nullptr, "PS5 extended init failed");
Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source after the PS5 extended init failed");
if (autoStart) {
Check(WaitFor([&] { return events.Seen(EventPlay); }), "auto start at 0x74 of the PS5 extended init did not start playback");
} else {
Check(WaitFor([&] { return events.Seen(EventReady); }), "ready event missing after the PS5 extended init");
std::this_thread::sleep_for(std::chrono::milliseconds(200));
Check(!events.Seen(EventPlay), "playback started although auto start at 0x74 of the PS5 extended init is 0");
Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "enable video after the PS5 extended init failed");
Check(sceAvPlayerStart(player) == 0, "start after the PS5 extended init failed");
}
Check(WaitFor([&] { return TextureCount() == Buffers + DecodeAheadBuffers; }), "video frame buffer count at 0x228 of the PS5 extended init not used");
Check(sceAvPlayerClose(player) == 0, "close after the PS5 extended init failed");
};
run(false);
run(true);
}
}
int main() {
@ -1005,6 +1046,7 @@ int main() {
TestOptionalVideoBuffersRespectMemoryLimit();
TestFileReplacementAutoStart();
TestHandedOutFramesStayIntact();
TestPs5ExtendedInitLayout();
std::puts("AvPlayer tests passed");
return 0;
} catch (const std::exception& error) {

View File

@ -171,6 +171,7 @@ Throughout the project, every function at every stage either **does exactly what
- [NGS2 streaming PCM waveforms](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - PPSA21402 streams its music by adding one block whose `num_samples` is the whole track and whose `data_size` is a 16 KiB first piece, with `CONTINUE`, and then adds each following piece with flag `0x2` (called `APPEND` here) and the same `num_samples`. No reference describes flag `0x2`. A 16-bit PCM block without skipped samples or repeats whose `num_samples` exceed its data is assumed to start such a stream; flag `0x2` is assumed to append the block's data to the last streaming block, ignoring its `num_samples`; the voice is assumed to output silence without advancing while it has played all appended data and `CONTINUE` is set; and the waveform is assumed to end at `num_samples` or, once `CONTINUE` is cleared, at the end of the appended data. `sceNgs2VoiceGetState` reports the bytes played as `decoded_data_size`, which the title uses to release its queued data, and the read position as `waveform_data`. Appending to a block that is not a stream, or other formats, skipped samples, repeats and partial frames, throw
- [sceAvPlayerStartEx](../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - start info layout unknown; it is ignored and playback starts as with `sceAvPlayerStart`
- [sceAvPlayerInit / sceAvPlayerInitEx](../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - behaviour without a memory replacement unknown; frame and sample buffers then come from the guest heap
- [sceAvPlayerInitEx](../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - the PS5 init data layout (`this_size` 0x230: auto start at 0x74, nine 0x30-byte thread parameter blocks from 0x78, output video frame buffer count at 0x228) follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/avPlayer.cpp), [prosper](https://github.com/mattias800/prosper/blob/main/prosper/src/hle/video/avplayer.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Media/AvPlayer.cs) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/AvPlayer/AvPlayerExports.cs), which agree on it. PPSA25063 fills the same fields, writes auto start 0 and starts playback with `sceAvPlayerStart`; a title that sets auto start has not been seen. A smaller `this_size` is read with the PS4 offsets. The thread parameters are ignored
- [SceAvPlayerVideoEx](../../core/libs/SceTypes.hpp) (libSceAvPlayer) - frame rate field and encoding unknown; it is left zero in frame and stream info
- [sceRazorCpuFlushOccurred](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature and return values (1 = flush since the last call, 0 = none) from [SharpProspero](https://github.com/SvenGDK/SharpProspero); without a capture nothing is flushed, so it returns 0 and writes 0 cycles to the optional out pointer (whether the real library writes it when nothing was flushed is unknown)
- [sceRazorCpuPushMarkerStatic, sceRazorCpuPopMarker](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures from PPSA26344's calls; (name, color, flags) assumed for the push; both do nothing without a capture