Fix C64 display rendering and keyboard matrix

- Remove android:background from C64DisplayView: SurfaceView's XML
    background painted over the surface, hiding all rendered content
  - Fix pixel byte order: copyPixelsFromBuffer reads [R,G,B,A] bytes, so
    physical colors must be packed as ABGR (not ARGB) to avoid R/B swap
  - Fix six wrong C64 keyboard matrix positions (1, 2, Q, E, R, Y) taken
    from the authoritative VICE hardware matrix table
This commit is contained in:
ml
2026-06-10 19:02:29 +02:00
parent 678108530c
commit 7a8cd492d5
9 changed files with 187 additions and 52 deletions
@@ -29,12 +29,14 @@ class C64DisplayView @JvmOverloads constructor(
/** 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 = holder.lockCanvas() ?: return
val canvas: Canvas = try { holder.lockCanvas() ?: return } catch (_: Exception) { return }
try {
canvas.drawBitmap(bitmap, srcRect, dstRect, null)
} finally {
@@ -40,9 +40,18 @@ class C64Engine {
companion object {
init { System.loadLibrary("vice_jni") }
// Convenient key-code constants (row << 8 | col in C64 matrix)
const val KEY_1 = (0 shl 8) or 0
const val KEY_2 = (0 shl 8) or 3
// C64 keyboard matrix: row/col from the hardware scan matrix
// Row\Col 0 1 2 3 4 5 6 7
// 0: DEL RET C-R/L F7 F1 F3 F5 C-U/D
// 1: 3 W A 4 Z S E LSHFT
// 2: 5 R D 6 C F T X
// 3: 7 Y G 8 B H U V
// 4: 9 I J 0 M K O N
// 5: + P L - . : @ ,
// 6: £ * ; HOME RSHFT = ^ /
// 7: 1 A-LFT CTRL 2 SPACE C= Q R/S
const val KEY_1 = (7 shl 8) or 0
const val KEY_2 = (7 shl 8) or 3
const val KEY_3 = (1 shl 8) or 0
const val KEY_4 = (1 shl 8) or 3
const val KEY_5 = (2 shl 8) or 0
@@ -51,11 +60,11 @@ 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
const val KEY_Q = (6 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_E = (0 shl 8) or 6
const val KEY_R = (1 shl 8) or 9 // placeholder — real values from VICE keytable
const val KEY_Y = (6 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_Y = (3 shl 8) or 1
const val KEY_N = (4 shl 8) or 7
const val KEY_RETURN = (0 shl 8) or 1
const val KEY_SPACE = (7 shl 8) or 4
@@ -15,6 +15,7 @@ import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
import fi.iki.elonen.NanoHTTPD
import java.io.File
import java.text.SimpleDateFormat
@@ -55,32 +56,41 @@ class MainActivity : AppCompatActivity() {
)
private val episodeButtons = mutableMapOf<DiskSlot, Button>()
// ---- ROM import (sequential: kernalbasicchargen) -----------------
// ---- ROM import (pick folder — kernal/basic/chargen have no extension) --
private val romNames = listOf("kernal", "basic", "chargen")
private var romImportIndex = 0
private val romPickerLauncher: ActivityResultLauncher<Intent> =
private val romDirPickerLauncher: ActivityResultLauncher<Intent> =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { uri ->
val target = romNames[romImportIndex]
importFileToAssets(uri, target)
appendLog("Imported: $target")
romImportIndex++
if (romImportIndex < romNames.size) {
launchPicker(romPickerLauncher, "Select '${romNames[romImportIndex]}' ROM file")
} else {
romImportIndex = 0
Toast.makeText(this, "ROMs imported — restarting emulator", Toast.LENGTH_SHORT).show()
recreate()
}
} ?: run { romImportIndex = 0 }
} else {
romImportIndex = 0
result.data?.data?.let { treeUri -> importRomDir(treeUri) }
}
}
private fun importRomDir(treeUri: Uri) {
val dir = DocumentFile.fromTreeUri(this, treeUri) ?: run {
appendLog("Cannot access selected folder")
return
}
val files = dir.listFiles()
var imported = 0
for (name in romNames) {
val file = files.firstOrNull { it.name?.lowercase() == name } ?: run {
appendLog("Not found: $name")
continue
}
importFileToAssets(file.uri, name)
appendLog("Imported: $name")
imported++
}
if (imported == romNames.size) {
appendLog("All ROMs imported — restarting emulator")
mainHandler.postDelayed({ recreate() }, 400)
} else {
appendLog("Only $imported/${romNames.size} ROMs found — check folder contents")
}
}
// ---- Disk import (multi-select) -----------------------------------------
private val diskPickerLauncher: ActivityResultLauncher<Intent> =
@@ -117,8 +127,7 @@ class MainActivity : AppCompatActivity() {
keyboard = findViewById(R.id.c64_keyboard)
findViewById<Button>(R.id.btn_import_roms).setOnClickListener {
romImportIndex = 0
launchPicker(romPickerLauncher, "Select 'kernal' ROM file")
romDirPickerLauncher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE))
}
findViewById<Button>(R.id.btn_import_disks).setOnClickListener {
launchPicker(diskPickerLauncher, "Select SCHWUM disk images", multiSelect = true)
@@ -21,6 +21,14 @@ target_link_libraries(vice_jni android log c++_shared z)
if(HAVE_VICE)
target_compile_definitions(vice_jni PRIVATE HAVE_VICE_SRC=1)
# Redirect archdep_vice_exit (which calls exit()) to __wrap_archdep_vice_exit
# in vice_jni.c, which calls pthread_exit() instead of killing the process.
target_link_options(vice_jni PRIVATE
-Wl,--wrap=archdep_vice_exit
-Wl,--wrap=maincpu_mainloop
-Wl,--wrap=init_main
-Wl,--wrap=machine_init
-Wl,--wrap=console_init)
target_include_directories(vice_jni PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/vice-libs/${ANDROID_ABI}"
"${VICE_SRC}/src"
@@ -15,8 +15,13 @@
#include <jni.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <android/log.h>
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "ViceJNI", __VA_ARGS__)
@@ -36,8 +41,16 @@ static int g_ready = 0;
#include "vice.h"
#include "main.h"
#include "machine.h"
#include "maincpu.h"
/* Provided by the linker --wrap symbols */
extern void __real_maincpu_mainloop(void);
extern int __real_init_main(void);
extern int __real_machine_init(void);
extern int __real_console_init(void);
#include "videoarch.h"
#include "video.h"
#include "palette.h"
#include "keyboard.h"
#include "attach.h"
@@ -51,7 +64,10 @@ void video_arch_canvas_init(video_canvas_t *c) { (void)c; }
int video_arch_cmdline_options_init(void) { return 0; }
int video_arch_resources_init(void) { return 0; }
void video_arch_resources_shutdown(void) {}
char video_canvas_can_resize(video_canvas_t *c) { (void)c; return 0; }
/* Return 1 so VICE uses visible_width/visible_height for canvas_width/canvas_height.
* With return 0, VICE would use canvas_physical_width/scalex but physical is never
* initialised, giving permanent 0x0 frames. */
char video_canvas_can_resize(video_canvas_t *c) { (void)c; return 1; }
void video_canvas_destroy(video_canvas_t *c) { (void)c; }
void video_canvas_resize(video_canvas_t *c, char r){ (void)c; (void)r; }
int video_init(void) { return 0; }
@@ -59,6 +75,34 @@ void video_shutdown(void) {}
int video_canvas_set_palette(video_canvas_t *canvas, struct palette_s *palette) {
canvas->palette = palette;
if (!palette || !canvas->videoconfig) return 0;
/* Force 1×1 pixel rendering. VICE chip caps set rendermode=2 (PAL_NTSC_2X2)
* by default for VICII, which doubles each pixel and row and only renders the
* left half of the screen. Override to 1 (PAL_NTSC_1X1) for correct
* 320×200 output into our fixed-size framebuffer. */
canvas->videoconfig->rendermode = VIDEO_RENDER_PAL_NTSC_1X1;
/* Force no CRT filter. VIDEO_FILTER_CRT (the default) routes to
* render_32_1x1_pal which uses YUV lookup tables built by VICE's PAL color
* science. Those tables stay zero in the headless arch (no GPU/SDL setup),
* so every output pixel is 0x00000000. VIDEO_FILTER_NONE routes instead to
* render_32_1x1_04, the simple renderer that reads from physical_colors[]
* which we correctly populate below via video_render_setphysicalcolor. */
canvas->videoconfig->filter = VIDEO_FILTER_NONE;
/* Populate the physical color table so the render pipeline writes real pixels.
* VICE never calls video_render_setphysicalcolor in the headless arch.
* Bitmap.copyPixelsFromBuffer reads raw bytes as [R,G,B,A] per pixel.
* On little-endian ARM a uint32_t 0xAABBGGRR has bytes [R,G,B,A] in memory,
* so we pack the color as ABGR (not ARGB) to get the correct byte order. */
for (unsigned int i = 0; i < palette->num_entries; i++) {
uint32_t col = 0xFF000000u
| ((uint32_t)palette->entries[i].blue << 16)
| ((uint32_t)palette->entries[i].green << 8)
| (uint32_t)palette->entries[i].red;
video_render_setphysicalcolor(canvas->videoconfig, (int)i, col, 32);
}
return 0;
}
@@ -72,35 +116,54 @@ video_canvas_t *video_canvas_create(video_canvas_t *canvas,
return canvas;
}
/* Called by VICE every time a frame is ready — copy pixels into g_framebuf. */
/* Called by VICE each time a screen region needs updating.
* The draw_buffer holds 8-bit palette indices (pitch = width in bytes).
* video_canvas_render() converts them to 32bpp ARGB using the physical color
* table we populated in video_canvas_set_palette(). */
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) {
if (!canvas->draw_buffer || !canvas->draw_buffer->draw_buffer) return;
/* Clamp destination to our framebuffer bounds. */
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;
if (w == 0 || h == 0) return;
pthread_mutex_lock(&g_lock);
const uint8_t *src = canvas->draw_buffer->draw_buffer
+ ys * canvas->draw_buffer->draw_buffer_pitch
+ xs * 4; /* 32 bpp = 4 bytes/pixel */
uint32_t *dst = g_framebuf + yi * FRAME_W + xi;
for (unsigned int row = 0; row < h; row++) {
memcpy(dst, src, w * 4);
src += canvas->draw_buffer->draw_buffer_pitch;
dst += FRAME_W;
}
video_canvas_render(canvas, (uint8_t *)g_framebuf,
(int)w, (int)h,
(int)xs, (int)ys,
(int)xi, (int)yi,
FRAME_W * 4);
pthread_mutex_unlock(&g_lock);
}
/* VICE main loop runs in this thread. */
static char *g_vice_argv[8];
static char *g_vice_argv[12];
static int g_vice_argc;
static void *vice_thread(void *arg) {
(void)arg;
main_program(g_vice_argc, g_vice_argv);
LOGI("vice_thread: starting main_program");
int rc = main_program(g_vice_argc, g_vice_argv);
LOGI("vice_thread: main_program returned %d", rc);
return NULL;
}
/* archdep_vice_exit normally calls exit() and would kill the whole process.
* --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);
pthread_exit(NULL);
}
void machine_mainloop(void) { __real_maincpu_mainloop(); }
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 */
/* =========================================================================
@@ -120,12 +183,54 @@ JNI_FN(jboolean, initEmulator)(JNIEnv *env, jobject obj, jstring romDir) {
strncpy(dir_copy, dir, sizeof(dir_copy) - 1);
(*env)->ReleaseStringUTFChars(env, romDir, dir);
g_vice_argc = 4;
g_vice_argv[0] = "x64";
g_vice_argv[1] = "-directory";
g_vice_argv[2] = dir_copy;
g_vice_argv[3] = "-silent";
g_vice_argv[4] = NULL;
/* archdep_create_user_cache_dir/config_dir (called from archdep_init) need a
* writable HOME to create ~/.cache/vice and ~/.config/vice. On Android,
* HOME is unset or points to a read-only root, so redirect it to romDir. */
setenv("HOME", dir_copy, 1);
/* sysfile_load() searches <Directory>/<machine_name>/<rom_name>.
* We pass -directory <romDir> so VICE searches romDir/C64/.
* The ROM files the user provides are named "kernal"/"basic"/"chargen";
* VICE's compiled-in defaults are "kernal-901227-03.bin" etc., so we
* also pass -kernal/-basic/-chargen to match the actual file names.
* Android FUSE rejects symlinks, so we copy the files into romDir/C64/. */
char vice_c64_dir[512];
snprintf(vice_c64_dir, sizeof(vice_c64_dir), "%s/C64", dir_copy);
mkdir(vice_c64_dir, 0755);
LOGI("ROM target dir: %s", vice_c64_dir);
const char *roms[] = {"kernal", "basic", "chargen", NULL};
for (int i = 0; roms[i]; i++) {
char src[512], dst[512];
snprintf(src, sizeof(src), "%s/%s", dir_copy, roms[i]);
snprintf(dst, sizeof(dst), "%s/%s", vice_c64_dir, roms[i]);
FILE *fsrc = fopen(src, "rb");
if (!fsrc) { LOGI("ROM not found: %s", src); continue; }
FILE *fdst = fopen(dst, "wb");
if (!fdst) { fclose(fsrc); LOGI("ROM copy open failed: %s errno=%d", dst, errno); 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", roms[i]);
}
static char dir_arg[512];
snprintf(dir_arg, sizeof(dir_arg), "%s", dir_copy);
/* -VICIIborders none → border mode 3 → no borders → 320×200 canvas.
* Default (mode 0 = normal) gives 384×272 which doesn't match our framebuffer. */
g_vice_argc = 11;
g_vice_argv[0] = "x64";
g_vice_argv[1] = "-directory";
g_vice_argv[2] = dir_arg;
g_vice_argv[3] = "-kernal";
g_vice_argv[4] = "kernal";
g_vice_argv[5] = "-basic";
g_vice_argv[6] = "basic";
g_vice_argv[7] = "-chargen";
g_vice_argv[8] = "chargen";
g_vice_argv[9] = "-VICIIborders";
g_vice_argv[10] = "none";
g_vice_argv[11] = NULL;
pthread_t tid;
int rc = pthread_create(&tid, NULL, vice_thread, NULL);
@@ -121,8 +121,7 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="3"
android:layout_marginTop="4dp"
android:background="#000044" />
android:layout_marginTop="4dp" />
<!-- Virtual keyboard -->
<de.ladkau.schwertundmagieonpebblecompanionapp.C64KeyboardView