SDL_Libretro
SDL3-power libretro frontend.
Loading...
Searching...
No Matches
SDL_libretro_core.h
Go to the documentation of this file.
1
7#if defined(SDL_LIBRETRO_IMPLEMENTATION) && !defined(SDL_LIBRETRO_CORE_IMPL_ONCE)
8#define SDL_LIBRETRO_CORE_IMPL_ONCE
9
10#define LOAD_SYM(sym) do { \
11 SDL_FunctionPointer fp = SDL_LoadFunction(lr->core.symbols.handle, #sym); \
12 SDL_memcpy(&lr->core.symbols.sym, &fp, sizeof(fp)); \
13 if (!fp) { \
14 SDL_SetError("[SDL_Libretro] Failed to load symbol '%s'", #sym); \
15 return false; \
16 } \
17} while (0)
18
24SDL_Libretro* SDL_Libretro_Create(void) {
25 SDL_Libretro* lr = (SDL_Libretro*)SDL_calloc(1, sizeof(SDL_Libretro));
26 if (!lr) {
27 SDL_SetError("[SDL_Libretro] Failed to allocate context");
28 return NULL;
29 }
30
31 // Initial state (the calloc above already zeroed the struct).
32 lr->rewindMaxBytes = SDL_LIBRETRO_REWIND_DEFAULT_MAX_BYTES;
33 SDL_Libretro_SetVFS(lr, NULL);
34 SDL_Libretro_SetVolume(lr, 1.0f);
35 SDL_Libretro_SetSpeed(lr, 1.0f);
36 SDL_Libretro_SetUsername(lr, "SDL_Libretro");
37
38 // Keyboard Mappings
39 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_B] = SDL_SCANCODE_Z;
40 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_Y] = SDL_SCANCODE_A;
41 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_SELECT] = SDL_SCANCODE_RSHIFT;
42 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_START] = SDL_SCANCODE_RETURN;
43 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_UP] = SDL_SCANCODE_UP;
44 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_DOWN] = SDL_SCANCODE_DOWN;
45 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_LEFT] = SDL_SCANCODE_LEFT;
46 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_RIGHT] = SDL_SCANCODE_RIGHT;
47 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_A] = SDL_SCANCODE_X;
48 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_X] = SDL_SCANCODE_S;
49 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_L] = SDL_SCANCODE_Q;
50 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_R] = SDL_SCANCODE_W;
51 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_L2] = SDL_SCANCODE_E;
52 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_R2] = SDL_SCANCODE_R;
53 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_L3] = SDL_SCANCODE_D;
54 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_R3] = SDL_SCANCODE_F;
55
56 return lr;
57}
58
64void SDL_Libretro_Destroy(SDL_Libretro* lr) {
65 if (!lr) return;
66
69
70 for (unsigned i = 0; i < 16; i++) {
71 if (lr->gamepads[i]) {
72 SDL_CloseGamepad(lr->gamepads[i]);
73 lr->gamepads[i] = NULL;
74 }
75 }
76
77
78 SDL_Libretro_FreeCoreLibrary(lr);
79 SDL_Libretro_FreeMessages(lr);
80 SDL_Libretro_CloseConfig(lr);
81
82 SDL_free(lr);
83}
84
92static const char* SDL_Libretro_GetCorePathFromName(const SDL_Libretro* lr, const char* coreName) {
93 if (!lr || !coreName || !lr->coreLibrary || SDL_strrchr(coreName, '.') != NULL) {
94 return NULL;
95 }
96 for (unsigned i = 0; i < lr->coreLibraryCount; i++) {
97 if (lr->coreLibrary[i].corename && SDL_strcasecmp(lr->coreLibrary[i].corename, coreName) == 0) {
98 return lr->coreLibrary[i].path;
99 }
100 }
101 return NULL;
102}
103
113bool SDL_Libretro_LoadCore(SDL_Libretro* lr, const char* corePath) {
114 if (!lr || !corePath) {
115 SDL_SetError("[SDL_Libretro] Invalid arguments");
116 return false;
117 }
118
119 if (SDL_Libretro_active && SDL_Libretro_active != lr) {
120 SDL_SetError("[SDL_Libretro] Another context already has a core loaded");
121 return false;
122 }
123
124 // Make sure the old core is unloaded.
126
127 // If the corePath is just a name, see if it lives in the loaded coreLibrary.
128 const char* path = SDL_Libretro_GetCorePathFromName(lr, corePath);
129 if (path) {
130 corePath = path;
131 }
132
133 // Load the core handle.
134 lr->core.symbols.handle = SDL_LoadObject(corePath);
135 if (!lr->core.symbols.handle) {
136 SDL_SetError("[SDL_Libretro] Failed to load core '%s': %s", corePath, SDL_GetError());
137 return false;
138 }
139
140 // Verify core API version.
141 LOAD_SYM(retro_api_version);
142 lr->core.apiVersion = lr->core.symbols.retro_api_version();
143 if (lr->core.apiVersion != 1) {
144 SDL_UnloadObject(lr->core.symbols.handle);
145 SDL_SetError("[SDL_Libretro] Unsupported Core API Version: %d", (int)lr->core.apiVersion);
146 SDL_memset(&lr->core, 0, sizeof(lr->core));
147 return false;
148 }
149
150 LOAD_SYM(retro_init);
151 LOAD_SYM(retro_deinit);
152 LOAD_SYM(retro_set_environment);
153 LOAD_SYM(retro_set_video_refresh);
154 LOAD_SYM(retro_set_audio_sample);
155 LOAD_SYM(retro_set_audio_sample_batch);
156 LOAD_SYM(retro_set_input_poll);
157 LOAD_SYM(retro_set_input_state);
158 LOAD_SYM(retro_get_system_info);
159 LOAD_SYM(retro_get_system_av_info);
160 LOAD_SYM(retro_set_controller_port_device);
161 LOAD_SYM(retro_reset);
162 LOAD_SYM(retro_run);
163 LOAD_SYM(retro_serialize_size);
164 LOAD_SYM(retro_serialize);
165 LOAD_SYM(retro_unserialize);
166 LOAD_SYM(retro_cheat_reset);
167 LOAD_SYM(retro_cheat_set);
168 LOAD_SYM(retro_load_game);
169 LOAD_SYM(retro_load_game_special);
170 LOAD_SYM(retro_unload_game);
171 LOAD_SYM(retro_get_region);
172 LOAD_SYM(retro_get_memory_data);
173 LOAD_SYM(retro_get_memory_size);
174
175 SDL_strlcpy(lr->core.corePath, corePath, sizeof(lr->core.corePath));
176
177 SDL_Libretro_active = lr;
178
179 // Tell the core that we should call our own enviornment callback.
180 lr->core.symbols.retro_set_environment(SDL_Libretro_EnvironmentCallback);
181
182 // Grab the initial system info from the core.
183 struct retro_system_info sysinfo = {0};
184 lr->core.symbols.retro_get_system_info(&sysinfo);
185 SDL_strlcpy(lr->core.libraryName, sysinfo.library_name ? sysinfo.library_name : "", sizeof(lr->core.libraryName));
186 SDL_strlcpy(lr->core.libraryVersion, sysinfo.library_version ? sysinfo.library_version : "", sizeof(lr->core.libraryVersion));
187 SDL_strlcpy(lr->core.validExtensions, sysinfo.valid_extensions ? sysinfo.valid_extensions : "", sizeof(lr->core.validExtensions));
188 lr->core.needFullpath = sysinfo.need_fullpath;
189
190 // Default the content name to the core's reported name.
191 SDL_strlcpy(lr->core.contentName, lr->core.libraryName, sizeof(lr->core.contentName));
192
193 // Config
194 SDL_Libretro_LoadCoreConfig(lr);
195
196 // Initialize the core
197 lr->core.symbols.retro_init();
198 lr->core.loaded = true;
199
200 SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[SDL_Libretro] Core loaded: %s %s", lr->core.libraryName, lr->core.libraryVersion);
201
202 return true;
203}
204
212void SDL_Libretro_UnloadCore(SDL_Libretro* lr) {
213 if (!lr || !lr->core.loaded) return;
214
216 SDL_Libretro_SaveCoreConfig(lr);
217
218 lr->core.symbols.retro_deinit();
219 if (lr->core.symbols.handle) {
220 SDL_UnloadObject(lr->core.symbols.handle);
221 }
222
223 SDL_Libretro_CloseSensors(lr);
224 SDL_Libretro_CloseMicrophone(lr);
225 SDL_Libretro_FreeCoreOptions(lr);
226 if (lr->core.inputDescriptors) {
227 SDL_free(lr->core.inputDescriptors);
228 }
229 if (lr->core.controllerInfo) {
230 SDL_free(lr->core.controllerInfo);
231 }
232 SDL_Libretro_FreeMemoryMap(lr);
233 SDL_Libretro_FreeContentInfoOverrides(lr);
234
235 SDL_memset(&lr->core, 0, sizeof(lr->core));
236 if (SDL_Libretro_active == lr) {
237 SDL_Libretro_active = NULL;
238 }
239
240 SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "[SDL_Libretro] Core unloaded");
241}
242
243bool SDL_Libretro_IsCoreReady(const SDL_Libretro* lr) {
244 return lr && lr->core.loaded;
245}
246
258size_t SDL_Libretro_GetFileName(char* dst, size_t dstSize, const char* path, bool withExtension) {
259 if (!dst || dstSize == 0) return 0;
260 dst[0] = '\0';
261 if (!path) return 0;
262
263 // Skip past the last path separator to the base name.
264 const char* base = SDL_strrchr(path, '/');
265 if (!base) base = SDL_strrchr(path, '\\');
266 base = base ? base + 1 : path;
267
268 SDL_strlcpy(dst, base, dstSize);
269
270 if (!withExtension) {
271 char* dot = SDL_strrchr(dst, '.');
272 if (dot) *dot = '\0';
273 }
274
275 return SDL_strlen(dst);
276}
277
289size_t SDL_Libretro_GetSavePath(const SDL_Libretro* lr, const char* extension, char* dst, size_t dstSize) {
290 if (!dst || dstSize == 0) return 0;
291 dst[0] = '\0';
292 if (!lr || lr->core.contentName[0] == '\0') return 0;
293
294 if (!extension) {
295 extension = "";
296 }
297
298 if (lr->saveDirectory[0] != '\0') {
299 SDL_snprintf(dst, dstSize, "%s/%s%s", lr->saveDirectory, lr->core.contentName, extension);
300 } else {
301 SDL_snprintf(dst, dstSize, "%s%s", lr->core.contentName, extension);
302 }
303
304 return SDL_strlen(dst);
305}
306
312static void SDL_Libretro_FreeContentInfoOverrides(SDL_Libretro* lr) {
313 if (lr->core.contentInfoOverrides) {
314 for (unsigned i = 0; i < lr->core.contentInfoOverrideCount; i++) {
315 SDL_free((void*)lr->core.contentInfoOverrides[i].extensions);
316 }
317 SDL_free(lr->core.contentInfoOverrides);
318 lr->core.contentInfoOverrides = NULL;
319 }
320 lr->core.contentInfoOverrideCount = 0;
321}
322
331static int SDL_Libretro_GetContentInfoOverride(const SDL_Libretro* lr, const char* ext) {
332 if (!lr || !ext || ext[0] == '\0') return -1;
333 for (unsigned i = 0; i < lr->core.contentInfoOverrideCount; i++) {
334 if (SDL_Libretro_ExtensionInList(ext, lr->core.contentInfoOverrides[i].extensions)) {
335 return (int)i;
336 }
337 }
338 return -1;
339}
340
346static bool SDL_Libretro_ContentNeedsFullpath(const SDL_Libretro* lr, const char* ext) {
347 int i = SDL_Libretro_GetContentInfoOverride(lr, ext);
348 return i >= 0 ? lr->core.contentInfoOverrides[i].need_fullpath : lr->core.needFullpath;
349}
350
356static bool SDL_Libretro_ContentPersistData(const SDL_Libretro* lr, const char* ext) {
357 int i = SDL_Libretro_GetContentInfoOverride(lr, ext);
358 return i >= 0 ? lr->core.contentInfoOverrides[i].persistent_data : false;
359}
360
368static void SDL_Libretro_ResetContentState(SDL_Libretro* lr) {
369 lr->core.contentPath[0] = '\0';
370 lr->core.contentDir[0] = '\0';
371 lr->core.contentExt[0] = '\0';
372 SDL_strlcpy(lr->core.contentName, lr->core.libraryName, sizeof(lr->core.contentName));
373
374 if (lr->core.gameInfoExt.persistent_data) {
375 SDL_free((void*)lr->core.gameInfoExt.data);
376 }
377 SDL_memset(&lr->core.gameInfoExt, 0, sizeof(lr->core.gameInfoExt));
378}
379
385static bool SDL_Libretro_LoadCoreForGame(SDL_Libretro* lr, const char* gamePath) {
386 if (!lr || !gamePath) return false;
387
388 // The game's file extension.
389 const char* dot = SDL_strrchr(gamePath, '.');
390 const char* extension = dot ? dot + 1 : "";
391 if (extension[0] == '\0') {
392 SDL_SetError("[SDL_Libretro] Game path '%s' has no file extension", gamePath);
393 return false;
394 }
395
396 // Try to load cores that match the extension.
397 for (unsigned i = 0; i < lr->coreLibraryCount; i++) {
398 // Check if the extension is in the lr->coreLibrary[i] list.
399 if (!SDL_Libretro_ExtensionInList(extension, lr->coreLibrary[i].supported_extensions)) {
400 continue;
401 }
402
403 // A candidate core claims this extension, try loading it.
404 if (SDL_Libretro_LoadCore(lr, lr->coreLibrary[i].path)) {
405 return true;
406 }
407 }
408
409 SDL_SetError("[SDL_Libretro] No core found for extension '%s'", extension);
410 return false;
411}
412
424bool SDL_Libretro_LoadGame(SDL_Libretro* lr, const char* gamePath) {
425 if (!lr) return false;
426
427 // Switching content: unload any game that's already running first.
429
430 // Try to find what can be loaded with it.
431 if (!SDL_Libretro_IsCoreReady(lr)) {
432 if (!SDL_Libretro_LoadCoreForGame(lr, gamePath)) {
433 SDL_SetError("[SDL_Libretro] Core not loaded");
434 return false;
435 }
436 }
437
438 // A core that didn't opt into no-content (SET_SUPPORT_NO_GAME) can't run without a game.
439 if (!gamePath && !lr->core.supportNoGame) {
440 SDL_SetError("[SDL_Libretro] This core requires content");
441 return false;
442 }
443
444 struct retro_game_info gameInfo = {0};
445 void* fileData = NULL;
446 bool persistData = false;
447
448 // Cleared here so a no-content load leaves GET_GAME_INFO_EXT invalid.
449 SDL_memset(&lr->core.gameInfoExt, 0, sizeof(lr->core.gameInfoExt));
450
451 if (gamePath) {
452 SDL_strlcpy(lr->core.contentPath, gamePath, sizeof(lr->core.contentPath));
453
454 // Content base name (no extension) and lower-case extension.
455 SDL_Libretro_GetFileName(lr->core.contentName, sizeof(lr->core.contentName), gamePath, false);
456 const char* ext = SDL_Libretro_GetContentExtension(lr);
457 SDL_strlcpy(lr->core.contentExt, ext, sizeof(lr->core.contentExt));
458 for (char* c = lr->core.contentExt; *c; c++)
459 *c = (char)SDL_tolower((unsigned char)*c);
460
461 // Directory containing the content file.
462 SDL_strlcpy(lr->core.contentDir, gamePath, sizeof(lr->core.contentDir));
463 char* sep = SDL_strrchr(lr->core.contentDir, '/');
464 if (!sep) sep = SDL_strrchr(lr->core.contentDir, '\\');
465 if (sep)
466 *sep = '\0';
467 else
468 lr->core.contentDir[0] = '\0';
469
470 bool needFullpath = SDL_Libretro_ContentNeedsFullpath(lr, ext);
471 persistData = SDL_Libretro_ContentPersistData(lr, ext);
472
473 gameInfo.path = lr->core.contentPath;
474
475 if (!needFullpath) {
476 size_t fileSize = 0;
477 fileData = SDL_LoadFile(gamePath, &fileSize);
478 if (!fileData) {
479 SDL_SetError("[SDL_Libretro] Failed to load game file '%s'", gamePath);
480 SDL_Libretro_ResetContentState(lr);
481 return false;
482 }
483 gameInfo.data = fileData;
484 gameInfo.size = fileSize;
485 }
486
487 // Update the game info.
488 lr->core.gameInfoExt.full_path = lr->core.contentPath;
489 lr->core.gameInfoExt.dir = lr->core.contentDir;
490 lr->core.gameInfoExt.name = lr->core.contentName;
491 lr->core.gameInfoExt.ext = lr->core.contentExt;
492 lr->core.gameInfoExt.data = gameInfo.data;
493 lr->core.gameInfoExt.size = gameInfo.size;
494 lr->core.gameInfoExt.persistent_data = persistData;
495 }
496
497 // Set the callbacks.
498 lr->core.symbols.retro_set_video_refresh(SDL_Libretro_VideoRefresh);
499 lr->core.symbols.retro_set_audio_sample(SDL_Libretro_AudioSample);
500 lr->core.symbols.retro_set_audio_sample_batch(SDL_Libretro_AudioSampleBatch);
501 lr->core.symbols.retro_set_input_poll(SDL_Libretro_InputPoll);
502 lr->core.symbols.retro_set_input_state(SDL_Libretro_InputState);
503
504 bool result = lr->core.symbols.retro_load_game(gamePath ? &gameInfo : NULL);
505
506 // The gameInfoExt.data owns the content buffer. Persistent content keeps it until unload, otherwise the data is only valid for the duration of the load, so free it now and clear the (now dangling) pointer.
507 if (fileData && !(persistData && result)) {
508 SDL_free(fileData);
509 lr->core.gameInfoExt.data = NULL;
510 lr->core.gameInfoExt.size = 0;
511 }
512
513 if (!result) {
514 SDL_Libretro_ResetContentState(lr);
515 SDL_SetError("[SDL_Libretro] Core failed to load the game");
516 return false;
517 }
518
519 lr->core.gameLoaded = true;
520
521 // Grab the Audio/Video data.
522 struct retro_system_av_info avInfo = {0};
523 lr->core.symbols.retro_get_system_av_info(&avInfo);
524 lr->core.width = avInfo.geometry.base_width;
525 lr->core.height = avInfo.geometry.base_height;
526 lr->core.fps = avInfo.timing.fps;
527 lr->core.sampleRate = avInfo.timing.sample_rate;
528 lr->core.aspectRatio = avInfo.geometry.aspect_ratio;
529
530 // Failed video initialization should not fail loading the game. It will
531 // hopefully initialize itself on the first render.
532 if (lr->renderer) {
533 if (!SDL_Libretro_InitVideo(lr)) {
534 SDL_LogWarn(SDL_LOG_CATEGORY_AUDIO, "[SDL_Libretro] Video failed to initialize: %s", SDL_GetError());
535 }
536 }
537
538 // A missing device shouldn't stop the game from running. Apps can re-init later with SDL_Libretro_InitAudio() or RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO
539 if (!SDL_Libretro_InitAudio(lr)) {
540 SDL_LogWarn(SDL_LOG_CATEGORY_AUDIO, "[SDL_Libretro] Audio failed to initialize: %s", SDL_GetError());
541 }
542
543 SDL_Log("[SDL_Libretro] Game loaded: %s [%ux%u @ %.2ffps]", lr->core.contentName,
544 lr->core.width, lr->core.height, lr->core.fps);
545
546 // Allocate rewind buffer now that serialize size is known.
547 if (lr->rewindEnabled && !lr->rewindReference && lr->rewindCapacity > 0) {
548 SDL_Libretro_SetRewindEnabled(lr, true, lr->rewindCapacity, lr->rewindCaptureInterval);
549 }
550
551 return true;
552}
553
559void SDL_Libretro_UnloadGame(SDL_Libretro* lr) {
560 if (!lr || !lr->core.gameLoaded) return;
561
562 // Unload the game before closing audio and video.
563 lr->core.symbols.retro_unload_game();
564 lr->core.gameLoaded = false;
565 SDL_Libretro_ResetContentState(lr);
566
567 SDL_Libretro_RewindFree(lr);
568 SDL_Libretro_CloseAudio(lr);
569 SDL_Libretro_CloseVideo(lr);
570 SDL_Log("[SDL_Libretro] Game unloaded");
571}
572
573bool SDL_Libretro_IsGameReady(const SDL_Libretro* lr) {
574 return lr && lr->core.gameLoaded;
575}
576
577bool SDL_Libretro_IsGameRequired(const SDL_Libretro* lr) {
578 return lr && !lr->core.supportNoGame;
579}
580
581bool SDL_Libretro_Reset(SDL_Libretro* lr) {
582 if (!lr || !lr->core.gameLoaded) {
583 SDL_SetError("[SDL_Libretro] No game loaded");
584 return false;
585 }
586 lr->core.symbols.retro_reset();
587
588 // Clear any rewind states.
589 SDL_Libretro_ClearRewind(lr);
590
591 // Drop the audio queued from before the reset so it doesn't continue into the reset.
592 if (lr->core.audioStream) {
593 SDL_ClearAudioStream(lr->core.audioStream);
594 lr->core.singleSampleCount = 0;
595 lr->core.drcDriftAvg = 0.0;
596 }
597 return true;
598}
599
603static void SDL_Libretro_Tick(SDL_Libretro* lr, retro_usec_t referenceUsec) {
604 if (lr->core.runloop_frame_time.callback) {
605 retro_usec_t delta = referenceUsec;
606 // First tick (or right after a reset) has no measured cadence yet, so fall back to the reference the core declared.
607 if (!lr->core.runloop_frame_time_last) {
608 delta = lr->core.runloop_frame_time.reference;
609 }
610 lr->core.runloop_frame_time_last = referenceUsec;
611 lr->core.runloop_frame_time.callback(delta);
612 }
613
614 // Report audio buffer occupancy so the core can frame-skip if an underrun looms. Per the libretro spec this fires right before retro_run().
615 SDL_Libretro_ReportAudioBufferStatus(lr);
616
617 // Run the frame.
618 lr->core.symbols.retro_run();
619
620 // Capture rewind state after each forward tick.
621 SDL_Libretro_RewindCapture(lr);
622
623 // Only pump the core's async-audio callback when audio is actually up.
624 if (lr->core.audioStream && lr->core.audio_callback.callback) {
625 lr->core.audio_callback.callback();
626 }
627}
628
629void SDL_Libretro_Update(SDL_Libretro* lr) {
630 if (!lr || !lr->core.gameLoaded) return;
631
632 // Pending Video Driver Reinit
633 if (lr->core.videoReinitPending) {
634 lr->core.videoReinitPending = false;
635 SDL_Libretro_InitVideo(lr);
636 }
637
638 // Pending Audio Driver Reinit
639 if (lr->core.audioReinitPending) {
640 lr->core.audioReinitPending = false;
641 SDL_Libretro_InitAudio(lr);
642 }
643
644 // Paused: Do nothing when speed is zero.
645 if (lr->speed == 0.0f) return;
646
647 // Rewind mode: step backwards when speed is negative.
648 if (lr->rewindEnabled && lr->speed < 0.0f) {
649 Uint64 nowNS = SDL_GetTicksNS();
650 if (lr->lastTickNS == 0) {
651 lr->lastTickNS = nowNS;
652 }
653 double frameTime = (double)(nowNS - lr->lastTickNS) / 1.0e9;
654 lr->lastTickNS = nowNS;
655 double framePeriod = (lr->core.fps > 0.0) ? (1.0 / lr->core.fps) : (1.0 / 60.0);
656 // Each stored snapshot spans captureInterval real frames (a snapshot is taken every Nth frame), so a single rewind step undoes that many frames of game time. Scale the per-step wall-clock cost by the interval; otherwise speed -1 would rewind captureInterval times faster than speed +1 plays forward.
657 unsigned interval = lr->rewindCaptureInterval > 0 ? lr->rewindCaptureInterval : 1;
658 double stepPeriod = framePeriod * (double)interval;
659 lr->speedAccumulator += frameTime * (double)(-lr->speed);
660 // Mute audio and neutralize input for the throwaway re-runs that produce the displayed frames while scrubbing backward.
661 lr->rewindActive = true;
662 while (lr->speedAccumulator >= stepPeriod) {
663 lr->speedAccumulator -= stepPeriod;
664 if (!SDL_Libretro_RewindStepState(lr)) break;
665 lr->core.symbols.retro_run();
666 }
667 lr->rewindActive = false;
668 return;
669 }
670
671 // Keep audio consumption locked to speed + nudge the queue toward its target fill. Done here, above all three return paths below, so it runs every frame.
672 SDL_Libretro_UpdateDRC(lr, lr->speed);
673
674 // Wall-clock delta since the previous RunFrame.
675 Uint64 nowNS = SDL_GetTicksNS();
676 if (lr->lastTickNS == 0) {
677 // First call: seed the clock and run exactly one tick.
678 lr->lastTickNS = nowNS;
679 lr->speedAccumulator = 0.0;
680 SDL_Libretro_Tick(lr, 0);
681 return;
682 }
683
684 // Calculate the frame time in seconds.
685 double frameTime = (double)(nowNS - lr->lastTickNS) / 1.0e9;
686 lr->lastTickNS = nowNS;
687
688 // Target frame period from the core's declared fps (default 60).
689 double framePeriod = (lr->core.fps > 0.0) ? (1.0 / lr->core.fps) : (1.0 / 60.0);
690
691 // Reference frame-time the core is told about, in microseconds.
692 retro_usec_t referenceUsec = (retro_usec_t)(framePeriod * 1.0e6 / (double)lr->speed);
693
694 // At normal speed, when the loop is already paced close to the core's frame rate (e.g. a vsync'd 60 Hz display with a ~60 fps core), run exactly one tick and discard the accumulator. This avoids the beat-frequency judder of occasionally emitting 0 or 2 ticks. Gating on the *measured* cadence keeps the core bounded when vsync is off / FPS uncapped.
695 double cadence = (framePeriod > 0.0) ? (frameTime / framePeriod) : 0.0;
696 if (lr->speed == 1.0f && cadence > 0.9 && cadence < 1.1) {
697 lr->speedAccumulator = 0.0;
698 SDL_Libretro_Tick(lr, referenceUsec);
699 return;
700 }
701
702 lr->speedAccumulator += frameTime * (double)lr->speed;
703
704 // Cap iterations to avoid a spiral of death on slow hardware.
705 int maxTicks = (int)(lr->speed + 1.0f);
706 if (maxTicks < 1) maxTicks = 1;
707
708 // Clamp the accumulator so a frame-time spike (game load, window drag, menu pause) can't leave a backlog that runs the core fast afterwards.
709 double maxAccumulator = framePeriod * (double)maxTicks;
710 if (lr->speedAccumulator > maxAccumulator) {
711 lr->speedAccumulator = maxAccumulator;
712 }
713
714 // Run the required number of ticks to catch up to what's needed.
715 while (lr->speedAccumulator >= framePeriod && maxTicks-- > 0) {
716 lr->speedAccumulator -= framePeriod;
717 SDL_Libretro_Tick(lr, referenceUsec);
718 }
719}
720
726bool SDL_Libretro_ShouldQuit(const SDL_Libretro* lr) {
727 return lr && lr->core.shutdown;
728}
729
730int SDL_Libretro_Version(void) {
732}
733
734// Directory
735
736static void SDL_Libretro_FreeCoreLibrary(SDL_Libretro* lr) {
737 if (lr->coreLibrary) {
738 for (unsigned i = 0; i < lr->coreLibraryCount; i++) {
739 SDL_free(lr->coreLibrary[i].corename);
740 SDL_free(lr->coreLibrary[i].supported_extensions);
741 SDL_free(lr->coreLibrary[i].path);
742 }
743 SDL_free(lr->coreLibrary);
744 lr->coreLibrary = NULL;
745 }
746 lr->coreLibraryCount = 0;
747}
748
749// The file extension for the cores on this platform
750#if defined(SDL_PLATFORM_WINDOWS)
751#define SDL_LIBRETRO_CORE_EXTENSION ".dll"
752#elif defined(SDL_PLATFORM_APPLE)
753#define SDL_LIBRETRO_CORE_EXTENSION ".dylib"
754#elif defined(SDL_PLATFORM_EMSCRIPTEN)
755#define SDL_LIBRETRO_CORE_EXTENSION ".wasm"
756#else
757#define SDL_LIBRETRO_CORE_EXTENSION ".so"
758#endif
759
760static SDL_EnumerationResult SDLCALL SDL_Libretro_SetCoreDirectory_Iterator(void *userdata, const char *dirname, const char *fname) {
761 SDL_Libretro* lr = (SDL_Libretro*)userdata;
762
763 // Find all .info files in the core directory.
764 const char* dot = SDL_strrchr(fname, '.');
765 if (!dot || SDL_strcasecmp(dot, ".info") != 0) {
766 return SDL_ENUM_CONTINUE;
767 }
768
769 // Get the path.
770 char infoPath[SDL_LIBRETRO_MAX_PATH];
771 SDL_snprintf(infoPath, sizeof(infoPath), "%s%s", dirname, fname);
772
773 // Load the .ini file.
774 SDL_ini* ini = INI_Load(infoPath);
775 if (!ini) {
776 return SDL_ENUM_CONTINUE;
777 }
778
779 // Make sure it's a valid info file.
780 const char* corename = INI_GetString(ini, NULL, "corename", NULL);
781 if (!corename) {
782 INI_Destroy(ini);
783 return SDL_ENUM_CONTINUE;
784 }
785
786 // Grow the core library by one entry.
787 SDL_Libretro_CoreInfo* grown = (SDL_Libretro_CoreInfo*)SDL_realloc(
788 lr->coreLibrary, (lr->coreLibraryCount + 1) * sizeof(SDL_Libretro_CoreInfo));
789 if (!grown) {
790 INI_Destroy(ini);
791 return SDL_ENUM_CONTINUE;
792 }
793 lr->coreLibrary = grown;
794
795 // Build the SDL_Libretro_CoreInfo structure.
796 SDL_Libretro_CoreInfo* entry = &lr->coreLibrary[lr->coreLibraryCount];
797 entry->corename = SDL_strdup(corename);
798
799 const char* extensions = INI_GetString(ini, NULL, "supported_extensions", NULL);
800 entry->supported_extensions = extensions ? SDL_strdup(extensions) : NULL;
801 entry->needs_fullpath = INI_GetBoolean(ini, NULL, "needs_fullpath", false);
802 entry->supports_no_game = INI_GetBoolean(ini, NULL, "supports_no_game", false);
803
804 // Build the path for the core.
805 char base[SDL_LIBRETRO_MAX_PATH];
806 size_t baseLen = (size_t)(dot - fname);
807 SDL_strlcpy(base, fname, (baseLen < sizeof(base)) ? baseLen + 1 : sizeof(base));
808 char corePath[SDL_LIBRETRO_MAX_PATH];
809 SDL_snprintf(corePath, sizeof(corePath), "%s%s%s", dirname, base, SDL_LIBRETRO_CORE_EXTENSION);
810 entry->path = SDL_strdup(corePath);
811
812 lr->coreLibraryCount++;
813
814 INI_Destroy(ini);
815 return SDL_ENUM_CONTINUE;
816}
817
821bool SDL_Libretro_SetCoreDirectory(SDL_Libretro* lr, const char* path) {
822 if (!lr) return false;
823 const char* newPath = path ? path : "";
824 SDL_strlcpy(lr->coreDirectory, newPath, sizeof(lr->coreDirectory));
825
826 // When the core directory changed, rebuild the core library.
827 SDL_Libretro_FreeCoreLibrary(lr);
828 SDL_EnumerateDirectory(lr->coreDirectory, &SDL_Libretro_SetCoreDirectory_Iterator, (void*)lr);
829 return true;
830}
831
832bool SDL_Libretro_SetSaveDirectory(SDL_Libretro* lr, const char* path) {
833 if (!lr) return false;
834 SDL_strlcpy(lr->saveDirectory, path ? path : "", sizeof(lr->saveDirectory));
835 return true;
836}
837
838bool SDL_Libretro_SetSystemDirectory(SDL_Libretro* lr, const char* path) {
839 if (!lr) return false;
840 SDL_strlcpy(lr->systemDirectory, path ? path : "", sizeof(lr->systemDirectory));
841 return true;
842}
843
844bool SDL_Libretro_SetCoreAssetsDirectory(SDL_Libretro* lr, const char* path) {
845 if (!lr) return false;
846 SDL_strlcpy(lr->coreAssetsDirectory, path ? path : "", sizeof(lr->coreAssetsDirectory));
847 return true;
848}
849
850const char* SDL_Libretro_GetCoreDirectory(SDL_Libretro* lr) {
851 if (!lr) return NULL;
852 return lr->coreDirectory[0] ? lr->coreDirectory : NULL;
853}
854
855const char* SDL_Libretro_GetSaveDirectory(SDL_Libretro* lr) {
856 if (!lr) return NULL;
857 return lr->saveDirectory[0] ? lr->saveDirectory : NULL;
858}
859
860const char* SDL_Libretro_GetSystemDirectory(SDL_Libretro* lr) {
861 if (!lr) return NULL;
862 return lr->systemDirectory[0] ? lr->systemDirectory : NULL;
863}
864
865const char* SDL_Libretro_GetCoreAssetsDirectory(SDL_Libretro* lr) {
866 if (!lr) return NULL;
867 return lr->coreAssetsDirectory[0] ? lr->coreAssetsDirectory : NULL;
868}
869
870bool SDL_Libretro_SetUsername(SDL_Libretro* lr, const char* username) {
871 if (!lr) return false;
872 SDL_strlcpy(lr->username, username ? username : "", sizeof(lr->username));
873 return true;
874}
875
876const char* SDL_Libretro_GetUsername(SDL_Libretro* lr) {
877 if (!lr) return NULL;
878 return lr->username;
879}
880
884void SDL_Libretro_SetVolume(SDL_Libretro* lr, float volume) {
885 if (!lr) return;
886 lr->volume = SDL_clamp(volume, 0.0f, 1.0f);
887 if (lr->core.audioStream) {
888 SDL_SetAudioStreamGain(lr->core.audioStream, lr->volume);
889 }
890}
891
892float SDL_Libretro_GetVolume(const SDL_Libretro* lr) {
893 return lr ? lr->volume : 0.0f;
894}
895
896void SDL_Libretro_SetSpeed(SDL_Libretro* lr, float speed) {
897 if (!lr) return;
898
899 if (speed < 0.0f && lr->rewindEnabled) {
900 lr->speed = speed;
901 if (lr->core.audioStream) {
902 lr->core.drcAdjustment = 1.0f;
903 lr->core.drcDriftAvg = 0.0;
904 SDL_ClearAudioStream(lr->core.audioStream);
905 // Pitch and consume the reversed audio at the rewind speed, mirroring
906 // the forward path (which sets the ratio to speed * drcAdjustment).
907 SDL_SetAudioStreamFrequencyRatio(lr->core.audioStream, -speed);
908 }
909 } else {
910 lr->speed = SDL_max(speed, 0.0f);
911 }
912
913 if (lr->core.audioStream && lr->speed > 0.0f) {
914 lr->core.drcAdjustment = 1.0f;
915 lr->core.drcDriftAvg = 0.0;
916 SDL_SetAudioStreamFrequencyRatio(lr->core.audioStream, lr->speed * lr->core.drcAdjustment);
917 }
918}
919
920float SDL_Libretro_GetSpeed(const SDL_Libretro* lr) {
921 return lr ? lr->speed : 1.0f;
922}
923
929void SDL_Libretro_SetLogLevel(SDL_Libretro* lr, SDL_LogPriority level) {
930 if (!lr) return;
931 switch (level) {
932 case SDL_LOG_PRIORITY_INFO: lr->logLevel = RETRO_LOG_INFO; break;
933 case SDL_LOG_PRIORITY_WARN: lr->logLevel = RETRO_LOG_WARN; break;
934 case SDL_LOG_PRIORITY_ERROR: lr->logLevel = RETRO_LOG_ERROR; break;
935 case SDL_LOG_PRIORITY_CRITICAL: lr->logLevel = RETRO_LOG_ERROR; break;
936 default: lr->logLevel = RETRO_LOG_DEBUG; break;
937 }
938}
939
943SDL_LogPriority SDL_Libretro_GetLogLevel(const SDL_Libretro* lr) {
944 if (!lr) return SDL_LOG_PRIORITY_INVALID;
945 switch (lr->logLevel) {
946 case RETRO_LOG_DEBUG: return SDL_LOG_PRIORITY_DEBUG;
947 case RETRO_LOG_INFO: return SDL_LOG_PRIORITY_INFO;
948 case RETRO_LOG_WARN: return SDL_LOG_PRIORITY_WARN;
949 case RETRO_LOG_ERROR: return SDL_LOG_PRIORITY_ERROR;
950 default: return SDL_LOG_PRIORITY_INVALID;
951 }
952}
953
957const char* SDL_Libretro_GetCoreName(const SDL_Libretro* lr) {
958 return (lr && lr->core.loaded) ? lr->core.libraryName : "";
959}
960
964const char* SDL_Libretro_GetCoreVersion(const SDL_Libretro* lr) {
965 return (lr && lr->core.loaded) ? lr->core.libraryVersion : "";
966}
967
971const char* SDL_Libretro_GetValidExtensions(const SDL_Libretro* lr) {
972 return (lr && lr->core.loaded) ? lr->core.validExtensions : "";
973}
974
983unsigned SDL_Libretro_GetPerformanceLevel(const SDL_Libretro* lr) {
984 return lr ? lr->core.performanceLevel : 0;
985}
986
995const char* SDL_Libretro_GetContentExtension(const SDL_Libretro* lr) {
996 if (!lr || lr->core.contentPath[0] == '\0') return "";
997 const char* dot = SDL_strrchr(lr->core.contentPath, '.');
998 return dot ? dot + 1 : "";
999}
1000
1001static bool SDL_Libretro_ExtensionInList(const char* ext, const char* pipeList) {
1002 if (!ext || !pipeList) return false;
1003 size_t extLen = SDL_strlen(ext);
1004 const char* p = pipeList;
1005 while (*p) {
1006 const char* sep = SDL_strchr(p, '|');
1007 size_t segLen = sep ? (size_t)(sep - p) : SDL_strlen(p);
1008 if (segLen == extLen && SDL_strncasecmp(p, ext, extLen) == 0) return true;
1009 if (!sep) break;
1010 p = sep + 1;
1011 }
1012 return false;
1013}
1014
1015#undef LOAD_SYM
1016
1017#endif /* SDL_LIBRETRO_CORE_IMPL_ONCE */
#define SDL_LIBRETRO_REWIND_DEFAULT_MAX_BYTES
Default ceiling on the encoded rewind history (delta data only), in bytes.
size_t SDL_Libretro_GetSavePath(const SDL_Libretro *lr, const char *extension, char *dst, size_t dstSize)
Build a path in the save directory for the currently loaded content.
bool SDL_Libretro_SetCoreDirectory(SDL_Libretro *lr, const char *path)
Sets the associated libretro core directory, where the default set of cores will be loaded from.
const char * SDL_Libretro_GetContentExtension(const SDL_Libretro *lr)
Get the extension of the loaded content, as it appears in the path.
bool SDL_Libretro_LoadCore(SDL_Libretro *lr, const char *corePath)
Loads a libretro core.
void SDL_Libretro_UnloadCore(SDL_Libretro *lr)
Unloads the actively loaded core.
void SDL_Libretro_SetLogLevel(SDL_Libretro *lr, SDL_LogPriority level)
Sets the threshold for logs to be posted.
size_t SDL_Libretro_GetFileName(char *dst, size_t dstSize, const char *path, bool withExtension)
Copy the file name portion of a path into a caller-provided buffer.
bool SDL_Libretro_SetRewindEnabled(SDL_Libretro *lr, bool enabled, unsigned bufferFrames, unsigned captureInterval)
Enable or disable the rewind system.
const char * SDL_Libretro_GetCoreVersion(const SDL_Libretro *lr)
Retrieves the version of the libretro core that's actively loaded.
unsigned SDL_Libretro_GetPerformanceLevel(const SDL_Libretro *lr)
Get the performance level the core requested via SET_PERFORMANCE_LEVEL.
const char * SDL_Libretro_GetValidExtensions(const SDL_Libretro *lr)
Gets the default set of valid extensions associated with the core, seperated by a "|".
SDL_LogPriority SDL_Libretro_GetLogLevel(const SDL_Libretro *lr)
Retrieve the threshold for logs that will be posted.
#define SDL_LIBRETRO_VERSION
Retrieves an integer representation of the of the SDL_Libretro version.
void SDL_Libretro_SetVolume(SDL_Libretro *lr, float volume)
Set the audio volume when playing sounds.
bool SDL_Libretro_LoadGame(SDL_Libretro *lr, const char *gamePath)
Loads a game at the given path.
bool SDL_Libretro_ShouldQuit(const SDL_Libretro *lr)
Indicates whether or not the core has requested to shutdown.
void SDL_Libretro_Destroy(SDL_Libretro *lr)
Destroys the given libretro context.
SDL_Libretro * SDL_Libretro_Create(void)
Builds a libretro context.
void SDL_Libretro_UnloadGame(SDL_Libretro *lr)
Unloads the actively loaded game.
const char * SDL_Libretro_GetCoreName(const SDL_Libretro *lr)
Retrieve the name of the libretro core that's actively loaded.