From 6abc7f9475c63e3137abe7846ef9fb866994fac2 Mon Sep 17 00:00:00 2001 From: tkrmagid Date: Sun, 17 May 2026 03:41:03 +0900 Subject: [PATCH] =?UTF-8?q?v0.4.27:=20don't=20join=20decoder=20worker=20on?= =?UTF-8?q?=20close=20=E2=80=94=20fix=20place-then-delete=20freeze?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stopWorker() ran a 2 s bounded join on the client tick thread. When a user runs /videoPlace and immediately /videoDelete, the decoder worker is still inside native grabber.start() doing the initial HTTP probe (probesize=8 MB, analyzeduration=2 s); that call doesn't honor the running flag and Thread.interrupt() doesn't unblock native I/O, so the join blocked the tick thread for as long as start() took — exactly the brief in-game freeze the user reported. Signal stop (running=false, audio line stop+flush, worker.interrupt()) and return. The worker is a daemon, the audio tail is already silenced, the Entry has been removed from the map, and the worker's finally still closes the grabber whenever start()/grab() eventually returns — nothing observable depends on the grabber being closed synchronously before close() returns. Co-Authored-By: Claude Opus 4.7 --- gradle.properties | 2 +- .../client/playback/JavaCvBackend.java | 31 +++++++++---------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/gradle.properties b/gradle.properties index 87539f0..f6e5d5e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ org.gradle.configuration-cache=false # Mod mod_id=video_player -mod_version=0.4.26 +mod_version=0.4.27 maven_group=com.ejclaw.videoplayer archives_base_name=video_player diff --git a/src/main/java/com/ejclaw/videoplayer/client/playback/JavaCvBackend.java b/src/main/java/com/ejclaw/videoplayer/client/playback/JavaCvBackend.java index a4bc993..806284c 100644 --- a/src/main/java/com/ejclaw/videoplayer/client/playback/JavaCvBackend.java +++ b/src/main/java/com/ejclaw/videoplayer/client/playback/JavaCvBackend.java @@ -180,27 +180,24 @@ public class JavaCvBackend implements VideoBackend { // stale srcAddr — closing the grabber there frees the av_frame plane and the next // memcpy crashes inside StubRoutines::jbyte_disjoint_arraycopy (exactly the 4K-delete // crash dump we saw). So the safe rule is: only the decoder thread touches the - // grabber. External stop signals `running=false`, stops the audio line, interrupts the - // worker, and joins briefly; the worker's own `finally` calls grabber.close(). Inside - // the loop, grab() unblocks via the rw_timeout/timeout options (3 s, set in runLoop) - // even on a stuck HTTP read, so the join below normally returns within a frame. + // grabber. External stop signals `running=false`, stops the audio line, interrupts + // the worker, and returns immediately; the worker's own `finally` calls + // grabber.close() whenever grab()/start() eventually returns. + // + // We deliberately do NOT join the worker. close() runs on the client tick thread (via + // VideoPlayback.tick → Entry.close), and the worker can spend several seconds inside + // the native FFmpeg probe at the top of runLoop — probesize=8 MB and + // analyzeduration=2 s do not honor the `running` flag and Thread.interrupt() doesn't + // unblock native I/O. A bounded join() there (the old 2 s) is exactly the "place then + // immediately delete freezes the game for a moment" symptom: the worker hasn't + // entered the grab() loop yet, so flipping running=false has no effect on it until + // start() returns. The worker is a daemon, the audio line is already silenced above, + // and Entry has been removed from the active map by the caller — nothing observable + // depends on the grabber having been closed before this method returns. Thread t = worker; worker = null; if (t != null) { t.interrupt(); - try { - t.join(2000); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - } - if (t.isAlive()) { - // Worker still blocked in native grab() — let it finish on its own. Its - // finally still closes the grabber when grab() eventually returns / throws. - // No native pointers leak in the meantime because we don't touch them here. - VideoPlayerMod.LOG.warn( - "[{}] decoder did not exit within 2 s of stop; orphaning until next grab() returns", - VideoPlayerMod.MOD_ID); - } } ready = false; }