Adding a save state panel
This commit is contained in:
+6
@@ -28,6 +28,12 @@ class C64Engine {
|
||||
/** Eject the disk and hard-reset the C64 to a BASIC ready prompt. */
|
||||
external fun resetMachine(): Boolean
|
||||
|
||||
/** Save a full machine snapshot to [path] (queued to the VICE thread). */
|
||||
external fun saveState(path: String): Boolean
|
||||
|
||||
/** Restore a machine snapshot from [path] (queued to the VICE thread). */
|
||||
external fun loadState(path: String): Boolean
|
||||
|
||||
/** Run the emulator for one video frame (~20 000 CPU cycles). */
|
||||
external fun runFrame()
|
||||
|
||||
|
||||
+107
@@ -170,6 +170,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.btn_help).setOnClickListener { showHelp() }
|
||||
findViewById<Button>(R.id.btn_savestate).setOnClickListener { showSaveStatePanel() }
|
||||
|
||||
findViewById<Button>(R.id.btn_import_disks).setOnClickListener {
|
||||
launchPicker(diskPickerLauncher, getString(R.string.picker_title), multiSelect = true)
|
||||
@@ -660,6 +661,112 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
// ---- helpers ------------------------------------------------------------
|
||||
|
||||
// ---- save state panel ---------------------------------------------------
|
||||
|
||||
private fun stateFile(slot: Int): File {
|
||||
val dir = getExternalFilesDir(null) ?: filesDir
|
||||
return File(dir, "SAVESTATE_$slot.VSF")
|
||||
}
|
||||
|
||||
private fun showSaveStatePanel() {
|
||||
val dp = { v: Int -> (v * resources.displayMetrics.density).toInt() }
|
||||
val dateFmt = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
|
||||
|
||||
val content = android.widget.LinearLayout(this).apply {
|
||||
orientation = android.widget.LinearLayout.VERTICAL
|
||||
setPadding(dp(16), dp(12), dp(16), dp(8))
|
||||
setBackgroundColor(Color.parseColor("#1A1A2E"))
|
||||
}
|
||||
|
||||
// Per-slot views that need refreshing after a Save
|
||||
val timeTvs = arrayOfNulls<TextView>(5)
|
||||
val loadBtns = arrayOfNulls<Button>(5)
|
||||
var dialog: AlertDialog? = null
|
||||
|
||||
for (slot in 1..5) {
|
||||
val file = stateFile(slot)
|
||||
val idx = slot - 1
|
||||
|
||||
val row = android.widget.LinearLayout(this).apply {
|
||||
orientation = android.widget.LinearLayout.HORIZONTAL
|
||||
gravity = android.view.Gravity.CENTER_VERTICAL
|
||||
layoutParams = android.widget.LinearLayout.LayoutParams(
|
||||
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
).apply { bottomMargin = dp(6) }
|
||||
}
|
||||
|
||||
row.addView(TextView(this).apply {
|
||||
text = getString(R.string.savestate_slot, slot)
|
||||
textSize = 12f
|
||||
setTextColor(Color.parseColor("#AAFFAA"))
|
||||
layoutParams = android.widget.LinearLayout.LayoutParams(dp(52),
|
||||
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
})
|
||||
|
||||
val timeTv = TextView(this).apply {
|
||||
val exists = file.exists()
|
||||
text = if (exists) dateFmt.format(Date(file.lastModified()))
|
||||
else getString(R.string.savestate_empty)
|
||||
textSize = 11f
|
||||
setTextColor(if (exists) Color.parseColor("#AAAAAA") else Color.parseColor("#444444"))
|
||||
layoutParams = android.widget.LinearLayout.LayoutParams(0,
|
||||
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
||||
}
|
||||
timeTvs[idx] = timeTv
|
||||
row.addView(timeTv)
|
||||
|
||||
val loadBtn = Button(this).apply {
|
||||
text = getString(R.string.savestate_load)
|
||||
textSize = 11f
|
||||
isEnabled = file.exists()
|
||||
layoutParams = android.widget.LinearLayout.LayoutParams(dp(80),
|
||||
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||
marginStart = dp(4)
|
||||
}
|
||||
setOnClickListener {
|
||||
engine.loadState(file.absolutePath)
|
||||
appendLog("State loaded: slot $slot")
|
||||
dialog?.dismiss()
|
||||
}
|
||||
}
|
||||
loadBtns[idx] = loadBtn
|
||||
row.addView(loadBtn)
|
||||
|
||||
row.addView(Button(this).apply {
|
||||
text = getString(R.string.savestate_save)
|
||||
textSize = 11f
|
||||
layoutParams = android.widget.LinearLayout.LayoutParams(dp(80),
|
||||
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT).apply {
|
||||
marginStart = dp(4)
|
||||
}
|
||||
setOnClickListener {
|
||||
engine.saveState(file.absolutePath)
|
||||
appendLog("State saved: slot $slot")
|
||||
// VICE writes asynchronously; refresh the row after one second
|
||||
mainHandler.postDelayed({
|
||||
if (file.exists()) {
|
||||
timeTvs[idx]?.text = dateFmt.format(Date(file.lastModified()))
|
||||
timeTvs[idx]?.setTextColor(Color.parseColor("#AAAAAA"))
|
||||
loadBtns[idx]?.isEnabled = true
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
|
||||
content.addView(row)
|
||||
}
|
||||
|
||||
dialog = AlertDialog.Builder(this)
|
||||
.setTitle(R.string.savestate_title)
|
||||
.setView(android.widget.ScrollView(this).apply {
|
||||
addView(content)
|
||||
setBackgroundColor(Color.parseColor("#1A1A2E"))
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun showHelp() {
|
||||
fun load(german: Boolean) = try {
|
||||
assets.open(if (german) "help_de.md" else "help_en.md").bufferedReader().readText()
|
||||
|
||||
@@ -30,7 +30,9 @@ if(HAVE_VICE)
|
||||
-Wl,--wrap=machine_init
|
||||
-Wl,--wrap=console_init
|
||||
-Wl,--wrap=ui_display_drive_led
|
||||
-Wl,--wrap=serial_trap_receive)
|
||||
-Wl,--wrap=serial_trap_receive
|
||||
-Wl,--wrap=event_snapshot_write_module
|
||||
-Wl,--wrap=event_snapshot_read_module)
|
||||
target_include_directories(vice_jni PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/vice-libs/${ANDROID_ABI}"
|
||||
"${VICE_SRC}/src"
|
||||
|
||||
@@ -36,16 +36,20 @@ 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/reset operations — written by Android thread, consumed by
|
||||
* video_canvas_refresh on the VICE thread (machine_trigger_reset and disk 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)
|
||||
* g_pending_reset → detach disk + hard reset to BASIC prompt */
|
||||
static pthread_mutex_t g_pending_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
/* Pending disk/reset/snapshot operations — written by Android thread, consumed by
|
||||
* video_canvas_refresh on the VICE thread (machine_trigger_reset, disk APIs, and
|
||||
* snapshot APIs must be called from the VICE thread to avoid corrupting state).
|
||||
* g_pending_disk → autostart_disk (resets machine, used for A-side disks)
|
||||
* g_pending_attach → file_system_attach_disk (hot-swap, B-side / hero disks)
|
||||
* g_pending_reset → detach disk + hard reset to BASIC prompt
|
||||
* g_pending_save_state → machine_write_snapshot (save full machine state)
|
||||
* g_pending_load_state → machine_read_snapshot (restore full machine state) */
|
||||
static pthread_mutex_t g_pending_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static char g_pending_disk[512];
|
||||
static char g_pending_attach[512];
|
||||
static volatile int g_pending_reset = 0;
|
||||
static volatile int g_pending_reset = 0;
|
||||
static char g_pending_save_state[512];
|
||||
static char g_pending_load_state[512];
|
||||
|
||||
/* =========================================================================
|
||||
* VICE integration (compiled only when libvice.a is linked in)
|
||||
@@ -278,6 +282,20 @@ static const sound_device_t g_android_sound_device = {
|
||||
/* Stubs for symbols that live in arch-specific or excluded files. */
|
||||
void main_exit(void) {}
|
||||
|
||||
/* event_snapshot_write/read_module: VICE's event-recording system allocates its
|
||||
* history buffer (start_snapshot.list.base) only when recording is explicitly
|
||||
* started via event_record_start(). In our headless build recording is never
|
||||
* started, so the buffer stays NULL and the real implementations crash with a
|
||||
* null-pointer dereference when called from machine_write_snapshot.
|
||||
* Replace them with no-ops: snapshots work fine without event playback data. */
|
||||
struct snapshot_s;
|
||||
int __wrap_event_snapshot_write_module(struct snapshot_s *s, int event_mode) {
|
||||
(void)s; (void)event_mode; return 0;
|
||||
}
|
||||
int __wrap_event_snapshot_read_module(struct snapshot_s *s) {
|
||||
(void)s; return 0;
|
||||
}
|
||||
|
||||
/* All functions below replace arch/headless/video.c (excluded from libvice.a). */
|
||||
|
||||
int video_arch_get_active_chip(void) { return 0; /* VIDEO_CHIP_VICII */ }
|
||||
@@ -384,6 +402,28 @@ void video_canvas_refresh(video_canvas_t *canvas,
|
||||
}
|
||||
}
|
||||
|
||||
/* Snapshot save / load. */
|
||||
pthread_mutex_lock(&g_pending_lock);
|
||||
char save_state_path[512] = {0};
|
||||
char load_state_path[512] = {0};
|
||||
if (g_pending_save_state[0] != '\0') {
|
||||
strncpy(save_state_path, g_pending_save_state, sizeof(save_state_path) - 1);
|
||||
g_pending_save_state[0] = '\0';
|
||||
}
|
||||
if (g_pending_load_state[0] != '\0') {
|
||||
strncpy(load_state_path, g_pending_load_state, sizeof(load_state_path) - 1);
|
||||
g_pending_load_state[0] = '\0';
|
||||
}
|
||||
pthread_mutex_unlock(&g_pending_lock);
|
||||
if (save_state_path[0] != '\0') {
|
||||
int r = machine_write_snapshot(save_state_path, 0, 1, 1);
|
||||
LOGI("machine_write_snapshot → %d", r);
|
||||
}
|
||||
if (load_state_path[0] != '\0') {
|
||||
int r = machine_read_snapshot(load_state_path, 0);
|
||||
LOGI("machine_read_snapshot → %d", r);
|
||||
}
|
||||
|
||||
/* 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.
|
||||
@@ -717,3 +757,35 @@ JNI_FN(void, setSoundEnabled)(JNIEnv *env, jobject obj, jboolean enabled) {
|
||||
(void)env; (void)obj;
|
||||
g_sound_enabled = enabled ? 1 : 0;
|
||||
}
|
||||
|
||||
JNI_FN(jboolean, saveState)(JNIEnv *env, jobject obj, jstring jpath) {
|
||||
(void)obj;
|
||||
#ifdef HAVE_VICE_SRC
|
||||
const char *p = (*env)->GetStringUTFChars(env, jpath, NULL);
|
||||
pthread_mutex_lock(&g_pending_lock);
|
||||
strncpy(g_pending_save_state, p, sizeof(g_pending_save_state) - 1);
|
||||
g_pending_save_state[sizeof(g_pending_save_state) - 1] = '\0';
|
||||
pthread_mutex_unlock(&g_pending_lock);
|
||||
(*env)->ReleaseStringUTFChars(env, jpath, p);
|
||||
return JNI_TRUE;
|
||||
#else
|
||||
(void)env; (void)jpath;
|
||||
return JNI_FALSE;
|
||||
#endif
|
||||
}
|
||||
|
||||
JNI_FN(jboolean, loadState)(JNIEnv *env, jobject obj, jstring jpath) {
|
||||
(void)obj;
|
||||
#ifdef HAVE_VICE_SRC
|
||||
const char *p = (*env)->GetStringUTFChars(env, jpath, NULL);
|
||||
pthread_mutex_lock(&g_pending_lock);
|
||||
strncpy(g_pending_load_state, p, sizeof(g_pending_load_state) - 1);
|
||||
g_pending_load_state[sizeof(g_pending_load_state) - 1] = '\0';
|
||||
pthread_mutex_unlock(&g_pending_lock);
|
||||
(*env)->ReleaseStringUTFChars(env, jpath, p);
|
||||
return JNI_TRUE;
|
||||
#else
|
||||
(void)env; (void)jpath;
|
||||
return JNI_FALSE;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -51,6 +51,17 @@
|
||||
android:layout_height="12dp"
|
||||
android:layout_marginEnd="4dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_savestate"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minWidth="0dp"
|
||||
android:padding="0dp"
|
||||
android:text="💾"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_help"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
|
||||
@@ -30,6 +30,13 @@
|
||||
<string name="hero_disk_created">Erstellt: %1$s</string>
|
||||
<string name="hero_disk_create_failed">Fehler beim Erstellen: %1$s</string>
|
||||
|
||||
<!-- Save state panel -->
|
||||
<string name="savestate_title">Spielstände</string>
|
||||
<string name="savestate_slot">Slot %1$d</string>
|
||||
<string name="savestate_empty">leer</string>
|
||||
<string name="savestate_save">Speichern</string>
|
||||
<string name="savestate_load">Laden</string>
|
||||
|
||||
<!-- Hero save editor -->
|
||||
<string name="editor_title">%1$s bearbeiten</string>
|
||||
<string name="editor_save">Speichern</string>
|
||||
|
||||
@@ -32,6 +32,13 @@
|
||||
<string name="hero_disk_created">Created: %1$s</string>
|
||||
<string name="hero_disk_create_failed">Failed to create: %1$s</string>
|
||||
|
||||
<!-- Save state panel -->
|
||||
<string name="savestate_title">Save States</string>
|
||||
<string name="savestate_slot">Slot %1$d</string>
|
||||
<string name="savestate_empty">empty</string>
|
||||
<string name="savestate_save">Save</string>
|
||||
<string name="savestate_load">Load</string>
|
||||
|
||||
<!-- Hero save editor -->
|
||||
<string name="editor_title">Edit %1$s</string>
|
||||
<string name="editor_save">Save</string>
|
||||
|
||||
Reference in New Issue
Block a user