diff --git a/TerranBASICexecutable/src/net/torvald/tsvm/VMGUI.kt b/TerranBASICexecutable/src/net/torvald/tsvm/VMGUI.kt index baaa991..7470175 100644 --- a/TerranBASICexecutable/src/net/torvald/tsvm/VMGUI.kt +++ b/TerranBASICexecutable/src/net/torvald/tsvm/VMGUI.kt @@ -122,8 +122,39 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe private var rebootRequested = false private fun reboot() { - vmRunner.close() - coroutineJob.interrupt() + // Order is critical: stop ALL execution first, then dispose peripherals + // before re-initialising. Without this, the old JS thread races the new + // one on shared VM memory / IO state and can SIGSEGV on disposed peripherals. + + // 1. Stop parallel/child contexts. park() interrupts and joins them. + vm.park() + vm.poke(-90L, -128) + + // 2. Interrupt the main runner thread and cancel the GraalVM context. + if (::coroutineJob.isInitialized) coroutineJob.interrupt() + try { if (::vmRunner.isInitialized) vmRunner.close() } catch (_: Throwable) {} + + // 3. Wait for the main runner thread to actually finish. + if (::coroutineJob.isInitialized && coroutineJob !== Thread.currentThread()) { + try { + coroutineJob.join(2000L) + if (coroutineJob.isAlive) { + System.err.println("[VMGUI] runner ${vm.id} did not exit within 2s; proceeding anyway") + coroutineJob.interrupt() + } + } + catch (_: InterruptedException) { + Thread.currentThread().interrupt() + } + } + + // 4. Now it's safe to release native resources held by peripherals. + for (i in 1 until vm.peripheralTable.size) { + try { + vm.peripheralTable[i].peripheral?.dispose() + } + catch (_: Throwable) {} + } vm.init() init() diff --git a/tsvm_core/src/net/torvald/tsvm/VM.kt b/tsvm_core/src/net/torvald/tsvm/VM.kt index 404a755..cddbaba 100644 --- a/tsvm_core/src/net/torvald/tsvm/VM.kt +++ b/tsvm_core/src/net/torvald/tsvm/VM.kt @@ -325,7 +325,13 @@ class VM( } fun killAllContexts() { - contexts.forEach { it.interrupt() } + // Snapshot first: interrupt() can race with the worker thread mutating `contexts` + // (see Parallel.kill / attachProgram) and we want to wait on every one of them. + val snapshot = contexts.toList() + snapshot.forEach { it.interrupt() } + snapshot.forEach { + try { it.join(500L) } catch (_: InterruptedException) { Thread.currentThread().interrupt() } + } contexts.clear() } diff --git a/tsvm_core/src/net/torvald/tsvm/VMSetupBroker.kt b/tsvm_core/src/net/torvald/tsvm/VMSetupBroker.kt index 5a6f8c6..24ca270 100644 --- a/tsvm_core/src/net/torvald/tsvm/VMSetupBroker.kt +++ b/tsvm_core/src/net/torvald/tsvm/VMSetupBroker.kt @@ -22,6 +22,16 @@ object VMSetupBroker { * @param coroutineJobs Hashmap on the host of VMs that holds the coroutine-job object for the currently running VM-instance. Key: Int(VM's identifier), value: [kotlin.coroutines.Job] */ fun initVMenv(vm: VM, profileJson: JsonValue, profileName: String, gpu: GraphicsAdapter, vmRunners: HashMap, coroutineJobs: HashMap, whatToDoOnVmException: (Throwable) -> Unit) { + // Refuse to start a new runner while the previous one is still alive: + // running both concurrently would race on the VM's memory / IO and lead + // to mixed text input, garbled rendering, and SIGSEGV on disposed peripherals. + coroutineJobs[vm.id]?.let { old -> + if (old.isAlive) { + System.err.println("[VMSetupBroker] previous runner for ${vm.id} is still alive; tearing it down before re-init") + killVMenv(vm, vmRunners, coroutineJobs) + } + } + vm.init() try { @@ -61,9 +71,38 @@ object VMSetupBroker { */ fun killVMenv(vm: VM, vmRunners: HashMap, coroutineJobs: HashMap) { + // Order is critical: stop ALL execution first, then dispose peripherals. + // If we disposed peripherals while the runner thread is still alive, the + // thread would touch destroyed UnsafePtrs and SIGSEGV. + + // 1. Stop parallel/child contexts. park() interrupts and joins them. vm.park() vm.poke(-90L, -128) + // 2. Interrupt the main runner thread and cancel the GraalVM context. + // context.close(true) cancels in-flight script evaluation. + val runnerThread = coroutineJobs[vm.id] + runnerThread?.interrupt() + try { vmRunners[vm.id]?.close() } catch (_: Throwable) {} + + // 3. Wait for the main runner thread to actually finish. + if (runnerThread != null && runnerThread !== Thread.currentThread()) { + try { + runnerThread.join(2000L) + if (runnerThread.isAlive) { + // Last resort: re-interrupt and accept that disposal will + // happen with the thread still alive. This is logged so + // diagnostics surface a stuck VM rather than failing silently. + System.err.println("[VMSetupBroker] runner ${vm.id} did not exit within 2s; proceeding anyway") + runnerThread.interrupt() + } + } + catch (_: InterruptedException) { + Thread.currentThread().interrupt() + } + } + + // 4. Now it's safe to release native resources held by peripherals. for (i in 1 until vm.peripheralTable.size) { try { vm.peripheralTable[i].peripheral?.dispose() @@ -71,8 +110,9 @@ object VMSetupBroker { catch (_: Throwable) {} } - coroutineJobs[vm.id]?.interrupt() - vmRunners[vm.id]?.close() + // 5. Drop runner / job handles so a subsequent initVMenv won't see stale entries. + vmRunners.remove(vm.id) + coroutineJobs.remove(vm.id) vm.getPrintStream = { TODO() } vm.getErrorStream = { TODO() } diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/GraphicsAdapter.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/GraphicsAdapter.kt index 44262f7..d771991 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/GraphicsAdapter.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/GraphicsAdapter.kt @@ -1362,8 +1362,12 @@ open class GraphicsAdapter(private val assetsRoot: String, val vm: VM, val confi textCursorIsOn = !textCursorIsOn } - // force light cursor up while typing - textCursorIsOn = textCursorIsOn || ((1..254).any { Gdx.input.isKeyPressed(it) }) + // force light cursor up while typing -- only honour global key state when + // this VM is the focused viewport; otherwise hidden VMs would react to + // keypresses meant for the focused one. + if (Gdx.input.inputProcessor === vm.getIO()) { + textCursorIsOn = textCursorIsOn || ((1..254).any { Gdx.input.isKeyPressed(it) }) + } } diff --git a/tsvm_core/src/net/torvald/tsvm/peripheral/IOSpace.kt b/tsvm_core/src/net/torvald/tsvm/peripheral/IOSpace.kt index 0a220c1..139c845 100644 --- a/tsvm_core/src/net/torvald/tsvm/peripheral/IOSpace.kt +++ b/tsvm_core/src/net/torvald/tsvm/peripheral/IOSpace.kt @@ -285,24 +285,35 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor { private var rtc = 0L fun update(delta: Float) { + // Only the VM whose IOSpace is wired up as the active InputProcessor (i.e. the + // currently focused viewport) may observe global keyboard/mouse state. Otherwise + // hidden VMs would all see the same keypresses as the focused one. + val isFocused = Gdx.input.inputProcessor === this + if (rawInputFunctionLatched) { rawInputFunctionLatched = false - // store mouse info - mouseX = (Gdx.input.x + guiPosX).toShort() - mouseY = (Gdx.input.y + guiPosY).toShort() - mouseDown = Gdx.input.isTouched - - // strobe keys to fill the key read buffer - var keysPushed = 0 keyEventBuffers.fill(0) - for (k in 1..254) { - if (Gdx.input.isKeyPressed(k)) { - keyEventBuffers[keysPushed] = k.toByte() - keysPushed += 1 - } - if (keysPushed >= 8) break + if (isFocused) { + // store mouse info + mouseX = (Gdx.input.x + guiPosX).toShort() + mouseY = (Gdx.input.y + guiPosY).toShort() + mouseDown = Gdx.input.isTouched + + // strobe keys to fill the key read buffer + var keysPushed = 0 + for (k in 1..254) { + if (Gdx.input.isKeyPressed(k)) { + keyEventBuffers[keysPushed] = k.toByte() + keysPushed += 1 + } + + if (keysPushed >= 8) break + } + } + else { + mouseDown = false } } @@ -316,26 +327,33 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor { rtc = vm.worldInterface.currentTimeInMills() } - // SIGTERM key combination: Ctrl+Shift+T+R - vm.stopDown = (Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT) && - Gdx.input.isKeyPressed(Input.Keys.CONTROL_LEFT) && - Gdx.input.isKeyPressed(Input.Keys.T) && - Gdx.input.isKeyPressed(Input.Keys.R)) || Gdx.input.isKeyPressed(Input.Keys.PAUSE) - if (vm.stopDown) println("[VM-${vm.id}] SIGTERM requested") + if (isFocused) { + // SIGTERM key combination: Ctrl+Shift+T+R + vm.stopDown = (Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT) && + Gdx.input.isKeyPressed(Input.Keys.CONTROL_LEFT) && + Gdx.input.isKeyPressed(Input.Keys.T) && + Gdx.input.isKeyPressed(Input.Keys.R)) || Gdx.input.isKeyPressed(Input.Keys.PAUSE) + if (vm.stopDown) println("[VM-${vm.id}] SIGTERM requested") - // RESET key combination: Ctrl+Shift+R+S - vm.resetDown = Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT) && - Gdx.input.isKeyPressed(Input.Keys.CONTROL_LEFT) && - Gdx.input.isKeyPressed(Input.Keys.R) && - Gdx.input.isKeyPressed(Input.Keys.S) - if (vm.resetDown) println("[VM-${vm.id}] RESET requested") + // RESET key combination: Ctrl+Shift+R+S + vm.resetDown = Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT) && + Gdx.input.isKeyPressed(Input.Keys.CONTROL_LEFT) && + Gdx.input.isKeyPressed(Input.Keys.R) && + Gdx.input.isKeyPressed(Input.Keys.S) + if (vm.resetDown) println("[VM-${vm.id}] RESET requested") - // SYSRQ key combination: Ctrl+Shift+S+Q - vm.sysrqDown = (Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT) && - Gdx.input.isKeyPressed(Input.Keys.CONTROL_LEFT) && - Gdx.input.isKeyPressed(Input.Keys.Q) && - Gdx.input.isKeyPressed(Input.Keys.S)) || Gdx.input.isKeyPressed(Input.Keys.PRINT_SCREEN) - if (vm.sysrqDown) println("[VM-${vm.id}] SYSRQ requested") + // SYSRQ key combination: Ctrl+Shift+S+Q + vm.sysrqDown = (Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT) && + Gdx.input.isKeyPressed(Input.Keys.CONTROL_LEFT) && + Gdx.input.isKeyPressed(Input.Keys.Q) && + Gdx.input.isKeyPressed(Input.Keys.S)) || Gdx.input.isKeyPressed(Input.Keys.PRINT_SCREEN) + if (vm.sysrqDown) println("[VM-${vm.id}] SYSRQ requested") + } + else { + vm.stopDown = false + vm.resetDown = false + vm.sysrqDown = false + } } override fun touchUp(p0: Int, p1: Int, p2: Int, p3: Int): Boolean { diff --git a/tsvm_core/src/net/torvald/tsvm/rom/FontROM7x14.png b/tsvm_core/src/net/torvald/tsvm/rom/FontROM7x14.png index 5e4b26f..da8dc64 100644 Binary files a/tsvm_core/src/net/torvald/tsvm/rom/FontROM7x14.png and b/tsvm_core/src/net/torvald/tsvm/rom/FontROM7x14.png differ diff --git a/tsvm_executable/src/net/torvald/tsvm/VMEmuExecutable.kt b/tsvm_executable/src/net/torvald/tsvm/VMEmuExecutable.kt index 0c5761e..0ef13c5 100644 --- a/tsvm_executable/src/net/torvald/tsvm/VMEmuExecutable.kt +++ b/tsvm_executable/src/net/torvald/tsvm/VMEmuExecutable.kt @@ -90,6 +90,11 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX: var vmRunners = HashMap() // var coroutineJobs = HashMap() // + // Per-VM rising-edge latch for the RESET key combo (Ctrl+Shift+R+S). The reboot + // only fires when the user releases the keys, otherwise we'd restart-spam every + // frame while the combo is held. + private val rebootLatched = HashMap() + internal val whatToDoOnVmExceptionQueue = ArrayList<() -> Unit>() companion object { @@ -288,15 +293,20 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX: } private fun reboot(profileName: String) { - val vm = currentlyLoadedProfiles[profileName]!! + val vm = currentlyLoadedProfiles[profileName] ?: return - /*vmRunners[vm.id]!!.close() - coroutineJobs[vm.id]!!.interrupt() + // Tear down the old session (joins the runner thread, then disposes + // peripherals) before spinning up a new one. Without the join, the old + // JS thread races the new one on shared VM memory / IO state. + killVMenv(vm) + initVMenv(vm, profileName) - vm.init() - initVMenv(vm, profileName)*/ - - // hypervisor will take over by monitoring MMIO addr 48 + // The old IOSpace was kept (peripheralTable[0] survives init/kill), so + // the InputProcessor reference is still valid; just make sure the + // currently focused viewport is still wired to it. + if (currentVMselection != null && vms[currentVMselection!!]?.vm?.id == vm.id) { + Gdx.input.inputProcessor = vm.getIO() + } } private fun updateGame(delta: Float) { @@ -312,8 +322,27 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX: } vms.forEachIndexed { index, it -> - if (it?.vm?.resetDown == true && index == currentVMselection) { reboot(it.profileName) } - if (it?.vm?.isRunning == true) it?.vm?.update(delta) + if (it == null) return@forEachIndexed + val vmId = it.vm.id + + // Trigger reboot on the *release* edge of the RESET key combo, and + // only for the focused viewport (resetDown for a hidden VM is + // already kept false by IOSpace.update; this guard is belt-and-braces). + if (index == currentVMselection) { + if (it.vm.resetDown) { + rebootLatched[vmId] = true + } + else if (rebootLatched[vmId] == true) { + rebootLatched[vmId] = false + reboot(it.profileName) + return@forEachIndexed // VM was just rebuilt; skip the update tick + } + } + else { + rebootLatched[vmId] = false + } + + if (it.vm.isRunning) it.vm.update(delta) } updateMenu() diff --git a/tsvm_executable/src/net/torvald/tsvm/VMGUI.kt b/tsvm_executable/src/net/torvald/tsvm/VMGUI.kt index 2bc8bc5..3be6ee1 100644 --- a/tsvm_executable/src/net/torvald/tsvm/VMGUI.kt +++ b/tsvm_executable/src/net/torvald/tsvm/VMGUI.kt @@ -172,8 +172,34 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe private fun killVMenv() { if (vmKilled.compareAndSet(0, System.currentTimeMillis())) { System.err.println("VMGUI is killing VM environment...") + + // Order is critical: stop ALL execution first, then dispose peripherals. + // If we disposed peripherals while the runner thread is still alive, the + // thread would touch destroyed UnsafePtrs and SIGSEGV. + + // 1. Stop parallel/child contexts. park() interrupts and joins them. vm.park() vm.poke(-90L, -128) + + // 2. Interrupt the main runner thread and cancel the GraalVM context. + if (::coroutineJob.isInitialized) coroutineJob.interrupt() + try { if (::vmRunner.isInitialized) vmRunner.close() } catch (_: Throwable) {} + + // 3. Wait for the main runner thread to actually finish. + if (::coroutineJob.isInitialized && coroutineJob !== Thread.currentThread()) { + try { + coroutineJob.join(2000L) + if (coroutineJob.isAlive) { + System.err.println("[VMGUI] runner ${vm.id} did not exit within 2s; proceeding anyway") + coroutineJob.interrupt() + } + } + catch (_: InterruptedException) { + Thread.currentThread().interrupt() + } + } + + // 4. Now it's safe to release native resources held by peripherals. for (i in 1 until vm.peripheralTable.size) { try { vm.peripheralTable[i].peripheral?.dispose() @@ -181,8 +207,7 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe catch (_: Throwable) { } } - coroutineJob.interrupt() - vmRunner.close() + vm.getPrintStream = { TODO() } vm.getErrorStream = { TODO() } vm.getInputStream = { TODO() } @@ -195,12 +220,12 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe private var rebootRequested = false private fun reboot() { - /*vmRunner.close() - coroutineJob.interrupt() - - init()*/ - - // hypervisor will take over by monitoring MMIO addr 48 + // Tear down the old session (joins the runner thread, then disposes + // peripherals) before re-initialising. Without the join, the old JS + // thread races the new one on shared VM memory / IO state and can + // SIGSEGV on disposed peripherals. + killVMenv() + init() } private var updateAkku = 0.0