attempting to fix VM reboot bug

This commit is contained in:
minjaesong
2026-05-04 15:44:59 +09:00
parent 2dcdff83c8
commit 4ff48bba1c
8 changed files with 208 additions and 55 deletions

View File

@@ -90,6 +90,11 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX:
var vmRunners = HashMap<VmId, VMRunner>() // <VM's identifier, VMRunner>
var coroutineJobs = HashMap<VmId, Thread>() // <VM's identifier, Job>
// 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<VmId, Boolean>()
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()

View File

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