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">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
||||
+37
-31
@@ -5,57 +5,63 @@ import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.View
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* Renders the VICE 320×200 ARGB framebuffer into a SurfaceView,
|
||||
* scaled to fill the view while keeping the 8:5 aspect ratio.
|
||||
* Renders the VICE 320×200 ARGB framebuffer into a View.
|
||||
*
|
||||
* 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(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : SurfaceView(context, attrs), SurfaceHolder.Callback {
|
||||
) : View(context, attrs) {
|
||||
|
||||
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 var dstRect = Rect()
|
||||
|
||||
init {
|
||||
holder.addCallback(this)
|
||||
setBackgroundColor(android.graphics.Color.BLACK)
|
||||
setLayerType(LAYER_TYPE_HARDWARE, null)
|
||||
}
|
||||
|
||||
/** Called from the emulator thread after every runFrame(). */
|
||||
fun updateFrame(engine: C64Engine) {
|
||||
if (!holder.surface.isValid) return
|
||||
|
||||
pixelBuf.rewind()
|
||||
engine.getVideoBuffer(pixelBuf)
|
||||
pixelBuf.rewind()
|
||||
bitmap.copyPixelsFromBuffer(pixelBuf)
|
||||
|
||||
val canvas: Canvas = try { holder.lockCanvas() ?: return } catch (_: Exception) { return }
|
||||
try {
|
||||
canvas.drawBitmap(bitmap, srcRect, dstRect, null)
|
||||
} finally {
|
||||
holder.unlockCanvasAndPost(canvas)
|
||||
}
|
||||
/** Called from the emulator thread — copies one C64 frame into the back buffer. */
|
||||
fun captureFrame(engine: C64Engine) {
|
||||
backBuf.rewind()
|
||||
engine.getVideoBuffer(backBuf)
|
||||
/* Swap: make the freshly-written buffer visible to onDraw() */
|
||||
val tmp = frontBuf
|
||||
frontBuf = backBuf
|
||||
backBuf = tmp
|
||||
}
|
||||
|
||||
override fun surfaceCreated(h: SurfaceHolder) = recalcDst()
|
||||
override fun surfaceChanged(h: SurfaceHolder, f: Int, w: Int, h2: Int) = recalcDst()
|
||||
override fun surfaceDestroyed(h: SurfaceHolder) {}
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
val buf = frontBuf
|
||||
buf.rewind()
|
||||
bitmap.copyPixelsFromBuffer(buf)
|
||||
canvas.drawBitmap(bitmap, srcRect, dstRect, null)
|
||||
}
|
||||
|
||||
private fun recalcDst() {
|
||||
val vw = width.toFloat()
|
||||
val vh = height.toFloat()
|
||||
val scale = minOf(vw / 320f, vh / 200f)
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
if (w <= 0 || h <= 0) return
|
||||
val scale = minOf(w.toFloat() / 320f, h.toFloat() / 200f)
|
||||
val sw = (320 * scale).toInt()
|
||||
val sh = (200 * scale).toInt()
|
||||
val ox = ((vw - sw) / 2).toInt()
|
||||
val oy = ((vh - sh) / 2).toInt()
|
||||
val ox = ((w - sw) / 2).toInt()
|
||||
val oy = ((h - sh) / 2).toInt()
|
||||
dstRect = Rect(ox, oy, ox + sw, oy + sh)
|
||||
}
|
||||
}
|
||||
|
||||
+41
-1
@@ -19,9 +19,12 @@ class C64Engine {
|
||||
/** Initialize VICE. romDir should contain kernal, basic, chargen. */
|
||||
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
|
||||
|
||||
/** 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). */
|
||||
external fun runFrame()
|
||||
|
||||
@@ -40,6 +43,12 @@ class C64Engine {
|
||||
/** Enable or disable audio output. Disabled by default. */
|
||||
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 {
|
||||
init { System.loadLibrary("vice_jni") }
|
||||
|
||||
@@ -63,14 +72,45 @@ class C64Engine {
|
||||
const val KEY_8 = (3 shl 8) or 3
|
||||
const val KEY_9 = (4 shl 8) or 0
|
||||
const val KEY_0 = (4 shl 8) or 3
|
||||
// Letters
|
||||
const val KEY_Q = (7 shl 8) or 6
|
||||
const val KEY_W = (1 shl 8) or 1
|
||||
const val KEY_E = (1 shl 8) or 6
|
||||
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_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_M = (4 shl 8) or 4
|
||||
// Control / special
|
||||
const val KEY_RETURN = (0 shl 8) or 1
|
||||
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_F3 = (0 shl 8) or 5
|
||||
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.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
|
||||
|
||||
/**
|
||||
* Virtual C64 keyboard. Draws four rows of keys and fires [onKeyEvent]
|
||||
* on touch down / up.
|
||||
*/
|
||||
class C64KeyboardView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
@@ -26,30 +32,63 @@ class C64KeyboardView @JvmOverloads constructor(
|
||||
var onKeyEvent: KeyEventListener? = null
|
||||
|
||||
private val rows: List<List<C64Key>> = listOf(
|
||||
// Row 1 — function keys + DEL / HOME / RUN·STOP
|
||||
listOf(
|
||||
C64Key("F1", C64Engine.KEY_F1), C64Key("F3", C64Engine.KEY_F3),
|
||||
C64Key("F5", C64Engine.KEY_F5), C64Key("F7", C64Engine.KEY_F7),
|
||||
C64Key("RUN\nSTOP", C64Engine.KEY_RUNSTOP, 1.5f)
|
||||
C64Key("F1", C64Engine.KEY_F1),
|
||||
C64Key("F3", C64Engine.KEY_F3),
|
||||
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(
|
||||
C64Key("1", C64Engine.KEY_1), C64Key("2", C64Engine.KEY_2),
|
||||
C64Key("3", C64Engine.KEY_3), C64Key("4", C64Engine.KEY_4),
|
||||
C64Key("5", C64Engine.KEY_5), C64Key("6", C64Engine.KEY_6),
|
||||
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(
|
||||
C64Key("Q", C64Engine.KEY_Q), C64Key("W", C64Engine.KEY_W),
|
||||
C64Key("E", C64Engine.KEY_E), C64Key("R", C64Engine.KEY_R),
|
||||
C64Key("Y", C64Engine.KEY_Y), C64Key("N", C64Engine.KEY_N),
|
||||
C64Key("RETURN", C64Engine.KEY_RETURN, 2f)
|
||||
C64Key("T", C64Engine.KEY_T), C64Key("Y", C64Engine.KEY_Y),
|
||||
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(
|
||||
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 keyPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
@@ -63,7 +102,6 @@ class C64KeyboardView @JvmOverloads constructor(
|
||||
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()
|
||||
|
||||
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) {
|
||||
val rowH = totalH / rows.size
|
||||
val pad = 4f
|
||||
val pad = 3f
|
||||
keyRects = rows.mapIndexed { rowIdx, row ->
|
||||
val totalUnits = row.sumOf { it.widthWeight.toDouble() }.toFloat()
|
||||
val unitW = totalW / totalUnits
|
||||
var x = 0f
|
||||
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)
|
||||
x += kw
|
||||
rect to key
|
||||
@@ -89,18 +127,27 @@ class C64KeyboardView @JvmOverloads constructor(
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
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 ((rect, key) in row) {
|
||||
val paint = if (key in held) pressedPaint else keyPaint
|
||||
canvas.drawRoundRect(rect, 6f, 6f, paint)
|
||||
val ty = rect.centerY() - (textPaint.ascent() + textPaint.descent()) / 2
|
||||
canvas.drawText(key.label, rect.centerX(), ty, textPaint)
|
||||
canvas.drawRoundRect(rect, 5f, 5f, if (key in held) pressedPaint else keyPaint)
|
||||
drawKeyLabel(canvas, rect, key.label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
val idx = event.actionIndex
|
||||
val pid = event.getPointerId(idx)
|
||||
|
||||
+74
-26
@@ -8,7 +8,12 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.OpenableColumns
|
||||
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.ImageView
|
||||
import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
@@ -39,8 +44,6 @@ private data class DiskSlot(
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var tvStatus: TextView
|
||||
private lateinit var tvTime: TextView
|
||||
private lateinit var tvLog: TextView
|
||||
private lateinit var scrollLog: ScrollView
|
||||
private lateinit var display: C64DisplayView
|
||||
@@ -53,9 +56,11 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private var server: CompanionServer? = null
|
||||
private val engine = C64Engine()
|
||||
private var emuThread: Thread? = null
|
||||
@Volatile private var emuRunning = false
|
||||
private lateinit var choreographer: Choreographer
|
||||
@Volatile private var renderActive = false
|
||||
private var soundEnabled = false
|
||||
private lateinit var driveLedDrawable: GradientDrawable
|
||||
@Volatile private var driveLedState = false
|
||||
|
||||
// ---- disk slots ---------------------------------------------------------
|
||||
|
||||
@@ -74,6 +79,7 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
private val episodeButtons = mutableMapOf<DiskSlot, Button>()
|
||||
private val loadedADisks = mutableSetOf<Int>()
|
||||
private var activeSlot: DiskSlot? = null
|
||||
|
||||
// ---- Disk import (multi-select) -----------------------------------------
|
||||
|
||||
@@ -105,6 +111,12 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
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).
|
||||
// Shift the main content down by the actual status bar height so the
|
||||
// hamburger button isn't hidden underneath it.
|
||||
@@ -115,8 +127,13 @@ class MainActivity : AppCompatActivity() {
|
||||
view.setPadding(pad8, bars.top + pad8, pad8, pad8)
|
||||
insets
|
||||
}
|
||||
tvStatus = findViewById(R.id.tv_status)
|
||||
tvTime = findViewById(R.id.tv_time)
|
||||
val navDrawer = findViewById<android.view.View>(R.id.nav_drawer)
|
||||
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)
|
||||
scrollLog = findViewById(R.id.scroll_log)
|
||||
display = findViewById(R.id.c64_display)
|
||||
@@ -155,10 +172,12 @@ class MainActivity : AppCompatActivity() {
|
||||
findViewById<Button>(btnId).setOnClickListener { confirmCreateHeroDisk(slot) }
|
||||
}
|
||||
|
||||
choreographer = Choreographer.getInstance()
|
||||
initEmulator()
|
||||
refreshDiskButtons()
|
||||
startHttpServer()
|
||||
wireKeyboard()
|
||||
dismissSplashDelayed()
|
||||
}
|
||||
|
||||
// ---- emulator -----------------------------------------------------------
|
||||
@@ -169,19 +188,28 @@ class MainActivity : AppCompatActivity() {
|
||||
copyBundledRoms(assetDir)
|
||||
val romDir = assetDir.absolutePath
|
||||
val ok = engine.initEmulator(romDir)
|
||||
tvStatus.text = if (ok) "Emulator ready" else "Emulator init failed"
|
||||
Log.d(TAG, "initEmulator=$ok romDir=$romDir")
|
||||
|
||||
emuRunning = true
|
||||
emuThread = Thread {
|
||||
while (emuRunning) {
|
||||
val t0 = System.currentTimeMillis()
|
||||
engine.runFrame()
|
||||
display.updateFrame(engine)
|
||||
val elapsed = System.currentTimeMillis() - t0
|
||||
if (elapsed < 20) Thread.sleep(20 - elapsed)
|
||||
/* Choreographer drives rendering: fires on every hardware vsync (60/90/120 Hz).
|
||||
* captureFrame() reads the VICE framebuffer on this main-thread callback, then
|
||||
* invalidate() causes onDraw() to blit it — all under vsync, no tearing. */
|
||||
renderActive = true
|
||||
choreographer.postFrameCallback(object : Choreographer.FrameCallback {
|
||||
override fun doFrame(frameTimeNanos: Long) {
|
||||
if (!renderActive) return
|
||||
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 ---------------------------------------------
|
||||
@@ -207,7 +235,7 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private fun wireKeyboard() {
|
||||
keyboard.onKeyEvent = { key, pressed ->
|
||||
engine.injectKey(key.code, pressed)
|
||||
for (code in key.codes) engine.injectKey(code, pressed)
|
||||
if (pressed) {
|
||||
Log.d(TAG, "Key pressed: ${key.label}")
|
||||
mainHandler.post { appendLog("KEY: ${key.label}") }
|
||||
@@ -219,9 +247,7 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private fun startHttpServer() {
|
||||
server = CompanionServer(PORT, timeFormat,
|
||||
onTimeFetched = { time ->
|
||||
mainHandler.post { tvTime.text = "Serving time: $time" }
|
||||
},
|
||||
onTimeFetched = { _ -> },
|
||||
onKey = { cmd ->
|
||||
Log.d(TAG, "Watch key: $cmd")
|
||||
mainHandler.post { appendLog("WATCH: $cmd") }
|
||||
@@ -244,7 +270,12 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private fun refreshDiskButtons() {
|
||||
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}")
|
||||
return
|
||||
}
|
||||
val ok = engine.loadDisk(file.absolutePath)
|
||||
if (ok && slot.part == 'A') loadedADisks.add(slot.episode)
|
||||
appendLog(if (ok) "Loaded: ${slot.label}" else "Load failed: ${slot.fileName}")
|
||||
// A-side episode disks restart the machine; B-side and hero disks are hot-swapped.
|
||||
val resetMachine = slot.part == 'A'
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -388,6 +426,17 @@ class MainActivity : AppCompatActivity() {
|
||||
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 ------------------------------------------------------------
|
||||
|
||||
private fun appendLog(entry: String) {
|
||||
@@ -404,8 +453,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
emuRunning = false
|
||||
emuThread?.join(500)
|
||||
renderActive = false
|
||||
server?.stop()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -28,7 +28,9 @@ if(HAVE_VICE)
|
||||
-Wl,--wrap=maincpu_mainloop
|
||||
-Wl,--wrap=init_main
|
||||
-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
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/vice-libs/${ANDROID_ABI}"
|
||||
"${VICE_SRC}/src"
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <pthread.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.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 int g_ready = 0;
|
||||
|
||||
/* Pending disk path — written by loadDisk (Android thread), consumed by
|
||||
* video_canvas_refresh (VICE thread). autostart_disk must be called from
|
||||
* the VICE thread; calling it cross-thread corrupts VICE internal state. */
|
||||
static pthread_mutex_t g_pending_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
/* Pending disk operations — written by Android thread, consumed by
|
||||
* video_canvas_refresh on the VICE thread (both attach and autostart APIs
|
||||
* must be called from the VICE thread to avoid corrupting internal state).
|
||||
* 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_attach[512];
|
||||
|
||||
/* =========================================================================
|
||||
* VICE integration (compiled only when libvice.a is linked in)
|
||||
@@ -61,6 +64,55 @@ extern int __real_console_init(void);
|
||||
#include "palette.h"
|
||||
#include "keyboard.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()
|
||||
@@ -122,8 +174,6 @@ static void sl_bufq_callback(SLAndroidSimpleBufferQueueItf bq, void *ctx) {
|
||||
static int android_sound_init(const char *param, int *speed,
|
||||
int *fragsize, int *fragnr, int *channels) {
|
||||
(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 < 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_next_buf = 0;
|
||||
LOGI("android_sound_init: OpenSL ES ready");
|
||||
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 = NULL; g_sl_engine = NULL;
|
||||
}
|
||||
LOGI("android_sound_close: done");
|
||||
}
|
||||
|
||||
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. */
|
||||
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). */
|
||||
|
||||
@@ -277,13 +325,22 @@ int video_canvas_set_palette(video_canvas_t *canvas, struct palette_s *palette)
|
||||
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,
|
||||
unsigned int *width, unsigned int *height,
|
||||
int mapped) {
|
||||
(void)mapped;
|
||||
canvas->created = 1;
|
||||
*width = FRAME_W;
|
||||
*height = FRAME_H;
|
||||
*width = PAL_W;
|
||||
*height = PAL_H;
|
||||
return canvas;
|
||||
}
|
||||
|
||||
@@ -295,23 +352,45 @@ void video_canvas_refresh(video_canvas_t *canvas,
|
||||
unsigned int xs, unsigned int ys,
|
||||
unsigned int xi, unsigned int yi,
|
||||
unsigned int w, unsigned int h) {
|
||||
/* Process any pending disk load on the VICE thread so autostart_disk is
|
||||
* never called cross-thread (it modifies complex internal VICE state). */
|
||||
/* Process any pending disk operations on the VICE thread. */
|
||||
pthread_mutex_lock(&g_pending_lock);
|
||||
if (g_pending_disk[0] != '\0') {
|
||||
char path[512];
|
||||
strncpy(path, g_pending_disk, sizeof(path) - 1);
|
||||
path[sizeof(path) - 1] = '\0';
|
||||
g_pending_disk[0] = '\0';
|
||||
pthread_mutex_unlock(&g_pending_lock);
|
||||
LOGI("autostart_disk: %s", path);
|
||||
int asd_ret = autostart_disk(8, 0, path, NULL, 0, AUTOSTART_MODE_RUN);
|
||||
LOGI("autostart_disk returned %d", asd_ret);
|
||||
} else {
|
||||
pthread_mutex_unlock(&g_pending_lock);
|
||||
char autostart_path[512] = {0};
|
||||
char attach_path[512] = {0};
|
||||
if (g_pending_disk[0] != '\0') { strncpy(autostart_path, g_pending_disk, sizeof(autostart_path)-1); g_pending_disk[0] = '\0'; }
|
||||
if (g_pending_attach[0] != '\0') { strncpy(attach_path, g_pending_attach, sizeof(attach_path)-1); g_pending_attach[0] = '\0'; }
|
||||
pthread_mutex_unlock(&g_pending_lock);
|
||||
if (autostart_path[0] != '\0')
|
||||
autostart_disk(8, 0, autostart_path, NULL, 0, AUTOSTART_MODE_RUN);
|
||||
if (attach_path[0] != '\0') {
|
||||
int r = file_system_attach_disk(8, 0, attach_path);
|
||||
if (r == 0) {
|
||||
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 + w > FRAME_W) w = FRAME_W - xi;
|
||||
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,
|
||||
FRAME_W * 4);
|
||||
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. */
|
||||
@@ -353,9 +447,7 @@ static void *stderr_reader_thread(void *arg) {
|
||||
|
||||
static void *vice_thread(void *arg) {
|
||||
(void)arg;
|
||||
LOGI("vice_thread: starting main_program");
|
||||
int rc = main_program(g_vice_argc, g_vice_argv);
|
||||
LOGI("vice_thread: main_program returned %d", rc);
|
||||
main_program(g_vice_argc, g_vice_argv);
|
||||
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
|
||||
* terminates instead. */
|
||||
void __wrap_archdep_vice_exit(int code) {
|
||||
LOGI("archdep_vice_exit(%d) — intercepted, terminating VICE thread", code);
|
||||
(void)code;
|
||||
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_machine_init(void) { return __real_machine_init(); }
|
||||
int __wrap_console_init(void) { return __real_console_init(); }
|
||||
|
||||
|
||||
#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) {
|
||||
#ifdef HAVE_VICE_SRC
|
||||
const char *dir = (*env)->GetStringUTFChars(env, romDir, NULL);
|
||||
LOGI("initEmulator: romDir=%s", dir);
|
||||
|
||||
/* Prepare argv for VICE: x64 -directory <romDir> -headless */
|
||||
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(dst, sizeof(dst), "%s/%s", vice_c64_dir, c64_roms[i]);
|
||||
FILE *fsrc = fopen(src, "rb");
|
||||
if (!fsrc) { LOGI("ROM not found: %s", src); continue; }
|
||||
if (!fsrc) continue;
|
||||
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;
|
||||
while ((n = fread(buf, 1, sizeof(buf), fsrc)) > 0) fwrite(buf, 1, n, 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).
|
||||
@@ -438,11 +530,8 @@ JNI_FN(jboolean, initEmulator)(JNIEnv *env, jobject obj, jstring romDir) {
|
||||
char buf[4096]; size_t n;
|
||||
while ((n = fread(buf, 1, sizeof(buf), fsrc)) > 0) fwrite(buf, 1, n, fdst);
|
||||
fclose(fdst);
|
||||
LOGI("ROM copied: 1541");
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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. */
|
||||
JNI_FN(void, runFrame)(JNIEnv *env, jobject 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"?>
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
<FrameLayout
|
||||
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:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
@@ -32,22 +36,20 @@
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_status"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="Initialising…"
|
||||
android:textColor="#AAFFAA"
|
||||
android:textSize="12sp" />
|
||||
android:text="Schwert und Magie"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="--:--:--"
|
||||
android:textColor="#AAAAFF"
|
||||
android:textSize="12sp" />
|
||||
<View
|
||||
android:id="@+id/drive_led"
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:layout_marginEnd="4dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- C64 display (320×200, scaled with correct aspect ratio) -->
|
||||
@@ -55,15 +57,15 @@
|
||||
android:id="@+id/c64_display"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="3"
|
||||
android:layout_weight="2"
|
||||
android:layout_marginTop="4dp" />
|
||||
|
||||
<!-- Virtual keyboard -->
|
||||
<!-- Virtual keyboard — 6 rows -->
|
||||
<de.ladkau.schwertundmagieonpebblecompanionapp.C64KeyboardView
|
||||
android:id="@+id/c64_keyboard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="2"
|
||||
android:layout_weight="1.8"
|
||||
android:layout_marginTop="4dp" />
|
||||
|
||||
<!-- Key-event log -->
|
||||
@@ -96,6 +98,7 @@
|
||||
|
||||
<!-- ── Drawer ────────────────────────────────────────────────────────── -->
|
||||
<LinearLayout
|
||||
android:id="@+id/nav_drawer"
|
||||
android:layout_width="280dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
@@ -110,7 +113,16 @@
|
||||
android:textColor="#AAFFAA"
|
||||
android:textSize="14sp"
|
||||
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"
|
||||
android:layout_width="match_parent" android:layout_height="wrap_content"
|
||||
@@ -234,3 +246,16 @@
|
||||
</LinearLayout>
|
||||
|
||||
</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