Add a drive LED vor virtual disk device
Cleanup code base Adding a splash screen
This commit is contained in:
@@ -15,7 +15,8 @@
|
|||||||
android:theme="@style/Theme.SchwertUndMagieOnPebbleCompanionApp">
|
android:theme="@style/Theme.SchwertUndMagieOnPebbleCompanionApp">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true">
|
android:exported="true"
|
||||||
|
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
|||||||
+37
-31
@@ -5,57 +5,63 @@ import android.graphics.Bitmap
|
|||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.SurfaceHolder
|
import android.view.View
|
||||||
import android.view.SurfaceView
|
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the VICE 320×200 ARGB framebuffer into a SurfaceView,
|
* Renders the VICE 320×200 ARGB framebuffer into a View.
|
||||||
* scaled to fill the view while keeping the 8:5 aspect ratio.
|
*
|
||||||
|
* Frame data is written from the emulator thread via captureFrame(), then the
|
||||||
|
* view is invalidated so onDraw() runs on the main thread under Choreographer
|
||||||
|
* vsync. All canvas operations stay on the main thread, giving smooth rendering
|
||||||
|
* synchronized with the hardware display refresh rate.
|
||||||
*/
|
*/
|
||||||
class C64DisplayView @JvmOverloads constructor(
|
class C64DisplayView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null
|
attrs: AttributeSet? = null
|
||||||
) : SurfaceView(context, attrs), SurfaceHolder.Callback {
|
) : View(context, attrs) {
|
||||||
|
|
||||||
private val bitmap = Bitmap.createBitmap(320, 200, Bitmap.Config.ARGB_8888)
|
private val bitmap = Bitmap.createBitmap(320, 200, Bitmap.Config.ARGB_8888)
|
||||||
private val pixelBuf: ByteBuffer = ByteBuffer.allocateDirect(320 * 200 * 4)
|
|
||||||
|
/* Two ByteBuffers: the emulator thread writes to backBuf, then atomically
|
||||||
|
* swaps it to frontBuf. onDraw() reads frontBuf without holding the lock. */
|
||||||
|
private val buf0: ByteBuffer = ByteBuffer.allocateDirect(320 * 200 * 4)
|
||||||
|
private val buf1: ByteBuffer = ByteBuffer.allocateDirect(320 * 200 * 4)
|
||||||
|
@Volatile private var frontBuf: ByteBuffer = buf0
|
||||||
|
private var backBuf: ByteBuffer = buf1
|
||||||
|
|
||||||
private val srcRect = Rect(0, 0, 320, 200)
|
private val srcRect = Rect(0, 0, 320, 200)
|
||||||
private var dstRect = Rect()
|
private var dstRect = Rect()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
holder.addCallback(this)
|
setBackgroundColor(android.graphics.Color.BLACK)
|
||||||
|
setLayerType(LAYER_TYPE_HARDWARE, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Called from the emulator thread after every runFrame(). */
|
/** Called from the emulator thread — copies one C64 frame into the back buffer. */
|
||||||
fun updateFrame(engine: C64Engine) {
|
fun captureFrame(engine: C64Engine) {
|
||||||
if (!holder.surface.isValid) return
|
backBuf.rewind()
|
||||||
|
engine.getVideoBuffer(backBuf)
|
||||||
pixelBuf.rewind()
|
/* Swap: make the freshly-written buffer visible to onDraw() */
|
||||||
engine.getVideoBuffer(pixelBuf)
|
val tmp = frontBuf
|
||||||
pixelBuf.rewind()
|
frontBuf = backBuf
|
||||||
bitmap.copyPixelsFromBuffer(pixelBuf)
|
backBuf = tmp
|
||||||
|
|
||||||
val canvas: Canvas = try { holder.lockCanvas() ?: return } catch (_: Exception) { return }
|
|
||||||
try {
|
|
||||||
canvas.drawBitmap(bitmap, srcRect, dstRect, null)
|
|
||||||
} finally {
|
|
||||||
holder.unlockCanvasAndPost(canvas)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun surfaceCreated(h: SurfaceHolder) = recalcDst()
|
override fun onDraw(canvas: Canvas) {
|
||||||
override fun surfaceChanged(h: SurfaceHolder, f: Int, w: Int, h2: Int) = recalcDst()
|
val buf = frontBuf
|
||||||
override fun surfaceDestroyed(h: SurfaceHolder) {}
|
buf.rewind()
|
||||||
|
bitmap.copyPixelsFromBuffer(buf)
|
||||||
|
canvas.drawBitmap(bitmap, srcRect, dstRect, null)
|
||||||
|
}
|
||||||
|
|
||||||
private fun recalcDst() {
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||||
val vw = width.toFloat()
|
if (w <= 0 || h <= 0) return
|
||||||
val vh = height.toFloat()
|
val scale = minOf(w.toFloat() / 320f, h.toFloat() / 200f)
|
||||||
val scale = minOf(vw / 320f, vh / 200f)
|
|
||||||
val sw = (320 * scale).toInt()
|
val sw = (320 * scale).toInt()
|
||||||
val sh = (200 * scale).toInt()
|
val sh = (200 * scale).toInt()
|
||||||
val ox = ((vw - sw) / 2).toInt()
|
val ox = ((w - sw) / 2).toInt()
|
||||||
val oy = ((vh - sh) / 2).toInt()
|
val oy = ((h - sh) / 2).toInt()
|
||||||
dstRect = Rect(ox, oy, ox + sw, oy + sh)
|
dstRect = Rect(ox, oy, ox + sw, oy + sh)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-1
@@ -19,9 +19,12 @@ class C64Engine {
|
|||||||
/** Initialize VICE. romDir should contain kernal, basic, chargen. */
|
/** Initialize VICE. romDir should contain kernal, basic, chargen. */
|
||||||
external fun initEmulator(romDir: String): Boolean
|
external fun initEmulator(romDir: String): Boolean
|
||||||
|
|
||||||
/** Mount a .d64/.t64 disk image to drive 8. */
|
/** Reset the machine and autostart from this disk image (use for A-side episode disks). */
|
||||||
external fun loadDisk(path: String): Boolean
|
external fun loadDisk(path: String): Boolean
|
||||||
|
|
||||||
|
/** Hot-swap disk in drive 8 without resetting (use for B-side / hero disks). */
|
||||||
|
external fun attachDisk(path: String): Boolean
|
||||||
|
|
||||||
/** Run the emulator for one video frame (~20 000 CPU cycles). */
|
/** Run the emulator for one video frame (~20 000 CPU cycles). */
|
||||||
external fun runFrame()
|
external fun runFrame()
|
||||||
|
|
||||||
@@ -40,6 +43,12 @@ class C64Engine {
|
|||||||
/** Enable or disable audio output. Disabled by default. */
|
/** Enable or disable audio output. Disabled by default. */
|
||||||
external fun setSoundEnabled(enabled: Boolean)
|
external fun setSoundEnabled(enabled: Boolean)
|
||||||
|
|
||||||
|
/** Returns true if drive 8's LED is currently on (disk read/write in progress). */
|
||||||
|
external fun getDriveLed(): Boolean
|
||||||
|
|
||||||
|
/** Monotonically increasing frame counter — incremented once per rendered C64 frame. */
|
||||||
|
external fun getFrameCount(): Int
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
init { System.loadLibrary("vice_jni") }
|
init { System.loadLibrary("vice_jni") }
|
||||||
|
|
||||||
@@ -63,14 +72,45 @@ class C64Engine {
|
|||||||
const val KEY_8 = (3 shl 8) or 3
|
const val KEY_8 = (3 shl 8) or 3
|
||||||
const val KEY_9 = (4 shl 8) or 0
|
const val KEY_9 = (4 shl 8) or 0
|
||||||
const val KEY_0 = (4 shl 8) or 3
|
const val KEY_0 = (4 shl 8) or 3
|
||||||
|
// Letters
|
||||||
const val KEY_Q = (7 shl 8) or 6
|
const val KEY_Q = (7 shl 8) or 6
|
||||||
const val KEY_W = (1 shl 8) or 1
|
const val KEY_W = (1 shl 8) or 1
|
||||||
const val KEY_E = (1 shl 8) or 6
|
const val KEY_E = (1 shl 8) or 6
|
||||||
const val KEY_R = (2 shl 8) or 1
|
const val KEY_R = (2 shl 8) or 1
|
||||||
|
const val KEY_T = (2 shl 8) or 6
|
||||||
const val KEY_Y = (3 shl 8) or 1
|
const val KEY_Y = (3 shl 8) or 1
|
||||||
|
const val KEY_U = (3 shl 8) or 6
|
||||||
|
const val KEY_I = (4 shl 8) or 1
|
||||||
|
const val KEY_O = (4 shl 8) or 6
|
||||||
|
const val KEY_P = (5 shl 8) or 1
|
||||||
|
const val KEY_A = (1 shl 8) or 2
|
||||||
|
const val KEY_S = (1 shl 8) or 5
|
||||||
|
const val KEY_D = (2 shl 8) or 2
|
||||||
|
const val KEY_F = (2 shl 8) or 5
|
||||||
|
const val KEY_G = (3 shl 8) or 2
|
||||||
|
const val KEY_H = (3 shl 8) or 5
|
||||||
|
const val KEY_J = (4 shl 8) or 2
|
||||||
|
const val KEY_K = (4 shl 8) or 5
|
||||||
|
const val KEY_L = (5 shl 8) or 2
|
||||||
|
const val KEY_Z = (1 shl 8) or 4
|
||||||
|
const val KEY_X = (2 shl 8) or 7
|
||||||
|
const val KEY_C = (2 shl 8) or 4
|
||||||
|
const val KEY_V = (3 shl 8) or 7
|
||||||
|
const val KEY_B = (3 shl 8) or 4
|
||||||
const val KEY_N = (4 shl 8) or 7
|
const val KEY_N = (4 shl 8) or 7
|
||||||
|
const val KEY_M = (4 shl 8) or 4
|
||||||
|
// Control / special
|
||||||
const val KEY_RETURN = (0 shl 8) or 1
|
const val KEY_RETURN = (0 shl 8) or 1
|
||||||
const val KEY_SPACE = (7 shl 8) or 4
|
const val KEY_SPACE = (7 shl 8) or 4
|
||||||
|
const val KEY_DEL = (0 shl 8) or 0
|
||||||
|
const val KEY_HOME = (6 shl 8) or 3
|
||||||
|
const val KEY_LSHIFT = (1 shl 8) or 7
|
||||||
|
const val KEY_RSHIFT = (6 shl 8) or 4
|
||||||
|
const val KEY_CTRL = (7 shl 8) or 2
|
||||||
|
const val KEY_CBM = (7 shl 8) or 5
|
||||||
|
const val KEY_CUR_UD = (0 shl 8) or 7 // alone = down; SHIFT+this = up
|
||||||
|
const val KEY_CUR_LR = (0 shl 8) or 2 // alone = right; SHIFT+this = left
|
||||||
|
// Function
|
||||||
const val KEY_F1 = (0 shl 8) or 4
|
const val KEY_F1 = (0 shl 8) or 4
|
||||||
const val KEY_F3 = (0 shl 8) or 5
|
const val KEY_F3 = (0 shl 8) or 5
|
||||||
const val KEY_F5 = (0 shl 8) or 6
|
const val KEY_F5 = (0 shl 8) or 6
|
||||||
|
|||||||
+70
-23
@@ -9,15 +9,21 @@ import android.util.AttributeSet
|
|||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
|
||||||
data class C64Key(val label: String, val code: Int, val widthWeight: Float = 1f)
|
/**
|
||||||
|
* A key can send one or more C64 matrix codes simultaneously.
|
||||||
|
* The two-code variant is used for shifted cursor keys (e.g. ↑ = LSHIFT + CUR_UD).
|
||||||
|
*/
|
||||||
|
data class C64Key(
|
||||||
|
val label: String,
|
||||||
|
val codes: List<Int>,
|
||||||
|
val widthWeight: Float = 1f
|
||||||
|
) {
|
||||||
|
constructor(label: String, code: Int, widthWeight: Float = 1f)
|
||||||
|
: this(label, listOf(code), widthWeight)
|
||||||
|
}
|
||||||
|
|
||||||
/** Callback fired when a key is pressed or released. */
|
|
||||||
typealias KeyEventListener = (key: C64Key, pressed: Boolean) -> Unit
|
typealias KeyEventListener = (key: C64Key, pressed: Boolean) -> Unit
|
||||||
|
|
||||||
/**
|
|
||||||
* Virtual C64 keyboard. Draws four rows of keys and fires [onKeyEvent]
|
|
||||||
* on touch down / up.
|
|
||||||
*/
|
|
||||||
class C64KeyboardView @JvmOverloads constructor(
|
class C64KeyboardView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null
|
attrs: AttributeSet? = null
|
||||||
@@ -26,30 +32,63 @@ class C64KeyboardView @JvmOverloads constructor(
|
|||||||
var onKeyEvent: KeyEventListener? = null
|
var onKeyEvent: KeyEventListener? = null
|
||||||
|
|
||||||
private val rows: List<List<C64Key>> = listOf(
|
private val rows: List<List<C64Key>> = listOf(
|
||||||
|
// Row 1 — function keys + DEL / HOME / RUN·STOP
|
||||||
listOf(
|
listOf(
|
||||||
C64Key("F1", C64Engine.KEY_F1), C64Key("F3", C64Engine.KEY_F3),
|
C64Key("F1", C64Engine.KEY_F1),
|
||||||
C64Key("F5", C64Engine.KEY_F5), C64Key("F7", C64Engine.KEY_F7),
|
C64Key("F3", C64Engine.KEY_F3),
|
||||||
C64Key("RUN\nSTOP", C64Engine.KEY_RUNSTOP, 1.5f)
|
C64Key("F5", C64Engine.KEY_F5),
|
||||||
|
C64Key("F7", C64Engine.KEY_F7),
|
||||||
|
C64Key("DEL", C64Engine.KEY_DEL),
|
||||||
|
C64Key("HOME", C64Engine.KEY_HOME),
|
||||||
|
C64Key("STOP", C64Engine.KEY_RUNSTOP, 1.5f),
|
||||||
),
|
),
|
||||||
|
// Row 2 — numbers
|
||||||
listOf(
|
listOf(
|
||||||
C64Key("1", C64Engine.KEY_1), C64Key("2", C64Engine.KEY_2),
|
C64Key("1", C64Engine.KEY_1), C64Key("2", C64Engine.KEY_2),
|
||||||
C64Key("3", C64Engine.KEY_3), C64Key("4", C64Engine.KEY_4),
|
C64Key("3", C64Engine.KEY_3), C64Key("4", C64Engine.KEY_4),
|
||||||
C64Key("5", C64Engine.KEY_5), C64Key("6", C64Engine.KEY_6),
|
C64Key("5", C64Engine.KEY_5), C64Key("6", C64Engine.KEY_6),
|
||||||
C64Key("7", C64Engine.KEY_7), C64Key("8", C64Engine.KEY_8),
|
C64Key("7", C64Engine.KEY_7), C64Key("8", C64Engine.KEY_8),
|
||||||
C64Key("9", C64Engine.KEY_9), C64Key("0", C64Engine.KEY_0)
|
C64Key("9", C64Engine.KEY_9), C64Key("0", C64Engine.KEY_0),
|
||||||
),
|
),
|
||||||
|
// Row 3 — Q … P
|
||||||
listOf(
|
listOf(
|
||||||
C64Key("Q", C64Engine.KEY_Q), C64Key("W", C64Engine.KEY_W),
|
C64Key("Q", C64Engine.KEY_Q), C64Key("W", C64Engine.KEY_W),
|
||||||
C64Key("E", C64Engine.KEY_E), C64Key("R", C64Engine.KEY_R),
|
C64Key("E", C64Engine.KEY_E), C64Key("R", C64Engine.KEY_R),
|
||||||
C64Key("Y", C64Engine.KEY_Y), C64Key("N", C64Engine.KEY_N),
|
C64Key("T", C64Engine.KEY_T), C64Key("Y", C64Engine.KEY_Y),
|
||||||
C64Key("RETURN", C64Engine.KEY_RETURN, 2f)
|
C64Key("U", C64Engine.KEY_U), C64Key("I", C64Engine.KEY_I),
|
||||||
|
C64Key("O", C64Engine.KEY_O), C64Key("P", C64Engine.KEY_P),
|
||||||
),
|
),
|
||||||
|
// Row 4 — A … L + RETURN
|
||||||
listOf(
|
listOf(
|
||||||
C64Key("SPACE", C64Engine.KEY_SPACE, 6f)
|
C64Key("A", C64Engine.KEY_A), C64Key("S", C64Engine.KEY_S),
|
||||||
)
|
C64Key("D", C64Engine.KEY_D), C64Key("F", C64Engine.KEY_F),
|
||||||
|
C64Key("G", C64Engine.KEY_G), C64Key("H", C64Engine.KEY_H),
|
||||||
|
C64Key("J", C64Engine.KEY_J), C64Key("K", C64Engine.KEY_K),
|
||||||
|
C64Key("L", C64Engine.KEY_L),
|
||||||
|
C64Key("RET", C64Engine.KEY_RETURN, 1.5f),
|
||||||
|
),
|
||||||
|
// Row 5 — SHIFT + Z … M + SHIFT
|
||||||
|
listOf(
|
||||||
|
C64Key("SHF", C64Engine.KEY_LSHIFT, 1.5f),
|
||||||
|
C64Key("Z", C64Engine.KEY_Z), C64Key("X", C64Engine.KEY_X),
|
||||||
|
C64Key("C", C64Engine.KEY_C), C64Key("V", C64Engine.KEY_V),
|
||||||
|
C64Key("B", C64Engine.KEY_B), C64Key("N", C64Engine.KEY_N),
|
||||||
|
C64Key("M", C64Engine.KEY_M),
|
||||||
|
C64Key("SHF", C64Engine.KEY_RSHIFT, 1.5f),
|
||||||
|
),
|
||||||
|
// Row 6 — modifiers + SPACE + cursor keys
|
||||||
|
// ↑ and ← are composite: LSHIFT + the cursor key
|
||||||
|
listOf(
|
||||||
|
C64Key("CTRL", C64Engine.KEY_CTRL),
|
||||||
|
C64Key("C=", C64Engine.KEY_CBM),
|
||||||
|
C64Key("SPACE", C64Engine.KEY_SPACE, 3f),
|
||||||
|
C64Key("←", listOf(C64Engine.KEY_LSHIFT, C64Engine.KEY_CUR_LR)),
|
||||||
|
C64Key("→", C64Engine.KEY_CUR_LR),
|
||||||
|
C64Key("↑", listOf(C64Engine.KEY_LSHIFT, C64Engine.KEY_CUR_UD)),
|
||||||
|
C64Key("↓", C64Engine.KEY_CUR_UD),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Map pointer-id → key currently held down by that finger
|
|
||||||
private val heldKeys = mutableMapOf<Int, C64Key>()
|
private val heldKeys = mutableMapOf<Int, C64Key>()
|
||||||
|
|
||||||
private val keyPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
private val keyPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
@@ -63,7 +102,6 @@ class C64KeyboardView @JvmOverloads constructor(
|
|||||||
textAlign = Paint.Align.CENTER
|
textAlign = Paint.Align.CENTER
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-computed key rectangles for hit-testing: rowIndex → list of (rect, key)
|
|
||||||
private var keyRects: List<List<Pair<RectF, C64Key>>> = emptyList()
|
private var keyRects: List<List<Pair<RectF, C64Key>>> = emptyList()
|
||||||
|
|
||||||
override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) {
|
override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) {
|
||||||
@@ -73,13 +111,13 @@ class C64KeyboardView @JvmOverloads constructor(
|
|||||||
|
|
||||||
private fun buildKeyRects(totalW: Float, totalH: Float) {
|
private fun buildKeyRects(totalW: Float, totalH: Float) {
|
||||||
val rowH = totalH / rows.size
|
val rowH = totalH / rows.size
|
||||||
val pad = 4f
|
val pad = 3f
|
||||||
keyRects = rows.mapIndexed { rowIdx, row ->
|
keyRects = rows.mapIndexed { rowIdx, row ->
|
||||||
val totalUnits = row.sumOf { it.widthWeight.toDouble() }.toFloat()
|
val totalUnits = row.sumOf { it.widthWeight.toDouble() }.toFloat()
|
||||||
val unitW = totalW / totalUnits
|
val unitW = totalW / totalUnits
|
||||||
var x = 0f
|
var x = 0f
|
||||||
row.map { key ->
|
row.map { key ->
|
||||||
val kw = key.widthWeight * unitW
|
val kw = key.widthWeight * unitW
|
||||||
val rect = RectF(x + pad, rowIdx * rowH + pad, x + kw - pad, (rowIdx + 1) * rowH - pad)
|
val rect = RectF(x + pad, rowIdx * rowH + pad, x + kw - pad, (rowIdx + 1) * rowH - pad)
|
||||||
x += kw
|
x += kw
|
||||||
rect to key
|
rect to key
|
||||||
@@ -89,18 +127,27 @@ class C64KeyboardView @JvmOverloads constructor(
|
|||||||
|
|
||||||
override fun onDraw(canvas: Canvas) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
val held = heldKeys.values.toSet()
|
val held = heldKeys.values.toSet()
|
||||||
textPaint.textSize = height / (rows.size * 2.5f)
|
// Scale text so it fits roughly in half the row height
|
||||||
|
textPaint.textSize = (height / rows.size) * 0.38f
|
||||||
|
|
||||||
for (row in keyRects) {
|
for (row in keyRects) {
|
||||||
for ((rect, key) in row) {
|
for ((rect, key) in row) {
|
||||||
val paint = if (key in held) pressedPaint else keyPaint
|
canvas.drawRoundRect(rect, 5f, 5f, if (key in held) pressedPaint else keyPaint)
|
||||||
canvas.drawRoundRect(rect, 6f, 6f, paint)
|
drawKeyLabel(canvas, rect, key.label)
|
||||||
val ty = rect.centerY() - (textPaint.ascent() + textPaint.descent()) / 2
|
|
||||||
canvas.drawText(key.label, rect.centerX(), ty, textPaint)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun drawKeyLabel(canvas: Canvas, rect: RectF, label: String) {
|
||||||
|
val lines = label.split("\n")
|
||||||
|
val lineH = textPaint.textSize * 1.15f
|
||||||
|
val totalTextH = lineH * lines.size
|
||||||
|
val startY = rect.centerY() - totalTextH / 2f + textPaint.textSize
|
||||||
|
lines.forEachIndexed { i, line ->
|
||||||
|
canvas.drawText(line, rect.centerX(), startY + i * lineH, textPaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
val idx = event.actionIndex
|
val idx = event.actionIndex
|
||||||
val pid = event.getPointerId(idx)
|
val pid = event.getPointerId(idx)
|
||||||
|
|||||||
+74
-26
@@ -8,7 +8,12 @@ import android.os.Handler
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.view.Choreographer
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
|
import android.widget.ImageView
|
||||||
import android.widget.ScrollView
|
import android.widget.ScrollView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@@ -39,8 +44,6 @@ private data class DiskSlot(
|
|||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var tvStatus: TextView
|
|
||||||
private lateinit var tvTime: TextView
|
|
||||||
private lateinit var tvLog: TextView
|
private lateinit var tvLog: TextView
|
||||||
private lateinit var scrollLog: ScrollView
|
private lateinit var scrollLog: ScrollView
|
||||||
private lateinit var display: C64DisplayView
|
private lateinit var display: C64DisplayView
|
||||||
@@ -53,9 +56,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private var server: CompanionServer? = null
|
private var server: CompanionServer? = null
|
||||||
private val engine = C64Engine()
|
private val engine = C64Engine()
|
||||||
private var emuThread: Thread? = null
|
private lateinit var choreographer: Choreographer
|
||||||
@Volatile private var emuRunning = false
|
@Volatile private var renderActive = false
|
||||||
private var soundEnabled = false
|
private var soundEnabled = false
|
||||||
|
private lateinit var driveLedDrawable: GradientDrawable
|
||||||
|
@Volatile private var driveLedState = false
|
||||||
|
|
||||||
// ---- disk slots ---------------------------------------------------------
|
// ---- disk slots ---------------------------------------------------------
|
||||||
|
|
||||||
@@ -74,6 +79,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
private val episodeButtons = mutableMapOf<DiskSlot, Button>()
|
private val episodeButtons = mutableMapOf<DiskSlot, Button>()
|
||||||
private val loadedADisks = mutableSetOf<Int>()
|
private val loadedADisks = mutableSetOf<Int>()
|
||||||
|
private var activeSlot: DiskSlot? = null
|
||||||
|
|
||||||
// ---- Disk import (multi-select) -----------------------------------------
|
// ---- Disk import (multi-select) -----------------------------------------
|
||||||
|
|
||||||
@@ -105,6 +111,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
drawerLayout = findViewById(R.id.drawer_layout)
|
drawerLayout = findViewById(R.id.drawer_layout)
|
||||||
|
|
||||||
|
driveLedDrawable = GradientDrawable().apply {
|
||||||
|
shape = GradientDrawable.OVAL
|
||||||
|
setColor(Color.parseColor("#2A2A2A"))
|
||||||
|
}
|
||||||
|
findViewById<android.view.View>(R.id.drive_led).background = driveLedDrawable
|
||||||
|
|
||||||
// On Android 15+ the app draws behind the status bar (edge-to-edge enforced).
|
// On Android 15+ the app draws behind the status bar (edge-to-edge enforced).
|
||||||
// Shift the main content down by the actual status bar height so the
|
// Shift the main content down by the actual status bar height so the
|
||||||
// hamburger button isn't hidden underneath it.
|
// hamburger button isn't hidden underneath it.
|
||||||
@@ -115,8 +127,13 @@ class MainActivity : AppCompatActivity() {
|
|||||||
view.setPadding(pad8, bars.top + pad8, pad8, pad8)
|
view.setPadding(pad8, bars.top + pad8, pad8, pad8)
|
||||||
insets
|
insets
|
||||||
}
|
}
|
||||||
tvStatus = findViewById(R.id.tv_status)
|
val navDrawer = findViewById<android.view.View>(R.id.nav_drawer)
|
||||||
tvTime = findViewById(R.id.tv_time)
|
ViewCompat.setOnApplyWindowInsetsListener(navDrawer) { view, insets ->
|
||||||
|
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val pad12 = (12 * resources.displayMetrics.density).toInt()
|
||||||
|
view.setPadding(pad12, bars.top + pad12, pad12, pad12)
|
||||||
|
insets
|
||||||
|
}
|
||||||
tvLog = findViewById(R.id.tv_log)
|
tvLog = findViewById(R.id.tv_log)
|
||||||
scrollLog = findViewById(R.id.scroll_log)
|
scrollLog = findViewById(R.id.scroll_log)
|
||||||
display = findViewById(R.id.c64_display)
|
display = findViewById(R.id.c64_display)
|
||||||
@@ -155,10 +172,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
findViewById<Button>(btnId).setOnClickListener { confirmCreateHeroDisk(slot) }
|
findViewById<Button>(btnId).setOnClickListener { confirmCreateHeroDisk(slot) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
choreographer = Choreographer.getInstance()
|
||||||
initEmulator()
|
initEmulator()
|
||||||
refreshDiskButtons()
|
refreshDiskButtons()
|
||||||
startHttpServer()
|
startHttpServer()
|
||||||
wireKeyboard()
|
wireKeyboard()
|
||||||
|
dismissSplashDelayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- emulator -----------------------------------------------------------
|
// ---- emulator -----------------------------------------------------------
|
||||||
@@ -169,19 +188,28 @@ class MainActivity : AppCompatActivity() {
|
|||||||
copyBundledRoms(assetDir)
|
copyBundledRoms(assetDir)
|
||||||
val romDir = assetDir.absolutePath
|
val romDir = assetDir.absolutePath
|
||||||
val ok = engine.initEmulator(romDir)
|
val ok = engine.initEmulator(romDir)
|
||||||
tvStatus.text = if (ok) "Emulator ready" else "Emulator init failed"
|
|
||||||
Log.d(TAG, "initEmulator=$ok romDir=$romDir")
|
Log.d(TAG, "initEmulator=$ok romDir=$romDir")
|
||||||
|
|
||||||
emuRunning = true
|
/* Choreographer drives rendering: fires on every hardware vsync (60/90/120 Hz).
|
||||||
emuThread = Thread {
|
* captureFrame() reads the VICE framebuffer on this main-thread callback, then
|
||||||
while (emuRunning) {
|
* invalidate() causes onDraw() to blit it — all under vsync, no tearing. */
|
||||||
val t0 = System.currentTimeMillis()
|
renderActive = true
|
||||||
engine.runFrame()
|
choreographer.postFrameCallback(object : Choreographer.FrameCallback {
|
||||||
display.updateFrame(engine)
|
override fun doFrame(frameTimeNanos: Long) {
|
||||||
val elapsed = System.currentTimeMillis() - t0
|
if (!renderActive) return
|
||||||
if (elapsed < 20) Thread.sleep(20 - elapsed)
|
display.captureFrame(engine)
|
||||||
|
display.invalidate()
|
||||||
|
val led = engine.getDriveLed()
|
||||||
|
if (led != driveLedState) {
|
||||||
|
driveLedState = led
|
||||||
|
driveLedDrawable.setColor(
|
||||||
|
if (led) Color.parseColor("#44DD66")
|
||||||
|
else Color.parseColor("#2A2A2A")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
choreographer.postFrameCallback(this)
|
||||||
}
|
}
|
||||||
}.also { it.name = "emu-loop"; it.isDaemon = true; it.start() }
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- bundled ROM extraction ---------------------------------------------
|
// ---- bundled ROM extraction ---------------------------------------------
|
||||||
@@ -207,7 +235,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun wireKeyboard() {
|
private fun wireKeyboard() {
|
||||||
keyboard.onKeyEvent = { key, pressed ->
|
keyboard.onKeyEvent = { key, pressed ->
|
||||||
engine.injectKey(key.code, pressed)
|
for (code in key.codes) engine.injectKey(code, pressed)
|
||||||
if (pressed) {
|
if (pressed) {
|
||||||
Log.d(TAG, "Key pressed: ${key.label}")
|
Log.d(TAG, "Key pressed: ${key.label}")
|
||||||
mainHandler.post { appendLog("KEY: ${key.label}") }
|
mainHandler.post { appendLog("KEY: ${key.label}") }
|
||||||
@@ -219,9 +247,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun startHttpServer() {
|
private fun startHttpServer() {
|
||||||
server = CompanionServer(PORT, timeFormat,
|
server = CompanionServer(PORT, timeFormat,
|
||||||
onTimeFetched = { time ->
|
onTimeFetched = { _ -> },
|
||||||
mainHandler.post { tvTime.text = "Serving time: $time" }
|
|
||||||
},
|
|
||||||
onKey = { cmd ->
|
onKey = { cmd ->
|
||||||
Log.d(TAG, "Watch key: $cmd")
|
Log.d(TAG, "Watch key: $cmd")
|
||||||
mainHandler.post { appendLog("WATCH: $cmd") }
|
mainHandler.post { appendLog("WATCH: $cmd") }
|
||||||
@@ -244,7 +270,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun refreshDiskButtons() {
|
private fun refreshDiskButtons() {
|
||||||
for (slot in diskSlots) {
|
for (slot in diskSlots) {
|
||||||
episodeButtons[slot]?.isEnabled = diskFile(slot) != null
|
val btn = episodeButtons[slot] ?: continue
|
||||||
|
btn.isEnabled = diskFile(slot) != null
|
||||||
|
btn.backgroundTintList = if (slot == activeSlot)
|
||||||
|
ColorStateList.valueOf(Color.parseColor("#1A4A1A"))
|
||||||
|
else
|
||||||
|
ColorStateList.valueOf(Color.TRANSPARENT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,9 +291,16 @@ class MainActivity : AppCompatActivity() {
|
|||||||
appendLog("Disk not found: ${slot.fileName}")
|
appendLog("Disk not found: ${slot.fileName}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val ok = engine.loadDisk(file.absolutePath)
|
// A-side episode disks restart the machine; B-side and hero disks are hot-swapped.
|
||||||
if (ok && slot.part == 'A') loadedADisks.add(slot.episode)
|
val resetMachine = slot.part == 'A'
|
||||||
appendLog(if (ok) "Loaded: ${slot.label}" else "Load failed: ${slot.fileName}")
|
val ok = if (resetMachine) engine.loadDisk(file.absolutePath)
|
||||||
|
else engine.attachDisk(file.absolutePath)
|
||||||
|
if (ok) {
|
||||||
|
if (slot.part == 'A') loadedADisks.add(slot.episode)
|
||||||
|
activeSlot = slot
|
||||||
|
refreshDiskButtons()
|
||||||
|
}
|
||||||
|
appendLog(if (ok) "Inserted: ${slot.label}" else "Insert failed: ${slot.fileName}")
|
||||||
if (ok) drawerLayout.closeDrawer(GravityCompat.START)
|
if (ok) drawerLayout.closeDrawer(GravityCompat.START)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,6 +426,17 @@ class MainActivity : AppCompatActivity() {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- splash screen ------------------------------------------------------
|
||||||
|
|
||||||
|
private fun dismissSplashDelayed() {
|
||||||
|
mainHandler.postDelayed({
|
||||||
|
val splash = findViewById<ImageView>(R.id.splash_screen) ?: return@postDelayed
|
||||||
|
splash.animate().alpha(0f).setDuration(700).withEndAction {
|
||||||
|
splash.visibility = android.view.View.GONE
|
||||||
|
}.start()
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
// ---- helpers ------------------------------------------------------------
|
// ---- helpers ------------------------------------------------------------
|
||||||
|
|
||||||
private fun appendLog(entry: String) {
|
private fun appendLog(entry: String) {
|
||||||
@@ -404,8 +453,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
emuRunning = false
|
renderActive = false
|
||||||
emuThread?.join(500)
|
|
||||||
server?.stop()
|
server?.stop()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ if(HAVE_VICE)
|
|||||||
-Wl,--wrap=maincpu_mainloop
|
-Wl,--wrap=maincpu_mainloop
|
||||||
-Wl,--wrap=init_main
|
-Wl,--wrap=init_main
|
||||||
-Wl,--wrap=machine_init
|
-Wl,--wrap=machine_init
|
||||||
-Wl,--wrap=console_init)
|
-Wl,--wrap=console_init
|
||||||
|
-Wl,--wrap=ui_display_drive_led
|
||||||
|
-Wl,--wrap=serial_trap_receive)
|
||||||
target_include_directories(vice_jni PRIVATE
|
target_include_directories(vice_jni PRIVATE
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/vice-libs/${ANDROID_ABI}"
|
"${CMAKE_CURRENT_SOURCE_DIR}/vice-libs/${ANDROID_ABI}"
|
||||||
"${VICE_SRC}/src"
|
"${VICE_SRC}/src"
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
#include <errno.h>
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <time.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
@@ -36,11 +36,14 @@ static uint32_t g_framebuf[FRAME_W * FRAME_H];
|
|||||||
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
|
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||||
static int g_ready = 0;
|
static int g_ready = 0;
|
||||||
|
|
||||||
/* Pending disk path — written by loadDisk (Android thread), consumed by
|
/* Pending disk operations — written by Android thread, consumed by
|
||||||
* video_canvas_refresh (VICE thread). autostart_disk must be called from
|
* video_canvas_refresh on the VICE thread (both attach and autostart APIs
|
||||||
* the VICE thread; calling it cross-thread corrupts VICE internal state. */
|
* must be called from the VICE thread to avoid corrupting internal state).
|
||||||
static pthread_mutex_t g_pending_lock = PTHREAD_MUTEX_INITIALIZER;
|
* g_pending_disk → autostart_disk (resets the machine, used for A-side disks)
|
||||||
|
* g_pending_attach → file_system_attach_disk (hot-swap, used for B-side / hero disks) */
|
||||||
|
static pthread_mutex_t g_pending_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||||
static char g_pending_disk[512];
|
static char g_pending_disk[512];
|
||||||
|
static char g_pending_attach[512];
|
||||||
|
|
||||||
/* =========================================================================
|
/* =========================================================================
|
||||||
* VICE integration (compiled only when libvice.a is linked in)
|
* VICE integration (compiled only when libvice.a is linked in)
|
||||||
@@ -61,6 +64,55 @@ extern int __real_console_init(void);
|
|||||||
#include "palette.h"
|
#include "palette.h"
|
||||||
#include "keyboard.h"
|
#include "keyboard.h"
|
||||||
#include "autostart.h"
|
#include "autostart.h"
|
||||||
|
#include "attach.h"
|
||||||
|
|
||||||
|
/* Drive LED state.
|
||||||
|
* g_io_frames: bumped by __wrap_serial_trap_receive; keeps LED lit ~100 ms after
|
||||||
|
* each KERNAL-trap byte read (intra-game loads, directory reads, etc.).
|
||||||
|
* g_load_frames: set to 3000 (~60 s at 50 fps) when autostart_in_progress()
|
||||||
|
* transitions from true to false. Approximates the real 1541 loading time that
|
||||||
|
* would otherwise keep the LED on throughout the GDG intro on real hardware.
|
||||||
|
* Reset to 0 on hot-disk-swap so a new disk's autostart gets a fresh window.
|
||||||
|
* g_attach_frames: short grace period so the LED lights immediately on hot-swap. */
|
||||||
|
static volatile int g_drive_led = 0;
|
||||||
|
static volatile int g_io_frames = 0;
|
||||||
|
static volatile int g_load_frames = 0;
|
||||||
|
static volatile int g_attach_frames = 0;
|
||||||
|
/* Incremented by video_canvas_refresh at the end of each rendered frame.
|
||||||
|
* Read by getFrameCount() from the Kotlin display loop so it only wakes up
|
||||||
|
* and blits to the SurfaceView when VICE has produced a new frame. */
|
||||||
|
static volatile int g_frame_count = 0;
|
||||||
|
|
||||||
|
/* --wrap=ui_display_drive_led: keep this wrap to prevent a duplicate-symbol link
|
||||||
|
* error (lld rejects our plain redefinition of the headless-arch stub even when
|
||||||
|
* our object comes first). The virtual device's drive LED is unreliable for us
|
||||||
|
* (via2d reset sets led_status=1 at startup and no 1541 CPU ever clears it), so
|
||||||
|
* we do NOT use it for g_drive_led. Just forward to the original no-op stub. */
|
||||||
|
void __real_ui_display_drive_led(unsigned int drive_number, unsigned int drive_base,
|
||||||
|
unsigned int led_pwm1, unsigned int led_pwm2);
|
||||||
|
void __wrap_ui_display_drive_led(unsigned int drive_number, unsigned int drive_base,
|
||||||
|
unsigned int led_pwm1, unsigned int led_pwm2) {
|
||||||
|
__real_ui_display_drive_led(drive_number, drive_base, led_pwm1, led_pwm2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --wrap=serial_trap_receive: fires once per byte that the C64 KERNAL reads from
|
||||||
|
* the virtual disk device. c64.c installs serial_trap_receive as a function
|
||||||
|
* pointer from a different TU than serial-trap.c where it is defined, so the
|
||||||
|
* --wrap redirect works. Reset g_io_frames to keep the drive LED lit while data
|
||||||
|
* is flowing; the countdown in video_canvas_refresh turns it off 5 frames (~100 ms)
|
||||||
|
* after the last byte arrives. */
|
||||||
|
int __real_serial_trap_receive(void);
|
||||||
|
int __wrap_serial_trap_receive(void) {
|
||||||
|
g_io_frames = 5;
|
||||||
|
return __real_serial_trap_receive();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override VICE's archdep_program_name so the startup banner shows a friendly
|
||||||
|
* name instead of "app_process64" (the actual Android process binary).
|
||||||
|
* Defining both symbols here prevents the linker from pulling in the conflicting
|
||||||
|
* archive member from libvice.a. */
|
||||||
|
const char *archdep_program_name(void) { return "ViceForSchwertUndMagie"; }
|
||||||
|
void archdep_program_name_free(void) { /* string literal — nothing to free */ }
|
||||||
|
|
||||||
/* =========================================================================
|
/* =========================================================================
|
||||||
* Android OpenSL ES sound driver — registered with VICE before main_program()
|
* Android OpenSL ES sound driver — registered with VICE before main_program()
|
||||||
@@ -122,8 +174,6 @@ static void sl_bufq_callback(SLAndroidSimpleBufferQueueItf bq, void *ctx) {
|
|||||||
static int android_sound_init(const char *param, int *speed,
|
static int android_sound_init(const char *param, int *speed,
|
||||||
int *fragsize, int *fragnr, int *channels) {
|
int *fragsize, int *fragnr, int *channels) {
|
||||||
(void)param;
|
(void)param;
|
||||||
LOGI("android_sound_init: speed=%d frag=%d nr=%d ch=%d",
|
|
||||||
*speed, *fragsize, *fragnr, *channels);
|
|
||||||
if (*channels > 2) *channels = 2;
|
if (*channels > 2) *channels = 2;
|
||||||
if (*channels < 1) *channels = 1;
|
if (*channels < 1) *channels = 1;
|
||||||
|
|
||||||
@@ -168,7 +218,6 @@ static int android_sound_init(const char *param, int *speed,
|
|||||||
(*g_sl_bq)->Enqueue(g_sl_bq, g_sl_bufs[i], AUDIO_BUF_SAMPS * sizeof(int16_t));
|
(*g_sl_bq)->Enqueue(g_sl_bq, g_sl_bufs[i], AUDIO_BUF_SAMPS * sizeof(int16_t));
|
||||||
}
|
}
|
||||||
g_sl_next_buf = 0;
|
g_sl_next_buf = 0;
|
||||||
LOGI("android_sound_init: OpenSL ES ready");
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +256,6 @@ static void android_sound_close(void) {
|
|||||||
(*g_sl_engine_obj)->Destroy(g_sl_engine_obj);
|
(*g_sl_engine_obj)->Destroy(g_sl_engine_obj);
|
||||||
g_sl_engine_obj = NULL; g_sl_engine = NULL;
|
g_sl_engine_obj = NULL; g_sl_engine = NULL;
|
||||||
}
|
}
|
||||||
LOGI("android_sound_close: done");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static const sound_device_t g_android_sound_device = {
|
static const sound_device_t g_android_sound_device = {
|
||||||
@@ -226,7 +274,7 @@ static const sound_device_t g_android_sound_device = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* Stubs for symbols that live in arch-specific or excluded files. */
|
/* Stubs for symbols that live in arch-specific or excluded files. */
|
||||||
void main_exit(void) { LOGI("main_exit called — ignoring in JNI context"); }
|
void main_exit(void) {}
|
||||||
|
|
||||||
/* All functions below replace arch/headless/video.c (excluded from libvice.a). */
|
/* All functions below replace arch/headless/video.c (excluded from libvice.a). */
|
||||||
|
|
||||||
@@ -277,13 +325,22 @@ int video_canvas_set_palette(video_canvas_t *canvas, struct palette_s *palette)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Full PAL raster dimensions — VICE allocates draw_buffer_line_size = *width
|
||||||
|
* bytes per row in the internal draw buffer. If we return 320 here, reading
|
||||||
|
* from xs=136 (PAL left edge of the visible area) wraps to the next row and
|
||||||
|
* scrambles the display. Returning the full PAL frame size (504×312) gives
|
||||||
|
* VICE enough room; video_canvas_render() writes into our 320×200 g_framebuf
|
||||||
|
* safely because the destination pitch is supplied separately as FRAME_W*4. */
|
||||||
|
#define PAL_W 504
|
||||||
|
#define PAL_H 312
|
||||||
|
|
||||||
video_canvas_t *video_canvas_create(video_canvas_t *canvas,
|
video_canvas_t *video_canvas_create(video_canvas_t *canvas,
|
||||||
unsigned int *width, unsigned int *height,
|
unsigned int *width, unsigned int *height,
|
||||||
int mapped) {
|
int mapped) {
|
||||||
(void)mapped;
|
(void)mapped;
|
||||||
canvas->created = 1;
|
canvas->created = 1;
|
||||||
*width = FRAME_W;
|
*width = PAL_W;
|
||||||
*height = FRAME_H;
|
*height = PAL_H;
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,23 +352,45 @@ void video_canvas_refresh(video_canvas_t *canvas,
|
|||||||
unsigned int xs, unsigned int ys,
|
unsigned int xs, unsigned int ys,
|
||||||
unsigned int xi, unsigned int yi,
|
unsigned int xi, unsigned int yi,
|
||||||
unsigned int w, unsigned int h) {
|
unsigned int w, unsigned int h) {
|
||||||
/* Process any pending disk load on the VICE thread so autostart_disk is
|
/* Process any pending disk operations on the VICE thread. */
|
||||||
* never called cross-thread (it modifies complex internal VICE state). */
|
|
||||||
pthread_mutex_lock(&g_pending_lock);
|
pthread_mutex_lock(&g_pending_lock);
|
||||||
if (g_pending_disk[0] != '\0') {
|
char autostart_path[512] = {0};
|
||||||
char path[512];
|
char attach_path[512] = {0};
|
||||||
strncpy(path, g_pending_disk, sizeof(path) - 1);
|
if (g_pending_disk[0] != '\0') { strncpy(autostart_path, g_pending_disk, sizeof(autostart_path)-1); g_pending_disk[0] = '\0'; }
|
||||||
path[sizeof(path) - 1] = '\0';
|
if (g_pending_attach[0] != '\0') { strncpy(attach_path, g_pending_attach, sizeof(attach_path)-1); g_pending_attach[0] = '\0'; }
|
||||||
g_pending_disk[0] = '\0';
|
pthread_mutex_unlock(&g_pending_lock);
|
||||||
pthread_mutex_unlock(&g_pending_lock);
|
if (autostart_path[0] != '\0')
|
||||||
LOGI("autostart_disk: %s", path);
|
autostart_disk(8, 0, autostart_path, NULL, 0, AUTOSTART_MODE_RUN);
|
||||||
int asd_ret = autostart_disk(8, 0, path, NULL, 0, AUTOSTART_MODE_RUN);
|
if (attach_path[0] != '\0') {
|
||||||
LOGI("autostart_disk returned %d", asd_ret);
|
int r = file_system_attach_disk(8, 0, attach_path);
|
||||||
} else {
|
if (r == 0) {
|
||||||
pthread_mutex_unlock(&g_pending_lock);
|
g_load_frames = 0; /* clear old cooldown; new autostart will set a fresh one */
|
||||||
|
g_attach_frames = 75; /* show LED for ~1.5 s immediately after hot-swap */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Clamp destination to our framebuffer bounds. */
|
/* Drive LED.
|
||||||
|
* VICE's virtual device (vdrive) loads far faster than a real 1541, so
|
||||||
|
* autostart_in_progress() is false long before the GDG intro finishes.
|
||||||
|
* When autostart ends, start a ~60-second cooldown (g_load_frames) that
|
||||||
|
* keeps the LED on throughout the typical intro duration, approximating
|
||||||
|
* the real 1541 loading experience. */
|
||||||
|
static int prev_autostart = -1;
|
||||||
|
int cur_autostart = autostart_in_progress();
|
||||||
|
if (prev_autostart == 1 && cur_autostart == 0)
|
||||||
|
g_load_frames = 3000; /* ~60 s at 50 fps */
|
||||||
|
prev_autostart = cur_autostart;
|
||||||
|
|
||||||
|
if (g_io_frames > 0) g_io_frames--;
|
||||||
|
if (g_load_frames > 0) g_load_frames--;
|
||||||
|
if (g_attach_frames > 0) g_attach_frames--;
|
||||||
|
g_drive_led = (cur_autostart || g_io_frames > 0 || g_load_frames > 0 || g_attach_frames > 0) ? 1 : 0;
|
||||||
|
|
||||||
|
/* Clamp source region so it stays within the PAL draw buffer. */
|
||||||
|
if (xs >= PAL_W || ys >= PAL_H) return;
|
||||||
|
if (xs + w > PAL_W) w = PAL_W - xs;
|
||||||
|
if (ys + h > PAL_H) h = PAL_H - ys;
|
||||||
|
/* Clamp destination to our 320×200 framebuffer. */
|
||||||
if (xi >= FRAME_W || yi >= FRAME_H) return;
|
if (xi >= FRAME_W || yi >= FRAME_H) return;
|
||||||
if (xi + w > FRAME_W) w = FRAME_W - xi;
|
if (xi + w > FRAME_W) w = FRAME_W - xi;
|
||||||
if (yi + h > FRAME_H) h = FRAME_H - yi;
|
if (yi + h > FRAME_H) h = FRAME_H - yi;
|
||||||
@@ -324,6 +403,21 @@ void video_canvas_refresh(video_canvas_t *canvas,
|
|||||||
(int)xi, (int)yi,
|
(int)xi, (int)yi,
|
||||||
FRAME_W * 4);
|
FRAME_W * 4);
|
||||||
pthread_mutex_unlock(&g_lock);
|
pthread_mutex_unlock(&g_lock);
|
||||||
|
|
||||||
|
/* Increment g_frame_count once per C64 video frame. VICE may call
|
||||||
|
* video_canvas_refresh multiple times per logical frame (dirty-rect
|
||||||
|
* updates), so guard with a 15 ms minimum interval (~75 % of a 20 ms
|
||||||
|
* frame period at 50 fps) — the first call per frame triggers the counter;
|
||||||
|
* subsequent calls within the same frame window are skipped. */
|
||||||
|
static struct timespec g_last_frame_ts;
|
||||||
|
struct timespec ts;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
int64_t now_ns = (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec;
|
||||||
|
int64_t last_ns = (int64_t)g_last_frame_ts.tv_sec * 1000000000LL + g_last_frame_ts.tv_nsec;
|
||||||
|
if (now_ns - last_ns >= 15000000LL) {
|
||||||
|
g_last_frame_ts = ts;
|
||||||
|
g_frame_count++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* VICE main loop runs in this thread. */
|
/* VICE main loop runs in this thread. */
|
||||||
@@ -353,9 +447,7 @@ static void *stderr_reader_thread(void *arg) {
|
|||||||
|
|
||||||
static void *vice_thread(void *arg) {
|
static void *vice_thread(void *arg) {
|
||||||
(void)arg;
|
(void)arg;
|
||||||
LOGI("vice_thread: starting main_program");
|
main_program(g_vice_argc, g_vice_argv);
|
||||||
int rc = main_program(g_vice_argc, g_vice_argv);
|
|
||||||
LOGI("vice_thread: main_program returned %d", rc);
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,7 +455,7 @@ static void *vice_thread(void *arg) {
|
|||||||
* --wrap=archdep_vice_exit redirects all calls here so only the VICE thread
|
* --wrap=archdep_vice_exit redirects all calls here so only the VICE thread
|
||||||
* terminates instead. */
|
* terminates instead. */
|
||||||
void __wrap_archdep_vice_exit(int code) {
|
void __wrap_archdep_vice_exit(int code) {
|
||||||
LOGI("archdep_vice_exit(%d) — intercepted, terminating VICE thread", code);
|
(void)code;
|
||||||
pthread_exit(NULL);
|
pthread_exit(NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,6 +464,8 @@ void __wrap_maincpu_mainloop(void) { __real_maincpu_mainloop(); }
|
|||||||
int __wrap_init_main(void) { return __real_init_main(); }
|
int __wrap_init_main(void) { return __real_init_main(); }
|
||||||
int __wrap_machine_init(void) { return __real_machine_init(); }
|
int __wrap_machine_init(void) { return __real_machine_init(); }
|
||||||
int __wrap_console_init(void) { return __real_console_init(); }
|
int __wrap_console_init(void) { return __real_console_init(); }
|
||||||
|
|
||||||
|
|
||||||
#endif /* HAVE_VICE_SRC */
|
#endif /* HAVE_VICE_SRC */
|
||||||
|
|
||||||
/* =========================================================================
|
/* =========================================================================
|
||||||
@@ -384,7 +478,6 @@ int __wrap_console_init(void) { return __real_console_init(); }
|
|||||||
JNI_FN(jboolean, initEmulator)(JNIEnv *env, jobject obj, jstring romDir) {
|
JNI_FN(jboolean, initEmulator)(JNIEnv *env, jobject obj, jstring romDir) {
|
||||||
#ifdef HAVE_VICE_SRC
|
#ifdef HAVE_VICE_SRC
|
||||||
const char *dir = (*env)->GetStringUTFChars(env, romDir, NULL);
|
const char *dir = (*env)->GetStringUTFChars(env, romDir, NULL);
|
||||||
LOGI("initEmulator: romDir=%s", dir);
|
|
||||||
|
|
||||||
/* Prepare argv for VICE: x64 -directory <romDir> -headless */
|
/* Prepare argv for VICE: x64 -directory <romDir> -headless */
|
||||||
static char dir_copy[512];
|
static char dir_copy[512];
|
||||||
@@ -412,13 +505,12 @@ JNI_FN(jboolean, initEmulator)(JNIEnv *env, jobject obj, jstring romDir) {
|
|||||||
snprintf(src, sizeof(src), "%s/%s", dir_copy, c64_roms[i]);
|
snprintf(src, sizeof(src), "%s/%s", dir_copy, c64_roms[i]);
|
||||||
snprintf(dst, sizeof(dst), "%s/%s", vice_c64_dir, c64_roms[i]);
|
snprintf(dst, sizeof(dst), "%s/%s", vice_c64_dir, c64_roms[i]);
|
||||||
FILE *fsrc = fopen(src, "rb");
|
FILE *fsrc = fopen(src, "rb");
|
||||||
if (!fsrc) { LOGI("ROM not found: %s", src); continue; }
|
if (!fsrc) continue;
|
||||||
FILE *fdst = fopen(dst, "wb");
|
FILE *fdst = fopen(dst, "wb");
|
||||||
if (!fdst) { fclose(fsrc); LOGI("ROM copy failed: %s", dst); continue; }
|
if (!fdst) { fclose(fsrc); continue; }
|
||||||
char buf[4096]; size_t n;
|
char buf[4096]; size_t n;
|
||||||
while ((n = fread(buf, 1, sizeof(buf), fsrc)) > 0) fwrite(buf, 1, n, fdst);
|
while ((n = fread(buf, 1, sizeof(buf), fsrc)) > 0) fwrite(buf, 1, n, fdst);
|
||||||
fclose(fsrc); fclose(fdst);
|
fclose(fsrc); fclose(fdst);
|
||||||
LOGI("ROM copied: %s", c64_roms[i]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Copy 1541 drive ROM into romDir/DRIVES/1541 (sysfile_load path for drives).
|
/* Copy 1541 drive ROM into romDir/DRIVES/1541 (sysfile_load path for drives).
|
||||||
@@ -438,11 +530,8 @@ JNI_FN(jboolean, initEmulator)(JNIEnv *env, jobject obj, jstring romDir) {
|
|||||||
char buf[4096]; size_t n;
|
char buf[4096]; size_t n;
|
||||||
while ((n = fread(buf, 1, sizeof(buf), fsrc)) > 0) fwrite(buf, 1, n, fdst);
|
while ((n = fread(buf, 1, sizeof(buf), fsrc)) > 0) fwrite(buf, 1, n, fdst);
|
||||||
fclose(fdst);
|
fclose(fdst);
|
||||||
LOGI("ROM copied: 1541");
|
|
||||||
}
|
}
|
||||||
fclose(fsrc);
|
fclose(fsrc);
|
||||||
} else {
|
|
||||||
LOGI("1541 ROM not found — drive may not work with fast loaders");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,6 +633,36 @@ JNI_FN(jboolean, loadDisk)(JNIEnv *env, jobject obj, jstring path) {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JNI_FN(jboolean, attachDisk)(JNIEnv *env, jobject obj, jstring path) {
|
||||||
|
(void)obj;
|
||||||
|
#ifdef HAVE_VICE_SRC
|
||||||
|
const char *p = (*env)->GetStringUTFChars(env, path, NULL);
|
||||||
|
pthread_mutex_lock(&g_pending_lock);
|
||||||
|
strncpy(g_pending_attach, p, sizeof(g_pending_attach) - 1);
|
||||||
|
g_pending_attach[sizeof(g_pending_attach) - 1] = '\0';
|
||||||
|
pthread_mutex_unlock(&g_pending_lock);
|
||||||
|
(*env)->ReleaseStringUTFChars(env, path, p);
|
||||||
|
return JNI_TRUE;
|
||||||
|
#else
|
||||||
|
(void)env; (void)path;
|
||||||
|
return JNI_FALSE;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
JNI_FN(jboolean, getDriveLed)(JNIEnv *env, jobject obj) {
|
||||||
|
(void)env; (void)obj;
|
||||||
|
#ifdef HAVE_VICE_SRC
|
||||||
|
return g_drive_led ? JNI_TRUE : JNI_FALSE;
|
||||||
|
#else
|
||||||
|
return JNI_FALSE;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
JNI_FN(jint, getFrameCount)(JNIEnv *env, jobject obj) {
|
||||||
|
(void)env; (void)obj;
|
||||||
|
return (jint)g_frame_count;
|
||||||
|
}
|
||||||
|
|
||||||
/* In thread mode runFrame() is a no-op: VICE drives its own loop. */
|
/* In thread mode runFrame() is a no-op: VICE drives its own loop. */
|
||||||
JNI_FN(void, runFrame)(JNIEnv *env, jobject obj) {
|
JNI_FN(void, runFrame)(JNIEnv *env, jobject obj) {
|
||||||
(void)env; (void)obj;
|
(void)env; (void)obj;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 278 KiB |
@@ -1,6 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.drawerlayout.widget.DrawerLayout
|
<FrameLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.drawerlayout.widget.DrawerLayout
|
||||||
android:id="@+id/drawer_layout"
|
android:id="@+id/drawer_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
@@ -32,22 +36,20 @@
|
|||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_status"
|
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:text="Initialising…"
|
android:text="Schwert und Magie"
|
||||||
android:textColor="#AAFFAA"
|
android:textColor="#FFFFFF"
|
||||||
android:textSize="12sp" />
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<TextView
|
<View
|
||||||
android:id="@+id/tv_time"
|
android:id="@+id/drive_led"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="12dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="12dp"
|
||||||
android:text="--:--:--"
|
android:layout_marginEnd="4dp" />
|
||||||
android:textColor="#AAAAFF"
|
|
||||||
android:textSize="12sp" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- C64 display (320×200, scaled with correct aspect ratio) -->
|
<!-- C64 display (320×200, scaled with correct aspect ratio) -->
|
||||||
@@ -55,15 +57,15 @@
|
|||||||
android:id="@+id/c64_display"
|
android:id="@+id/c64_display"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="3"
|
android:layout_weight="2"
|
||||||
android:layout_marginTop="4dp" />
|
android:layout_marginTop="4dp" />
|
||||||
|
|
||||||
<!-- Virtual keyboard -->
|
<!-- Virtual keyboard — 6 rows -->
|
||||||
<de.ladkau.schwertundmagieonpebblecompanionapp.C64KeyboardView
|
<de.ladkau.schwertundmagieonpebblecompanionapp.C64KeyboardView
|
||||||
android:id="@+id/c64_keyboard"
|
android:id="@+id/c64_keyboard"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="2"
|
android:layout_weight="1.8"
|
||||||
android:layout_marginTop="4dp" />
|
android:layout_marginTop="4dp" />
|
||||||
|
|
||||||
<!-- Key-event log -->
|
<!-- Key-event log -->
|
||||||
@@ -96,6 +98,7 @@
|
|||||||
|
|
||||||
<!-- ── Drawer ────────────────────────────────────────────────────────── -->
|
<!-- ── Drawer ────────────────────────────────────────────────────────── -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/nav_drawer"
|
||||||
android:layout_width="280dp"
|
android:layout_width="280dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="start"
|
android:layout_gravity="start"
|
||||||
@@ -110,7 +113,16 @@
|
|||||||
android:textColor="#AAFFAA"
|
android:textColor="#AAFFAA"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:paddingBottom="8dp" />
|
android:paddingBottom="4dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Disketten"
|
||||||
|
android:textColor="#AAFFAA"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:paddingBottom="4dp" />
|
||||||
|
|
||||||
<Button android:id="@+id/btn_ep_1a" style="?attr/materialButtonOutlinedStyle"
|
<Button android:id="@+id/btn_ep_1a" style="?attr/materialButtonOutlinedStyle"
|
||||||
android:layout_width="match_parent" android:layout_height="wrap_content"
|
android:layout_width="match_parent" android:layout_height="wrap_content"
|
||||||
@@ -234,3 +246,16 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.drawerlayout.widget.DrawerLayout>
|
</androidx.drawerlayout.widget.DrawerLayout>
|
||||||
|
|
||||||
|
<!-- Splash screen overlay — covers everything until VICE is ready -->
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/splash_screen"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:src="@drawable/splash_screen"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:background="#000000"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|||||||
Reference in New Issue
Block a user