From 9fc0af0b10f95757a70a2138b8f2243e5e08cd53 Mon Sep 17 00:00:00 2001 From: Yassine Date: Sat, 10 Oct 2026 15:16:42 +0000 Subject: [PATCH] fix(avplayer): do not let auto start restart playback the title started sceAvPlayerAddSource queues an auto start for a player created with auto start or without an event callback. When the title enabled its streams and called sceAvPlayerStart before the controller thread ran it, the auto start called Start again on the playing player, which stopped it, released every video buffer, including the frame the title held, and started the film over. guest_avplayer occasionally caught this as "video frame is not a texture allocation"; right after the test's own start, 8 texture buffers were released in 15 of 15 runs. The auto start now starts the player only while it is still ready, and checks that under the player lock together with the start. TestAutoStartKeepsStartedPlayback starts a callback-less player itself and checks that no texture buffer is released afterwards and that the frame it holds stays allocated. It fails on every run without the fix. --- .../prx/libSceAvPlayer/include/AvPlayer.hpp | 2 ++ core/libs/prx/libSceAvPlayer/src/Player.cpp | 15 ++++++++++- core/libs/tests/GuestAvPlayer.cpp | 27 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp b/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp index 0eeb944fb..c401749ba 100644 --- a/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp +++ b/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp @@ -130,6 +130,8 @@ private: void deliver(const Event& event); void autoStart(); void checkEndOfFile(); + bool readyLocked() const; + int startLocked(); int stopLocked(); AvPlayerFileReplacement file{}; diff --git a/core/libs/prx/libSceAvPlayer/src/Player.cpp b/core/libs/prx/libSceAvPlayer/src/Player.cpp index 1e265d395..bdf69bfc3 100644 --- a/core/libs/prx/libSceAvPlayer/src/Player.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Player.cpp @@ -136,7 +136,15 @@ void Player::deliver(const Event& event) { callback.event_callback(callback.object_ptr, event.id, 0, event.id == EventWarningId ? &warning : nullptr); } +bool Player::readyLocked() const { + return source && state == State::Ready; +} + void Player::autoStart() { + { + std::lock_guard lock(mutex); + if (!readyLocked()) return; + } const int count = StreamCount(); if (count <= 0) { Stop(); @@ -155,7 +163,8 @@ void Player::autoStart() { } if (video != -1) EnableStream(static_cast(video)); if (audio != -1) EnableStream(static_cast(audio)); - Start(); + std::lock_guard lock(mutex); + if (readyLocked()) startLocked(); } void Player::checkEndOfFile() { @@ -246,6 +255,10 @@ int Player::ChangeStream(std::uint32_t from, std::uint32_t to) { int Player::Start() { std::lock_guard lock(mutex); + return startLocked(); +} + +int Player::startLocked() { if (!source) return SCE_AVPLAYER_ERROR_OPERATION_FAILED; if (state != State::Ready && state != State::Stop && stopLocked() != SCE_OK) return SCE_AVPLAYER_ERROR_OPERATION_FAILED; const int result = source->Start(); diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index 2fb32a5db..1d50a1497 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -1034,6 +1034,32 @@ void TestPs5ExtendedInitLayout() { run(true); } +void TestAutoStartKeepsStartedPlayback() { + bool startedByTitle = false; + for (int attempt = 0; attempt < 10 && !startedByTitle; ++attempt) { + AvPlayerInitData init = InitData(nullptr); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source failed"); + startedByTitle = sceAvPlayerEnableStream(player, VideoStream) == 0 && sceAvPlayerEnableStream(player, EnglishAudioStream) == 0; + if (startedByTitle) { + const auto releasedTextures = [] { + std::lock_guard lock(allocations.mutex); + return allocations.textures - ActiveTextureCountLocked(); + }; + Check(sceAvPlayerStart(player) == 0, "start failed"); + const int released = releasedTextures(); + AvPlayerFrameInfoEx frame{}; + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "no video after the title's start"); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Check(releasedTextures() == released, "auto-start restarted playback the title had started"); + Check(IsAllocation(frame.p_data, true), "auto-start released the frame the title holds"); + } + Check(sceAvPlayerClose(player) == 0, "close failed"); + } + Check(startedByTitle, "auto-start won every race against the title's own start"); +} + void TestUnsyncedVideoKeepsUpWithAudio() { AvPlayerInitData init = InitData(nullptr); auto* player = sceAvPlayerInit(&init); @@ -1075,6 +1101,7 @@ int main() { TestFileReplacementAutoStart(); TestHandedOutFramesStayIntact(); TestPs5ExtendedInitLayout(); + TestAutoStartKeepsStartedPlayback(); TestUnsyncedVideoKeepsUpWithAudio(); std::puts("AvPlayer tests passed"); return 0; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4656d0fbb..559981ba2 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -198,6 +198,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 +- [AvPlayer auto start](../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - the auto start that `sceAvPlayerAddSource` queues for a player with auto start or without an event callback starts the player only while it is still ready: a title that starts the player itself before the auto start runs keeps its playback and the frames it holds. Not confirmed on a title. - [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