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.
This commit is contained in:
Yassine 2026-10-10 15:16:42 +00:00 committed by MarcZX100
parent 57f96fd2b3
commit 9fc0af0b10
4 changed files with 44 additions and 1 deletions

View File

@ -130,6 +130,8 @@ private:
void deliver(const Event& event);
void autoStart();
void checkEndOfFile();
bool readyLocked() const;
int startLocked();
int stopLocked();
AvPlayerFileReplacement file{};

View File

@ -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<std::uint32_t>(video));
if (audio != -1) EnableStream(static_cast<std::uint32_t>(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();

View File

@ -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;

View File

@ -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