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) 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) unsafe.copyMemory(src.ptr + fromIndex, dest.ptr + toIndex, copyLength)
}
fun memcpy(srcAddress: Long, destAddress: Long, copyLength: Long) = fun memcpy(srcAddress: Long, destAddress: Long, copyLength: Long) =
unsafe.copyMemory(srcAddress, destAddress, copyLength) unsafe.copyMemory(srcAddress, destAddress, copyLength)
fun memcpyRaw(srcObj: Any?, srcPos: Long, destObj: Any?, destPos: Long, len: Long) = 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. * Returns true when the operation should proceed; false when the pointer is destroyed
//// You may break the glass and use this tool when some fucking incomprehensible bugs ("vittujen vitun bugit") * (so the caller short-circuits to a safe no-op / zero return).
//// appear (e.g. getting garbage values when it fucking shouldn't) *
* Why no exception: a JS worker thread that survives killVMenv (because it wasn't
// if (destroyed) { throw DanglingPointerException("The pointer is already destroyed ($this)") } * tracked in vm.contexts, e.g. raw java.lang.Thread spawned by JS code) will keep
// if (index !in 0 until size) throw AddressOverflowException("Index: $index; alloc size: $size; pointer: ${this}\n${Thread.currentThread().stackTrace.joinToString("\n", limit=10) { " $it" }}") * 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 { operator fun get(index: Long): Byte {
checkNullPtr(index) if (!aliveAt(index)) return 0
return UnsafeHelper.unsafe.getByte(ptr + index) return UnsafeHelper.unsafe.getByte(ptr + index)
} }
operator fun set(index: Long, value: Byte) { operator fun set(index: Long, value: Byte) {
checkNullPtr(index) if (!aliveAt(index)) return
UnsafeHelper.unsafe.putByte(ptr + index, value) UnsafeHelper.unsafe.putByte(ptr + index, value)
} }
fun getFloatFree(index: Long): Float { fun getFloatFree(index: Long): Float {
checkNullPtr(index) if (!aliveAt(index + 3)) return 0f
return UnsafeHelper.unsafe.getFloat(ptr + index) return UnsafeHelper.unsafe.getFloat(ptr + index)
} }
fun getFloat(unit: Long): Float { fun getFloat(unit: Long): Float {
checkNullPtr(unit * 4L) val idx = unit * 4L
return UnsafeHelper.unsafe.getFloat(ptr + (unit * 4L)) if (!aliveAt(idx + 3)) return 0f
return UnsafeHelper.unsafe.getFloat(ptr + idx)
} }
fun getIntFree(index: Long): Int { fun getIntFree(index: Long): Int {
checkNullPtr(index) if (!aliveAt(index + 3)) return 0
return UnsafeHelper.unsafe.getInt(ptr + index) return UnsafeHelper.unsafe.getInt(ptr + index)
} }
fun getInt(unit: Long): Int { fun getInt(unit: Long): Int {
checkNullPtr(unit * 4L) val idx = unit * 4L
return UnsafeHelper.unsafe.getInt(ptr + (unit * 4L)) if (!aliveAt(idx + 3)) return 0
return UnsafeHelper.unsafe.getInt(ptr + idx)
} }
fun getShortFree(index: Long): Short { fun getShortFree(index: Long): Short {
checkNullPtr(index) if (!aliveAt(index + 1)) return 0
return UnsafeHelper.unsafe.getShort(ptr + index) return UnsafeHelper.unsafe.getShort(ptr + index)
} }
fun getShort(unit: Long): Short { fun getShort(unit: Long): Short {
checkNullPtr(unit * 2L) val idx = unit * 2L
return UnsafeHelper.unsafe.getShort(ptr + (unit * 2L)) if (!aliveAt(idx + 1)) return 0
return UnsafeHelper.unsafe.getShort(ptr + idx)
} }
fun setFloatFree(index: Long, value: Float) { fun setFloatFree(index: Long, value: Float) {
checkNullPtr(index) if (!aliveAt(index + 3)) return
UnsafeHelper.unsafe.putFloat(ptr + index, value) UnsafeHelper.unsafe.putFloat(ptr + index, value)
} }
fun setFloat(unit: Long, value: Float) { fun setFloat(unit: Long, value: Float) {
checkNullPtr(unit * 4L) val idx = unit * 4L
UnsafeHelper.unsafe.putFloat(ptr + (unit * 4L), value) if (!aliveAt(idx + 3)) return
UnsafeHelper.unsafe.putFloat(ptr + idx, value)
} }
fun setIntFree(index: Long, value: Int) { fun setIntFree(index: Long, value: Int) {
checkNullPtr(index) if (!aliveAt(index + 3)) return
UnsafeHelper.unsafe.putInt(ptr + index, value) UnsafeHelper.unsafe.putInt(ptr + index, value)
} }
fun setInt(unit: Long, value: Int) { fun setInt(unit: Long, value: Int) {
checkNullPtr(unit * 4L) val idx = unit * 4L
UnsafeHelper.unsafe.putInt(ptr + (unit * 4L), value) if (!aliveAt(idx + 3)) return
UnsafeHelper.unsafe.putInt(ptr + idx, value)
} }
fun setShortFree(index: Long, value: Short) { fun setShortFree(index: Long, value: Short) {
checkNullPtr(index) if (!aliveAt(index + 1)) return
UnsafeHelper.unsafe.putShort(ptr + index, value) UnsafeHelper.unsafe.putShort(ptr + index, value)
} }
fun setShortUnit(unit: Long, value: Short) { fun setShortUnit(unit: Long, value: Short) {
checkNullPtr(unit * 2L) val idx = unit * 2L
UnsafeHelper.unsafe.putShort(ptr + (unit * 2L), value) if (!aliveAt(idx + 1)) return
UnsafeHelper.unsafe.putShort(ptr + idx, value)
} }
fun fillWith(byte: Byte) { fun fillWith(byte: Byte) {
if (destroyed) return
UnsafeHelper.unsafe.setMemory(ptr, size, byte) UnsafeHelper.unsafe.setMemory(ptr, size, byte)
} }

View File

@@ -333,6 +333,29 @@ class VM(
try { it.join(500L) } catch (_: InterruptedException) { Thread.currentThread().interrupt() } try { it.join(500L) } catch (_: InterruptedException) { Thread.currentThread().interrupt() }
} }
contexts.clear() 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?, shader: ShaderProgram?,
uiFBO: FrameBuffer? uiFBO: FrameBuffer?
) { ) {
if (disposed) return
batch.shader = null batch.shader = null
batch.inUse { batch.inUse {
batch.color = Color.WHITE batch.color = Color.WHITE

View File

@@ -40,6 +40,7 @@ class CharacterLCDdisplay(assetsRoot: String, vm: VM) : GraphicsAdapter(assetsRo
shader: ShaderProgram?, shader: ShaderProgram?,
uiFBO: FrameBuffer? uiFBO: FrameBuffer?
) { ) {
if (disposed) return
batch.shader = null batch.shader = null
batch.inUse { batch.inUse {
batch.color = Color.WHITE 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) {} try { this.dispose() } catch (_: GdxRuntimeException) {} catch (_: IllegalArgumentException) {}
} }
@Volatile var disposed = false; private set
override fun dispose() { override fun dispose() {
if (disposed) return
disposed = true
//testTex.dispose() //testTex.dispose()
// paletteShader.tryDispose() // paletteShader.tryDispose()
// textShader.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) 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) { 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() uiFBO?.end()