SDL_Libretro 0.1.0
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
15#ifndef SDL_LIBRETRO_GET_TICKS_NS
16#define SDL_LIBRETRO_GET_TICKS_NS() SDL_GetTicksNS()
17#endif
18
24static void SDL_Libretro_ResetPortDevices(SDL_Libretro* lr) {
25 for (unsigned i = 0; i < SDL_LIBRETRO_MAX_USERS; i++) {
26 lr->core.portDeviceMap[i] = RETRO_DEVICE_JOYPAD;
27 }
28}
29
36#define LOAD_SYM(sym) \
37 do { \
38 SDL_FunctionPointer fp = SDL_LoadFunction(lr->core.symbols.handle, #sym); \
39 SDL_memcpy(&lr->core.symbols.sym, &fp, sizeof(fp)); \
40 if (!fp) { \
41 SDL_SetError("[SDL_Libretro] Failed to load symbol '%s'", #sym); \
42 SDL_UnloadObject(lr->core.symbols.handle); \
43 SDL_memset(&lr->core, 0, sizeof(lr->core)); \
44 SDL_Libretro_ResetPortDevices(lr); \
45 return false; \
46 } \
47 } while (0)
48
62static void SDL_Libretro_ReserveEvents(void) {
63 static SDL_AtomicInt reserved;
64 if (!SDL_CompareAndSwapAtomicInt(&reserved, 0, 1)) {
65 return;
66 }
67
68 // Probe where SDL's next user event sits, then claim up to the end of
69 // the range in one block.
70 const Uint32 next = SDL_RegisterEvents(1);
71 if (next == 0 || next >= SDL_EVENT_LIBRETRO) {
72 SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "[SDL_Libretro] Registered user events reach the SDL_EVENT_LIBRETRO range (0x%x-0x%x); they may collide", (unsigned)SDL_EVENT_LIBRETRO, (unsigned)SDL_EVENT_LIBRETRO_LAST);
73 return;
74 }
75 const int count = (int)(SDL_EVENT_LIBRETRO_LAST - next);
76 const Uint32 base = SDL_RegisterEvents(count);
77 if (base == 0 || base > SDL_EVENT_LIBRETRO) {
78 SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "[SDL_Libretro] Failed to reserve the SDL_EVENT_LIBRETRO range (0x%x-0x%x); registered user events may collide", (unsigned)SDL_EVENT_LIBRETRO, (unsigned)SDL_EVENT_LIBRETRO_LAST);
79 }
80}
81
96 SDL_Libretro* lr = (SDL_Libretro*)SDL_calloc(1, sizeof(SDL_Libretro));
97 if (!lr) {
98 SDL_SetError("[SDL_Libretro] Failed to allocate context");
99 return NULL;
100 }
101
102 SDL_Libretro_ReserveEvents();
103
104 // Initial state (the calloc above already zeroed the struct).
105 lr->rewindMaxBytes = SDL_LIBRETRO_REWIND_DEFAULT_MAX_BYTES;
106 SDL_Libretro_ResetPortDevices(lr);
107 SDL_Libretro_SetVFS(lr, NULL);
108 SDL_Libretro_SetVolume(lr, 1.0f);
109 SDL_Libretro_SetSpeed(lr, 1.0f);
110 SDL_Libretro_SetUsername(lr, "SDL_libretro");
112
113 // Keyboard Mappings
114 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_B] = SDL_SCANCODE_Z;
115 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_Y] = SDL_SCANCODE_A;
116 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_SELECT] = SDL_SCANCODE_RSHIFT;
117 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_START] = SDL_SCANCODE_RETURN;
118 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_UP] = SDL_SCANCODE_UP;
119 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_DOWN] = SDL_SCANCODE_DOWN;
120 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_LEFT] = SDL_SCANCODE_LEFT;
121 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_RIGHT] = SDL_SCANCODE_RIGHT;
122 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_A] = SDL_SCANCODE_X;
123 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_X] = SDL_SCANCODE_S;
124 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_L] = SDL_SCANCODE_Q;
125 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_R] = SDL_SCANCODE_W;
126 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_L2] = SDL_SCANCODE_E;
127 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_R2] = SDL_SCANCODE_R;
128 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_L3] = SDL_SCANCODE_D;
129 lr->keyboardPlayer1[RETRO_DEVICE_ID_JOYPAD_R3] = SDL_SCANCODE_F;
130
131 // Gamepad Mappings; analogToDigital stays zeroed (None) from the calloc.
132 for (unsigned port = 0; port < SDL_LIBRETRO_MAX_USERS; port++) {
133 SDL_memcpy(lr->gamepadButtons[port], SDL_Libretro_DefaultGamepadButtons, sizeof(lr->gamepadButtons[port]));
134 }
135
136 return lr;
137}
138
150void SDL_Libretro_Destroy(SDL_Libretro* lr) {
151 if (!lr) return;
152
155
156 // Tear down PhysFS if it was initialized.
158
159 for (unsigned i = 0; i < SDL_LIBRETRO_MAX_USERS; i++) {
160 if (lr->gamepads[i]) {
161 SDL_CloseGamepad(lr->gamepads[i]);
162 lr->gamepads[i] = NULL;
163 }
164 }
165
166 SDL_Libretro_FreeCoreLibrary(lr);
167 SDL_Libretro_FreeMessages(lr);
168 SDL_Libretro_CloseConfig(lr);
169
170 // Queued SDL_libretro events (including the unloads pushed above) carry
171 // pointers into this context; drop them before it goes away.
173
174 SDL_free(lr);
175}
176
184static const char* SDL_Libretro_GetCorePathFromName(const SDL_Libretro* lr, const char* coreName) {
185 if (!lr || !coreName || !lr->coreLibrary || SDL_strrchr(coreName, '.') != NULL) {
186 return NULL;
187 }
188 for (unsigned i = 0; i < lr->coreLibraryCount; i++) {
189 if (lr->coreLibrary[i].corename && SDL_strcasecmp(lr->coreLibrary[i].corename, coreName) == 0) {
190 return lr->coreLibrary[i].path;
191 }
192 }
193 return NULL;
194}
195
210bool SDL_Libretro_LoadCore(SDL_Libretro* lr, const char* core) {
211 if (!lr || !core) {
212 SDL_SetError("[SDL_Libretro] Invalid arguments");
213 return false;
214 }
215
216 if (SDL_Libretro_active && SDL_Libretro_active != lr) {
217 SDL_SetError("[SDL_Libretro] Another context already has a core loaded");
218 return false;
219 }
220
221 // Make sure the old core is unloaded.
223
224 // Ensure the Saves and System directories exist, since the core may need them.
225 if (lr->saveDirectory[0] != '\0') {
226 SDL_CreateDirectory(lr->saveDirectory);
227 }
228 if (lr->systemDirectory[0] != '\0') {
229 SDL_CreateDirectory(lr->systemDirectory);
230 }
231
232 // If the corePath is just a name, see if it lives in the loaded coreLibrary.
233 const char* path = SDL_Libretro_GetCorePathFromName(lr, core);
234 if (path) {
235 core = path;
236 }
237
238 // Load the core handle.
239 lr->core.symbols.handle = SDL_LoadObject(core);
240 if (!lr->core.symbols.handle) {
241 SDL_SetError("[SDL_Libretro] Failed to load core '%s': %s", core, SDL_GetError());
242 return false;
243 }
244
245 // Verify core API version.
246 LOAD_SYM(retro_api_version);
247 lr->core.apiVersion = lr->core.symbols.retro_api_version();
248 if (lr->core.apiVersion != 1) {
249 SDL_UnloadObject(lr->core.symbols.handle);
250 SDL_SetError("[SDL_Libretro] Unsupported Core API Version: %d", (int)lr->core.apiVersion);
251 SDL_memset(&lr->core, 0, sizeof(lr->core));
252 SDL_Libretro_ResetPortDevices(lr);
253 return false;
254 }
255
256 LOAD_SYM(retro_init);
257 LOAD_SYM(retro_deinit);
258 LOAD_SYM(retro_set_environment);
259 LOAD_SYM(retro_set_video_refresh);
260 LOAD_SYM(retro_set_audio_sample);
261 LOAD_SYM(retro_set_audio_sample_batch);
262 LOAD_SYM(retro_set_input_poll);
263 LOAD_SYM(retro_set_input_state);
264 LOAD_SYM(retro_get_system_info);
265 LOAD_SYM(retro_get_system_av_info);
266 LOAD_SYM(retro_set_controller_port_device);
267 LOAD_SYM(retro_reset);
268 LOAD_SYM(retro_run);
269 LOAD_SYM(retro_serialize_size);
270 LOAD_SYM(retro_serialize);
271 LOAD_SYM(retro_unserialize);
272 LOAD_SYM(retro_cheat_reset);
273 LOAD_SYM(retro_cheat_set);
274 LOAD_SYM(retro_load_game);
275 LOAD_SYM(retro_load_game_special);
276 LOAD_SYM(retro_unload_game);
277 LOAD_SYM(retro_get_region);
278 LOAD_SYM(retro_get_memory_data);
279 LOAD_SYM(retro_get_memory_size);
280
281 SDL_strlcpy(lr->core.corePath, core, sizeof(lr->core.corePath));
282
283 // Default to normal speed for the freshly loaded core (lr->core was zeroed
284 // above, so speed does not carry over from a previously loaded core).
285 lr->core.speed = 1.0f;
286
287 SDL_Libretro_active = lr;
288
289 // Tell the core that we should call our own enviornment callback.
290 lr->core.symbols.retro_set_environment(SDL_Libretro_EnvironmentCallback);
291
292 // Grab the initial system info from the core.
293 struct retro_system_info sysinfo = {0};
294 lr->core.symbols.retro_get_system_info(&sysinfo);
295 SDL_strlcpy(lr->core.libraryName, sysinfo.library_name ? sysinfo.library_name : "", sizeof(lr->core.libraryName));
296 SDL_strlcpy(lr->core.libraryVersion, sysinfo.library_version ? sysinfo.library_version : "", sizeof(lr->core.libraryVersion));
297 SDL_strlcpy(lr->core.validExtensions, sysinfo.valid_extensions ? sysinfo.valid_extensions : "", sizeof(lr->core.validExtensions));
298 lr->core.needFullpath = sysinfo.need_fullpath;
299 lr->core.blockExtract = sysinfo.block_extract;
300
301 // Default the content name to the core's reported name.
302 SDL_strlcpy(lr->core.contentName, lr->core.libraryName, sizeof(lr->core.contentName));
303
304 // Config
305 SDL_Libretro_LoadCoreConfig(lr);
306
307 // Initialize the core
308 lr->core.symbols.retro_init();
309
310 SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[SDL_Libretro] Core loaded: %s %s", lr->core.libraryName, lr->core.libraryVersion);
311
312 SDL_Libretro_PushEvent(lr, SDL_EVENT_LIBRETRO_CORE_LOADED, lr->core.libraryName);
313
314 return true;
315}
316
326void SDL_Libretro_UnloadCore(SDL_Libretro* lr) {
327 if (!lr) return;
328 bool hadCore = SDL_Libretro_IsCoreReady(lr);
329
331
332 // Tear down the loaded core module, if one is present.
333 if (SDL_Libretro_IsCoreReady(lr)) {
334 SDL_Libretro_SaveCoreConfig(lr);
335 lr->core.symbols.retro_deinit();
336 SDL_UnloadObject(lr->core.symbols.handle);
337 }
338
339 // Unload any frontend resources associated with the core.
340 SDL_Libretro_CloseSensors(lr);
341 SDL_Libretro_CloseMicrophone(lr);
342 SDL_Libretro_FreeCoreOptions(lr);
343 SDL_Libretro_FreeInputDescriptors(lr);
344 SDL_Libretro_FreeControllerInfo(lr);
345 SDL_Libretro_FreeMemoryMap(lr);
346 SDL_Libretro_FreeContentInfoOverrides(lr);
347 SDL_Libretro_FreeSubsystems(lr);
348 SDL_Libretro_StopRumble(lr);
349
350 SDL_memset(&lr->core, 0, sizeof(lr->core));
351 SDL_Libretro_ResetPortDevices(lr);
352 if (SDL_Libretro_active == lr) {
353 SDL_Libretro_active = NULL;
354 }
355 lr->coreGeneration++;
356
357 SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "[SDL_Libretro] Core unloaded");
358
359 if (hadCore) {
360 SDL_Libretro_PushEvent(lr, SDL_EVENT_LIBRETRO_CORE_UNLOADED, NULL);
361 }
362}
363
373bool SDL_Libretro_IsCoreReady(const SDL_Libretro* lr) {
374 return lr && lr->core.symbols.handle != NULL;
375}
376
388size_t SDL_Libretro_GetFileName(char* dst, size_t dstSize, const char* path, bool withExtension) {
389 if (!dst || dstSize == 0) return 0;
390 dst[0] = '\0';
391 if (!path) return 0;
392
393 // Skip past the last path separator to the base name.
394 const char* base = SDL_strrchr(path, '/');
395 if (!base) base = SDL_strrchr(path, '\\');
396 base = base ? base + 1 : path;
397
398 SDL_strlcpy(dst, base, dstSize);
399
400 if (!withExtension) {
401 char* dot = SDL_strrchr(dst, '.');
402 if (dot) *dot = '\0';
403 }
404
405 return SDL_strlen(dst);
406}
407
412static const char* SDL_Libretro_GetExtension(const char* path) {
413 const char* dot = SDL_strrchr(path, '.');
414 return dot ? dot + 1 : "";
415}
416
428size_t SDL_Libretro_GetSavePath(const SDL_Libretro* lr, const char* extension, char* dst, size_t dstSize) {
429 if (!dst || dstSize == 0) return 0;
430 dst[0] = '\0';
431 if (!lr || lr->core.contentName[0] == '\0') return 0;
432
433 if (!extension) {
434 extension = "";
435 }
436
437 if (lr->saveDirectory[0] != '\0') {
438 SDL_snprintf(dst, dstSize, "%s/%s%s", lr->saveDirectory, lr->core.contentName, extension);
439 }
440 else {
441 SDL_snprintf(dst, dstSize, "%s%s", lr->core.contentName, extension);
442 }
443
444 return SDL_strlen(dst);
445}
446
451static void SDL_Libretro_FreeContentInfoOverrides(SDL_Libretro* lr) {
452 if (lr->core.contentInfoOverrides) {
453 for (unsigned i = 0; i < lr->core.contentInfoOverrideCount; i++) {
454 SDL_free((void*)lr->core.contentInfoOverrides[i].extensions);
455 }
456 SDL_free(lr->core.contentInfoOverrides);
457 lr->core.contentInfoOverrides = NULL;
458 }
459 lr->core.contentInfoOverrideCount = 0;
460}
461
465static void SDL_Libretro_FreeSubsystems(SDL_Libretro* lr) {
466 if (!lr || !lr->core.subsystems) return;
467 for (unsigned i = 0; i < lr->core.subsystemCount; i++) {
468 // Subsystem Data
469 SDL_free((void*)lr->core.subsystems[i].desc);
470 SDL_free((void*)lr->core.subsystems[i].ident);
471
472 // Roms
473 if (lr->core.subsystems[i].roms) {
474 for (unsigned r = 0; r < lr->core.subsystems[i].num_roms; r++) {
475 SDL_free((void*)lr->core.subsystems[i].roms[r].desc);
476 SDL_free((void*)lr->core.subsystems[i].roms[r].valid_extensions);
477
478 // Rom Memory
479 if (lr->core.subsystems[i].roms[r].memory) {
480 for (unsigned m = 0; m < lr->core.subsystems[i].roms[r].num_memory; m++) {
481 SDL_free((void*)lr->core.subsystems[i].roms[r].memory[m].extension);
482 }
483 SDL_free((void*)lr->core.subsystems[i].roms[r].memory);
484 }
485 }
486 SDL_free((void*)lr->core.subsystems[i].roms);
487 }
488 }
489 SDL_free(lr->core.subsystems);
490 lr->core.subsystems = NULL;
491 lr->core.subsystemCount = 0;
492}
493
497static void SDL_Libretro_FreeInputDescriptors(SDL_Libretro* lr) {
498 if (!lr || !lr->core.inputDescriptors) return;
499 for (unsigned i = 0; i < lr->core.inputDescriptorsCount; i++) {
500 SDL_free((void*)lr->core.inputDescriptors[i].description);
501 }
502 SDL_free(lr->core.inputDescriptors);
503 lr->core.inputDescriptors = NULL;
504 lr->core.inputDescriptorsCount = 0;
505}
506
510static void SDL_Libretro_FreeControllerInfo(SDL_Libretro* lr) {
511 if (!lr || !lr->core.controllerInfo) return;
512 for (unsigned i = 0; i < lr->core.controllerInfoCount; i++) {
513 if (lr->core.controllerInfo[i].types) {
514 for (unsigned t = 0; t < lr->core.controllerInfo[i].num_types; t++) {
515 SDL_free((void*)lr->core.controllerInfo[i].types[t].desc);
516 }
517 SDL_free((void*)lr->core.controllerInfo[i].types);
518 }
519 }
520 SDL_free(lr->core.controllerInfo);
521 lr->core.controllerInfo = NULL;
522 lr->core.controllerInfoCount = 0;
523}
524
528static const struct retro_subsystem_info* SDL_Libretro_GetSubsystemById(const SDL_Libretro* lr, unsigned subsystemId) {
529 if (!lr) return NULL;
530 for (unsigned i = 0; i < lr->core.subsystemCount; i++) {
531 if (lr->core.subsystems[i].id == subsystemId) {
532 return &lr->core.subsystems[i];
533 }
534 }
535 return NULL;
536}
537
541static const struct retro_subsystem_info* SDL_Libretro_GetSubsystemByName(const SDL_Libretro* lr, const char* name) {
542 if (!lr || !name) return NULL;
543 for (unsigned i = 0; i < lr->core.subsystemCount; i++) {
544 const struct retro_subsystem_info* subsys = &lr->core.subsystems[i];
545 if ((subsys->ident && SDL_strcasecmp(subsys->ident, name) == 0) ||
546 (subsys->desc && SDL_strcasecmp(subsys->desc, name) == 0)) {
547 return subsys;
548 }
549 }
550 return NULL;
551}
552
561static int SDL_Libretro_GetContentInfoOverride(const SDL_Libretro* lr, const char* ext) {
562 if (!lr || !ext || ext[0] == '\0') return -1;
563 for (unsigned i = 0; i < lr->core.contentInfoOverrideCount; i++) {
564 if (SDL_Libretro_ExtensionInList(ext, lr->core.contentInfoOverrides[i].extensions)) {
565 return (int)i;
566 }
567 }
568 return -1;
569}
570
575static bool SDL_Libretro_ContentNeedsFullpath(const SDL_Libretro* lr, const char* ext) {
576 int i = SDL_Libretro_GetContentInfoOverride(lr, ext);
577 return i >= 0 ? lr->core.contentInfoOverrides[i].need_fullpath : lr->core.needFullpath;
578}
579
584static bool SDL_Libretro_ContentPersistData(const SDL_Libretro* lr, const char* ext) {
585 int i = SDL_Libretro_GetContentInfoOverride(lr, ext);
586 return i >= 0 ? lr->core.contentInfoOverrides[i].persistent_data : false;
587}
588
593static void SDL_Libretro_FreeGameInfoExtArray(SDL_Libretro* lr) {
594 for (unsigned i = 0; i < lr->core.gameInfoExtCount; i++) {
595 struct retro_game_info_ext* entry = &lr->core.gameInfoExtArray[i];
596 SDL_free((void*)entry->full_path);
597 SDL_free((void*)entry->dir);
598 SDL_free((void*)entry->name);
599 SDL_free((void*)entry->ext);
600 }
601 SDL_free(lr->core.gameInfoExtArray);
602 lr->core.gameInfoExtArray = NULL;
603 lr->core.gameInfoExtCount = 0;
604}
605
610static void SDL_Libretro_FillGameInfoExt(struct retro_game_info_ext* entry, const char* path) {
611 if (!path) return;
612 entry->full_path = SDL_strdup(path);
613
614 char buffer[SDL_LIBRETRO_MAX_PATH];
615 SDL_Libretro_GetFileName(buffer, sizeof(buffer), path, false);
616 entry->name = SDL_strdup(buffer);
617
618 SDL_Libretro_GetFileName(buffer, sizeof(buffer), path, true);
619 const char* dot = SDL_strrchr(buffer, '.');
620 char* ext = SDL_strdup(dot ? dot + 1 : "");
621 if (ext) {
622 for (char* c = ext; *c; c++) *c = (char)SDL_tolower((unsigned char)*c);
623 }
624 entry->ext = ext;
625
626 SDL_strlcpy(buffer, path, sizeof(buffer));
627 char* sep = SDL_strrchr(buffer, '/');
628 char* backslash = SDL_strrchr(buffer, '\\');
629 if (!sep || (backslash && backslash > sep)) sep = backslash;
630 if (sep)
631 *sep = '\0';
632 else
633 buffer[0] = '\0';
634 entry->dir = SDL_strdup(buffer);
635}
636
643static void SDL_Libretro_ResetContentState(SDL_Libretro* lr) {
644 lr->core.contentPath[0] = '\0';
645 lr->core.contentDir[0] = '\0';
646 lr->core.contentExt[0] = '\0';
647 SDL_strlcpy(lr->core.contentName, lr->core.libraryName, sizeof(lr->core.contentName));
648
649 // Remove the file SDL_Libretro_LoadGame_IO() spilled for a need_fullpath core.
650 if (lr->core.contentTempPath[0] != '\0') {
651 SDL_RemovePath(lr->core.contentTempPath);
652 lr->core.contentTempPath[0] = '\0';
653 }
654
655 // Remove an archive extracted for a need_fullpath core, with everything in it.
656 if (lr->core.contentTempDir[0] != '\0') {
657 SDL_Libretro_RemoveTree(lr->core.contentTempDir);
658 lr->core.contentTempDir[0] = '\0';
659 }
660
661 if (lr->core.gameInfoExt.persistent_data) {
662 SDL_free((void*)lr->core.gameInfoExt.data);
663 }
664 SDL_memset(&lr->core.gameInfoExt, 0, sizeof(lr->core.gameInfoExt));
665 SDL_Libretro_FreeGameInfoExtArray(lr);
666
667 // Images kept by SDL_Libretro_AddDiskImage() and _IO() belong to the game.
668 SDL_Libretro_FreeDiskImages(lr);
669
670 // Restart the frame-time clock, so the first tick of new content reports the core's reference.
671 lr->core.runloop_frame_time_last = 0;
672
673 // The disk control interface is core-lifetime state (registered from
674 // retro_set_environment/retro_init), so it's kept until SDL_Libretro_UnloadCore().
675}
676
695static const char* SDL_Libretro_SetContentIdentity(SDL_Libretro* lr, const char* path, const char* identityPath) {
696 SDL_strlcpy(lr->core.contentPath, path, sizeof(lr->core.contentPath));
697 if (!identityPath) identityPath = path;
698
699 // Content base name (no extension) and lower-case extension.
700 SDL_Libretro_GetFileName(lr->core.contentName, sizeof(lr->core.contentName), identityPath, false);
701 const char* ext = SDL_Libretro_GetContentExtension(lr);
702 SDL_strlcpy(lr->core.contentExt, SDL_Libretro_GetExtension(identityPath), sizeof(lr->core.contentExt));
703 for (char* c = lr->core.contentExt; *c; c++)
704 *c = (char)SDL_tolower((unsigned char)*c);
705
706 // Directory containing the content file.
707 SDL_strlcpy(lr->core.contentDir, identityPath, sizeof(lr->core.contentDir));
708 char* sep = SDL_strrchr(lr->core.contentDir, '/');
709 if (!sep) sep = SDL_strrchr(lr->core.contentDir, '\\');
710 if (sep)
711 *sep = '\0';
712 else
713 lr->core.contentDir[0] = '\0';
714
715 // Initial gameInfoExt.
716 lr->core.gameInfoExt.full_path = lr->core.contentPath;
717 lr->core.gameInfoExt.dir = lr->core.contentDir;
718 lr->core.gameInfoExt.name = lr->core.contentName;
719 lr->core.gameInfoExt.ext = lr->core.contentExt;
720
721 return ext;
722}
723
733static bool SDL_Libretro_LoadCoreForGame(SDL_Libretro* lr, const char* gamePath) {
734 if (!lr || !gamePath) {
735 SDL_SetError("[SDL_Libretro] No core loaded, and no content to pick one by");
736 return false;
737 }
738
739 // The game's file extension.
740 const char* extension = SDL_Libretro_GetExtension(gamePath);
741 if (extension[0] == '\0') {
742 SDL_SetError("[SDL_Libretro] Game path '%s' has no file extension", gamePath);
743 return false;
744 }
745
746 // Try to load cores that match the extension.
747 bool claimed = false;
748 for (unsigned i = 0; i < lr->coreLibraryCount; i++) {
749 // Check if the extension is in the lr->coreLibrary[i] list.
750 if (!SDL_Libretro_ExtensionInList(extension, lr->coreLibrary[i].supported_extensions)) {
751 continue;
752 }
753
754 // A candidate core claims this extension, try loading it.
755 claimed = true;
756 if (SDL_Libretro_LoadCore(lr, lr->coreLibrary[i].path)) {
757 return true;
758 }
759 }
760
761 // When a core claimed the extension but failed to load, its error says why.
762 if (!claimed) {
763 SDL_SetError("[SDL_Libretro] No core found for extension '%s'", extension);
764 }
765 return false;
766}
767
774static void SDL_Libretro_SetCoreCallbacks(SDL_Libretro* lr) {
775 if (!lr) return;
776 if (lr->core.symbols.retro_set_video_refresh) lr->core.symbols.retro_set_video_refresh(SDL_Libretro_VideoRefresh);
777 if (lr->core.symbols.retro_set_audio_sample) lr->core.symbols.retro_set_audio_sample(SDL_Libretro_AudioSample);
778 if (lr->core.symbols.retro_set_audio_sample_batch) lr->core.symbols.retro_set_audio_sample_batch(SDL_Libretro_AudioSampleBatch);
779 if (lr->core.symbols.retro_set_input_poll) lr->core.symbols.retro_set_input_poll(SDL_Libretro_InputPoll);
780 if (lr->core.symbols.retro_set_input_state) lr->core.symbols.retro_set_input_state(SDL_Libretro_InputState);
781}
782
789static bool SDL_Libretro_FinishGameLoad(SDL_Libretro* lr) {
790 lr->core.gameLoaded = true;
791
792 // Re-apply the app's port devices; cores expect them after retro_load_game().
793 for (unsigned port = 0; port < SDL_LIBRETRO_MAX_USERS; port++) {
794 if (lr->core.portDeviceMap[port] != RETRO_DEVICE_JOYPAD) {
795 lr->core.symbols.retro_set_controller_port_device(port, lr->core.portDeviceMap[port]);
796 }
797 }
798
799 // Restore the game's battery save before it first runs.
800 SDL_Libretro_AutoLoadSRAM(lr);
801
802 // Grab the Audio/Video data.
803 struct retro_system_av_info avInfo = {0};
804 lr->core.symbols.retro_get_system_av_info(&avInfo);
805 lr->core.width = avInfo.geometry.base_width;
806 lr->core.height = avInfo.geometry.base_height;
807 lr->core.fps = avInfo.timing.fps;
808 lr->core.sampleRate = avInfo.timing.sample_rate;
809 lr->core.aspectRatio = avInfo.geometry.aspect_ratio;
810
811 // Failed video initialization should not fail loading the game. Video is
812 // deferred until a renderer is set via SDL_Libretro_SetRenderer().
813 if (lr->renderer && !SDL_Libretro_InitVideo(lr)) {
814 SDL_LogWarn(SDL_LOG_CATEGORY_VIDEO, "[SDL_Libretro] Video failed to initialize: %s", SDL_GetError());
815 }
816
817 // A missing device shouldn't stop the game from running; it plays silently
818 // until the game is loaded again.
819 if (!SDL_Libretro_InitAudio(lr)) {
820 SDL_LogWarn(SDL_LOG_CATEGORY_AUDIO, "[SDL_Libretro] Audio failed to initialize: %s", SDL_GetError());
821 }
822
823 SDL_Log("[SDL_Libretro] Game loaded: %s [%ux%u @ %.2ffps]", lr->core.contentName, lr->core.width, lr->core.height, lr->core.fps);
824
825 // Allocate the rewind buffer now that the serialize size is known. A core
826 // that doesn't report one yet gets it on a later capture instead.
827 if (lr->rewindEnabled && !lr->rewindReference && lr->core.symbols.retro_serialize_size() > 0) {
828 SDL_Libretro_RewindAllocate(lr);
829 }
830
831 SDL_Libretro_PushEvent(lr, SDL_EVENT_LIBRETRO_GAME_LOADED, lr->core.contentName);
832
833 return true;
834}
835
849static bool SDL_Libretro_ContentExists(const SDL_Libretro* lr, const char* path) {
850 // Look through the VFS the core will use, which also sees PhysFS mounts.
851 if (lr->vfs_interface.stat) {
852 return (lr->vfs_interface.stat(path, NULL) & RETRO_VFS_STAT_IS_VALID) != 0;
853 }
854 return SDL_GetPathInfo(path, NULL);
855}
856
857static bool SDL_Libretro_LoadGameCommon(SDL_Libretro* lr, const char* gamePath, const char* identityPath, void* fileData, size_t fileSize) {
858 // Check what can be checked before unloading anything, so a request that's
859 // bound to fail leaves the running game alone.
860
861 // A core that didn't opt into no-content (SET_SUPPORT_NO_GAME) can't run without a game.
862 if (!gamePath && SDL_Libretro_IsGameRequired(lr)) {
863 SDL_free(fileData);
864 SDL_SetError("[SDL_Libretro] This core requires content");
865 return false;
866 }
867
868 // The content has to be there: read the bytes for cores that want them,
869 // or make sure the file exists for cores that want its path.
870 if (gamePath && !fileData) {
871 if (!SDL_Libretro_ContentNeedsFullpath(lr, SDL_Libretro_GetExtension(identityPath ? identityPath : gamePath))) {
872 fileData = SDL_LoadFile(gamePath, &fileSize);
873 if (!fileData) {
874 SDL_SetError("[SDL_Libretro] Failed to load game file '%s'", gamePath);
875 return false;
876 }
877 }
878 else if (!SDL_Libretro_ContentExists(lr, gamePath)) {
879 SDL_SetError("[SDL_Libretro] Game file '%s' not found", gamePath);
880 return false;
881 }
882 }
883
884 // Switching content: unload any game that's already running first.
886
887 struct retro_game_info gameInfo = {0};
888 bool persistData = false;
889
890 // Cleared here so a no-content load leaves GET_GAME_INFO_EXT invalid.
891 SDL_memset(&lr->core.gameInfoExt, 0, sizeof(lr->core.gameInfoExt));
892
893 if (gamePath) {
894 const char* ext = SDL_Libretro_SetContentIdentity(lr, gamePath, identityPath);
895
896 bool needFullpath = SDL_Libretro_ContentNeedsFullpath(lr, ext);
897 persistData = SDL_Libretro_ContentPersistData(lr, ext);
898
899 gameInfo.path = lr->core.contentPath;
900
901 if (!needFullpath) {
902 if (!fileData) {
903 fileData = SDL_LoadFile(gamePath, &fileSize);
904 if (!fileData) {
905 SDL_SetError("[SDL_Libretro] Failed to load game file '%s'", gamePath);
906 SDL_Libretro_ResetContentState(lr);
907 return false;
908 }
909 }
910 gameInfo.data = fileData;
911 gameInfo.size = fileSize;
912 }
913 else if (fileData) {
914 // The core wants a path, not bytes; drop a pre-read buffer.
915 SDL_free(fileData);
916 fileData = NULL;
917 }
918
919 // The string fields are set above; fill in the content buffer.
920 lr->core.gameInfoExt.data = gameInfo.data;
921 lr->core.gameInfoExt.size = gameInfo.size;
922 lr->core.gameInfoExt.persistent_data = persistData;
923 }
924 else if (fileData) {
925 SDL_free(fileData);
926 fileData = NULL;
927 }
928
929 SDL_Libretro_SetCoreCallbacks(lr);
930
931 bool result = lr->core.symbols.retro_load_game(gamePath ? &gameInfo : NULL);
932
933 // 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.
934 if (fileData && !(persistData && result)) {
935 SDL_free(fileData);
936 lr->core.gameInfoExt.data = NULL;
937 lr->core.gameInfoExt.size = 0;
938 }
939
940 if (!result) {
941 SDL_Libretro_ResetContentState(lr);
942 SDL_SetError("[SDL_Libretro] Core failed to load the game");
943 return false;
944 }
945
946 SDL_Libretro_FinishGameLoad(lr);
947 return true;
948}
949
957static bool SDL_Libretro_LoadGameFile(SDL_Libretro* lr, const char* gamePath) {
958 // Try to find what can be loaded with it.
959 if (!SDL_Libretro_IsCoreReady(lr) && !SDL_Libretro_LoadCoreForGame(lr, gamePath)) {
960 return false;
961 }
962
963 return SDL_Libretro_LoadGameCommon(lr, gamePath, NULL, NULL, 0);
964}
965
987bool SDL_Libretro_LoadGame(SDL_Libretro* lr, const char* gamePath) {
988 if (!lr) return false;
989
990#if defined(SDL_LIBRETRO_ENABLE_PHYSFS) && !defined(SDL_LIBRETRO_DISABLE_PHYSFS)
991 // Route archives through the PhysFS loader. It only ever falls back to
992 // SDL_Libretro_LoadGameFile(), never this dispatcher, so there is no loop.
993 if (gamePath && SDL_strcasecmp(SDL_Libretro_GetExtension(gamePath), "zip") == 0) {
994 return SDL_Libretro_PhysFS_LoadGame(lr, gamePath);
995 }
996#endif
997
998 return SDL_Libretro_LoadGameFile(lr, gamePath);
999}
1000
1010static bool SDL_Libretro_GetTempDirectory(SDL_Libretro* lr, char* dir, size_t dirSize) {
1011 const char* saveDir = SDL_Libretro_GetSaveDirectory(lr);
1012 if (saveDir && saveDir[0]) {
1013 SDL_snprintf(dir, dirSize, "%s/tmp/", saveDir);
1014 }
1015 else if (SDL_Libretro_GetPrefDirectory(dir, dirSize)) {
1016 SDL_strlcat(dir, "/tmp/", dirSize);
1017 }
1018 else {
1019 const char* base = SDL_GetBasePath();
1020 SDL_snprintf(dir, dirSize, "%stmp/", base ? base : "./");
1021 }
1022 if (!SDL_CreateDirectory(dir)) {
1023 return SDL_SetError("[SDL_Libretro] Failed to create temp directory '%s': %s", dir, SDL_GetError());
1024 }
1025 return true;
1026}
1027
1032static SDL_EnumerationResult SDLCALL SDL_Libretro_RemoveTreeEntry(void* userdata, const char* dirname, const char* fname) {
1033 (void)userdata;
1034 char path[SDL_LIBRETRO_MAX_PATH];
1035 size_t len = SDL_strlen(dirname);
1036 bool slash = len > 0 && (dirname[len - 1] == '/' || dirname[len - 1] == '\\');
1037 SDL_snprintf(path, sizeof(path), "%s%s%s", dirname, slash ? "" : "/", fname);
1038 SDL_PathInfo info;
1039 if (SDL_GetPathInfo(path, &info) && info.type == SDL_PATHTYPE_DIRECTORY) {
1040 SDL_EnumerateDirectory(path, SDL_Libretro_RemoveTreeEntry, NULL);
1041 }
1042 SDL_RemovePath(path);
1043 return SDL_ENUM_CONTINUE;
1044}
1045
1050static void SDL_Libretro_RemoveTree(const char* path) {
1051 SDL_EnumerateDirectory(path, SDL_Libretro_RemoveTreeEntry, NULL);
1052 SDL_RemovePath(path);
1053}
1054
1072static bool SDL_Libretro_SpillToTempFile(SDL_Libretro* lr, SDL_IOStream* src, const char* path, bool closeio, char* tempPath, size_t tempPathSize) {
1073 // Read the content first, so the stream is handled on every path.
1074 size_t fileSize = 0;
1075 void* fileData = SDL_LoadFile_IO(src, &fileSize, closeio);
1076 if (!fileData) return false;
1077
1078 char name[SDL_LIBRETRO_MAX_PATH];
1079 if (!path || SDL_Libretro_GetFileName(name, sizeof(name), path, true) == 0) {
1080 SDL_strlcpy(name, "content.bin", sizeof(name));
1081 }
1082
1083 // Pick the temp directory and make sure it exists.
1084 char dir[SDL_LIBRETRO_MAX_PATH];
1085 if (!SDL_Libretro_GetTempDirectory(lr, dir, sizeof(dir))) {
1086 SDL_free(fileData);
1087 return false;
1088 }
1089
1090 // Find a file name in the directory that doesn't exist yet.
1091 SDL_snprintf(tempPath, tempPathSize, "%s%s", dir, name);
1092 const char* dot = SDL_strrchr(name, '.');
1093 int stemLen = dot ? (int)(dot - name) : (int)SDL_strlen(name);
1094 for (int attempt = 1; SDL_GetPathInfo(tempPath, NULL); attempt++) {
1095 if (attempt > 99) {
1096 SDL_SetError("[SDL_Libretro] Could not find a free file name for '%s' in '%s'", name, dir);
1097 SDL_free(fileData);
1098 return false;
1099 }
1100 SDL_snprintf(tempPath, tempPathSize, "%s%.*s-%d%s", dir, stemLen, name, attempt, dot ? dot : "");
1101 }
1102
1103 bool saved = SDL_SaveFile(tempPath, fileData, fileSize);
1104 SDL_free(fileData);
1105 if (!saved) {
1106 SDL_SetError("[SDL_Libretro] Failed to save content to '%s'", tempPath);
1107 return false;
1108 }
1109 return true;
1110}
1111
1131bool SDL_Libretro_LoadGame_IO(SDL_Libretro* lr, SDL_IOStream* src, const char* path, bool closeio) {
1132 if (!lr || !src || !path) {
1133 if (src && closeio) SDL_CloseIO(src);
1134 return SDL_InvalidParamError("src, path");
1135 }
1136
1137 // Try to find what can be loaded with it.
1138 if (!SDL_Libretro_IsCoreReady(lr) && !SDL_Libretro_LoadCoreForGame(lr, path)) {
1139 if (closeio) SDL_CloseIO(src);
1140 return false;
1141 }
1142
1143 // The raw-case extension drives the need_fullpath resolution.
1144 if (!SDL_Libretro_ContentNeedsFullpath(lr, SDL_Libretro_GetExtension(path))) {
1145 // Byte-oriented content: hand the stream contents to the core.
1146 size_t fileSize = 0;
1147 void* fileData = SDL_LoadFile_IO(src, &fileSize, closeio);
1148 if (!fileData) return false;
1149 return SDL_Libretro_LoadGameCommon(lr, path, NULL, fileData, fileSize);
1150 }
1151
1152 // The core wants a real file: spill the stream to the temp directory and
1153 // load from there. The content keeps the identity of `path`, so save files
1154 // are named after it rather than the temp file.
1155 char tempPath[SDL_LIBRETRO_MAX_PATH];
1156 if (!SDL_Libretro_SpillToTempFile(lr, src, path, closeio, tempPath, sizeof(tempPath))) {
1157 return false;
1158 }
1159
1160 // The temp path is recorded only after the load, so the UnloadGame inside
1161 // SDL_Libretro_LoadGameCommon() doesn't remove the file it's about to use.
1162 bool result = SDL_Libretro_LoadGameCommon(lr, tempPath, path, NULL, 0);
1163 if (result) {
1164 SDL_strlcpy(lr->core.contentTempPath, tempPath, sizeof(lr->core.contentTempPath));
1165 }
1166 else {
1167 SDL_RemovePath(tempPath);
1168 }
1169 return result;
1170}
1171
1189bool SDL_Libretro_LoadGameSpecialById(SDL_Libretro* lr, unsigned subsystemId, const char** paths, unsigned numPaths) {
1190 if (!SDL_Libretro_IsCoreReady(lr) || !paths || numPaths == 0) {
1191 SDL_SetError("[SDL_Libretro] Invalid arguments for LoadGameSpecial");
1192 return false;
1193 }
1194
1195 // Find the subsystem info to determine per-ROM need_fullpath.
1196 const struct retro_subsystem_info* subsys = SDL_Libretro_GetSubsystemById(lr, subsystemId);
1197 if (!subsys) {
1198 SDL_SetError("[SDL_Libretro] Unknown subsystem id %u", subsystemId);
1199 return false;
1200 }
1201
1202 // The paths must fit the ROMs the subsystem declares, with every required
1203 // one given, or the core only rejects them with no hint why.
1204 unsigned requiredCount = 0;
1205 bool missingRequired = false;
1206 for (unsigned i = 0; i < subsys->num_roms; i++) {
1207 if (subsys->roms[i].required) {
1208 requiredCount++;
1209 missingRequired = missingRequired || i >= numPaths || !paths[i];
1210 }
1211 }
1212 if (numPaths > subsys->num_roms || missingRequired) {
1213 SDL_SetError("[SDL_Libretro] Subsystem '%s' expects %u ROM(s) (%u required), got %u", subsys->desc ? subsys->desc : (subsys->ident ? subsys->ident : ""), subsys->num_roms, requiredCount, numPaths);
1214 return false;
1215 }
1216
1217 // Build the game info with the given data. The files are read before the
1218 // running game is unloaded, so a missing one leaves it running.
1219 struct retro_game_info* gameInfos = (struct retro_game_info*)SDL_calloc(numPaths, sizeof(struct retro_game_info));
1220 void** fileBuffers = (void**)SDL_calloc(numPaths, sizeof(void*));
1221 if (!gameInfos || !fileBuffers) {
1222 SDL_free(gameInfos);
1223 SDL_free(fileBuffers);
1224 SDL_SetError("[SDL_Libretro] Allocation failed");
1225 return false;
1226 }
1227
1228 for (unsigned i = 0; i < numPaths; i++) {
1229 if (!paths[i]) continue;
1230 gameInfos[i].path = paths[i];
1231
1232 bool needFullpath = lr->core.needFullpath;
1233 if (i < subsys->num_roms) {
1234 needFullpath = subsys->roms[i].need_fullpath;
1235 }
1236
1237 if (!needFullpath) {
1238 size_t fileSize = 0;
1239 fileBuffers[i] = SDL_LoadFile(paths[i], &fileSize);
1240 if (!fileBuffers[i]) {
1241 SDL_SetError("[SDL_Libretro] Failed to load file '%s'", paths[i]);
1242 for (unsigned j = 0; j < i; j++) SDL_free(fileBuffers[j]);
1243 SDL_free(fileBuffers);
1244 SDL_free(gameInfos);
1245 return false;
1246 }
1247 gameInfos[i].data = fileBuffers[i];
1248 gameInfos[i].size = fileSize;
1249 }
1250 else if (!SDL_Libretro_ContentExists(lr, paths[i])) {
1251 SDL_SetError("[SDL_Libretro] File '%s' not found", paths[i]);
1252 for (unsigned j = 0; j < i; j++) SDL_free(fileBuffers[j]);
1253 SDL_free(fileBuffers);
1254 SDL_free(gameInfos);
1255 return false;
1256 }
1257 }
1258
1260
1261 // Populate the game info from the primary path, so
1262 // GET_GAME_INFO_EXT is valid while loading the game.
1263 SDL_memset(&lr->core.gameInfoExt, 0, sizeof(lr->core.gameInfoExt));
1264 if (paths[0]) {
1265 SDL_Libretro_SetContentIdentity(lr, paths[0], NULL);
1266 lr->core.gameInfoExt.data = gameInfos[0].data;
1267 lr->core.gameInfoExt.size = gameInfos[0].size;
1268 lr->core.gameInfoExt.persistent_data = false;
1269 }
1270 else {
1271 SDL_strlcpy(lr->core.contentName, lr->core.libraryName, sizeof(lr->core.contentName));
1272 }
1273
1274 // GET_GAME_INFO_EXT hands subsystem cores one entry per content file, in subsystem order. Files that weren't given keep a NULL full_path.
1275 SDL_Libretro_FreeGameInfoExtArray(lr);
1276 lr->core.gameInfoExtArray = (struct retro_game_info_ext*)SDL_calloc(numPaths, sizeof(struct retro_game_info_ext));
1277 if (lr->core.gameInfoExtArray) {
1278 lr->core.gameInfoExtCount = numPaths;
1279 for (unsigned i = 0; i < numPaths; i++) {
1280 SDL_Libretro_FillGameInfoExt(&lr->core.gameInfoExtArray[i], paths[i]);
1281 lr->core.gameInfoExtArray[i].data = gameInfos[i].data;
1282 lr->core.gameInfoExtArray[i].size = gameInfos[i].size;
1283 lr->core.gameInfoExtArray[i].persistent_data = false;
1284 }
1285 }
1286
1287 SDL_Libretro_SetCoreCallbacks(lr);
1288
1289 bool result = lr->core.symbols.retro_load_game_special(subsystemId, gameInfos, numPaths);
1290
1291 // The frontend owns the ROM buffers for the load only, free them
1292 // and drop gameInfoExt.
1293 for (unsigned i = 0; i < numPaths; i++) SDL_free(fileBuffers[i]);
1294 SDL_free(fileBuffers);
1295 SDL_free(gameInfos);
1296 lr->core.gameInfoExt.data = NULL;
1297 lr->core.gameInfoExt.size = 0;
1298 for (unsigned i = 0; i < lr->core.gameInfoExtCount; i++) {
1299 lr->core.gameInfoExtArray[i].data = NULL;
1300 lr->core.gameInfoExtArray[i].size = 0;
1301 }
1302
1303 if (!result) {
1304 SDL_Libretro_ResetContentState(lr);
1305 SDL_SetError("[SDL_Libretro] Core failed to load the special game");
1306 return false;
1307 }
1308
1309 SDL_Libretro_FinishGameLoad(lr);
1310 return true;
1311}
1312
1330bool SDL_Libretro_LoadGameSpecial(SDL_Libretro* lr, const char* subsystem, const char** paths, unsigned numPaths) {
1331 // Match by ident or human-readable description.
1332 const struct retro_subsystem_info* subsys = SDL_Libretro_GetSubsystemByName(lr, subsystem);
1333 if (!subsys) {
1334 SDL_SetError("[SDL_Libretro] Unknown subsystem '%s'", subsystem ? subsystem : "(null)");
1335 return false;
1336 }
1337 return SDL_Libretro_LoadGameSpecialById(lr, subsys->id, paths, numPaths);
1338}
1339
1349void SDL_Libretro_UnloadGame(SDL_Libretro* lr) {
1350 if (!lr || !lr->core.gameLoaded) return;
1351
1352 // Keep the game's battery save while the core still has it.
1353 SDL_Libretro_AutoSaveSRAM(lr);
1354
1355 // Unload the game before closing audio and video.
1356 lr->core.symbols.retro_unload_game();
1357 lr->core.gameLoaded = false;
1358 // A shutdown request was for the game that's now gone.
1359 lr->core.shutdown = false;
1360 SDL_Libretro_ResetContentState(lr);
1361 SDL_Libretro_StopRumble(lr);
1362
1363 SDL_Libretro_RewindFree(lr);
1364 SDL_Libretro_CloseAudio(lr);
1365 SDL_Libretro_CloseVideo(lr);
1366 SDL_Libretro_PhysFS_ClearMount(lr);
1367 SDL_Log("[SDL_Libretro] Game unloaded");
1368
1369 SDL_Libretro_PushEvent(lr, SDL_EVENT_LIBRETRO_GAME_UNLOADED, NULL);
1370}
1371
1383bool SDL_Libretro_IsGameReady(const SDL_Libretro* lr) {
1384 return lr && lr->core.gameLoaded;
1385}
1386
1400bool SDL_Libretro_IsGameRequired(const SDL_Libretro* lr) {
1401 return lr && !lr->core.supportNoGame;
1402}
1403
1412bool SDL_Libretro_Reset(SDL_Libretro* lr) {
1413 if (!lr || !lr->core.gameLoaded) {
1414 SDL_SetError("[SDL_Libretro] No game loaded");
1415 return false;
1416 }
1417 lr->core.symbols.retro_reset();
1418
1419 // Restart the frame-time clock, so the first tick after the reset reports the core's reference.
1420 lr->core.runloop_frame_time_last = 0;
1421
1422 // Clear any rewind states.
1423 SDL_Libretro_ClearRewind(lr);
1424
1425 // Drop the audio queued from before the reset so it doesn't continue into the reset.
1426 if (lr->core.audioStream) {
1427 SDL_ClearAudioStream(lr->core.audioStream);
1428 lr->core.singleSampleCount = 0;
1429 lr->core.drcDriftAvg = 0.0;
1430 }
1431 return true;
1432}
1433
1441#define SDL_LIBRETRO_FRAME_TIME_MAX_GAP_NS (250 * SDL_NS_PER_MS)
1442
1448static retro_usec_t SDL_Libretro_FrameTimeDelta(SDL_Libretro* lr, retro_usec_t referenceUsec, int ticks) {
1449 Uint64 nowNS = SDL_LIBRETRO_GET_TICKS_NS();
1450 Uint64 lastNS = lr->core.runloop_frame_time_last;
1451 lr->core.runloop_frame_time_last = nowNS;
1452 if (!lastNS || referenceUsec <= 0 || nowNS - lastNS > SDL_LIBRETRO_FRAME_TIME_MAX_GAP_NS) {
1453 return lr->core.runloop_frame_time.reference;
1454 }
1455 if (lr->core.speed != 1.0f) {
1456 return referenceUsec;
1457 }
1458 retro_usec_t measured = (retro_usec_t)((nowNS - lastNS) / SDL_NS_PER_US);
1459 return ticks > 1 ? measured / ticks : measured;
1460}
1461
1467static void SDL_Libretro_Tick(SDL_Libretro* lr, retro_usec_t frameTimeUsec) {
1468 if (lr->core.runloop_frame_time.callback) {
1469 lr->core.runloop_frame_time.callback(frameTimeUsec);
1470 }
1471
1472 // Report audio buffer occupancy so the core can frame-skip if an underrun looms. Per the libretro spec this fires right before retro_run().
1473 SDL_Libretro_ReportAudioBufferStatus(lr);
1474
1475 // Run the frame.
1476 lr->core.symbols.retro_run();
1477
1478 // Capture rewind state after each forward tick.
1479 SDL_Libretro_RewindCapture(lr);
1480
1481 // Only pump the core's async-audio callback when audio is actually up.
1482 if (lr->core.audioStream && lr->core.audio_callback.callback) {
1483 lr->core.audio_callback.callback();
1484 }
1485}
1486
1501void SDL_Libretro_Update(SDL_Libretro* lr) {
1502 // Once the core asks to shut down, it doesn't expect to run again.
1503 if (!lr || !lr->core.gameLoaded || lr->core.shutdown) return;
1504
1505 // Pending Video Driver Reinit
1506 if (lr->core.videoReinitPending) {
1507 lr->core.videoReinitPending = false;
1508 SDL_Libretro_InitVideo(lr);
1509 }
1510
1511 // Pending Audio Driver Reinit
1512 if (lr->core.audioReinitPending) {
1513 lr->core.audioReinitPending = false;
1514 SDL_Libretro_InitAudio(lr);
1515 }
1516
1517 // Paused: Do nothing when speed is zero. The frame-time clock restarts on resume, so the pause isn't reported as one long frame.
1518 if (lr->core.speed == 0.0f) {
1519 lr->core.runloop_frame_time_last = 0;
1520 return;
1521 }
1522
1523 // Keep any active rumble going until the core sets it back to 0.
1524 for (unsigned port = 0; port < SDL_LIBRETRO_MAX_USERS; port++) {
1525 if (lr->core.rumbleStrong[port] > 0.0f || lr->core.rumbleWeak[port] > 0.0f) {
1526 SDL_Libretro_ApplyRumble(lr, port);
1527 }
1528 }
1529
1530 // Rewind mode: step backwards when speed is negative.
1531 if (lr->rewindEnabled && lr->core.speed < 0.0f) {
1532 Uint64 nowNS = SDL_LIBRETRO_GET_TICKS_NS();
1533 if (lr->core.lastTickNS == 0) {
1534 lr->core.lastTickNS = nowNS;
1535 }
1536 double frameTime = (double)(nowNS - lr->core.lastTickNS) / 1.0e9;
1537 lr->core.lastTickNS = nowNS;
1538 double framePeriod = (lr->core.fps > 0.0) ? (1.0 / lr->core.fps) : (1.0 / 60.0);
1539 // 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.
1540 unsigned interval = lr->rewindCaptureInterval > 0 ? lr->rewindCaptureInterval : 1;
1541 double stepPeriod = framePeriod * (double)interval;
1542 lr->core.speedAccumulator += frameTime * (double)(-lr->core.speed);
1543
1544 // Protect against infinite rewinds, and clamp it to 1 tick.
1545 int maxTicks = (int)(-lr->core.speed + 1.0f);
1546 if (maxTicks < 1) maxTicks = 1;
1547
1548 // Protect with a maximum accumulator, so frame spikes don't drain the rewind buffer.
1549 double maxAccumulator = stepPeriod * (double)maxTicks;
1550 if (lr->core.speedAccumulator > maxAccumulator) {
1551 lr->core.speedAccumulator = maxAccumulator;
1552 }
1553
1554 // Mute audio and neutralize input for the throwaway re-runs that produce the displayed frames while scrubbing backward.
1555 lr->rewindActive = true;
1556 while (lr->core.speedAccumulator >= stepPeriod && maxTicks-- > 0 && !lr->core.shutdown) {
1557 lr->core.speedAccumulator -= stepPeriod;
1558 if (!SDL_Libretro_RewindStepState(lr)) break;
1559 lr->core.symbols.retro_run();
1560 }
1561 lr->rewindActive = false;
1562 // Rewind steps bypass the frame-time callback, so restart its clock for when forward play resumes.
1563 lr->core.runloop_frame_time_last = 0;
1564 return;
1565 }
1566
1567 // 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.
1568 SDL_Libretro_UpdateDRC(lr, lr->core.speed);
1569
1570 // Wall-clock delta since the previous RunFrame.
1571 Uint64 nowNS = SDL_LIBRETRO_GET_TICKS_NS();
1572 if (lr->core.lastTickNS == 0) {
1573 // First call: seed the clock and run exactly one tick.
1574 lr->core.lastTickNS = nowNS;
1575 lr->core.speedAccumulator = 0.0;
1576 SDL_Libretro_Tick(lr, SDL_Libretro_FrameTimeDelta(lr, 0, 1));
1577 return;
1578 }
1579
1580 // Calculate the frame time in seconds.
1581 double frameTime = (double)(nowNS - lr->core.lastTickNS) / 1.0e9;
1582 lr->core.lastTickNS = nowNS;
1583
1584 // Target frame period from the core's declared fps (default 60).
1585 double framePeriod = (lr->core.fps > 0.0) ? (1.0 / lr->core.fps) : (1.0 / 60.0);
1586
1587 // Reference frame-time the core is told about, in microseconds.
1588 retro_usec_t referenceUsec = (retro_usec_t)(framePeriod * 1.0e6 / (double)lr->core.speed);
1589
1590 // 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.
1591 double cadence = (framePeriod > 0.0) ? (frameTime / framePeriod) : 0.0;
1592 if (lr->core.speed == 1.0f && cadence > 0.9 && cadence < 1.1) {
1593 lr->core.speedAccumulator = 0.0;
1594 SDL_Libretro_Tick(lr, SDL_Libretro_FrameTimeDelta(lr, referenceUsec, 1));
1595 return;
1596 }
1597
1598 lr->core.speedAccumulator += frameTime * (double)lr->core.speed;
1599
1600 // Cap iterations to avoid a spiral of death on slow hardware.
1601 int maxTicks = (int)(lr->core.speed + 1.0f);
1602 if (maxTicks < 1) maxTicks = 1;
1603
1604 // 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.
1605 double maxAccumulator = framePeriod * (double)maxTicks;
1606 if (lr->core.speedAccumulator > maxAccumulator) {
1607 lr->core.speedAccumulator = maxAccumulator;
1608 }
1609
1610 // Count the ticks needed to catch up, so the measured frame time can be shared between them.
1611 int ticks = 0;
1612 while (lr->core.speedAccumulator >= framePeriod && ticks < maxTicks) {
1613 lr->core.speedAccumulator -= framePeriod;
1614 ticks++;
1615 }
1616 if (ticks == 0) return;
1617
1618 // Run the required number of ticks.
1619 retro_usec_t frameTimeUsec = SDL_Libretro_FrameTimeDelta(lr, referenceUsec, ticks);
1620 for (int i = 0; i < ticks && !lr->core.shutdown; i++) {
1621 SDL_Libretro_Tick(lr, frameTimeUsec);
1622 }
1623}
1624
1636bool SDL_Libretro_ShouldQuit(const SDL_Libretro* lr) {
1637 return lr && lr->core.shutdown;
1638}
1639
1648 return SDL_LIBRETRO_VERSION;
1649}
1650
1656static void SDL_Libretro_PushEvent(SDL_Libretro* lr, Uint32 type, void* data) {
1657 SDL_Event event;
1658 SDL_zero(event);
1659 event.user.type = type;
1660 event.user.data1 = lr;
1661 event.user.data2 = data;
1662 SDL_PushEvent(&event);
1663}
1664
1671static bool SDLCALL SDL_Libretro_KeepUnclaimedEnvEvent(void* userdata, SDL_Event* event) {
1672 const SDL_UserEvent* pushed = (const SDL_UserEvent*)userdata;
1673 return !(event->type == pushed->type && event->user.data1 == pushed->data1 && event->user.reserved == pushed->reserved);
1674}
1675
1692static bool SDL_Libretro_PushEnvEvent(SDL_Libretro* lr, unsigned cmd, void* data) {
1693 // Only commands that map onto their own event type are reported, so an
1694 // out-of-range number can't pose as another command or a library event.
1695 unsigned flags = cmd & (unsigned)(RETRO_ENVIRONMENT_EXPERIMENTAL | RETRO_ENVIRONMENT_PRIVATE);
1696 if ((cmd & ~flags) >= 0x400 || flags == (unsigned)(RETRO_ENVIRONMENT_EXPERIMENTAL | RETRO_ENVIRONMENT_PRIVATE)) {
1697 return false;
1698 }
1699
1700 static SDL_AtomicInt sequence;
1701 SDL_Event event;
1702 SDL_zero(event);
1703 event.user.type = SDL_EVENT_LIBRETRO_ENVIRONMENT(cmd);
1704 event.user.reserved = (Uint32)SDL_AddAtomicInt(&sequence, 1) + 1;
1705 event.user.data1 = lr;
1706 event.user.data2 = data;
1707 const SDL_UserEvent pushed = event.user;
1708
1709 // SDL_PushEvent() returns false when the filter drops the event, which a
1710 // filter that claimed the command may do, so only user.code decides.
1711 const bool queued = SDL_PushEvent(&event);
1712 if (event.user.code == 0) {
1713 return false;
1714 }
1715
1716 // A watch implemented the command, so each event is delivered exactly
1717 // once: the queued copy would only re-report it with an expired data
1718 // pointer, so drop it. Unclaimed commands stay queued for observation.
1719 if (queued) {
1720 SDL_FilterEvents(SDL_Libretro_KeepUnclaimedEnvEvent, (void*)&pushed);
1721 }
1722 return true;
1723}
1724
1725// Directory
1726
1727static void SDL_Libretro_FreeCoreLibrary(SDL_Libretro* lr) {
1728 if (lr->coreLibrary) {
1729 for (unsigned i = 0; i < lr->coreLibraryCount; i++) {
1730 SDL_free(lr->coreLibrary[i].corename);
1731 SDL_free(lr->coreLibrary[i].supported_extensions);
1732 SDL_free(lr->coreLibrary[i].path);
1733 }
1734 SDL_free(lr->coreLibrary);
1735 lr->coreLibrary = NULL;
1736 }
1737 lr->coreLibraryCount = 0;
1738}
1739
1740// The file extension for the cores on this platform
1741#if defined(SDL_PLATFORM_WINDOWS)
1742#define SDL_LIBRETRO_CORE_EXTENSION ".dll"
1743#elif defined(SDL_PLATFORM_APPLE)
1744#define SDL_LIBRETRO_CORE_EXTENSION ".dylib"
1745#elif defined(SDL_PLATFORM_EMSCRIPTEN)
1746#define SDL_LIBRETRO_CORE_EXTENSION ".wasm"
1747#else
1748#define SDL_LIBRETRO_CORE_EXTENSION ".so"
1749#endif
1750
1751static SDL_EnumerationResult SDLCALL SDL_Libretro_SetCoreDirectory_Iterator(void* userdata, const char* dirname, const char* fname) {
1752 SDL_Libretro* lr = (SDL_Libretro*)userdata;
1753
1754 // Find all .info files in the core directory.
1755 const char* dot = SDL_strrchr(fname, '.');
1756 if (!dot || SDL_strcasecmp(dot, ".info") != 0) {
1757 return SDL_ENUM_CONTINUE;
1758 }
1759
1760 // Get the path.
1761 char infoPath[SDL_LIBRETRO_MAX_PATH];
1762 SDL_snprintf(infoPath, sizeof(infoPath), "%s%s", dirname, fname);
1763
1764 // Load the .ini file.
1765 SDL_ini* ini = INI_Load(infoPath);
1766 if (!ini) {
1767 return SDL_ENUM_CONTINUE;
1768 }
1769
1770 // Make sure it's a valid info file.
1771 const char* corename = INI_GetString(ini, NULL, "corename", NULL);
1772 if (!corename) {
1773 INI_Destroy(ini);
1774 return SDL_ENUM_CONTINUE;
1775 }
1776
1777 // Grow the core library by one entry.
1778 SDL_Libretro_CoreInfo* grown = (SDL_Libretro_CoreInfo*)SDL_realloc(
1779 lr->coreLibrary,
1780 (lr->coreLibraryCount + 1) * sizeof(SDL_Libretro_CoreInfo));
1781 if (!grown) {
1782 INI_Destroy(ini);
1783 SDL_SetError("[SDL_Libretro] Out of memory building the core library");
1784 return SDL_ENUM_FAILURE;
1785 }
1786 lr->coreLibrary = grown;
1787
1788 // Build the SDL_Libretro_CoreInfo structure.
1789 SDL_Libretro_CoreInfo* entry = &lr->coreLibrary[lr->coreLibraryCount];
1790 const char* extensions = INI_GetString(ini, NULL, "supported_extensions", NULL);
1791 entry->corename = SDL_strdup(corename);
1792 entry->supported_extensions = extensions ? SDL_strdup(extensions) : NULL;
1793 entry->needs_fullpath = INI_GetBoolean(ini, NULL, "needs_fullpath", false);
1794 entry->supports_no_game = INI_GetBoolean(ini, NULL, "supports_no_game", false);
1795 entry->block_extract = INI_GetBoolean(ini, NULL, "block_extract", false);
1796
1797 // Build the path for the core.
1798 char base[SDL_LIBRETRO_MAX_PATH];
1799 size_t baseLen = (size_t)(dot - fname);
1800 SDL_strlcpy(base, fname, (baseLen < sizeof(base)) ? baseLen + 1 : sizeof(base));
1801 char corePath[SDL_LIBRETRO_MAX_PATH];
1802 SDL_snprintf(corePath, sizeof(corePath), "%s%s%s", dirname, base, SDL_LIBRETRO_CORE_EXTENSION);
1803 entry->path = SDL_strdup(corePath);
1804
1805 INI_Destroy(ini);
1806
1807 // Only publish the entry once every string copied, so the library never holds NULL names or paths.
1808 if (!entry->corename || !entry->path || (extensions && !entry->supported_extensions)) {
1809 SDL_free(entry->corename);
1810 SDL_free(entry->supported_extensions);
1811 SDL_free(entry->path);
1812 SDL_SetError("[SDL_Libretro] Out of memory building the core library");
1813 return SDL_ENUM_FAILURE;
1814 }
1815 lr->coreLibraryCount++;
1816 return SDL_ENUM_CONTINUE;
1817}
1818
1824static int SDLCALL SDL_Libretro_CompareCoreInfo(const void* a, const void* b) {
1825 const SDL_Libretro_CoreInfo* left = (const SDL_Libretro_CoreInfo*)a;
1826 const SDL_Libretro_CoreInfo* right = (const SDL_Libretro_CoreInfo*)b;
1827 int result = SDL_strcasecmp(left->corename, right->corename);
1828 return result != 0 ? result : SDL_strcmp(left->path, right->path);
1829}
1830
1844bool SDL_Libretro_SetCoreDirectory(SDL_Libretro* lr, const char* path) {
1845 if (!lr) return false;
1846 const char* newPath = path ? path : "";
1847 SDL_strlcpy(lr->coreDirectory, newPath, sizeof(lr->coreDirectory));
1848
1849 // When the core directory changed, rebuild the core library.
1850 SDL_Libretro_FreeCoreLibrary(lr);
1851 if (lr->coreDirectory[0] == '\0') return true;
1852
1853 SDL_PathInfo info;
1854 if (!SDL_GetPathInfo(lr->coreDirectory, &info) || info.type != SDL_PATHTYPE_DIRECTORY) {
1855 SDL_SetError("[SDL_Libretro] Core directory '%s' is not a directory", lr->coreDirectory);
1856 return false;
1857 }
1858
1859 if (!SDL_EnumerateDirectory(lr->coreDirectory, &SDL_Libretro_SetCoreDirectory_Iterator, (void*)lr)) {
1860 SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "[SDL_Libretro] Failed to scan core directory '%s': %s", lr->coreDirectory, SDL_GetError());
1861 return false;
1862 }
1863
1864 // Directory order varies by platform and filesystem, so sort for a stable pick.
1865 if (lr->coreLibraryCount > 1) {
1866 SDL_qsort(lr->coreLibrary, lr->coreLibraryCount, sizeof(SDL_Libretro_CoreInfo), SDL_Libretro_CompareCoreInfo);
1867 }
1868 return true;
1869}
1870
1882static bool SDL_Libretro_GetPrefDirectory(char* dst, size_t dstSize) {
1883 const char* appName = SDL_GetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING);
1884 const char* orgName = SDL_GetAppMetadataProperty(SDL_PROP_APP_METADATA_CREATOR_STRING);
1885 char* prefPath = SDL_GetPrefPath(orgName && orgName[0] ? orgName : "SDL_libretro", appName && appName[0] ? appName : "SDL_libretro");
1886 if (!prefPath) return false;
1887
1888 // Ensure it's a clean path; directory paths are composed as "<dir>/<file>".
1889 size_t len = SDL_strlen(prefPath);
1890 while (len > 0 && (prefPath[len - 1] == '/' || prefPath[len - 1] == '\\')) {
1891 prefPath[--len] = '\0';
1892 }
1893 SDL_strlcpy(dst, prefPath, dstSize);
1894 SDL_free(prefPath);
1895 return dst[0] != '\0';
1896}
1897
1910bool SDL_Libretro_SetSaveDirectory(SDL_Libretro* lr, const char* path) {
1911 if (!lr) return false;
1912
1913 if (!path) {
1914 // NULL resets to the default: "saves" inside the app's pref path.
1915 char pref[SDL_LIBRETRO_MAX_PATH];
1916 if (SDL_Libretro_GetPrefDirectory(pref, sizeof(pref))) {
1917 SDL_snprintf(lr->saveDirectory, sizeof(lr->saveDirectory), "%s/saves", pref);
1918 }
1919 else {
1920 lr->saveDirectory[0] = '\0';
1921 }
1922 return true;
1923 }
1924
1925 SDL_strlcpy(lr->saveDirectory, path, sizeof(lr->saveDirectory));
1926 return true;
1927}
1928
1940bool SDL_Libretro_SetSystemDirectory(SDL_Libretro* lr, const char* path) {
1941 if (!lr) return false;
1942
1943 if (!path) {
1944 // NULL resets to the default: "system" inside the app's pref path.
1945 char pref[SDL_LIBRETRO_MAX_PATH];
1946 if (SDL_Libretro_GetPrefDirectory(pref, sizeof(pref))) {
1947 SDL_snprintf(lr->systemDirectory, sizeof(lr->systemDirectory), "%s/system", pref);
1948 }
1949 else {
1950 lr->systemDirectory[0] = '\0';
1951 }
1952 return true;
1953 }
1954
1955 SDL_strlcpy(lr->systemDirectory, path, sizeof(lr->systemDirectory));
1956 return true;
1957}
1958
1968bool SDL_Libretro_SetCoreAssetsDirectory(SDL_Libretro* lr, const char* path) {
1969 if (!lr) return false;
1970 SDL_strlcpy(lr->coreAssetsDirectory, path ? path : "", sizeof(lr->coreAssetsDirectory));
1971 return true;
1972}
1973
1982const char* SDL_Libretro_GetCoreDirectory(SDL_Libretro* lr) {
1983 if (!lr) return NULL;
1984 return lr->coreDirectory[0] ? lr->coreDirectory : NULL;
1985}
1986
1995const char* SDL_Libretro_GetSaveDirectory(SDL_Libretro* lr) {
1996 if (!lr) return NULL;
1997 return lr->saveDirectory[0] ? lr->saveDirectory : NULL;
1998}
1999
2008const char* SDL_Libretro_GetSystemDirectory(SDL_Libretro* lr) {
2009 if (!lr) return NULL;
2010 return lr->systemDirectory[0] ? lr->systemDirectory : NULL;
2011}
2012
2021const char* SDL_Libretro_GetCoreAssetsDirectory(SDL_Libretro* lr) {
2022 if (!lr) return NULL;
2023 return lr->coreAssetsDirectory[0] ? lr->coreAssetsDirectory : NULL;
2024}
2025
2037bool SDL_Libretro_SetUsername(SDL_Libretro* lr, const char* username) {
2038 if (!lr) return false;
2039 SDL_strlcpy(lr->username, username ? username : "", sizeof(lr->username));
2040 return true;
2041}
2042
2051const char* SDL_Libretro_GetUsername(SDL_Libretro* lr) {
2052 if (!lr) return NULL;
2053 return lr->username;
2054}
2055
2066void SDL_Libretro_SetVolume(SDL_Libretro* lr, float volume) {
2067 if (!lr) return;
2068 lr->volume = SDL_clamp(volume, 0.0f, 1.0f);
2069 if (lr->core.audioStream) {
2070 SDL_SetAudioStreamGain(lr->core.audioStream, lr->volume);
2071 }
2072}
2073
2082float SDL_Libretro_GetVolume(const SDL_Libretro* lr) {
2083 return lr ? lr->volume : 0.0f;
2084}
2085
2105void SDL_Libretro_SetSpeed(SDL_Libretro* lr, float speed) {
2106 if (!lr) return;
2107
2108 if (lr->core.fastforwardOverrideActive && lr->core.fastforwardOverride.inhibit_toggle) return;
2109
2110 if (speed < 0.0f && lr->rewindEnabled && lr->rewindReference) {
2111 lr->core.speed = speed;
2112 if (lr->core.audioStream) {
2113 lr->core.drcAdjustment = 1.0f;
2114 lr->core.drcDriftAvg = 0.0;
2115 SDL_ClearAudioStream(lr->core.audioStream);
2116 // Pitch and consume the reversed audio at the rewind speed, mirroring
2117 // the forward path (which sets the ratio to speed * drcAdjustment).
2118 SDL_SetAudioStreamFrequencyRatio(lr->core.audioStream, -speed);
2119 }
2120 }
2121 else {
2122 lr->core.speed = SDL_max(speed, 0.0f);
2123 }
2124
2125 if (lr->core.audioStream && lr->core.speed > 0.0f) {
2126 lr->core.drcAdjustment = 1.0f;
2127 lr->core.drcDriftAvg = 0.0;
2128 SDL_SetAudioStreamFrequencyRatio(lr->core.audioStream, lr->core.speed * lr->core.drcAdjustment);
2129 }
2130}
2131
2142float SDL_Libretro_GetSpeed(const SDL_Libretro* lr) {
2143 return lr ? lr->core.speed : 1.0f;
2144}
2145
2157bool SDL_Libretro_IsFastforwardOverrideActive(const SDL_Libretro* lr) {
2158 return lr && lr->core.fastforwardOverrideActive;
2159}
2160
2170void SDL_Libretro_SetLogLevel(SDL_Libretro* lr, SDL_LogPriority level) {
2171 if (!lr) return;
2172 switch (level) {
2173 case SDL_LOG_PRIORITY_INFO: lr->logLevel = RETRO_LOG_INFO; break;
2174 case SDL_LOG_PRIORITY_WARN: lr->logLevel = RETRO_LOG_WARN; break;
2175 case SDL_LOG_PRIORITY_ERROR: lr->logLevel = RETRO_LOG_ERROR; break;
2176 case SDL_LOG_PRIORITY_CRITICAL: lr->logLevel = RETRO_LOG_ERROR; break;
2177 default: lr->logLevel = RETRO_LOG_DEBUG; break;
2178 }
2179}
2180
2189SDL_LogPriority SDL_Libretro_GetLogLevel(const SDL_Libretro* lr) {
2190 if (!lr) return SDL_LOG_PRIORITY_INVALID;
2191 switch (lr->logLevel) {
2192 case RETRO_LOG_DEBUG: return SDL_LOG_PRIORITY_DEBUG;
2193 case RETRO_LOG_INFO: return SDL_LOG_PRIORITY_INFO;
2194 case RETRO_LOG_WARN: return SDL_LOG_PRIORITY_WARN;
2195 case RETRO_LOG_ERROR: return SDL_LOG_PRIORITY_ERROR;
2196 default: return SDL_LOG_PRIORITY_INVALID;
2197 }
2198}
2199
2206const char* SDL_Libretro_GetCoreName(const SDL_Libretro* lr) {
2207 return SDL_Libretro_IsCoreReady(lr) ? lr->core.libraryName : "";
2208}
2209
2219const char* SDL_Libretro_GetGameName(const SDL_Libretro* lr) {
2220 return SDL_Libretro_IsGameReady(lr) ? lr->core.contentName : "";
2221}
2222
2229const char* SDL_Libretro_GetCoreVersion(const SDL_Libretro* lr) {
2230 return SDL_Libretro_IsCoreReady(lr) ? lr->core.libraryVersion : "";
2231}
2232
2239const char* SDL_Libretro_GetValidExtensions(const SDL_Libretro* lr) {
2240 return SDL_Libretro_IsCoreReady(lr) ? lr->core.validExtensions : "";
2241}
2242
2251unsigned SDL_Libretro_GetPerformanceLevel(const SDL_Libretro* lr) {
2252 return lr ? lr->core.performanceLevel : 0;
2253}
2254
2263enum retro_savestate_context SDL_Libretro_GetSavestateContext(const SDL_Libretro* lr) {
2264 return lr ? lr->core.savestateContext : RETRO_SAVESTATE_CONTEXT_NORMAL;
2265}
2266
2282void SDL_Libretro_SetSavestateContext(SDL_Libretro* lr, enum retro_savestate_context context) {
2283 if (lr) lr->core.savestateContext = context;
2284}
2285
2292bool SDL_Libretro_GetBlockExtract(const SDL_Libretro* lr) {
2293 return SDL_Libretro_IsCoreReady(lr) && lr->core.blockExtract;
2294}
2295
2304const char* SDL_Libretro_GetContentExtension(const SDL_Libretro* lr) {
2305 if (!lr || lr->core.contentPath[0] == '\0') return "";
2306 return SDL_Libretro_GetExtension(lr->core.contentPath);
2307}
2308
2316static bool SDL_Libretro_ExtensionInList(const char* ext, const char* pipeList) {
2317 if (!ext || !pipeList) return false;
2318 size_t extLen = SDL_strlen(ext);
2319 const char* p = pipeList;
2320 while (*p) {
2321 const char* sep = SDL_strchr(p, '|');
2322 size_t segLen = sep ? (size_t)(sep - p) : SDL_strlen(p);
2323 if (segLen == extLen && SDL_strncasecmp(p, ext, extLen) == 0) return true;
2324 if (!sep) break;
2325 p = sep + 1;
2326 }
2327 return false;
2328}
2329
2330#undef LOAD_SYM
2331
2332#endif /* SDL_LIBRETRO_CORE_IMPL_ONCE */
#define SDL_LIBRETRO_MAX_USERS
The number of controller ports (gamepads) the frontend tracks.
#define LOAD_SYM(sym)
Load a required core symbol in lr->core.
#define SDL_LIBRETRO_GET_TICKS_NS()
The clock that paces SDL_Libretro_Update() and the frame-time callback, in nanoseconds.
#define SDL_LIBRETRO_REWIND_DEFAULT_MAX_BYTES
Default ceiling on the encoded rewind history (delta data only), in bytes.
bool SDL_Libretro_IsCoreReady(const SDL_Libretro *lr)
Indicates whether a core is loaded.
bool SDL_Libretro_SetCoreAssetsDirectory(SDL_Libretro *lr, const char *path)
Sets where cores look for their assets, reported through RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY.
bool SDL_Libretro_SetSystemDirectory(SDL_Libretro *lr, const char *path)
Sets where cores look for system files (BIOS, firmware, etc).
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_GetUsername(SDL_Libretro *lr)
Get the username reported to cores.
const char * SDL_Libretro_GetContentExtension(const SDL_Libretro *lr)
Get the extension of the loaded content, as it appears in the path.
void SDL_Libretro_UnloadCore(SDL_Libretro *lr)
Unloads the actively loaded core.
bool SDL_Libretro_LoadGameSpecial(SDL_Libretro *lr, const char *subsystem, const char **paths, unsigned numPaths)
Loads multi-ROM subsystem content (e.g.
void SDL_Libretro_SetLogLevel(SDL_Libretro *lr, SDL_LogPriority level)
Sets the threshold for logs to be posted.
bool SDL_Libretro_Reset(SDL_Libretro *lr)
Resets the loaded game, like pressing the console's reset button.
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_PhysFS_LoadGame(SDL_Libretro *lr, const char *gamePath)
Archive-aware replacement for SDL_Libretro_LoadGame().
const char * SDL_Libretro_GetGameName(const SDL_Libretro *lr)
Retrieve the human-readable name of the loaded game.
int SDL_Libretro_GetVersion(void)
Get the version of SDL_libretro the implementation was compiled with.
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.
void SDL_Libretro_Update(SDL_Libretro *lr)
Runs the loaded game; call it once per frame of the application's loop.
const char * SDL_Libretro_GetValidExtensions(const SDL_Libretro *lr)
Gets the default set of valid extensions associated with the core, seperated by a "|".
bool SDL_Libretro_LoadGame_IO(SDL_Libretro *lr, SDL_IOStream *src, const char *path, bool closeio)
Loads a game from an SDL_IOStream.
bool SDL_Libretro_GetBlockExtract(const SDL_Libretro *lr)
Retrieves whether the core expects archives to be loaded directly.
void SDL_Libretro_SetVFS(SDL_Libretro *lr, void *vfs)
Set the Virtual File System the core uses for file access.
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.
float SDL_Libretro_GetSpeed(const SDL_Libretro *lr)
Get how fast the game runs, as a multiple of its normal speed.
void SDL_Libretro_PhysFS_Quit(SDL_Libretro *lr)
Tear down PhysFS, unmounting any active archive and restoring the plain SDL VFS on the given context.
#define SDL_EVENT_LIBRETRO
The base SDL event type for SDL_libretro events.
enum retro_savestate_context SDL_Libretro_GetSavestateContext(const SDL_Libretro *lr)
Get the savestate context reported to cores through RETRO_ENVIRONMENT_GET_SAVESTATE_CONTEXT.
void SDL_Libretro_SetVolume(SDL_Libretro *lr, float volume)
Set the audio volume when playing sounds.
const char * SDL_Libretro_GetCoreAssetsDirectory(SDL_Libretro *lr)
Get the core assets directory.
#define SDL_EVENT_LIBRETRO_LAST
The last SDL event type in the SDL_libretro range.
struct SDL_Libretro SDL_Libretro
A libretro frontend context: one loaded core, its game, and the settings that carry across cores.
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.
float SDL_Libretro_GetVolume(const SDL_Libretro *lr)
Get the audio volume.
void SDL_Libretro_Destroy(SDL_Libretro *lr)
Destroys the given libretro context.
#define SDL_EVENT_LIBRETRO_CORE_UNLOADED
The core was unloaded, along with any game it ran.
SDL_Libretro * SDL_Libretro_Create(void)
Creates a new libretro context.
#define SDL_EVENT_LIBRETRO_GAME_UNLOADED
The game was unloaded, whether directly or through the core unloading.
const char * SDL_Libretro_GetSystemDirectory(SDL_Libretro *lr)
Get the system directory.
void SDL_Libretro_UnloadGame(SDL_Libretro *lr)
Unloads the actively loaded game.
bool SDL_Libretro_SetSaveDirectory(SDL_Libretro *lr, const char *path)
Sets where content will be saved.
bool SDL_Libretro_LoadGameSpecialById(SDL_Libretro *lr, unsigned subsystemId, const char **paths, unsigned numPaths)
Load multi-ROM subsystem content by its numeric subsystem id.
bool SDL_Libretro_IsGameReady(const SDL_Libretro *lr)
Indicates whether a game is loaded and running.
#define SDL_EVENT_LIBRETRO_CORE_LOADED
A core finished loading; data2 is the core name, valid until the next load or unload.
bool SDL_Libretro_LoadCore(SDL_Libretro *lr, const char *core)
Loads a libretro core.
bool SDL_Libretro_SetUsername(SDL_Libretro *lr, const char *username)
Sets the username reported to cores through RETRO_ENVIRONMENT_GET_USERNAME.
bool SDL_Libretro_IsGameRequired(const SDL_Libretro *lr)
Indicates whether the loaded core needs content to run.
#define SDL_EVENT_LIBRETRO_ENVIRONMENT(cmd)
The SDL event type a RETRO_ENVIRONMENT_* command arrives as, when SDL_libretro leaves the command to ...
const char * SDL_Libretro_GetCoreDirectory(SDL_Libretro *lr)
Get the core directory.
const char * SDL_Libretro_GetSaveDirectory(SDL_Libretro *lr)
Get the save directory.
void SDL_Libretro_SetSavestateContext(SDL_Libretro *lr, enum retro_savestate_context context)
Sets the savestate context reported to cores through RETRO_ENVIRONMENT_GET_SAVESTATE_CONTEXT.
bool SDL_Libretro_IsFastforwardOverrideActive(const SDL_Libretro *lr)
Indicates whether the core is forcing fast-forward through RETRO_ENVIRONMENT_SET_FASTFORWARDING_OVERR...
void SDL_Libretro_SetLanguage(SDL_Libretro *lr, int language)
Override the language reported to cores through RETRO_ENVIRONMENT_GET_LANGUAGE.
#define SDL_EVENT_LIBRETRO_GAME_LOADED
A game finished loading, whether directly or through the menu; data2 is the game name,...
const char * SDL_Libretro_GetCoreName(const SDL_Libretro *lr)
Retrieve the name of the libretro core that's actively loaded.
void SDL_Libretro_SetSpeed(SDL_Libretro *lr, float speed)
Sets how fast the game runs, as a multiple of its normal speed.