Fix snapshot save/load: route both through CPU trap to sync registers

machine_write_snapshot reads maincpu_regs.pc to record the current
  program counter. When called directly from video_canvas_refresh,
  maincpu_regs is stale — the CPU loop keeps registers as stack-local
  variables and only syncs them to maincpu_regs inside DO_INTERRUPT via
  EXPORT_REGISTERS(). The snapshot therefore recorded the wrong PC.

  On restore (already trap-based), the wrong PC was faithfully written
  back into reg_pc via IMPORT_REGISTERS(), causing the CPU to resume at
  a bad address. The game screen looked correct (memory/VIC/SID state
  all restored) but the CPU never reached the input-polling code, so
  button presses had no effect.

  Fix: route machine_write_snapshot through interrupt_maincpu_trigger_trap
  so DO_INTERRUPT(IK_TRAP) calls EXPORT_REGISTERS() before the trap
  function, capturing the true current PC before the snapshot is written.
This commit is contained in:
ml
2026-06-17 20:50:51 +02:00
parent b9153ac973
commit 463c85e62f
33 changed files with 85 additions and 220 deletions
@@ -32,7 +32,10 @@ if(HAVE_VICE)
-Wl,--wrap=ui_display_drive_led -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_write_module
-Wl,--wrap=event_snapshot_read_module) -Wl,--wrap=event_snapshot_read_module
-Wl,--wrap=machine_trigger_reset
-Wl,--wrap=machine_reset
-Wl,--wrap=maincpu_reset)
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"
@@ -77,6 +77,7 @@ extern int __real_console_init(void);
#include "keyboard.h" #include "keyboard.h"
#include "autostart.h" #include "autostart.h"
#include "attach.h" #include "attach.h"
#include "interrupt.h"
/* Drive LED state. /* Drive LED state.
* g_io_frames: bumped by __wrap_serial_trap_receive; keeps LED lit ~100 ms after * g_io_frames: bumped by __wrap_serial_trap_receive; keeps LED lit ~100 ms after
@@ -302,6 +303,69 @@ int __wrap_event_snapshot_read_module(struct snapshot_s *s) {
(void)s; return 0; (void)s; return 0;
} }
/* --wrap=machine_trigger_reset / machine_reset / maincpu_reset: required by the
* linker --wrap flags in CMakeLists.txt; kept as simple pass-throughs so the
* symbols exist. Previously these suppressed resets during snapshot load, but
* the trap-based snapshot load (see load_state_trap below) makes suppression
* unnecessary — snapshot load now runs inside DO_INTERRUPT(IK_TRAP) which
* provides the EXPORT/IMPORT register sync that makes the CPU resume from the
* correct saved PC without any post-load machine reset. */
void __real_machine_trigger_reset(unsigned int reset_mode);
void __wrap_machine_trigger_reset(unsigned int reset_mode) {
__real_machine_trigger_reset(reset_mode);
}
void __real_machine_reset(void);
void __wrap_machine_reset(void) {
__real_machine_reset();
}
void __real_maincpu_reset(void);
void __wrap_maincpu_reset(void) {
__real_maincpu_reset();
}
/* Snapshot load via VICE CPU trap.
*
* machine_read_snapshot() restores CPU registers into maincpu_regs (a global
* struct), but maincpu_mainloop keeps its own stack-local copies (reg_pc,
* reg_a, etc.). If we call machine_read_snapshot() directly from
* video_canvas_refresh(), the loop never re-imports maincpu_regs, so the CPU
* continues from the *old* reg_pc in the *new* (restored) memory — wrong.
*
* The fix: schedule a VICE CPU trap. DO_INTERRUPT(IK_TRAP) in 6510core.c
* calls EXPORT_REGISTERS() before the trap function and IMPORT_REGISTERS()
* after. machine_read_snapshot() inside the trap overwrites maincpu_regs
* with the saved state; IMPORT_REGISTERS() then propagates those values
* (including the saved PC) back into the CPU loop's stack locals. Result:
* the game resumes from exactly the saved execution point. */
/* Both save and load run inside a CPU trap so DO_INTERRUPT(IK_TRAP) calls
* EXPORT_REGISTERS() before the trap function and IMPORT_REGISTERS() after.
*
* For save: EXPORT_REGISTERS() syncs the CPU loop's stack-local reg_pc into
* maincpu_regs before machine_write_snapshot reads it. Without the trap,
* maincpu_regs.pc is stale (last synced at the previous DO_INTERRUPT), so the
* snapshot would record the wrong PC and restore would resume at the wrong
* address — input polling code never reached, buttons appear dead.
*
* For load: machine_read_snapshot overwrites maincpu_regs with the saved state;
* IMPORT_REGISTERS() then propagates the saved PC back into reg_pc so the CPU
* resumes from the correct saved execution point. */
static char g_save_trap_path[512] = {0};
static char g_load_trap_path[512] = {0};
static void save_state_trap(uint16_t address, void *data) {
(void)address;
int r = machine_write_snapshot((const char *)data, 0, 0, 0);
LOGI("machine_write_snapshot → %d", r);
}
static void load_state_trap(uint16_t address, void *data) {
(void)address;
int r = machine_read_snapshot((const char *)data, 0);
LOGI("machine_read_snapshot → %d", r);
}
/* All functions below replace arch/headless/video.c (excluded from libvice.a). */ /* All functions below replace arch/headless/video.c (excluded from libvice.a). */
int video_arch_get_active_chip(void) { return 0; /* VIDEO_CHIP_VICII */ } int video_arch_get_active_chip(void) { return 0; /* VIDEO_CHIP_VICII */ }
@@ -425,19 +489,19 @@ void video_canvas_refresh(video_canvas_t *canvas,
} }
pthread_mutex_unlock(&g_pending_lock); pthread_mutex_unlock(&g_pending_lock);
if (save_state_path[0] != '\0') { if (save_state_path[0] != '\0') {
/* save_roms=0, save_disks=0: disk management is handled by our UI; strncpy(g_save_trap_path, save_state_path, sizeof(g_save_trap_path) - 1);
* skipping the drive module avoids path-resolution failures on restore. */ g_save_trap_path[sizeof(g_save_trap_path) - 1] = '\0';
int r = machine_write_snapshot(save_state_path, 0, 0, 0); interrupt_maincpu_trigger_trap(save_state_trap, g_save_trap_path);
LOGI("machine_write_snapshot → %d", r);
} }
if (load_state_path[0] != '\0') { if (load_state_path[0] != '\0') {
int r = machine_read_snapshot(load_state_path, 0); /* Schedule a VICE CPU trap so machine_read_snapshot runs inside
LOGI("machine_read_snapshot → %d", r); * DO_INTERRUPT(IK_TRAP). That macro calls EXPORT_REGISTERS() before
/* After snapshot restore the virtual device loses its D64 attachment * the trap and IMPORT_REGISTERS() after, so the saved PC written into
* and falls back to host-filesystem mode. Re-attach the same disk so * maincpu_regs by machine_read_snapshot is picked up by the CPU loop
* the C64 program can continue reading from the image it expected. */ * immediately — the game resumes from the correct saved execution point. */
if (r == 0 && g_current_disk_path[0] != '\0') strncpy(g_load_trap_path, load_state_path, sizeof(g_load_trap_path) - 1);
file_system_attach_disk(8, 0, g_current_disk_path); g_load_trap_path[sizeof(g_load_trap_path) - 1] = '\0';
interrupt_maincpu_trigger_trap(load_state_trap, g_load_trap_path);
} }
/* Drive LED. /* Drive LED.
@@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@android:color/transparent"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" /> </adaptive-icon>
</adaptive-icon>
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@android:color/transparent"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" /> </adaptive-icon>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB