attempting to fix VM reboot bug(2)

This commit is contained in:
minjaesong
2026-05-04 16:00:39 +09:00
parent 4ff48bba1c
commit 1e482e32a8
5 changed files with 79 additions and 28 deletions

View File

@@ -29,8 +29,10 @@ internal object UnsafeHelper {
return UnsafePtr(ptr, size, caller)
}
fun memcpy(src: UnsafePtr, fromIndex: Long, dest: UnsafePtr, toIndex: Long, copyLength: Long) =
fun memcpy(src: UnsafePtr, fromIndex: Long, dest: UnsafePtr, toIndex: Long, copyLength: Long) {
if (src.destroyed || dest.destroyed) return
unsafe.copyMemory(src.ptr + fromIndex, dest.ptr + toIndex, copyLength)
}
fun memcpy(srcAddress: Long, destAddress: Long, copyLength: Long) =
unsafe.copyMemory(srcAddress, destAddress, copyLength)
fun memcpyRaw(srcObj: Any?, srcPos: Long, destObj: Any?, destPos: Long, len: Long) =
@@ -84,81 +86,96 @@ internal class UnsafePtr(pointer: Long, allocSize: Long, private val caller: Any
}
}
private inline fun checkNullPtr(index: Long) { // ignore what IDEA says and do inline this
//// commenting out because of the suspected (or minor?) performance impact.
//// You may break the glass and use this tool when some fucking incomprehensible bugs ("vittujen vitun bugit")
//// appear (e.g. getting garbage values when it fucking shouldn't)
// if (destroyed) { throw DanglingPointerException("The pointer is already destroyed ($this)") }
// if (index !in 0 until size) throw AddressOverflowException("Index: $index; alloc size: $size; pointer: ${this}\n${Thread.currentThread().stackTrace.joinToString("\n", limit=10) { " $it" }}")
/**
* Returns true when the operation should proceed; false when the pointer is destroyed
* (so the caller short-circuits to a safe no-op / zero return).
*
* Why no exception: a JS worker thread that survives killVMenv (because it wasn't
* tracked in vm.contexts, e.g. raw java.lang.Thread spawned by JS code) will keep
* poking peripheral memory for one or more iterations after dispose(). Letting it
* actually call unsafe.putByte on freed memory corrupts the malloc heap and crashes
* the JVM with `free_list_checksum_botch`. Returning quietly turns the race into a
* harmless no-op until the thread drains.
*/
private inline fun aliveAt(index: Long): Boolean {
if (destroyed) return false
if (index < 0 || index >= size) return false
return true
}
operator fun get(index: Long): Byte {
checkNullPtr(index)
if (!aliveAt(index)) return 0
return UnsafeHelper.unsafe.getByte(ptr + index)
}
operator fun set(index: Long, value: Byte) {
checkNullPtr(index)
if (!aliveAt(index)) return
UnsafeHelper.unsafe.putByte(ptr + index, value)
}
fun getFloatFree(index: Long): Float {
checkNullPtr(index)
if (!aliveAt(index + 3)) return 0f
return UnsafeHelper.unsafe.getFloat(ptr + index)
}
fun getFloat(unit: Long): Float {
checkNullPtr(unit * 4L)
return UnsafeHelper.unsafe.getFloat(ptr + (unit * 4L))
val idx = unit * 4L
if (!aliveAt(idx + 3)) return 0f
return UnsafeHelper.unsafe.getFloat(ptr + idx)
}
fun getIntFree(index: Long): Int {
checkNullPtr(index)
if (!aliveAt(index + 3)) return 0
return UnsafeHelper.unsafe.getInt(ptr + index)
}
fun getInt(unit: Long): Int {
checkNullPtr(unit * 4L)
return UnsafeHelper.unsafe.getInt(ptr + (unit * 4L))
val idx = unit * 4L
if (!aliveAt(idx + 3)) return 0
return UnsafeHelper.unsafe.getInt(ptr + idx)
}
fun getShortFree(index: Long): Short {
checkNullPtr(index)
if (!aliveAt(index + 1)) return 0
return UnsafeHelper.unsafe.getShort(ptr + index)
}
fun getShort(unit: Long): Short {
checkNullPtr(unit * 2L)
return UnsafeHelper.unsafe.getShort(ptr + (unit * 2L))
val idx = unit * 2L
if (!aliveAt(idx + 1)) return 0
return UnsafeHelper.unsafe.getShort(ptr + idx)
}
fun setFloatFree(index: Long, value: Float) {
checkNullPtr(index)
if (!aliveAt(index + 3)) return
UnsafeHelper.unsafe.putFloat(ptr + index, value)
}
fun setFloat(unit: Long, value: Float) {
checkNullPtr(unit * 4L)
UnsafeHelper.unsafe.putFloat(ptr + (unit * 4L), value)
val idx = unit * 4L
if (!aliveAt(idx + 3)) return
UnsafeHelper.unsafe.putFloat(ptr + idx, value)
}
fun setIntFree(index: Long, value: Int) {
checkNullPtr(index)
if (!aliveAt(index + 3)) return
UnsafeHelper.unsafe.putInt(ptr + index, value)
}
fun setInt(unit: Long, value: Int) {
checkNullPtr(unit * 4L)
UnsafeHelper.unsafe.putInt(ptr + (unit * 4L), value)
val idx = unit * 4L
if (!aliveAt(idx + 3)) return
UnsafeHelper.unsafe.putInt(ptr + idx, value)
}
fun setShortFree(index: Long, value: Short) {
checkNullPtr(index)
if (!aliveAt(index + 1)) return
UnsafeHelper.unsafe.putShort(ptr + index, value)
}
fun setShortUnit(unit: Long, value: Short) {
checkNullPtr(unit * 2L)
UnsafeHelper.unsafe.putShort(ptr + (unit * 2L), value)
val idx = unit * 2L
if (!aliveAt(idx + 1)) return
UnsafeHelper.unsafe.putShort(ptr + idx, value)
}
fun fillWith(byte: Byte) {
if (destroyed) return
UnsafeHelper.unsafe.setMemory(ptr, size, byte)
}

View File

@@ -333,6 +333,29 @@ class VM(
try { it.join(500L) } catch (_: InterruptedException) { Thread.currentThread().interrupt() }
}
contexts.clear()
// Some JS code (e.g. TVDOS) spawns workers that aren't routed through Parallel.attachProgram,
// so they never land in `contexts`. We can still find them by walking the JVM thread set and
// matching the per-VM suffix that VMRunnerFactory uses for thread names ("…!<vmId>").
val suffix = "!${id.text}"
val all = arrayOfNulls<Thread>(Thread.activeCount() * 2)
val n = Thread.enumerate(all)
for (i in 0 until n) {
val t = all[i] ?: continue
if (t === Thread.currentThread()) continue
val name = t.name
if (name.endsWith(suffix) || name == "VmRunner:${id.text}") {
t.interrupt()
}
}
for (i in 0 until n) {
val t = all[i] ?: continue
if (t === Thread.currentThread()) continue
val name = t.name
if (name.endsWith(suffix) || name == "VmRunner:${id.text}") {
try { t.join(500L) } catch (_: InterruptedException) { Thread.currentThread().interrupt() }
}
}
}
/**

View File

@@ -45,6 +45,7 @@ class CLCDDisplay(assetsRoot: String, vm: VM) : GraphicsAdapter(assetsRoot, vm,
shader: ShaderProgram?,
uiFBO: FrameBuffer?
) {
if (disposed) return
batch.shader = null
batch.inUse {
batch.color = Color.WHITE

View File

@@ -40,6 +40,7 @@ class CharacterLCDdisplay(assetsRoot: String, vm: VM) : GraphicsAdapter(assetsRo
shader: ShaderProgram?,
uiFBO: FrameBuffer?
) {
if (disposed) return
batch.shader = null
batch.inUse {
batch.color = Color.WHITE

View File

@@ -942,7 +942,11 @@ open class GraphicsAdapter(private val assetsRoot: String, val vm: VM, val confi
try { this.dispose() } catch (_: GdxRuntimeException) {} catch (_: IllegalArgumentException) {}
}
@Volatile var disposed = false; private set
override fun dispose() {
if (disposed) return
disposed = true
//testTex.dispose()
// paletteShader.tryDispose()
// textShader.tryDispose()
@@ -986,6 +990,11 @@ open class GraphicsAdapter(private val assetsRoot: String, val vm: VM, val confi
private val isRefSize = (WIDTH == 560 && HEIGHT == 448)
open fun render(delta: Float, uiBatch: SpriteBatch, xoff: Float, yoff: Float, flipY: Boolean = false, shader: ShaderProgram? = null, uiFBO: FrameBuffer? = null) {
// Bail out if the adapter has already been torn down. Otherwise touching
// any of the disposed Pixmaps / Textures / native buffers below would
// raise a GdxRuntimeException or SIGSEGV.
if (disposed) return
uiFBO?.end()