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:
@@ -4,10 +4,10 @@
|
|||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
<DropdownSelection timestamp="2026-05-31T15:36:41.858231245Z">
|
<DropdownSelection timestamp="2026-06-10T16:34:31.851340592Z">
|
||||||
<Target type="DEFAULT_BOOT">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="LocalEmulator" identifier="path=/home/ml/.android/avd/Medium_Phone.avd" />
|
<DeviceId pluginId="PhysicalDevice" identifier="serial=adf68432" />
|
||||||
</handle>
|
</handle>
|
||||||
</Target>
|
</Target>
|
||||||
</DropdownSelection>
|
</DropdownSelection>
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
implementation(libs.nanohttpd)
|
implementation(libs.nanohttpd)
|
||||||
|
implementation(libs.androidx.documentfile)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
|
|||||||
+3
-1
@@ -29,12 +29,14 @@ class C64DisplayView @JvmOverloads constructor(
|
|||||||
|
|
||||||
/** Called from the emulator thread after every runFrame(). */
|
/** Called from the emulator thread after every runFrame(). */
|
||||||
fun updateFrame(engine: C64Engine) {
|
fun updateFrame(engine: C64Engine) {
|
||||||
|
if (!holder.surface.isValid) return
|
||||||
|
|
||||||
pixelBuf.rewind()
|
pixelBuf.rewind()
|
||||||
engine.getVideoBuffer(pixelBuf)
|
engine.getVideoBuffer(pixelBuf)
|
||||||
pixelBuf.rewind()
|
pixelBuf.rewind()
|
||||||
bitmap.copyPixelsFromBuffer(pixelBuf)
|
bitmap.copyPixelsFromBuffer(pixelBuf)
|
||||||
|
|
||||||
val canvas: Canvas = holder.lockCanvas() ?: return
|
val canvas: Canvas = try { holder.lockCanvas() ?: return } catch (_: Exception) { return }
|
||||||
try {
|
try {
|
||||||
canvas.drawBitmap(bitmap, srcRect, dstRect, null)
|
canvas.drawBitmap(bitmap, srcRect, dstRect, null)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
+16
-7
@@ -40,9 +40,18 @@ class C64Engine {
|
|||||||
companion object {
|
companion object {
|
||||||
init { System.loadLibrary("vice_jni") }
|
init { System.loadLibrary("vice_jni") }
|
||||||
|
|
||||||
// Convenient key-code constants (row << 8 | col in C64 matrix)
|
// C64 keyboard matrix: row/col from the hardware scan matrix
|
||||||
const val KEY_1 = (0 shl 8) or 0
|
// Row\Col 0 1 2 3 4 5 6 7
|
||||||
const val KEY_2 = (0 shl 8) or 3
|
// 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_3 = (1 shl 8) or 0
|
||||||
const val KEY_4 = (1 shl 8) or 3
|
const val KEY_4 = (1 shl 8) or 3
|
||||||
const val KEY_5 = (2 shl 8) or 0
|
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_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
|
||||||
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_W = (1 shl 8) or 1
|
||||||
const val KEY_E = (0 shl 8) or 6
|
const val KEY_E = (1 shl 8) or 6
|
||||||
const val KEY_R = (1 shl 8) or 9 // placeholder — real values from VICE keytable
|
const val KEY_R = (2 shl 8) or 1
|
||||||
const val KEY_Y = (6 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_N = (4 shl 8) or 7
|
||||||
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
|
||||||
|
|||||||
+27
-18
@@ -15,6 +15,7 @@ import android.widget.Toast
|
|||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import fi.iki.elonen.NanoHTTPD
|
import fi.iki.elonen.NanoHTTPD
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@@ -55,29 +56,38 @@ class MainActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
private val episodeButtons = mutableMapOf<DiskSlot, Button>()
|
private val episodeButtons = mutableMapOf<DiskSlot, Button>()
|
||||||
|
|
||||||
// ---- ROM import (sequential: kernal → basic → chargen) -----------------
|
// ---- ROM import (pick folder — kernal/basic/chargen have no extension) --
|
||||||
|
|
||||||
private val romNames = listOf("kernal", "basic", "chargen")
|
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 ->
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
result.data?.data?.let { uri ->
|
result.data?.data?.let { treeUri -> importRomDir(treeUri) }
|
||||||
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 }
|
}
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
romImportIndex = 0
|
appendLog("Only $imported/${romNames.size} ROMs found — check folder contents")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,8 +127,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
keyboard = findViewById(R.id.c64_keyboard)
|
keyboard = findViewById(R.id.c64_keyboard)
|
||||||
|
|
||||||
findViewById<Button>(R.id.btn_import_roms).setOnClickListener {
|
findViewById<Button>(R.id.btn_import_roms).setOnClickListener {
|
||||||
romImportIndex = 0
|
romDirPickerLauncher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE))
|
||||||
launchPicker(romPickerLauncher, "Select 'kernal' ROM file")
|
|
||||||
}
|
}
|
||||||
findViewById<Button>(R.id.btn_import_disks).setOnClickListener {
|
findViewById<Button>(R.id.btn_import_disks).setOnClickListener {
|
||||||
launchPicker(diskPickerLauncher, "Select SCHWUM disk images", multiSelect = true)
|
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)
|
if(HAVE_VICE)
|
||||||
target_compile_definitions(vice_jni PRIVATE HAVE_VICE_SRC=1)
|
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
|
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"
|
||||||
|
|||||||
@@ -15,8 +15,13 @@
|
|||||||
|
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
|
|
||||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "ViceJNI", __VA_ARGS__)
|
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "ViceJNI", __VA_ARGS__)
|
||||||
@@ -36,8 +41,16 @@ static int g_ready = 0;
|
|||||||
#include "vice.h"
|
#include "vice.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "machine.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 "videoarch.h"
|
||||||
#include "video.h"
|
#include "video.h"
|
||||||
|
#include "palette.h"
|
||||||
#include "keyboard.h"
|
#include "keyboard.h"
|
||||||
#include "attach.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_cmdline_options_init(void) { return 0; }
|
||||||
int video_arch_resources_init(void) { return 0; }
|
int video_arch_resources_init(void) { return 0; }
|
||||||
void video_arch_resources_shutdown(void) {}
|
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_destroy(video_canvas_t *c) { (void)c; }
|
||||||
void video_canvas_resize(video_canvas_t *c, char r){ (void)c; (void)r; }
|
void video_canvas_resize(video_canvas_t *c, char r){ (void)c; (void)r; }
|
||||||
int video_init(void) { return 0; }
|
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) {
|
int video_canvas_set_palette(video_canvas_t *canvas, struct palette_s *palette) {
|
||||||
canvas->palette = 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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,35 +116,54 @@ video_canvas_t *video_canvas_create(video_canvas_t *canvas,
|
|||||||
return 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,
|
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) {
|
||||||
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);
|
pthread_mutex_lock(&g_lock);
|
||||||
const uint8_t *src = canvas->draw_buffer->draw_buffer
|
video_canvas_render(canvas, (uint8_t *)g_framebuf,
|
||||||
+ ys * canvas->draw_buffer->draw_buffer_pitch
|
(int)w, (int)h,
|
||||||
+ xs * 4; /* 32 bpp = 4 bytes/pixel */
|
(int)xs, (int)ys,
|
||||||
uint32_t *dst = g_framebuf + yi * FRAME_W + xi;
|
(int)xi, (int)yi,
|
||||||
for (unsigned int row = 0; row < h; row++) {
|
FRAME_W * 4);
|
||||||
memcpy(dst, src, w * 4);
|
|
||||||
src += canvas->draw_buffer->draw_buffer_pitch;
|
|
||||||
dst += FRAME_W;
|
|
||||||
}
|
|
||||||
pthread_mutex_unlock(&g_lock);
|
pthread_mutex_unlock(&g_lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* VICE main loop runs in this thread. */
|
/* 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 int g_vice_argc;
|
||||||
|
|
||||||
static void *vice_thread(void *arg) {
|
static void *vice_thread(void *arg) {
|
||||||
(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;
|
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 */
|
#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);
|
strncpy(dir_copy, dir, sizeof(dir_copy) - 1);
|
||||||
(*env)->ReleaseStringUTFChars(env, romDir, dir);
|
(*env)->ReleaseStringUTFChars(env, romDir, dir);
|
||||||
|
|
||||||
g_vice_argc = 4;
|
/* 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[0] = "x64";
|
||||||
g_vice_argv[1] = "-directory";
|
g_vice_argv[1] = "-directory";
|
||||||
g_vice_argv[2] = dir_copy;
|
g_vice_argv[2] = dir_arg;
|
||||||
g_vice_argv[3] = "-silent";
|
g_vice_argv[3] = "-kernal";
|
||||||
g_vice_argv[4] = NULL;
|
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;
|
pthread_t tid;
|
||||||
int rc = pthread_create(&tid, NULL, vice_thread, NULL);
|
int rc = pthread_create(&tid, NULL, vice_thread, NULL);
|
||||||
|
|||||||
@@ -121,8 +121,7 @@
|
|||||||
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="3"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp" />
|
||||||
android:background="#000044" />
|
|
||||||
|
|
||||||
<!-- Virtual keyboard -->
|
<!-- Virtual keyboard -->
|
||||||
<de.ladkau.schwertundmagieonpebblecompanionapp.C64KeyboardView
|
<de.ladkau.schwertundmagieonpebblecompanionapp.C64KeyboardView
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "9.2.1"
|
agp = "9.2.1"
|
||||||
coreKtx = "1.10.1"
|
coreKtx = "1.10.1"
|
||||||
|
documentfile = "1.0.1"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.1.5"
|
junitVersion = "1.1.5"
|
||||||
espressoCore = "3.5.1"
|
espressoCore = "3.5.1"
|
||||||
@@ -20,6 +21,7 @@ material = { group = "com.google.android.material", name = "material", version.r
|
|||||||
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
|
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
|
||||||
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||||
nanohttpd = { group = "org.nanohttpd", name = "nanohttpd", version.ref = "nanohttpd" }
|
nanohttpd = { group = "org.nanohttpd", name = "nanohttpd", version.ref = "nanohttpd" }
|
||||||
|
androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user