Skip to content

[Feature Request] Implement retro_serialize / retro_unserialize for savestate support #44

Description

@Ataraxiainc2

Summary

The Cannonball libretro core currently stubs out all three serialization functions in src/main/libretro/main.cpp:

size_t retro_serialize_size(void) { return 0; }
bool   retro_serialize(void *data, size_t size)       { return false; }
bool   retro_unserialize(const void *data, size_t size) { return false; }
This means RetroArch (and any libretro frontend) cannot offer save states, rewind, or netplay for OutRun via this core. The official documentation at https://docs.libretro.com/library/cannonball/ confirms Save States, Rewind and Netplay are all marked ✕.

Why this matters
OutRun under Cannonball is the only way to play the arcade version on many platforms. The lack of savestates breaks UX parity with every other libretro core.
Educational and accessibility use cases (e.g. apps that let learners pause/resume at any point) are blocked by the core.
Rewind and netplay depend on serialization and cannot be added later unless serialize lands first.
Proposed implementation
Cannonball is a standalone C++ port (not an emulator), so there is no CPU memory blob to dump. Serialization must walk the engine's singleton state manually. The mutable state lives across ~22 objects in src/main/engine/:

outrun, ostats, oferrari, oroad, oinitengine, otraffic, osprites, omap, omusic, ohud, olevelobjs, opalette, osmoke, otiles, oattractai, oanimseq, obonus, ocrash, ohiscore, ologo, oinputs, ooutputs

Plus the frontend globals state, frame, tick_frame, pause_engine in src/main/libretro/main.cpp.

Suggested layout
[ magic "CNBL" u32 ][ version u32 ][ payload_size u32 ][ payload... ]
Each singleton exposes:

void save(BinaryWriter& w) const;
void load(BinaryReader& r, uint32_t version);
retro_serialize_size() returns the fixed sum of all save() outputs plus the 12-byte header (estimated ~40-80 KB based on field inventory).

Audio handling
The audio and osoundint subsystems should NOT serialize the PCM ring buffer. On load, flush the output stream and re-trigger the active stage's music loop from its start marker. This avoids clicks without visibly affecting UX (OutRun music loops are short).

Pointer policy
Pointers into ROM data (e.g. sprite tables, road curvature tables) are read-only and must NOT be serialized — they're re-derivable from the ROMs.
Heap-allocated engine state (if any) should be serialized as value copies with a version-stable layout.
Versioning
Start at version = 1. Bump on any layout change. Reject mismatched versions in retro_unserialize with a log message instead of crashing.

Side issue (low priority, same file)
retro_reset() is also currently a no-op:

void retro_reset(void) {}
A minimal implementation calling outrun.init() and re-initializing the core singletons would enable the "Reset" feature in frontends.

Test matrix proposal
Acceptance criteria the implementer can verify:

Save on title screen → load → HUD score = 0, full timer, stage = Coconut Beach.
Save mid-race → load → Ferrari position / speed / rotation bit-exact.
Save during crash animation → load → animation resumes correctly.
Save with 3 lives → load → 3 lives.
Save on bonus screen (stage 5 ending) → load → bonus sequence resumes.
50 save/load cycles with no leaks or drift.
Save on device A → load on device B (same build) → works (determinism).
Load with wrong magic → rejected, no crash.
Load with version mismatch → rejected, no crash.
Willing to contribute
[Adjust this paragraph to your reality — e.g.:]
We can help with testing on Android (arm64-v8a) and sponsor/fund an implementation if a maintainer is available. Happy to discuss scope or provide additional test scenarios.

Thanks for maintaining this core — Cannonball is a joy to play.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions