Skip to content

Add ED64 V-series save writeback support - #344

Open
lewilou22 wants to merge 6 commits into
Polprzewodnikowy:mainfrom
lewilou22:main
Open

Add ED64 V-series save writeback support#344
lewilou22 wants to merge 6 commits into
Polprzewodnikowy:mainfrom
lewilou22:main

Conversation

@lewilou22

@lewilou22 lewilou22 commented Jun 2, 2026

Copy link
Copy Markdown

Description

Added the fix for V2, V3 ED64 Flashcarts
Saves are now operational after a reset

Motivation and Context

#44

How Has This Been Tested?

We have tested via Discord

https://discord.com/channels/205520502922543113/1262536014623146125/1510408587049177170

Screenshots

Types of changes

  • [ X] Improvement (non-breaking change that adds a new feature)
  • [X ] Bug fix (fixes an issue)
  • Breaking change (breaking change)
  • Documentation Improvement
  • [X ] Config and build (change in the configuration and build system, has no impact on code or features)

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • [ X] I have added tests to cover my changes.
  • All new and existing tests passed.

You agree with the license terms and that other license types may be granted with permission of the original N64FlashcartMenu project license holders.

Signed-off-by: GITHUB_USER <GITHUB_USER_EMAIL>

Summary by CodeRabbit

  • New Features

    • Full EverDrive‑64 V‑series flashcart support with automatic hardware detection.
    • Support for multiple save types (EEPROM 4K/16K, SRAM, SRAM 128K, FlashRAM) and reliable save read/write paths.
    • SD card speed/timing optimizations and hardware variant reporting.
    • Persistent ED64 state and safe SD→cart save writeback with retry/disable handling.
    • Public lifecycle APIs to trigger post‑init/SD refresh and expose hardware variant.
  • Refactor

    • V‑series module simplified to a thin wrapper delegating to the unified ED64 implementation.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds EverDrive-64 V-series support: low-level hardware (ed64_ll), high-level ED64 logic with SD save writeback/state (ed64), state persistence (ed64_state), vseries delegation, and Makefile inclusion of the new sources.

Changes

ED64 Implementation Refactor

Layer / File(s) Summary
Build system with ED64 source files
Makefile
Makefile adds three ED64 implementation files (flashcart/ed64/ed64.c, flashcart/ed64/ed64_ll.c, flashcart/ed64/ed64_state.c) to the SRCS compilation list. All other build rules and targets remain unchanged.
Low-level ED64 hardware interface
src/flashcart/ed64/ed64_ll.h, src/flashcart/ed64/ed64_ll.c
New low-level module defines save-type and hardware-model enums, register addresses, and public functions for hardware detection, battery-backed-save/reset queries, SD card timing, and SRAM/EEPROM/FRAM buffer read/write paths. Implementation handles PI DMA transfers, cache synchronization, and hardware-specific save configuration.
ED64 persisted state
src/flashcart/ed64/ed64_state.h, src/flashcart/ed64/ed64_state.c
New state module persists ED64 pseudo-writeback fields (expecting-writeback, save type, last save path) to an INI file and exposes init/load/save/free APIs.
High-level ED64 module
src/flashcart/ed64/ed64.h, src/flashcart/ed64/ed64.c
New module provides firmware/version and hardware-variant reporting, feature detection, ROM loading into SDRAM (chunked FATFS reads with 64MiB bounds and zero-padding), arbitrary file loading, SD save loading, and SD→cartridge writeback using persisted state. Wires a flashcart_t vtable and exposes ed64_get_flashcart(), ed64_post_init(), and ed64_get_hardware_variant().
ED64 vseries wrapper and flashcart integration
src/flashcart/ed64/ed64_vseries.h, src/flashcart/ed64/ed64_vseries.c, src/flashcart/flashcart.c
Refactors vseries module into a thin wrapper delegating to the ed64 module. Adds ed64_vseries_post_init() and ed64_vseries_get_hardware_variant(), and top-level flashcart_post_init(), flashcart_refresh_sd_speed(), and flashcart_get_hardware_variant() that forward to the ED64 implementations for CART_ED.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

bug, enhancement

Poem

A rabbit hops through ROM and RAM,
Tuning PI, SD, and save-write jam;
Chunked reads hum into SDRAM's night,
INI keeps the writeback flag alight—
Tiny paws press the flashcart's plan. 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title 'Add ED64 V-series save writeback support' directly summarizes the main change: adding save writeback functionality for ED64 V-series flashcarts, which is clearly reflected throughout the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/flashcart/ed64/ed64_ll.c`:
- Around line 202-214: ed64_ll_get_sram performs a PI DMA into the CPU-visible
buffer but never invalidates the CPU data cache afterwards, so add a data-cache
invalidation for the buffer region immediately after the dma_wait() that follows
pi_dma_from_sram (i.e., after dma_wait() and before ed64_ll_set_sdcard_timing())
to ensure the CPU sees fresh data; use the platform's cache-invalidate routine
appropriate for your codebase (call it with the buffer pointer and size and
respect any alignment requirements).
- Around line 129-135: In ed64_ll_set_sdcard_timing the fourth timing byte is
extracted with cfg >> 20 which is incorrect for the 4th byte of ED64_PI_DOM2_SD
(bytes are at 0,8,16,24); update the extraction for PI_BSD_DOM2_RLS_REG to use
cfg >> 24 instead of cfg >> 20 so the function ed64_ll_set_sdcard_timing writes
the correct 8-bit RLS value (references: ED64_PI_DOM2_SD,
ed64_ll_set_sdcard_timing, PI_BSD_DOM2_RLS_REG; other extractions
PI_BSD_DOM2_LAT_REG, PI_BSD_DOM2_PWD_REG, PI_BSD_DOM2_PGS_REG remain unchanged).

In `@src/flashcart/ed64/ed64.c`:
- Around line 223-232: The loop reading the ROM doesn't handle short reads: when
f_read returns FR_OK but br < block_size, the code still advances offset and
leaves gaps; update the loop that calls f_read (and uses variables fil, br,
block_size, offset, ROM_ADDRESS) to treat a short read as an immediate error by
closing fil and returning FLASHCART_ERR_LOAD; ensure you perform this check
right after f_read and before updating offset/progress so partial reads fail
fast.
- Around line 347-349: ed64_set_save_type currently updates
current_state.save_type before calling ed64_apply_save_type, causing an invalid
save_type to be persisted even if the hardware rejects it; modify
ed64_set_save_type to call ed64_apply_save_type(save_type) first, check its
success, and only then assign current_state.save_type = save_type (so subsequent
calls like ed64_writeback_save won't act on unsupported types and won't trigger
ed64_fail_writeback due to a prematurely stored value).
- Around line 263-274: Reject underflow/OOF cases before doing the subtractions:
check that rom_offset <= ED64_SDRAM_SIZE and that file_offset <= f_size(&fil)
(or that file_offset < = f_size - 0) and return FLASHCART_ERR_ARGS if not,
before computing file_size or doing ED64_SDRAM_SIZE - rom_offset; then proceed
with existing logic that computes file_size, compares it to (ED64_SDRAM_SIZE -
rom_offset), seeks and calls f_read into (ROM_ADDRESS + rom_offset). Ensure you
reference the rom_offset, file_offset, ED64_SDRAM_SIZE, f_size(&fil), file_size
and the f_read destination (ROM_ADDRESS + rom_offset) when adding the checks.
- Around line 294-342: Update logic so current_state.last_save_path is recorded
immediately from save_path before attempting f_open, and treat the f_open return
value FR_NO_FILE as an empty-save case instead of an error: strdup save_path
into current_state.last_save_path (freeing any previous value) and set
current_state.is_expecting_save_writeback = true and call
ed64_state_save(&current_state) before opening; then call f_open and if it
returns FR_NO_FILE continue with save_size = 0 and zeroed cartsave_data (or skip
reads), but for other non-FR_OK errors return FLASHCART_ERR_LOAD; afterwards
proceed to determine ed64_ll_get_save_type() and call the appropriate
ed64_ll_set_* functions with the (possibly zero-length) buffer and save_size,
and preserve current_state.is_fram_save_type handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6ed40775-a13a-4d34-a464-75c737c52d69

📥 Commits

Reviewing files that changed from the base of the PR and between 6407ab1 and b946fd2.

📒 Files selected for processing (8)
  • Makefile
  • src/flashcart/ed64/ed64.c
  • src/flashcart/ed64/ed64.h
  • src/flashcart/ed64/ed64_ll.c
  • src/flashcart/ed64/ed64_ll.h
  • src/flashcart/ed64/ed64_vseries.c
  • src/flashcart/ed64/ed64_vseries.h
  • src/flashcart/flashcart.c

Comment on lines +129 to +135
void ed64_ll_set_sdcard_timing (void) {
uint32_t cfg = ED64_PI_DOM2_SD;
io_write(PI_BSD_DOM2_LAT_REG, cfg >> 0);
io_write(PI_BSD_DOM2_PWD_REG, cfg >> 8);
io_write(PI_BSD_DOM2_PGS_REG, cfg >> 16);
io_write(PI_BSD_DOM2_RLS_REG, cfg >> 20);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Incorrect bit shift for RLS register: should be 24, not 20.

ED64_PI_DOM2_SD encodes four 8-bit values in bytes 0-3. The extraction for PI_BSD_DOM2_RLS_REG shifts by 20 bits instead of 24, yielding 0x0803 (truncated to lower bits) instead of the intended 0x80.

🐛 Proposed fix
 void ed64_ll_set_sdcard_timing (void) {
     uint32_t cfg = ED64_PI_DOM2_SD;
     io_write(PI_BSD_DOM2_LAT_REG, cfg >>  0);
     io_write(PI_BSD_DOM2_PWD_REG, cfg >>  8);
     io_write(PI_BSD_DOM2_PGS_REG, cfg >> 16);
-    io_write(PI_BSD_DOM2_RLS_REG, cfg >> 20);
+    io_write(PI_BSD_DOM2_RLS_REG, cfg >> 24);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/flashcart/ed64/ed64_ll.c` around lines 129 - 135, In
ed64_ll_set_sdcard_timing the fourth timing byte is extracted with cfg >> 20
which is incorrect for the 4th byte of ED64_PI_DOM2_SD (bytes are at 0,8,16,24);
update the extraction for PI_BSD_DOM2_RLS_REG to use cfg >> 24 instead of cfg >>
20 so the function ed64_ll_set_sdcard_timing writes the correct 8-bit RLS value
(references: ED64_PI_DOM2_SD, ed64_ll_set_sdcard_timing, PI_BSD_DOM2_RLS_REG;
other extractions PI_BSD_DOM2_LAT_REG, PI_BSD_DOM2_PWD_REG, PI_BSD_DOM2_PGS_REG
remain unchanged).

Comment on lines +202 to +214
void ed64_ll_get_sram (uint8_t *buffer, int size) {
int offset = (size == (int) KiB(32)) ? 0 : SRAM_128K_OFFSET;

pi_initialize_sram();
dma_wait();
pi_initialize();
wait_ms(250);

pi_dma_from_sram(buffer, offset, size);
dma_wait();

ed64_ll_set_sdcard_timing();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing cache invalidation after DMA read may cause stale data.

After PI DMA writes to DRAM (lines 210-211), the CPU data cache may still hold stale values for the buffer region. Without invalidating the cache after dma_wait(), subsequent CPU reads could return old data.

Note that ed64_ll_get_fram (line 227) invalidates before calling this function, but direct callers of ed64_ll_get_sram could be affected.

🐛 Proposed fix
 void ed64_ll_get_sram (uint8_t *buffer, int size) {
     int offset = (size == (int) KiB(32)) ? 0 : SRAM_128K_OFFSET;

     pi_initialize_sram();
     dma_wait();
     pi_initialize();
     wait_ms(250);

     pi_dma_from_sram(buffer, offset, size);
     dma_wait();
+    data_cache_hit_invalidate(buffer, size);

     ed64_ll_set_sdcard_timing();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void ed64_ll_get_sram (uint8_t *buffer, int size) {
int offset = (size == (int) KiB(32)) ? 0 : SRAM_128K_OFFSET;
pi_initialize_sram();
dma_wait();
pi_initialize();
wait_ms(250);
pi_dma_from_sram(buffer, offset, size);
dma_wait();
ed64_ll_set_sdcard_timing();
}
void ed64_ll_get_sram (uint8_t *buffer, int size) {
int offset = (size == (int) KiB(32)) ? 0 : SRAM_128K_OFFSET;
pi_initialize_sram();
dma_wait();
pi_initialize();
wait_ms(250);
pi_dma_from_sram(buffer, offset, size);
dma_wait();
data_cache_hit_invalidate(buffer, size);
ed64_ll_set_sdcard_timing();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/flashcart/ed64/ed64_ll.c` around lines 202 - 214, ed64_ll_get_sram
performs a PI DMA into the CPU-visible buffer but never invalidates the CPU data
cache afterwards, so add a data-cache invalidation for the buffer region
immediately after the dma_wait() that follows pi_dma_from_sram (i.e., after
dma_wait() and before ed64_ll_set_sdcard_timing()) to ensure the CPU sees fresh
data; use the platform's cache-invalidate routine appropriate for your codebase
(call it with the buffer pointer and size and respect any alignment
requirements).

Comment thread src/flashcart/ed64/ed64.c
Comment on lines +223 to +232
size_t chunk_size = KiB(128);
for (size_t offset = 0; offset < rom_size; offset += chunk_size) {
size_t block_size = MIN(rom_size - offset, chunk_size);
if (f_read(&fil, (void *) (ROM_ADDRESS + offset), block_size, &br) != FR_OK) {
f_close(&fil);
return FLASHCART_ERR_LOAD;
}
if (progress) {
progress(f_tell(&fil) / (float) f_size(&fil));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle short ROM reads immediately.

f_read can return FR_OK with br < block_size. This loop still advances offset by the full chunk, so a short read leaves a gap in SDRAM and only fails later at Lines 235-237. Treat short reads as an immediate load failure here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/flashcart/ed64/ed64.c` around lines 223 - 232, The loop reading the ROM
doesn't handle short reads: when f_read returns FR_OK but br < block_size, the
code still advances offset and leaves gaps; update the loop that calls f_read
(and uses variables fil, br, block_size, offset, ROM_ADDRESS) to treat a short
read as an immediate error by closing fil and returning FLASHCART_ERR_LOAD;
ensure you perform this check right after f_read and before updating
offset/progress so partial reads fail fast.

Comment thread src/flashcart/ed64/ed64.c
Comment on lines +263 to +274
size_t file_size = f_size(&fil) - file_offset;
if (file_size > (ED64_SDRAM_SIZE - rom_offset)) {
f_close(&fil);
return FLASHCART_ERR_ARGS;
}

if (f_lseek(&fil, file_offset) != FR_OK) {
f_close(&fil);
return FLASHCART_ERR_LOAD;
}

if (f_read(&fil, (void *) (ROM_ADDRESS + rom_offset), file_size, &br) != FR_OK) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Reject out-of-range offsets before subtracting.

If rom_offset > ED64_SDRAM_SIZE, ED64_SDRAM_SIZE - rom_offset underflows and the f_read at Line 274 can write past the 64 MiB ED64 SDRAM window. file_offset > f_size(&fil) has the same problem for file_size. Add explicit bounds checks before either subtraction.

Suggested fix
-    size_t file_size = f_size(&fil) - file_offset;
-    if (file_size > (ED64_SDRAM_SIZE - rom_offset)) {
+    size_t total_size = f_size(&fil);
+    if (rom_offset > ED64_SDRAM_SIZE || file_offset > total_size) {
+        f_close(&fil);
+        return FLASHCART_ERR_ARGS;
+    }
+
+    size_t file_size = total_size - file_offset;
+    if (file_size > (ED64_SDRAM_SIZE - rom_offset)) {
         f_close(&fil);
         return FLASHCART_ERR_ARGS;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
size_t file_size = f_size(&fil) - file_offset;
if (file_size > (ED64_SDRAM_SIZE - rom_offset)) {
f_close(&fil);
return FLASHCART_ERR_ARGS;
}
if (f_lseek(&fil, file_offset) != FR_OK) {
f_close(&fil);
return FLASHCART_ERR_LOAD;
}
if (f_read(&fil, (void *) (ROM_ADDRESS + rom_offset), file_size, &br) != FR_OK) {
size_t total_size = f_size(&fil);
if (rom_offset > ED64_SDRAM_SIZE || file_offset > total_size) {
f_close(&fil);
return FLASHCART_ERR_ARGS;
}
size_t file_size = total_size - file_offset;
if (file_size > (ED64_SDRAM_SIZE - rom_offset)) {
f_close(&fil);
return FLASHCART_ERR_ARGS;
}
if (f_lseek(&fil, file_offset) != FR_OK) {
f_close(&fil);
return FLASHCART_ERR_LOAD;
}
if (f_read(&fil, (void *) (ROM_ADDRESS + rom_offset), file_size, &br) != FR_OK) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/flashcart/ed64/ed64.c` around lines 263 - 274, Reject underflow/OOF cases
before doing the subtractions: check that rom_offset <= ED64_SDRAM_SIZE and that
file_offset <= f_size(&fil) (or that file_offset < = f_size - 0) and return
FLASHCART_ERR_ARGS if not, before computing file_size or doing ED64_SDRAM_SIZE -
rom_offset; then proceed with existing logic that computes file_size, compares
it to (ED64_SDRAM_SIZE - rom_offset), seeks and calls f_read into (ROM_ADDRESS +
rom_offset). Ensure you reference the rom_offset, file_offset, ED64_SDRAM_SIZE,
f_size(&fil), file_size and the f_read destination (ROM_ADDRESS + rom_offset)
when adding the checks.

Comment thread src/flashcart/ed64/ed64.c
Comment on lines +294 to +342
if (f_open(&fil, strip_fs_prefix(save_path), FA_READ) != FR_OK) {
return FLASHCART_ERR_LOAD;
}

size_t save_size = f_size(&fil);
if (save_size > KiB(128)) {
f_close(&fil);
return FLASHCART_ERR_LOAD;
}

uint8_t cartsave_data[KiB(128)];
if (f_read(&fil, cartsave_data, save_size, &br) != FR_OK) {
f_close(&fil);
return FLASHCART_ERR_LOAD;
}
if (br != save_size) {
f_close(&fil);
return FLASHCART_ERR_LOAD;
}
if (f_close(&fil) != FR_OK) {
return FLASHCART_ERR_LOAD;
}

current_state.is_fram_save_type = false;

ed64_save_type_t type = ed64_ll_get_save_type();
switch (type) {
case SAVE_TYPE_EEPROM_4K:
case SAVE_TYPE_EEPROM_16K:
ed64_ll_set_eeprom(cartsave_data, save_size);
break;
case SAVE_TYPE_SRAM:
case SAVE_TYPE_SRAM_128K:
ed64_ll_set_sram(cartsave_data, save_size);
break;
case SAVE_TYPE_FLASHRAM:
ed64_ll_set_fram(cartsave_data, save_size);
current_state.is_fram_save_type = true;
break;
default:
break;
}

if (current_state.last_save_path) {
free(current_state.last_save_path);
}
current_state.last_save_path = strdup(save_path);
current_state.is_expecting_save_writeback = true;
ed64_state_save(&current_state);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Register the save path even when no .sav exists yet.

last_save_path is only refreshed after f_open succeeds here. That means a first-run game with no existing save file never records its destination. On V3, Lines 57-60 will still sync battery-backed RAM on the next boot, but into the previous game's last_save_path, corrupting the wrong save file. Record the new path before opening the file and treat FR_NO_FILE as the empty-save case instead of an error.

Suggested fix
 static flashcart_err_t ed64_load_save (char *save_path) {
     FIL fil;
     UINT br;
+    FRESULT fr;
+
+    if (current_state.last_save_path) {
+        free(current_state.last_save_path);
+    }
+    current_state.last_save_path = strdup(save_path);
+    if (!current_state.last_save_path) {
+        return FLASHCART_ERR_LOAD;
+    }
+    current_state.is_expecting_save_writeback = true;
+    current_state.is_fram_save_type = false;
 
-    if (f_open(&fil, strip_fs_prefix(save_path), FA_READ) != FR_OK) {
+    fr = f_open(&fil, strip_fs_prefix(save_path), FA_READ);
+    if (fr == FR_NO_FILE) {
+        ed64_state_save(&current_state);
+        return FLASHCART_OK;
+    }
+    if (fr != FR_OK) {
         return FLASHCART_ERR_LOAD;
     }
@@
-    current_state.is_fram_save_type = false;
-
     ed64_save_type_t type = ed64_ll_get_save_type();
@@
-    if (current_state.last_save_path) {
-        free(current_state.last_save_path);
-    }
-    current_state.last_save_path = strdup(save_path);
-    current_state.is_expecting_save_writeback = true;
     ed64_state_save(&current_state);
Based on learnings, this driver is ED64-specific and should correctly handle the affected flashcart rather than relying on behavior from other carts.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/flashcart/ed64/ed64.c` around lines 294 - 342, Update logic so
current_state.last_save_path is recorded immediately from save_path before
attempting f_open, and treat the f_open return value FR_NO_FILE as an empty-save
case instead of an error: strdup save_path into current_state.last_save_path
(freeing any previous value) and set current_state.is_expecting_save_writeback =
true and call ed64_state_save(&current_state) before opening; then call f_open
and if it returns FR_NO_FILE continue with save_size = 0 and zeroed
cartsave_data (or skip reads), but for other non-FR_OK errors return
FLASHCART_ERR_LOAD; afterwards proceed to determine ed64_ll_get_save_type() and
call the appropriate ed64_ll_set_* functions with the (possibly zero-length)
buffer and save_size, and preserve current_state.is_fram_save_type handling.

Comment thread src/flashcart/ed64/ed64.c
Comment on lines +347 to +349
static flashcart_err_t ed64_set_save_type (flashcart_save_type_t save_type) {
current_state.save_type = save_type;
return ed64_apply_save_type(save_type);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only persist save_type after the hardware accepts it.

If ed64_apply_save_type(save_type) rejects an unsupported value, this code still leaves current_state.save_type updated. The next boot then reuses that invalid type in ed64_writeback_save, which disables writeback via ed64_fail_writeback(). Save the state only after ed64_apply_save_type succeeds.

Suggested fix
 static flashcart_err_t ed64_set_save_type (flashcart_save_type_t save_type) {
-    current_state.save_type = save_type;
-    return ed64_apply_save_type(save_type);
+    flashcart_err_t err = ed64_apply_save_type(save_type);
+    if (err != FLASHCART_OK) {
+        return err;
+    }
+    current_state.save_type = save_type;
+    return FLASHCART_OK;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/flashcart/ed64/ed64.c` around lines 347 - 349, ed64_set_save_type
currently updates current_state.save_type before calling ed64_apply_save_type,
causing an invalid save_type to be persisted even if the hardware rejects it;
modify ed64_set_save_type to call ed64_apply_save_type(save_type) first, check
its success, and only then assign current_state.save_type = save_type (so
subsequent calls like ed64_writeback_save won't act on unsupported types and
won't trigger ed64_fail_writeback due to a prematurely stored value).

@networkfusion

Copy link
Copy Markdown
Collaborator

Linking to #342 which was probably better.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/flashcart/ed64/ed64_state.c`:
- Around line 9-14: ed64_state_init currently assigns ed64_state_path =
strdup(path) without checking for allocation failure; add a NULL check after
strdup in ed64_state_init and handle failure (e.g., leave previous path
unchanged or free and set to NULL and return an error) so later functions like
ed64_state_load/ed64_state_save calling ini_try_load(ed64_state_path) won't
dereference NULL; specifically update ed64_state_init to detect if strdup
returned NULL and implement a graceful fallback or return/report an error to
callers that rely on ed64_state_path.
- Around line 16-29: ed64_state_load should check strdup failures and avoid
conflating allocation failure with "no path"; fetch the string into a local
const char* (e.g. path = ini_get_string(...)), free the old
state->last_save_path as already done, then if path is empty set
state->last_save_path = NULL (avoid allocating an empty string), otherwise
attempt state->last_save_path = strdup(path) and if strdup returns NULL handle
the allocation failure (set state->last_save_path = NULL and log/handle the
error appropriately) so downstream checks on state->last_save_path distinguish
"no path" from allocation failure.
- Around line 31-41: The function ed64_state_save currently ignores the return
value of ini_save which can hide persistence failures; update ed64_state_save
(and its callers) to check ini_save's return and handle failures: modify
ed64_state_save to return an error code or boolean instead of void, call
ini_save(ini, ed64_state_path) and if it fails log an error with context
(include ed64_state_path and state->is_expecting_save_writeback) and return a
failure value, otherwise return success; update any callers of ed64_state_save
to check the new return value and propagate or handle the error appropriately.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 88f09a19-51a6-4baf-b8b1-a4ef57a0a517

📥 Commits

Reviewing files that changed from the base of the PR and between b946fd2 and 94ffa59.

📒 Files selected for processing (2)
  • src/flashcart/ed64/ed64_state.c
  • src/flashcart/ed64/ed64_state.h

Comment on lines +9 to +14
void ed64_state_init (const char *path) {
if (ed64_state_path) {
free(ed64_state_path);
}
ed64_state_path = strdup(path);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Add null check after strdup to prevent crash on allocation failure.

If strdup fails and returns NULL, subsequent calls to ed64_state_load or ed64_state_save will dereference ed64_state_path when calling ini_try_load(ed64_state_path), causing a crash.

🛡️ Proposed fix to add null check
 void ed64_state_init (const char *path) {
     if (ed64_state_path) {
         free(ed64_state_path);
     }
     ed64_state_path = strdup(path);
+    if (!ed64_state_path) {
+        /* Fatal: cannot proceed without state path */
+        return;
+    }
 }

Alternatively, consider returning an error code or using a fallback path to allow graceful degradation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/flashcart/ed64/ed64_state.c` around lines 9 - 14, ed64_state_init
currently assigns ed64_state_path = strdup(path) without checking for allocation
failure; add a NULL check after strdup in ed64_state_init and handle failure
(e.g., leave previous path unchanged or free and set to NULL and return an
error) so later functions like ed64_state_load/ed64_state_save calling
ini_try_load(ed64_state_path) won't dereference NULL; specifically update
ed64_state_init to detect if strdup returned NULL and implement a graceful
fallback or return/report an error to callers that rely on ed64_state_path.

Comment on lines +16 to +29
void ed64_state_load (ed64_pseudo_writeback_t *state) {
ini_t *ini = ini_try_load(ed64_state_path);

state->is_expecting_save_writeback = ini_get_bool(ini, "ed64", "is_expecting_save_writeback", false);
state->is_fram_save_type = ini_get_bool(ini, "ed64", "is_fram_save_type", false);
state->save_type = (flashcart_save_type_t) ini_get_int(ini, "ed64", "save_type", FLASHCART_SAVE_TYPE_NONE);

if (state->last_save_path) {
free(state->last_save_path);
}
state->last_save_path = strdup(ini_get_string(ini, "ed64", "last_save_path", ""));

ini_free(ini);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Check strdup result to detect allocation failures.

If strdup fails on line 26, state->last_save_path becomes NULL, which is indistinguishable from "no path set." According to context snippet 2 (ed64.c:52-118), the downstream check if (!current_state.last_save_path || !current_state.last_save_path[0]) will silently skip writeback, potentially losing save data when allocation fails rather than when no path is configured.

🛡️ Proposed fix to handle allocation failure
-    state->last_save_path = strdup(ini_get_string(ini, "ed64", "last_save_path", ""));
+    const char *path = ini_get_string(ini, "ed64", "last_save_path", "");
+    state->last_save_path = (*path != '\0') ? strdup(path) : NULL;
+    if (*path != '\0' && !state->last_save_path) {
+        /* Could not allocate memory for save path; treat as fatal or log error */
+    }

This also avoids allocating memory for an empty string when NULL would suffice.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/flashcart/ed64/ed64_state.c` around lines 16 - 29, ed64_state_load should
check strdup failures and avoid conflating allocation failure with "no path";
fetch the string into a local const char* (e.g. path = ini_get_string(...)),
free the old state->last_save_path as already done, then if path is empty set
state->last_save_path = NULL (avoid allocating an empty string), otherwise
attempt state->last_save_path = strdup(path) and if strdup returns NULL handle
the allocation failure (set state->last_save_path = NULL and log/handle the
error appropriately) so downstream checks on state->last_save_path distinguish
"no path" from allocation failure.

Comment on lines +31 to +41
void ed64_state_save (ed64_pseudo_writeback_t *state) {
ini_t *ini = ini_try_load(ed64_state_path);

ini_set_bool(ini, "ed64", "is_expecting_save_writeback", state->is_expecting_save_writeback);
ini_set_bool(ini, "ed64", "is_fram_save_type", state->is_fram_save_type);
ini_set_int(ini, "ed64", "save_type", state->save_type);
ini_set_string(ini, "ed64", "last_save_path", state->last_save_path ? state->last_save_path : "");

ini_save(ini, ed64_state_path);
ini_free(ini);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Check the return value of ini_save to detect persistence failures.

If ini_save fails on line 39 (e.g., disk full, write error), the state will not be persisted, but the function returns successfully. This could break the save-writeback flow described in the PR objectives: when is_expecting_save_writeback fails to persist, subsequent resets may either skip writeback or enter a boot loop.

🛡️ Proposed fix to return or log save errors

Option 1: Return an error code:

-void ed64_state_save (ed64_pseudo_writeback_t *state) {
+bool ed64_state_save (ed64_pseudo_writeback_t *state) {
     ini_t *ini = ini_try_load(ed64_state_path);
 
     ini_set_bool(ini, "ed64", "is_expecting_save_writeback", state->is_expecting_save_writeback);
     ini_set_bool(ini, "ed64", "is_fram_save_type", state->is_fram_save_type);
     ini_set_int(ini, "ed64", "save_type", state->save_type);
     ini_set_string(ini, "ed64", "last_save_path", state->last_save_path ? state->last_save_path : "");
 
-    ini_save(ini, ed64_state_path);
+    bool success = ini_save(ini, ed64_state_path);
     ini_free(ini);
+    return success;
 }

Then update callers to check the return value.

Option 2 (minimal): At least check and log:

-    ini_save(ini, ed64_state_path);
+    if (!ini_save(ini, ed64_state_path)) {
+        /* Log or handle failure to persist ED64 state */
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void ed64_state_save (ed64_pseudo_writeback_t *state) {
ini_t *ini = ini_try_load(ed64_state_path);
ini_set_bool(ini, "ed64", "is_expecting_save_writeback", state->is_expecting_save_writeback);
ini_set_bool(ini, "ed64", "is_fram_save_type", state->is_fram_save_type);
ini_set_int(ini, "ed64", "save_type", state->save_type);
ini_set_string(ini, "ed64", "last_save_path", state->last_save_path ? state->last_save_path : "");
ini_save(ini, ed64_state_path);
ini_free(ini);
}
bool ed64_state_save (ed64_pseudo_writeback_t *state) {
ini_t *ini = ini_try_load(ed64_state_path);
ini_set_bool(ini, "ed64", "is_expecting_save_writeback", state->is_expecting_save_writeback);
ini_set_bool(ini, "ed64", "is_fram_save_type", state->is_fram_save_type);
ini_set_int(ini, "ed64", "save_type", state->save_type);
ini_set_string(ini, "ed64", "last_save_path", state->last_save_path ? state->last_save_path : "");
bool success = ini_save(ini, ed64_state_path);
ini_free(ini);
return success;
}
Suggested change
void ed64_state_save (ed64_pseudo_writeback_t *state) {
ini_t *ini = ini_try_load(ed64_state_path);
ini_set_bool(ini, "ed64", "is_expecting_save_writeback", state->is_expecting_save_writeback);
ini_set_bool(ini, "ed64", "is_fram_save_type", state->is_fram_save_type);
ini_set_int(ini, "ed64", "save_type", state->save_type);
ini_set_string(ini, "ed64", "last_save_path", state->last_save_path ? state->last_save_path : "");
ini_save(ini, ed64_state_path);
ini_free(ini);
}
void ed64_state_save (ed64_pseudo_writeback_t *state) {
ini_t *ini = ini_try_load(ed64_state_path);
ini_set_bool(ini, "ed64", "is_expecting_save_writeback", state->is_expecting_save_writeback);
ini_set_bool(ini, "ed64", "is_fram_save_type", state->is_fram_save_type);
ini_set_int(ini, "ed64", "save_type", state->save_type);
ini_set_string(ini, "ed64", "last_save_path", state->last_save_path ? state->last_save_path : "");
if (!ini_save(ini, ed64_state_path)) {
/* Log or handle failure to persist ED64 state */
}
ini_free(ini);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/flashcart/ed64/ed64_state.c` around lines 31 - 41, The function
ed64_state_save currently ignores the return value of ini_save which can hide
persistence failures; update ed64_state_save (and its callers) to check
ini_save's return and handle failures: modify ed64_state_save to return an error
code or boolean instead of void, call ini_save(ini, ed64_state_path) and if it
fails log an error with context (include ed64_state_path and
state->is_expecting_save_writeback) and return a failure value, otherwise return
success; update any callers of ed64_state_save to check the new return value and
propagate or handle the error appropriately.

@lewilou22 lewilou22 changed the title V Series Fix Updated Add ED64 V-series save writeback support Jun 3, 2026
@lewilou22

Copy link
Copy Markdown
Author

should be fixed to the original #342

@networkfusion
networkfusion changed the base branch from main to develop July 25, 2026 15:21
@networkfusion
networkfusion changed the base branch from develop to main July 25, 2026 15:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants