From 04aa651ff18354a05d6ac06cd1f95405c89330bd Mon Sep 17 00:00:00 2001 From: minjaesong Date: Sat, 20 Jun 2026 12:18:27 +0900 Subject: [PATCH] printing stacktrace on host exception --- assets/disk0/tvdos/bin/command.js | 8 ++- .../net/torvald/tsvm/AudioJSR223Delegate.kt | 4 +- tsvm_core/src/net/torvald/tsvm/VM.kt | 4 +- .../src/net/torvald/tsvm/VMJSR223Delegate.kt | 54 ++++++++++++++++++- .../torvald/tsvm/peripheral/AudioAdapter.kt | 42 +++++++++------ 5 files changed, 88 insertions(+), 24 deletions(-) diff --git a/assets/disk0/tvdos/bin/command.js b/assets/disk0/tvdos/bin/command.js index f04a745..4f57844 100644 --- a/assets/disk0/tvdos/bin/command.js +++ b/assets/disk0/tvdos/bin/command.js @@ -895,8 +895,12 @@ shell.execute = function(line, nameOverride) { catch (e) { gotError = true - serial.printerr(`[command.js] program quit with ${e}:\n${e.stack || '(stack trace unavailable)'}`) - printerrln(`Program quit with ${e}:\n${e.stack || '(stack trace unavailable)'}`) + // A host (Java) exception has no JS `.stack`, so `e.stack` alone is + // "(stack trace unavailable)". Recover the real host trace (to stderr + string). + let hostTrace = "" + try { hostTrace = sys.printStackTrace(e) } catch (_) {} + serial.printerr(`[command.js] program quit with ${e}:\n${e.stack || hostTrace || '(stack trace unavailable)'}`) + printerrln(`Program quit with ${e}:\n${e.stack || hostTrace || '(stack trace unavailable)'}`) if (`${e}`.startsWith("InterruptedException")) errorlevel = SIGTERM.name diff --git a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt index e57f99a..e3f4a91 100644 --- a/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/AudioJSR223Delegate.kt @@ -585,7 +585,7 @@ class AudioJSR223Delegate(private val vm: VM) { getFirstSnd()?.let { snd -> val ba = ByteArray(2304) UnsafeHelper.memcpyRaw(null, snd.mediaDecodedBin.ptr, ba, UnsafeHelper.getArrayOffset(ba), 2304) - snd.playheads[playhead].pcmQueue.addLast(ba) + snd.playheads[playhead].pcmQueue.add(ba) } } @@ -600,7 +600,7 @@ class AudioJSR223Delegate(private val vm: VM) { getFirstSnd()?.let { snd -> val ba = ByteArray(sampleLength * 2) // 32768 samples * 2 channels UnsafeHelper.memcpyRaw(null, snd.tadDecodedBin.ptr, ba, UnsafeHelper.getArrayOffset(ba), sampleLength * 2L) - snd.playheads[playhead].pcmQueue.addLast(ba) + snd.playheads[playhead].pcmQueue.add(ba) } } diff --git a/tsvm_core/src/net/torvald/tsvm/VM.kt b/tsvm_core/src/net/torvald/tsvm/VM.kt index 2a4b341..b71c9a4 100644 --- a/tsvm_core/src/net/torvald/tsvm/VM.kt +++ b/tsvm_core/src/net/torvald/tsvm/VM.kt @@ -778,7 +778,7 @@ class VM( // MMIO area else if (from in -1048576..-1 && (from - len) in -1048577..-1) { val fromIndex = (-from-1) / 131072 - val dev = peripheralTable[fromIndex.toInt()].peripheral ?: return null + val dev = peripheralTable.getOrNull(fromIndex.toInt())?.peripheral ?: return null val fromRel = (-from-1) % 131072 if (fromRel + len > 131072) return null @@ -807,7 +807,7 @@ class VM( // memory area else { val fromIndex = (-from-1) / 1048576 - val dev = peripheralTable[fromIndex.toInt()].peripheral ?: return null + val dev = peripheralTable.getOrNull(fromIndex.toInt())?.peripheral ?: return null val fromRel = (-from-1) % 1048576 if (fromRel + len > 1048576) return null diff --git a/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt index 3bf938f..c1b60d9 100644 --- a/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt @@ -25,7 +25,7 @@ class VMJSR223Delegate(private val vm: VM) { // MMIO area else if (from in -1048576..-1 && (from - len) in -1048577..-1) { val fromIndex = ((-from-1) / 131072).absoluteValue - val dev = vm.peripheralTable[fromIndex.toInt()].peripheral ?: return null + val dev = vm.peripheralTable.getOrNull(fromIndex.toInt())?.peripheral ?: return null val fromRel = (-from-1) % 131072 if (fromRel + len > 131072) return null @@ -55,7 +55,7 @@ class VMJSR223Delegate(private val vm: VM) { // memory area else { val fromIndex = (-from-1) / 1048576 - val dev = vm.peripheralTable[fromIndex.toInt()].peripheral ?: return null + val dev = vm.peripheralTable.getOrNull(fromIndex.toInt())?.peripheral ?: return null val fromRel = (-from-1) % 1048576 if (fromRel + len > 1048576) return null @@ -88,6 +88,56 @@ class VMJSR223Delegate(private val vm: VM) { fun getVmId() = vm.id.toString() + /** + * Recover the FULL host (Java) stack trace of an exception caught in JS, write it to the host + * stderr, and also RETURN it as a string so the caller can show it on the VM screen too. + * + * When a host (Kotlin) function throws, GraalVM surfaces it to JS as an exception object whose + * `toString()` is only the message — so `catch (e) { printerrln(e) }` shows the message on the + * VM screen while the Java stack trace (the part that says *where* it blew up) is lost and the + * host stdout/stderr gets nothing. Re-throwing the caught value back across the polyglot + * boundary recovers the original Throwable so its stack trace can be printed. Call from a JS + * catch block: `let tr = sys.printStackTrace(e)` (then optionally `printerrln(tr)`). + */ + fun printStackTrace(e: org.graalvm.polyglot.Value?): String { + val sb = StringBuilder("===== host stack trace =====\n") + if (e == null) { + sb.append("(the caught value was null/undefined)\n") + } else { + try { + if (e.isException) { + e.throwException() // throws a PolyglotException wrapping the original Throwable + } else if (e.isHostObject && e.asHostObject() is Throwable) { + // Some configs surface a caught host exception as a host object, not an + // exception object — unwrap the Throwable directly. + val sw = java.io.StringWriter() + (e.asHostObject() as Throwable).printStackTrace(java.io.PrintWriter(sw)) + sb.append("host object Throwable:\n").append(sw) + } else { + sb.append("caught value is not an exception object: ").append(e).append('\n') + } + } catch (pe: org.graalvm.polyglot.PolyglotException) { + if (pe.isHostException) { + val sw = java.io.StringWriter() + pe.asHostException().printStackTrace(java.io.PrintWriter(sw)) + sb.append("host (Java) exception:\n").append(sw) + } else { + sb.append("guest exception: ").append(pe.message).append('\n') + } + sb.append("guest stack frames:\n") + for (frame in pe.polyglotStackTrace) sb.append(" at ").append(frame).append('\n') + } catch (t: Throwable) { + val sw = java.io.StringWriter() + t.printStackTrace(java.io.PrintWriter(sw)) + sb.append("could not recover the trace; got:\n").append(sw) + } + } + val s = sb.toString() + System.err.println(s) + System.err.flush() + return s + } + fun poke(addr: Int, value: Int) = vm.poke(addr.toLong(), value.toByte()) fun peek(addr: Int) = vm.peek(addr.toLong())!!.toInt().and(255) diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt index ff29aa6..8578b92 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/AudioAdapter.kt @@ -3,7 +3,7 @@ package net.torvald.tsvm.peripheral import com.badlogic.gdx.Gdx import com.badlogic.gdx.backends.lwjgl3.audio.OpenALLwjgl3Audio import com.badlogic.gdx.utils.GdxRuntimeException -import com.badlogic.gdx.utils.Queue +import java.util.concurrent.ConcurrentLinkedQueue import io.airlift.compress.zstd.ZstdInputStream import net.torvald.UnsafeHelper import net.torvald.UnsafePtr @@ -32,21 +32,26 @@ private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable { val writeQueue = playhead.pcmQueue - if (playhead.isPlaying && writeQueue.notEmpty()) { + if (playhead.isPlaying) { - printdbg("Taking samples from queue (queue size: ${writeQueue.size}/${playhead.getPcmQueueCapacity()})") + // Single atomic poll — `pcmQueue` has concurrent producers (queueing thread / + // JS thread), so a separate notEmpty()/removeFirst() would race and corrupt it. + val samples = writeQueue.poll() - val samples = writeQueue.removeFirst() - playhead.position = writeQueue.size + if (samples != null) { + printdbg("Taking samples from queue (queue size: ${writeQueue.size}/${playhead.getPcmQueueCapacity()})") - playhead.audioDevice.writeSamplesUI8(samples, 0, samples.size) + playhead.position = writeQueue.size - Thread.sleep(6) - } - else if (playhead.isPlaying && writeQueue.isEmpty) { - printdbg("!! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED ") + playhead.audioDevice.writeSamplesUI8(samples, 0, samples.size) - Thread.sleep(6) + Thread.sleep(6) + } + else { + printdbg("!! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED ") + + Thread.sleep(6) + } } } else { // Tracker mode @@ -92,7 +97,7 @@ private class WriteQueueingRunnable(val playhead: AudioAdapter.Playhead, val pcm UnsafeHelper.getArrayOffset(samples), it.pcmUploadLength.toLong() ) - it.pcmQueue.addLast(samples) + it.pcmQueue.add(samples) it.pcmUploadLength = 0 it.position = it.pcmQueue.size @@ -442,11 +447,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { val writeQueue = playhead.pcmQueue - if (playhead.isPlaying && writeQueue.notEmpty()) { + val samples = if (playhead.isPlaying) writeQueue.poll() else null + if (samples != null) { printdbg("Taking samples from queue (queue size: ${writeQueue.size})") - val samples = writeQueue.removeFirst() playhead.position = writeQueue.size // printdbg("P${playhead.index+1} Vol ${playhead.masterVolume}; LpP ${playhead.pcmUploadLength}; start playback...") @@ -4793,7 +4798,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { var globalVolume: Int = 0x80, // 8-bit, default $80 (spec §5). Mutated by V $xx00. var mixingVolume: Int = 0x80, // 8-bit, default $80 (spec §5). Final-mix scaler, set once per song. - var pcmQueue: Queue = Queue(), + // PCM playback queue. ConcurrentLinkedQueue (not the libGDX Queue) because it is written + // by the queueing thread (WAV/raw PCM via WriteQueueingRunnable) and the JS thread + // (mp2UploadDecoded / tadUploadDecoded), drained by the render thread, and purged by the JS + // thread — concurrent addLast/removeFirst on a non-thread-safe queue corrupted head/tail and + // threw a sporadic ArrayIndexOutOfBoundsException during PCM playback. + var pcmQueue: ConcurrentLinkedQueue = ConcurrentLinkedQueue(), var pcmQueueSizeIndex: Int = 0, val audioDevice: OpenALBufferedAudioDevice, ) { @@ -4825,7 +4835,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) { if (field && !value) { // println("!! inserting dummy bytes") if (isPcmMode) { - pcmQueue.addLast(ByteArray(audioDevice.bufferSize * audioDevice.bufferCount)) + pcmQueue.add(ByteArray(audioDevice.bufferSize * audioDevice.bufferCount)) } } field = value