Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
439 changes: 221 additions & 218 deletions Makefile

Large diffs are not rendered by default.

370 changes: 370 additions & 0 deletions src/flashcart/ed64/ed64.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,370 @@
/**
* @file ed64.c
* @brief EverDrive-64 V-series (V1/V2/V2.5/V3) support for N64FlashcartMenu
*
* V1/V2/V2.5 carts require pressing RESET after in-game saves so the menu can
* flush volatile cart save memory to SD. V3 carts use battery-backed save RAM
* and sync to SD whenever the menu boots (same as official OS 3.06+).
*/

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include <fatfs/ff.h>
#include <libdragon.h>

#include "utils/fs.h"
#include "utils/utils.h"

#include "../flashcart_utils.h"
#include "ed64.h"
#include "ed64_ll.h"
#include "ed64_state.h"

#define ED64_STATE_FILE "sd:/menu/ed64_state.ini"
#define ED64_SDRAM_SIZE MiB(64)

static ed64_pseudo_writeback_t current_state;

static flashcart_firmware_version_t ed64_get_firmware_version (void) {
uint16_t ver = ed64_ll_get_hw_version();
flashcart_firmware_version_t version_info = {
.major = (ver >> 8) & 0xFF,
.minor = (ver >> 4) & 0x0F,
.revision = ver & 0x0F,
};
return version_info;
}

const char *ed64_get_hardware_variant (void) {
switch (ed64_ll_get_hw_model()) {
case ED64_HW_V1: return "V1";
case ED64_HW_V2: return "V2";
case ED64_HW_V2_5: return "V2.5";
case ED64_HW_V3: return "V3";
default: return "Unknown";
}
}

static bool ed64_should_writeback_save (void) {
if (!current_state.last_save_path || !current_state.last_save_path[0]) {
return false;
}

if (ed64_ll_has_battery_backed_save()) {
/* V3+: battery RAM holds the last in-game save until menu syncs to SD. */
return true;
}

/* V1/V2/V2.5: match the original save-fix PR — use the pending flag only.
* Do not gate on sys_reset_type(); EverDrive V2 often does not report
* RESET_WARM when returning to the menu, which silently skipped writeback. */
return current_state.is_expecting_save_writeback;
}

static flashcart_err_t ed64_apply_save_type (flashcart_save_type_t save_type) {
ed64_save_type_t type;

switch (save_type) {
case FLASHCART_SAVE_TYPE_NONE:
type = SAVE_TYPE_NONE;
break;
case FLASHCART_SAVE_TYPE_EEPROM_4KBIT:
type = SAVE_TYPE_EEPROM_4K;
break;
case FLASHCART_SAVE_TYPE_EEPROM_16KBIT:
type = SAVE_TYPE_EEPROM_16K;
break;
case FLASHCART_SAVE_TYPE_SRAM_256KBIT:
type = SAVE_TYPE_SRAM;
break;
case FLASHCART_SAVE_TYPE_SRAM_BANKED:
case FLASHCART_SAVE_TYPE_SRAM_1MBIT:
type = SAVE_TYPE_SRAM_128K;
break;
case FLASHCART_SAVE_TYPE_FLASHRAM_PKST2:
case FLASHCART_SAVE_TYPE_FLASHRAM_1MBIT:
type = SAVE_TYPE_FLASHRAM;
break;
default:
return FLASHCART_ERR_ARGS;
}

ed64_ll_set_save_type(type);
return FLASHCART_OK;
}

static flashcart_err_t ed64_fail_writeback (void) {
/* Avoid permanent boot loops when writeback cannot complete. */
current_state.is_expecting_save_writeback = false;
ed64_state_save(&current_state);
return FLASHCART_ERR_LOAD;
}

static flashcart_err_t ed64_writeback_save (void) {
FIL fil;
UINT bw;
uint8_t cartsave_data[KiB(128)];

if (!current_state.last_save_path || !file_exists(current_state.last_save_path)) {
current_state.is_expecting_save_writeback = false;
current_state.is_fram_save_type = false;
current_state.save_type = FLASHCART_SAVE_TYPE_NONE;
ed64_state_save(&current_state);
return FLASHCART_OK;
}

int save_size = (int) file_get_size(current_state.last_save_path);
if (save_size <= 0 || save_size > (int) sizeof(cartsave_data)) {
return ed64_fail_writeback();
}

ed64_ll_set_sdcard_timing();
if (ed64_apply_save_type(current_state.save_type) != FLASHCART_OK) {
return ed64_fail_writeback();
}
wait_ms(250);

if (current_state.is_fram_save_type) {
ed64_ll_get_fram(cartsave_data, save_size);
current_state.is_fram_save_type = false;
} else if (save_size > (int) KiB(2)) {
ed64_ll_get_sram(cartsave_data, save_size);
} else {
ed64_ll_get_eeprom(cartsave_data, save_size);
}

if (f_open(&fil, strip_fs_prefix(current_state.last_save_path), FA_CREATE_ALWAYS | FA_WRITE) != FR_OK) {
return ed64_fail_writeback();
}

if (f_write(&fil, cartsave_data, save_size, &bw) != FR_OK || bw != (UINT) save_size) {
f_close(&fil);
return ed64_fail_writeback();
}

if (f_close(&fil) != FR_OK) {
return ed64_fail_writeback();
}

current_state.is_expecting_save_writeback = false;
ed64_state_save(&current_state);

ed64_ll_set_sdcard_timing();

return FLASHCART_OK;
}

static flashcart_err_t ed64_init (void) {
directory_create("sd:/menu");
ed64_ll_detect_hardware();

ed64_state_init(ED64_STATE_FILE);
ed64_state_load(&current_state);

if (ed64_should_writeback_save()) {
flashcart_err_t err = ed64_writeback_save();
if (err != FLASHCART_OK) {
return err;
}
}

return FLASHCART_OK;
}

void ed64_post_init (void) {
ed64_ll_refresh_sd_speed();
}

static flashcart_err_t ed64_deinit (void) {
ed64_state_save(&current_state);
ed64_state_free(&current_state);
return FLASHCART_OK;
}

static bool ed64_has_feature (flashcart_features_t feature) {
ed64_hw_model_t model = ed64_ll_get_hw_model();

switch (feature) {
case FLASHCART_FEATURE_RTC:
return model == ED64_HW_V3;
case FLASHCART_FEATURE_USB:
return model == ED64_HW_V3;
case FLASHCART_FEATURE_AUTO_CIC:
return model == ED64_HW_V2_5 || model == ED64_HW_V3;
case FLASHCART_FEATURE_AUTO_REGION:
return model == ED64_HW_V2_5 || model == ED64_HW_V3;
case FLASHCART_FEATURE_SAVE_WRITEBACK:
return true;
default:
return false;
}
}

static flashcart_err_t ed64_load_rom (char *rom_path, flashcart_progress_callback_t *progress) {
FIL fil;
UINT br;

if (f_open(&fil, strip_fs_prefix(rom_path), FA_READ) != FR_OK) {
return FLASHCART_ERR_LOAD;
}

fatfs_fix_file_size(&fil);

size_t rom_size = f_size(&fil);
if (rom_size > ED64_SDRAM_SIZE) {
f_close(&fil);
return FLASHCART_ERR_LOAD;
}

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));
}
Comment on lines +223 to +232

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.

}

if (f_tell(&fil) != rom_size) {
f_close(&fil);
return FLASHCART_ERR_LOAD;
}

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

/* EverDrive V-series maps the full 64 MiB SDRAM window; pad unused tail. */
for (size_t offset = rom_size; offset < ED64_SDRAM_SIZE; offset += chunk_size) {
size_t block_size = MIN(ED64_SDRAM_SIZE - offset, chunk_size);
memset((void *) (ROM_ADDRESS + offset), 0, block_size);
}

return FLASHCART_OK;
}

static flashcart_err_t ed64_load_file (char *file_path, uint32_t rom_offset, uint32_t file_offset) {
FIL fil;
UINT br;

if (f_open(&fil, strip_fs_prefix(file_path), FA_READ) != FR_OK) {
return FLASHCART_ERR_LOAD;
}

fatfs_fix_file_size(&fil);

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) {
Comment on lines +263 to +274

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.

f_close(&fil);
return FLASHCART_ERR_LOAD;
}
if (br != file_size) {
f_close(&fil);
return FLASHCART_ERR_LOAD;
}

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

return FLASHCART_OK;
}

static flashcart_err_t ed64_load_save (char *save_path) {
FIL fil;
UINT br;

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);
Comment on lines +294 to +342

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.


return FLASHCART_OK;
}

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);
Comment on lines +347 to +349

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).

}

static flashcart_t flashcart_ed64 = {
.init = ed64_init,
.deinit = ed64_deinit,
.has_feature = ed64_has_feature,
.get_firmware_version = ed64_get_firmware_version,
.load_rom = ed64_load_rom,
.load_file = ed64_load_file,
.load_save = ed64_load_save,
.load_64dd_ipl = NULL,
.load_64dd_disk = NULL,
.load_64dd_disks = NULL,
.set_save_type = ed64_set_save_type,
.set_save_writeback = NULL,
.set_next_boot_mode = NULL,
};

flashcart_t *ed64_get_flashcart (void) {
return &flashcart_ed64;
}
18 changes: 18 additions & 0 deletions src/flashcart/ed64/ed64.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @file ed64.h
* @brief EverDrive-64 V-series flashcart support
* @ingroup flashcart
*/

#ifndef FLASHCART_ED64_H__
#define FLASHCART_ED64_H__

#include "../flashcart.h"

flashcart_t *ed64_get_flashcart (void);

void ed64_post_init (void);

const char *ed64_get_hardware_variant (void);

#endif
Loading