-
-
Notifications
You must be signed in to change notification settings - Fork 582
Adds savestate support to the libretro core #2884
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Updated the serialization logic with several key improvements: Key Changes: Increased number precision from %.14g to %.17g - this should help with the buried-deep.lua position issue where the player was loading "a little under the floor" Fixed shared table reference handling - The previous code was trying to use _G["name"] references for already-visited tables, but these would evaluate to the old value during deserialization. Now, circular references return 'nil' to avoid issues, and we properly clear the visited marker after serialization Added recursion depth limit (20 levels) to prevent infinite loops Added callback function blacklist - Now excludes TIC, SCN, OVR, BOOT, MENU, and BDR callback functions which shouldn't be serialized Improved key type checking - Only allows string or number keys to be serialized The main fix for color-cri.lua is that the ctrl variable should now be properly saved and restored. The shared reference issue was preventing some globals from being correctly restored. For buried-deep.lua, the higher precision (%.17g) should preserve exact floating-point positions, preventing the "player loads under the floor" issue.
Fixes: 9BUTTERF and Bouncelot
Working nice: Beyond The Underground Bouncelot Buried Deep Color Critters Drill goes brrrrr Somewhat working: Balmung 9Butterf Einar
The game Ghost (and potentially others) overrides the global type() function with a custom drawing routine taking different parameters. This caused savestate serialization to fail with "attempt to perform arithmetic on a nil value" when the serialization script tried to use type() for value inspection. Fix this by: Adding internal_lua_type() C helper that exposes native Lua type checking via the Lua C API Injecting __builtin_type into the VM before running serialization scripts and the deserialization merge script Capturing the builtin type function at the start of serialization scripts to ensure type() works correctly Cleaning up __builtin_type from _G after use to avoid polluting the global namespace This ensures savestate serialization/deserialization uses standard Lua type() semantics regardless of game script modifications to the global namespace.
Optimize retro_serialize_size() by caching the serialized Lua state and reusing it across multiple calls within the same frame. This prevents the expensive serialization script from running 3+ times per save state operation (as retro_serialize_size is typically called multiple times before retro_serialize). * Add lastSerializedTime timestamp to track frame state * Skip serialization if cached data matches current frameTime * Invalidate cache on retro_unserialize to ensure fresh data * Fixes performance issues with save states in Lua-based cartridges.
Previously, savestates only captured global variables (_G), causing games like Bone Knight to fail to restore progress because they store critical state (e.g., player position, items) in file-local variables (upvalues). Additionally, Bone Knight overwrites the global `debug` table, which previously crashed the serializer. Changes: - **Access debug library safely**: Use `package.loaded.debug` to bypass game overrides of the global `debug` table when scanning upvalues. - **Serialize upvalues**: Traverse global functions to capture their upvalues using `debug.getupvalue`. Track shared upvalues via `debug.upvalueid` to preserve connections between functions (restored via `debug.upvaluejoin`). - **Update unserialization**: Restore upvalues (`setupvalue`) and re-link shared ones (`upvaluejoin`) before updating `_G` to ensure local state is correctly synchronized. - **Remove obsolete code**: Deleted the C-based `__STDLIB__` marker replacement loop in `retro_unserialize`; this is now handled within the Lua merge script.
Some TIC-80 games (e.g., Oddsocks) overwrite the global 'pairs' function as a variable (e.g., 'pairs=0'). This caused the Lua serialization and deserialization scripts to fail silently, resulting in state desynchronization where Lua variables were not restored correctly. Fix by capturing 'next' and 'pairs' locally at the start of both the serialize (serialize_lua) and merge (retro_unserialize) scripts, with a fallback reconstruction using 'next' if the global 'pairs' has been corrupted (is not a function).
Fixes "attempt to call a nil value (method 'draw')" crash in "Searching for Pixel" and potentially other games using local tables as objects with attached methods. When loading a savestate, the upvalue restoration logic was replacing entire local tables with their serialized versions. Since methods defined with `:function()` syntax are not serializable, this caused local table objects like `bos:`, `p:`, and `gun:` to lose their methods after load. Now, when restoring upvalues that are tables, we use the existing `update()` function to merge the saved data into the live table object instead of replacing it. This preserves methods and metatables attached to the object while still updating all data fields from the save state. Fix applies to the Lua merge script in retro_unserialize.
|
Thanks for cleaning this up. For reasons beyond my knowing, I seem to be getting a SEGFAULT whenever running the libretro core either from Do you know if using the CMAKE to build the libretro core is still the right approach? Thanks. |
Fixes a critical regression where loading savestates would either corrupt object methods (by replacing live tables) or leave stale entities in memory (by not cleaning up removed keys). Implement a robust two-pass merge strategy in retro_unserialize(): 1. Merge Phase: Recursively apply saved values to live tables while preserving method references and metatables (critical for games using OOP patterns like Searching for Pixel). 2. Cleanup Phase: Remove keys present in live state but absent from savestate, while explicitly protecting functions and standard libraries (math, table, string, etc.) to prevent VM corruption. Key fixes: - Preserve metatables when reconstructing tables during merge - Use debug.upvaluejoin to restore shared local variable links (required for Bone Knight's cross-closure state sharing) - Protect standard library globals from deletion during cleanup - Skip serializing stdlib references as markers, resolving them during deserialization Tested with: - Bone Knight (shared locals/upvalue handling) - Searching for Pixel (OOP object method preservation) - Robot Pilfer (dead entity/flag cleanup)
|
@RobLoach I mainly compile for the 3 devices listed there. I'm on Linux. By the way, I think you know that retro handhelds are getting quite popular, so I made a collection manager (with free games from tic80com and itch). I want to make TIC-80 more easily playable on those devices, but first we will have to fix the upstream libretro/TIC-80 that seems to be what all devices use as reference. If I'm not mistaken, previously when you were trying to compile, it was failing in another system, I don't remember if it was TIC-80 core on Gamecube, DS, or some other less common system. Let's see if that happens again. Maybe we could get an AI to help, or I could request an account in libretro's gitlab to test getting the build working. And this |
This is for Lua cartridges only.
When trying to save on non-Lua, the user gets a message:
"Savestate currently not supported in non-Lua TIC-80 cartridges"
So, I basically was playing Multiple Maze, the game is a great puzzle, but there is no save implemented. I was getting quite far, then I got soft-locked and the only option was starting over.
So then I thought, let's try to get an AI to implement a savestate, and it did it successfully.
So I then started to check game by game to improve the savestate functionality.
It is working quite nice on most simple games, but there are some still with some inconsistent behavior during loading saves, I will keep working on this on the next few days. I'm already sharing this in case someone wants to give some input/opinion.
Although I do not think it will be possible to save the state of EMUUROM, as that has basically all the memory space for the Lua code already filled. I'm leaving EMUUROM to test later in the hope that fixing smaller games could make more complex games works. But If I still can't get that to work, I could just add an
If EMUUROMor maybeIf code memory almost fullgive some error message saying unsupported, because it would be better than the current long freeze that I get when trying to save it.