SDL_Libretro
SDL3-power libretro frontend.
Loading...
Searching...
No Matches
SDL_libretro_serialize.h
Go to the documentation of this file.
1
7#if defined(SDL_LIBRETRO_IMPLEMENTATION) && !defined(SDL_LIBRETRO_SERIALIZE_IMPL_ONCE)
8#define SDL_LIBRETRO_SERIALIZE_IMPL_ONCE
9
10#ifdef __DOXYGEN
21#define SDL_LIBRETRO_ENABLE_REWIND_DELTA
22#endif
23
24
25#ifndef SDL_LIBRETRO_REWIND_DEFAULT_MAX_BYTES
31#define SDL_LIBRETRO_REWIND_DEFAULT_MAX_BYTES ((size_t)256 * 1024 * 1024)
32#endif
33
34// Cheats
35
36bool SDL_Libretro_SetCheat(SDL_Libretro* lr, unsigned index, bool enabled, const char* code) {
37 if (!lr || !lr->core.gameLoaded) return false;
38 lr->core.symbols.retro_cheat_set(index, enabled, code);
39 return true;
40}
41
42void SDL_Libretro_ResetCheats(SDL_Libretro* lr) {
43 if (!lr || !lr->core.gameLoaded) return;
44 lr->core.symbols.retro_cheat_reset();
45}
46
47// Save States
48
52size_t SDL_Libretro_GetStateSize(const SDL_Libretro* lr) {
53 if (!lr || !lr->core.gameLoaded) return 0;
54 return lr->core.symbols.retro_serialize_size();
55}
56
60bool SDL_Libretro_SaveState_IO(SDL_Libretro* lr, SDL_IOStream* dst, bool closeio) {
61 bool ok = false;
62 if (!lr || !lr->core.gameLoaded || !dst) {
63 SDL_SetError("[SDL_Libretro] Invalid SaveState_IO arguments");
64 } else {
65 size_t size = lr->core.symbols.retro_serialize_size();
66 if (size == 0) {
67 SDL_SetError("[SDL_Libretro] Core does not support save states");
68 } else {
69 void* data = SDL_malloc(size);
70 if (data) {
71 if (lr->core.symbols.retro_serialize(data, size)) {
72 ok = (SDL_WriteIO(dst, data, size) == size);
73 }
74 SDL_free(data);
75 }
76 }
77 }
78 if (closeio && dst) SDL_CloseIO(dst);
79 return ok;
80}
81
85bool SDL_Libretro_SaveState(SDL_Libretro* lr, const char* file) {
86 if (!lr || !lr->core.gameLoaded || !file) {
87 SDL_SetError("[SDL_Libretro] Invalid SaveState arguments");
88 return false;
89 }
90
91 SDL_IOStream* io = SDL_IOFromFile(file, "wb");
92 if (!io) return false;
93 return SDL_Libretro_SaveState_IO(lr, io, true);
94}
95
96bool SDL_Libretro_LoadState_IO(SDL_Libretro* lr, SDL_IOStream* src, bool closeio) {
97 if (!lr || !lr->core.gameLoaded || !src) {
98 SDL_SetError("[SDL_Libretro] Invalid LoadState_IO arguments");
99 if (closeio && src) SDL_CloseIO(src);
100 return false;
101 }
102 size_t size = 0;
103 void* data = SDL_LoadFile_IO(src, &size, closeio);
104 if (!data) return false;
105 bool ok = (size > 0) && lr->core.symbols.retro_unserialize(data, size);
106 SDL_free(data);
107 // A load is a timeline discontinuity: drop rewind history so a subsequent rewind can't walk back across it into the pre-load timeline.
108 if (ok && lr->rewindEnabled) SDL_Libretro_ClearRewind(lr);
109 return ok;
110}
111
115bool SDL_Libretro_LoadState(SDL_Libretro* lr, const char* file) {
116 if (!lr || !lr->core.gameLoaded || !file) {
117 SDL_SetError("[SDL_Libretro] Invalid LoadState arguments");
118 return false;
119 }
120 SDL_IOStream* io = SDL_IOFromFile(file, "rb");
121 if (!io) return false;
122 return SDL_Libretro_LoadState_IO(lr, io, true);
123}
124
125// Memory
126
139void* SDL_Libretro_GetMemoryData(const SDL_Libretro* lr, unsigned memoryType, size_t* size) {
140 if (!lr || !lr->core.gameLoaded || !lr->core.symbols.retro_get_memory_data || !lr->core.symbols.retro_get_memory_size) {
141 if (size) *size = 0;
142 return NULL;
143 }
144 void* ptr = lr->core.symbols.retro_get_memory_data(memoryType);
145 if (size) *size = lr->core.symbols.retro_get_memory_size(memoryType);
146 return ptr;
147}
148
162bool SDL_Libretro_SetMemoryData(SDL_Libretro* lr, unsigned memoryType, const void* data, size_t size) {
163 if (!lr || !data) {
164 SDL_SetError("[SDL_Libretro] Invalid SetMemoryData arguments");
165 return false;
166 }
167 size_t capacity = 0;
168 void* dst = SDL_Libretro_GetMemoryData(lr, memoryType, &capacity);
169 if (!dst || capacity == 0) {
170 SDL_SetError("[SDL_Libretro] Memory type %u unavailable", memoryType);
171 return false;
172 }
173 size_t copySize = size < capacity ? size : capacity;
174 SDL_memcpy(dst, data, copySize);
175 return true;
176}
177
184static void SDL_Libretro_FreeMemoryMap(SDL_Libretro* lr) {
185 if (lr->core.memoryMapDescriptors) {
186 for (unsigned i = 0; i < lr->core.memoryMapDescriptorCount; i++) {
187 SDL_free((void*)lr->core.memoryMapDescriptors[i].addrspace);
188 }
189 SDL_free(lr->core.memoryMapDescriptors);
190 lr->core.memoryMapDescriptors = NULL;
191 }
192 lr->core.memoryMapDescriptorCount = 0;
193}
194
207unsigned SDL_Libretro_GetMemoryMapCount(const SDL_Libretro* lr) {
208 return (lr && lr->core.gameLoaded) ? lr->core.memoryMapDescriptorCount : 0;
209}
210
232bool SDL_Libretro_GetMemoryMapDescriptor(const SDL_Libretro* lr, unsigned index,
233 Uint64* flags, void** ptr, size_t* offset, size_t* start,
234 size_t* select, size_t* disconnect, size_t* len, const char** addrspace) {
235 if (!lr || !lr->core.gameLoaded || index >= lr->core.memoryMapDescriptorCount) {
236 return false;
237 }
238 const struct retro_memory_descriptor* d = &lr->core.memoryMapDescriptors[index];
239 if (flags) *flags = d->flags;
240 if (ptr) *ptr = d->ptr;
241 if (offset) *offset = d->offset;
242 if (start) *start = d->start;
243 if (select) *select = d->select;
244 if (disconnect) *disconnect = d->disconnect;
245 if (len) *len = d->len;
246 if (addrspace) *addrspace = d->addrspace;
247 return true;
248}
249
272void* SDL_Libretro_GetMapAddress(const SDL_Libretro* lr, size_t address, size_t* regionRemaining) {
273 if (regionRemaining) *regionRemaining = 0;
274 if (!lr || !lr->core.gameLoaded) return NULL;
275
276 for (unsigned i = 0; i < lr->core.memoryMapDescriptorCount; i++) {
277 const struct retro_memory_descriptor* d = &lr->core.memoryMapDescriptors[i];
278 if (!d->ptr || d->len == 0) continue;
279
280 // Does this descriptor's address space contain `address`?
281 if (d->select != 0) {
282 if (((address ^ d->start) & d->select) != 0) continue;
283 } else if (address < d->start || (address - d->start) >= d->len) {
284 continue;
285 }
286
287 // Fold out disconnected bits, then offset into the host buffer.
288 size_t masked = address & ~d->disconnect;
289 if (masked < d->start) continue;
290 size_t within = masked - d->start;
291 if (within >= d->len) continue;
292
293 if (regionRemaining) *regionRemaining = d->len - within;
294 return (Uint8*)d->ptr + d->offset + within;
295 }
296 return NULL;
297}
298
304static const char* SDL_Libretro_GetMemoryTypeName(unsigned memoryType) {
305 switch (memoryType) {
306 case RETRO_MEMORY_SAVE_RAM: return "SRAM";
307 case RETRO_MEMORY_RTC: return "RTC";
308 case RETRO_MEMORY_SYSTEM_RAM: return "system RAM";
309 case RETRO_MEMORY_VIDEO_RAM: return "video RAM";
310 default: return "memory";
311 }
312}
313
325bool SDL_Libretro_SaveMemory_IO(SDL_Libretro* lr, unsigned memoryType, SDL_IOStream* dst, bool closeio) {
326 bool ok = false;
327 if (!lr || !lr->core.gameLoaded || !dst) {
328 SDL_SetError("[SDL_Libretro] Invalid SaveMemory_IO arguments");
329 } else {
330 size_t size = 0;
331 void* mem = SDL_Libretro_GetMemoryData(lr, memoryType, &size);
332 if (!mem || size == 0) {
333 ok = true; // core has no such memory; nothing to save
334 } else {
335 ok = (SDL_WriteIO(dst, mem, size) == size);
336 }
337 }
338 if (closeio && dst) SDL_CloseIO(dst);
339 return ok;
340}
341
353bool SDL_Libretro_SaveMemory(SDL_Libretro* lr, unsigned memoryType, const char* file) {
354 if (!lr || !lr->core.gameLoaded || !file) {
355 SDL_SetError("[SDL_Libretro] Invalid SaveMemory arguments");
356 return false;
357 }
358
359 size_t size = 0;
360 void* mem = SDL_Libretro_GetMemoryData(lr, memoryType, &size);
361 // Nothing to save, so don't create an empty file.
362 if (!mem || size == 0) return true;
363
364 SDL_IOStream* io = SDL_IOFromFile(file, "wb");
365 if (!io) return false;
366 bool ok = SDL_Libretro_SaveMemory_IO(lr, memoryType, io, true);
367 if (ok) {
368 SDL_Log("[SDL_Libretro] %s saved to %s (%zu bytes)",
369 SDL_Libretro_GetMemoryTypeName(memoryType), file, size);
370 }
371 return ok;
372}
373
388bool SDL_Libretro_LoadMemory_IO(SDL_Libretro* lr, unsigned memoryType, SDL_IOStream* src, bool closeio) {
389 if (!lr || !lr->core.gameLoaded || !src) {
390 SDL_SetError("[SDL_Libretro] Invalid LoadMemory_IO arguments");
391 if (closeio && src) SDL_CloseIO(src);
392 return false;
393 }
394
395 // Bail before reading the stream when there's nowhere to put the data.
396 size_t capacity = 0;
397 if (!SDL_Libretro_GetMemoryData(lr, memoryType, &capacity) || capacity == 0) {
398 SDL_SetError("[SDL_Libretro] Core has no %s", SDL_Libretro_GetMemoryTypeName(memoryType));
399 if (closeio) SDL_CloseIO(src);
400 return false;
401 }
402
403 size_t fileSize = 0;
404 void* data = SDL_LoadFile_IO(src, &fileSize, closeio);
405 if (!data) return false;
406
407 // A size mismatch is the usual cause of a save that loads only partially.
408 if (fileSize != capacity) {
409 SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
410 "[SDL_Libretro] %s size mismatch (file %zu bytes, region %zu bytes); loading %zu",
411 SDL_Libretro_GetMemoryTypeName(memoryType), fileSize, capacity,
412 fileSize < capacity ? fileSize : capacity);
413 }
414
415 // SetMemoryData clamps to the region capacity.
416 bool ok = SDL_Libretro_SetMemoryData(lr, memoryType, data, fileSize);
417 SDL_free(data);
418 return ok;
419}
420
431bool SDL_Libretro_LoadMemory(SDL_Libretro* lr, unsigned memoryType, const char* file) {
432 if (!lr || !lr->core.gameLoaded || !file) {
433 SDL_SetError("[SDL_Libretro] Invalid LoadMemory arguments");
434 return false;
435 }
436 SDL_IOStream* io = SDL_IOFromFile(file, "rb");
437 if (!io) return false;
438 bool ok = SDL_Libretro_LoadMemory_IO(lr, memoryType, io, true);
439 if (ok) {
440 SDL_Log("[SDL_Libretro] %s loaded from %s",
441 SDL_Libretro_GetMemoryTypeName(memoryType), file);
442 }
443 return ok;
444}
445
446// SRAM convenience wrappers (RETRO_MEMORY_SAVE_RAM)
447
448bool SDL_Libretro_SaveSRAM_IO(SDL_Libretro* lr, SDL_IOStream* dst, bool closeio) {
449 return SDL_Libretro_SaveMemory_IO(lr, RETRO_MEMORY_SAVE_RAM, dst, closeio);
450}
451
455bool SDL_Libretro_SaveSRAM(SDL_Libretro* lr, const char* file) {
456 return SDL_Libretro_SaveMemory(lr, RETRO_MEMORY_SAVE_RAM, file);
457}
458
459bool SDL_Libretro_LoadSRAM_IO(SDL_Libretro* lr, SDL_IOStream* src, bool closeio) {
460 return SDL_Libretro_LoadMemory_IO(lr, RETRO_MEMORY_SAVE_RAM, src, closeio);
461}
462
466bool SDL_Libretro_LoadSRAM(SDL_Libretro* lr, const char* file) {
467 return SDL_Libretro_LoadMemory(lr, RETRO_MEMORY_SAVE_RAM, file);
468}
469
470// Rewind
471
472#ifdef SDL_LIBRETRO_ENABLE_REWIND_DELTA
473
487static size_t SDL_Libretro_RewindMaxEncodedSize(size_t len) {
488 return len + (len / 128) + 1;
489}
490
499static size_t SDL_Libretro_RewindMatchRun(const unsigned char* a, const unsigned char* b, size_t max) {
500 size_t i = 0;
501 for (; i + sizeof(size_t) <= max; i += sizeof(size_t)) {
502 size_t wa, wb;
503 SDL_memcpy(&wa, a + i, sizeof(size_t));
504 SDL_memcpy(&wb, b + i, sizeof(size_t));
505 if (wa != wb) break;
506 }
507 while (i < max && a[i] == b[i]) i++;
508 return i;
509}
510
525static size_t SDL_Libretro_RewindEncodeDelta(const unsigned char* cur, const unsigned char* ref, size_t len, unsigned char* out, size_t outCap) {
526 // A matching gap shorter than this is cheaper to fold into a literal than to emit as a skip. A skip also forces a new literal header afterwards.
527 const size_t kMinSkip = 3;
528
529 size_t op = 0, i = 0;
530 while (i < len) {
531 if (cur[i] == ref[i]) {
532 size_t run = SDL_Libretro_RewindMatchRun(cur + i, ref + i, len - i);
533 i += run;
534 while (run > 0) {
535 if (run <= 127) {
536 if (out) { if (op >= outCap) return 0; out[op] = (unsigned char)run; }
537 op++;
538 run = 0;
539 } else if (run < 255) {
540 // Two short skips (2 bytes) beat one extended skip (3)
541 if (out) { if (op >= outCap) return 0; out[op] = 127; }
542 op++;
543 run -= 127;
544 } else {
545 size_t chunk = run > 65535 ? 65535 : run;
546 if (out) {
547 if (op + 3 > outCap) return 0;
548 out[op] = 0x00;
549 out[op + 1] = (unsigned char)(chunk & 0xFF);
550 out[op + 2] = (unsigned char)(chunk >> 8);
551 }
552 op += 3;
553 run -= chunk;
554 }
555 }
556 } else {
557 // Literal segment: a span of differing bytes that absorbs any matching gaps shorter than kMinSkip (folded as zero XOR bytes).
558 size_t segStart = i;
559 size_t segEnd = i;
560 size_t j = i;
561 for (;;) {
562 while (j < len && cur[j] != ref[j]) j++;
563 segEnd = j;
564 if (j >= len) break;
565 size_t gapStart = j;
566 j += SDL_Libretro_RewindMatchRun(cur + j, ref + j, len - j);
567 if ((j - gapStart) >= kMinSkip || j >= len) break;
568 }
569 i = segEnd;
570 for (size_t pos = segStart; pos < segEnd; ) {
571 size_t chunk = segEnd - pos;
572 if (chunk > 128) chunk = 128;
573 if (out) {
574 if (op + 1 + chunk > outCap) return 0;
575 out[op] = (unsigned char)(0x80 | (chunk - 1));
576 }
577 op++;
578 for (size_t k = pos; k < pos + chunk; k++) {
579 if (out) out[op] = cur[k] ^ ref[k];
580 op++;
581 }
582 pos += chunk;
583 }
584 }
585 }
586 return op;
587}
588
598static bool SDL_Libretro_RewindDecodeDelta(const unsigned char* delta, size_t deltaLen, unsigned char* state, size_t stateLen) {
599 size_t dp = 0, sp = 0;
600 while (dp < deltaLen && sp < stateLen) {
601 unsigned char tag = delta[dp++];
602 if (tag == 0x00) {
603 if (dp + 2 > deltaLen) return false;
604 size_t skip = delta[dp] | ((size_t)delta[dp + 1] << 8);
605 dp += 2;
606 sp += skip;
607 } else if (tag <= 0x7F) {
608 sp += tag;
609 } else {
610 size_t count = (tag & 0x7F) + 1;
611 if (dp + count > deltaLen || sp + count > stateLen) return false;
612 for (size_t j = 0; j < count; j++)
613 state[sp++] ^= delta[dp++];
614 }
615 }
616 return (sp <= stateLen);
617}
618
619#endif /* SDL_LIBRETRO_ENABLE_REWIND_DELTA */
620
637bool SDL_Libretro_SetRewindEnabled(SDL_Libretro* lr, bool enabled, unsigned bufferFrames, unsigned captureInterval) {
638 if (!lr) return false;
639
640 SDL_Libretro_RewindFree(lr);
641
642 if (!enabled) {
643 lr->rewindEnabled = false;
644 return true;
645 }
646
647 // Sane defaults.
648 if (bufferFrames == 0) bufferFrames = 300;
649 if (captureInterval == 0) captureInterval = 1;
650
651 // Allow enabling rewind, when a core isn't loaded.
652 if (!lr->core.loaded) {
653 lr->rewindEnabled = true;
654 lr->rewindCapacity = bufferFrames;
655 lr->rewindCaptureInterval = captureInterval;
656 return true;
657 }
658
659 // Figure out how large the state needs to be.
660 size_t slotSize = lr->core.symbols.retro_serialize_size();
661 if (slotSize == 0) {
662 SDL_SetError("[SDL_Libretro] Core does not support serialization");
663 lr->rewindEnabled = false;
664 return false;
665 }
666
667 // Cores that flag their state as incomplete warn the frontend not to rely on it for frame-sensitive features (netplay, rerecording).
668 if (lr->core.serializationQuirks & RETRO_SERIALIZATION_QUIRK_INCOMPLETE) {
669 SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
670 "[SDL_Libretro] Core reports incomplete serialization, so rewind may be unreliable");
671 }
672
673 // Initialize the rewind slots.
674 unsigned char* ref = (unsigned char*)SDL_calloc(1, slotSize);
675 unsigned char* scratch = (unsigned char*)SDL_malloc(slotSize);
676 SDL_LibretroRewindDelta* entries = (SDL_LibretroRewindDelta*)SDL_calloc(bufferFrames, sizeof(*entries));
677 unsigned char* encScratch = NULL;
678 bool ok = (ref && scratch && entries);
679#ifdef SDL_LIBRETRO_ENABLE_REWIND_DELTA
680 // Delta mode keeps a reusable worst-case-sized buffer for single-pass encoding. Full-state mode stores raw snapshots and needs none.
681 encScratch = (unsigned char*)SDL_malloc(SDL_Libretro_RewindMaxEncodedSize(slotSize));
682 ok = ok && (encScratch != NULL);
683#endif
684 if (!ok) {
685 SDL_free(ref);
686 SDL_free(scratch);
687 SDL_free(encScratch);
688 SDL_free(entries);
689 SDL_SetError("[SDL_Libretro] Failed to allocate rewind buffers");
690 lr->rewindEnabled = false;
691 return false;
692 }
693
694 // Set the initial state.
695 lr->rewindReference = ref;
696 lr->rewindScratch = scratch;
697 lr->rewindEncodeScratch = encScratch; // NULL in full-state mode
698 lr->rewindEntries = entries;
699 lr->rewindSlotSize = slotSize;
700 lr->rewindBytes = 0;
701 lr->rewindCapacity = bufferFrames;
702 lr->rewindCaptureInterval = captureInterval;
703 lr->rewindHead = 0;
704 lr->rewindCount = 0;
705 lr->rewindFrameCounter = 0;
706 lr->rewindEnabled = true;
707 lr->rewindHasReference = false;
708 lr->rewindActive = false;
709 return true;
710}
711
718bool SDL_Libretro_GetRewindEnabled(const SDL_Libretro* lr) {
719 return lr && lr->rewindEnabled;
720}
721
728double SDL_Libretro_GetRewindRemaining(const SDL_Libretro* lr) {
729 if (!lr || !lr->rewindEnabled || lr->rewindCount == 0) return 0.0;
730 double fps = lr->core.fps > 0.0 ? lr->core.fps : 60.0;
731 return (double)lr->rewindCount * (double)lr->rewindCaptureInterval / fps;
732}
733
743size_t SDL_Libretro_GetRewindMemoryUsage(const SDL_Libretro* lr) {
744 if (!lr || !lr->rewindEnabled) return 0;
745 size_t total = lr->rewindBytes;
746 if (lr->rewindReference) total += lr->rewindSlotSize;
747 if (lr->rewindScratch) total += lr->rewindSlotSize;
748 total += (size_t)lr->rewindCapacity * sizeof(SDL_LibretroRewindDelta);
749 return total;
750}
751
763void SDL_Libretro_SetRewindMemoryLimit(SDL_Libretro* lr, size_t maxBytes) {
764 if (!lr) return;
765 lr->rewindMaxBytes = maxBytes;
766 SDL_Libretro_RewindEvictToBudget(lr);
767}
768
775size_t SDL_Libretro_GetRewindMemoryLimit(const SDL_Libretro* lr) {
776 return lr ? lr->rewindMaxBytes : 0;
777}
778
795bool SDL_Libretro_SetRewindMemoryDuration(SDL_Libretro* lr, double seconds) {
796 if (!lr) return false;
797 if (!(seconds > 0.0)) {
798 SDL_SetError("[SDL_Libretro] Rewind duration must be positive");
799 return false;
800 }
801
802 // Per-snapshot worst-case size. Prefer the size the rewind buffer is already
803 // using; otherwise query the core directly so this works before rewind is enabled.
804 size_t slotSize = lr->rewindSlotSize;
805 if (slotSize == 0 && lr->core.loaded && lr->core.symbols.retro_serialize_size) {
806 slotSize = lr->core.symbols.retro_serialize_size();
807 }
808 if (slotSize == 0) {
809 SDL_SetError("[SDL_Libretro] Rewind state size unknown; load a serializable core first");
810 return false;
811 }
812
813 double fps = (lr->core.fps > 0.0) ? lr->core.fps : 60.0;
814 unsigned interval = (lr->rewindCaptureInterval > 0) ? lr->rewindCaptureInterval : 1;
815
816 // Number of snapshots that cover the requested span, rounded up so the budget
817 // never falls short of the duration.
818 double snapshots = (seconds * fps) / (double)interval;
819 if (snapshots < 1.0) snapshots = 1.0;
820 size_t snaps = (size_t)snapshots;
821 if ((double)snaps < snapshots) snaps++;
822
823 SDL_Libretro_SetRewindMemoryLimit(lr, snaps * slotSize);
824 return true;
825}
826
832static void SDL_Libretro_RewindCapture(SDL_Libretro* lr) {
833 if (!lr->rewindEnabled || !lr->rewindReference) return;
834
835 lr->rewindFrameCounter++;
836 if (lr->rewindFrameCounter < lr->rewindCaptureInterval) return;
837 lr->rewindFrameCounter = 0;
838
839 // A core may change its serialize size mid-session
840 // (RETRO_SERIALIZATION_QUIRK_CORE_VARIABLE_SIZE). The reference/scratch
841 // buffers and every stored delta are sized to the old state, so resize the
842 // working buffers and drop the now-incompatible history before continuing.
843 // Without this, retro_serialize() into an undersized scratch fails and rewind
844 // silently stops recording.
845 size_t curSize = lr->core.symbols.retro_serialize_size();
846 if (curSize == 0) return;
847 if (curSize != lr->rewindSlotSize) {
848 unsigned char* nref = (unsigned char*)SDL_calloc(1, curSize);
849 unsigned char* nscr = (unsigned char*)SDL_malloc(curSize);
850 unsigned char* nenc = NULL;
851 bool ok = (nref && nscr);
852#ifdef SDL_LIBRETRO_ENABLE_REWIND_DELTA
853 nenc = (unsigned char*)SDL_malloc(SDL_Libretro_RewindMaxEncodedSize(curSize));
854 ok = ok && (nenc != NULL);
855#endif
856 if (!ok) {
857 SDL_free(nref);
858 SDL_free(nscr);
859 SDL_free(nenc);
860 return;
861 }
862 SDL_free(lr->rewindReference);
863 SDL_free(lr->rewindScratch);
864 SDL_free(lr->rewindEncodeScratch);
865 lr->rewindReference = nref;
866 lr->rewindScratch = nscr;
867 lr->rewindEncodeScratch = nenc; // NULL in full-state mode
868 lr->rewindSlotSize = curSize;
869 SDL_Libretro_ClearRewind(lr);
870 }
871
872 if (!lr->core.symbols.retro_serialize(lr->rewindScratch, lr->rewindSlotSize)) return;
873
874 if (!lr->rewindHasReference) {
875 SDL_memcpy(lr->rewindReference, lr->rewindScratch, lr->rewindSlotSize);
876 lr->rewindHasReference = true;
877 return;
878 }
879
880 // Store the step needed to walk back to the previous state. Both modes keep
881 // `reference` holding the newest state and the slot holding the previous one,
882 // so the head always represents "now" and step-back semantics are identical.
883 SDL_LibretroRewindDelta* slot = &lr->rewindEntries[lr->rewindHead];
884#ifdef SDL_LIBRETRO_ENABLE_REWIND_DELTA
885 // Delta mode: encode the change between the new state (scratch) and the
886 // previous one (reference) once into the reusable worst-case-sized buffer,
887 // then copy just the produced bytes into the slot. The old approach ran the
888 // encoder twice (a NULL pass to size, then a real pass), scanning the whole
889 // state both times; for multi-megabyte states (e.g. PSX) that second pass
890 // dominated capture cost. The copy here is only of the compressed delta.
891 size_t storeSize = SDL_Libretro_RewindEncodeDelta(
892 lr->rewindScratch, lr->rewindReference, lr->rewindSlotSize,
893 lr->rewindEncodeScratch, SDL_Libretro_RewindMaxEncodedSize(lr->rewindSlotSize));
894 if (storeSize == 0) return;
895
896 // Reuse the slot's existing allocation when it's already big enough; only (re)allocate when the snapshot needs more room. At steady state this stops allocating entirely, avoiding a malloc/free on every captured frame.
897 //
898 // rewindBytes tracks allocated snapshot memory (capacity), so it stays accurate whether or not a slot is overwritten in place.
899 if (slot->capacity < storeSize) {
900 unsigned char* data = (unsigned char*)SDL_realloc(slot->data, storeSize);
901 if (!data) return;
902 lr->rewindBytes += storeSize - slot->capacity;
903 slot->data = data;
904 slot->capacity = storeSize;
905 }
906 SDL_memcpy(slot->data, lr->rewindEncodeScratch, storeSize);
907 slot->length = storeSize;
908
909 // Ping-pong: the freshly serialized state in `scratch` becomes the new reference, and the now-stale reference buffer is recycled as next frame's scratch (overwritten by the next retro_serialize). Swapping pointers avoids a full state-sized memcpy on every captured frame.
910 unsigned char* swap = lr->rewindReference;
911 lr->rewindReference = lr->rewindScratch;
912 lr->rewindScratch = swap;
913#else
914 // Full-state mode. The slot must hold the previous state verbatim, which is exactly what `reference` already contains. Rather than memcpy a multi-MB state into the slot every frame, donate the reference buffer to the slot and recycle the slot's old (evicted) buffer as the next scratch, a few pointer assignments instead of a full state-sized copy. The new state in `scratch` becomes the reference. Both buffers are sized to slotSize, so in steady state no allocation happens at all.
915 unsigned char* recycled = slot->data; // old state being evicted from this slot (NULL until first wrap)
916 size_t recycledCap = slot->capacity;
917 if (recycledCap < lr->rewindSlotSize) {
918 // Slot's buffer can't serve as a full-state scratch; grow it before mutating anything so a failure leaves the ring and buffers untouched.
919 unsigned char* grown = (unsigned char*)SDL_realloc(recycled, lr->rewindSlotSize);
920 if (!grown) return;
921 recycled = grown;
922 recycledCap = lr->rewindSlotSize;
923 }
924
925 lr->rewindBytes += lr->rewindSlotSize - slot->capacity;
926 slot->data = lr->rewindReference; // holds the previous state
927 slot->length = lr->rewindSlotSize;
928 slot->capacity = lr->rewindSlotSize;
929 lr->rewindReference = lr->rewindScratch; // holds the new state
930 lr->rewindScratch = recycled;
931#endif
932
933 lr->rewindHead = (lr->rewindHead + 1) % lr->rewindCapacity;
934 if (lr->rewindCount < lr->rewindCapacity) {
935 lr->rewindCount++;
936 }
937
938 // Keep total delta memory under the configured budget by dropping the oldest snapshots; this bounds worst-case memory for large/incompressible states.
939 SDL_Libretro_RewindEvictToBudget(lr);
940}
941
947static void SDL_Libretro_RewindFreeEntry(SDL_Libretro* lr, SDL_LibretroRewindDelta* entry) {
948 lr->rewindBytes -= entry->capacity;
949 SDL_free(entry->data);
950 entry->data = NULL;
951 entry->length = 0;
952 entry->capacity = 0;
953}
954
962static bool SDL_Libretro_RewindStepState(SDL_Libretro* lr) {
963 if (!lr->rewindEnabled || !lr->rewindReference || lr->rewindCount == 0) return false;
964
965 lr->rewindHead = (lr->rewindHead == 0) ? (lr->rewindCapacity - 1) : (lr->rewindHead - 1);
966 lr->rewindCount--;
967
968 SDL_LibretroRewindDelta* entry = &lr->rewindEntries[lr->rewindHead];
969 if (!entry->data || entry->length == 0) return false;
970
971#ifdef SDL_LIBRETRO_ENABLE_REWIND_DELTA
972 bool reconstructed = SDL_Libretro_RewindDecodeDelta(entry->data, entry->length,
973 lr->rewindReference, lr->rewindSlotSize);
974#else
975 // Full-state mode: the entry is the previous state verbatim.
976 bool reconstructed = (entry->length == lr->rewindSlotSize);
977 if (reconstructed) SDL_memcpy(lr->rewindReference, entry->data, lr->rewindSlotSize);
978#endif
979
980 SDL_Libretro_RewindFreeEntry(lr, entry);
981
982 // A failed reconstruction (a malformed or partial XOR decode) or a rejected
983 // unserialize leaves `reference` out of sync with the state the core still
984 // holds, and the remaining deltas are anchored to a reference we can no
985 // longer rebuild. Discard the now-untrustworthy history so the next forward
986 // capture re-seeds from a clean serialize instead of encoding the next delta
987 // against a corrupt base.
988 if (!reconstructed || !lr->core.symbols.retro_unserialize(lr->rewindReference, lr->rewindSlotSize)) {
989 SDL_Libretro_ClearRewind(lr);
990 return false;
991 }
992 return true;
993}
994
1003static bool SDL_Libretro_RewindStep(SDL_Libretro* lr) {
1004 if (!lr || !lr->core.gameLoaded || !lr->rewindEnabled) {
1005 SDL_SetError("[SDL_Libretro] Rewind is not enabled");
1006 return false;
1007 }
1008 if (!SDL_Libretro_RewindStepState(lr)) return false;
1009
1010 lr->rewindActive = true;
1011 lr->core.symbols.retro_run();
1012 lr->rewindActive = false;
1013 return true;
1014}
1015
1023static void SDL_Libretro_RewindEvictToBudget(SDL_Libretro* lr) {
1024 if (!lr->rewindEnabled || lr->rewindMaxBytes == 0 || !lr->rewindEntries) return;
1025
1026 while (lr->rewindBytes > lr->rewindMaxBytes && lr->rewindCount > 1) {
1027 unsigned tail = (lr->rewindHead + lr->rewindCapacity - lr->rewindCount) % lr->rewindCapacity;
1028 SDL_Libretro_RewindFreeEntry(lr, &lr->rewindEntries[tail]);
1029 lr->rewindCount--;
1030 }
1031}
1032
1042static void SDL_Libretro_ClearRewind(SDL_Libretro* lr) {
1043 if (!lr) return;
1044 if (lr->rewindEntries) {
1045 for (unsigned i = 0; i < lr->rewindCapacity; i++) {
1046 SDL_free(lr->rewindEntries[i].data);
1047 lr->rewindEntries[i].data = NULL;
1048 lr->rewindEntries[i].length = 0;
1049 lr->rewindEntries[i].capacity = 0;
1050 }
1051 }
1052 lr->rewindBytes = 0;
1053 lr->rewindHead = 0;
1054 lr->rewindCount = 0;
1055 lr->rewindFrameCounter = 0;
1056 lr->rewindHasReference = false;
1057}
1058
1064static void SDL_Libretro_RewindFree(SDL_Libretro* lr) {
1065 // Clear the rewind data.
1066 SDL_Libretro_ClearRewind(lr);
1067
1068 // Free all of the allocated memory.
1069 SDL_free(lr->rewindEntries);
1070 lr->rewindEntries = NULL;
1071 SDL_free(lr->rewindReference);
1072 lr->rewindReference = NULL;
1073 SDL_free(lr->rewindScratch);
1074 lr->rewindScratch = NULL;
1075 SDL_free(lr->rewindEncodeScratch);
1076 lr->rewindEncodeScratch = NULL;
1077 lr->rewindSlotSize = 0;
1078 lr->rewindActive = false;
1079}
1080
1081#endif /* SDL_LIBRETRO_SERIALIZE_IMPL_ONCE */
bool SDL_Libretro_SaveState_IO(SDL_Libretro *lr, SDL_IOStream *dst, bool closeio)
Save the current libretro state to the given SDL_IOStream.
void SDL_Libretro_SetRewindMemoryLimit(SDL_Libretro *lr, size_t maxBytes)
Set the maximum number of bytes of encoded delta history to retain.
bool SDL_Libretro_SaveSRAM(SDL_Libretro *lr, const char *file)
Saves the current SRAM to the given file.
bool SDL_Libretro_GetMemoryMapDescriptor(const SDL_Libretro *lr, unsigned index, Uint64 *flags, void **ptr, size_t *offset, size_t *start, size_t *select, size_t *disconnect, size_t *len, const char **addrspace)
Retrieve one memory-map descriptor by index.
bool SDL_Libretro_LoadSRAM(SDL_Libretro *lr, const char *file)
Loads the current SRAM to the given file.
void * SDL_Libretro_GetMemoryData(const SDL_Libretro *lr, unsigned memoryType, size_t *size)
Get a pointer to a core memory region and its size.
bool SDL_Libretro_SetRewindEnabled(SDL_Libretro *lr, bool enabled, unsigned bufferFrames, unsigned captureInterval)
Enable or disable the rewind system.
bool SDL_Libretro_SetRewindMemoryDuration(SDL_Libretro *lr, double seconds)
Set the rewind memory budget by target duration instead of raw bytes.
size_t SDL_Libretro_GetRewindMemoryUsage(const SDL_Libretro *lr)
Get the approximate memory currently held by the rewind buffer, in bytes.
unsigned SDL_Libretro_GetMemoryMapCount(const SDL_Libretro *lr)
Get the number of memory-map descriptors the core has published.
bool SDL_Libretro_LoadState(SDL_Libretro *lr, const char *file)
Loads the libretro state from the given file.
size_t SDL_Libretro_GetStateSize(const SDL_Libretro *lr)
Retrieves the size of serialized states.
bool SDL_Libretro_SaveMemory(SDL_Libretro *lr, unsigned memoryType, const char *file)
Write a core memory region to a file.
bool SDL_Libretro_LoadMemory_IO(SDL_Libretro *lr, unsigned memoryType, SDL_IOStream *src, bool closeio)
Load a core memory region from a stream.
bool SDL_Libretro_LoadMemory(SDL_Libretro *lr, unsigned memoryType, const char *file)
Load a core memory region from a file.
bool SDL_Libretro_GetRewindEnabled(const SDL_Libretro *lr)
Check whether the rewind system is enabled (independent of current direction).
bool SDL_Libretro_SaveState(SDL_Libretro *lr, const char *file)
Saves the current libretro state to a file.
size_t SDL_Libretro_GetRewindMemoryLimit(const SDL_Libretro *lr)
Get the current rewind memory budget in bytes (0 if unbounded).
bool SDL_Libretro_SetMemoryData(SDL_Libretro *lr, unsigned memoryType, const void *data, size_t size)
Overwrite a core memory region with caller-provided bytes.
double SDL_Libretro_GetRewindRemaining(const SDL_Libretro *lr)
Calculates the amount of rewind time remaining in the buffer.
void * SDL_Libretro_GetMapAddress(const SDL_Libretro *lr, size_t address, size_t *regionRemaining)
Translate an emulated (guest) address to a live host pointer via the memory map.
bool SDL_Libretro_SaveMemory_IO(SDL_Libretro *lr, unsigned memoryType, SDL_IOStream *dst, bool closeio)
Write a core memory region to a stream.