SDL_Libretro 0.1.0
SDL3-power libretro frontend.
Loading...
Searching...
No Matches
SDL_libretro

A libretro frontend library for SDL3.

Features

  • Header-only C99 library
  • Audio input and dynamic rate control output
  • Input with gamepad, keyboard, mouse, lightgun, or pointer
  • Rumble, accelerometer, gyroscope
  • Save states and SRAM
  • Fast-forward, slow-motion, rewind
  • Disk control for multi-disc games
  • Virtual file system
  • Core options
  • Config system for saving and loading settings
  • On-screen display messages
  • Zip loading with PhysicsFS (optional)
  • Menu with nuklear_console (optional)

Usage

Define SDL_LIBRETRO_IMPLEMENTATION in exactly one .c file before including the header:

#define SDL_LIBRETRO_IMPLEMENTATION
#include "SDL_libretro.h"
SDL_libretro - SDL3 libretro frontend library.

All other files include SDL_libretro.h normally without the define.

Quickstart

SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMEPAD);
SDL_Window* window = SDL_CreateWindow("SDL_libretro", 800, 600, SDL_WINDOW_RESIZABLE);
SDL_Renderer* renderer = SDL_CreateRenderer(window, NULL);
SDL_Libretro_LoadCore(lr, "core.so");
SDL_Libretro_LoadGame(lr, "game.rom");
bool running = true;
while (running && !SDL_Libretro_ShouldQuit(lr)) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_EVENT_QUIT) {
running = false;
}
}
SDL_RenderClear(renderer);
SDL_Libretro_Render(lr, renderer, NULL);
SDL_RenderPresent(renderer);
}
bool SDL_Libretro_Render(SDL_Libretro *lr, SDL_Renderer *renderer, const SDL_FRect *dstRect)
Render the libretro context in the given renderer.
void SDL_Libretro_Update(SDL_Libretro *lr)
Runs the loaded game; call it once per frame of the application's loop.
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.
void SDL_Libretro_Destroy(SDL_Libretro *lr)
Destroys the given libretro context.
SDL_Libretro * SDL_Libretro_Create(void)
Creates a new libretro context.
bool SDL_Libretro_LoadCore(SDL_Libretro *lr, const char *core)
Loads a libretro core.
void SDL_Libretro_HandleEvent(SDL_Libretro *lr, const SDL_Event *event)
Call this function within the SDL_PollEvent() loop to ensure libretro knows of important events.

Cores and Content

SDL_Libretro_SetCoreDirectory() scans a directory for cores and their .info files, and returns false when the directory doesn't exist. The core library is sorted by core name, so when SDL_Libretro_LoadGame() is called with no core loaded, the first core in that order that supports the game's extension wins.

Cores that support running without content (see SDL_Libretro_IsGameRequired()) start with a NULL game:

SDL_Libretro_LoadCore(lr, "core.so");

Input and Language

Disk Control

Multi-disc games swap disks with SDL_Libretro_EjectDisk(), SDL_Libretro_SetDiskIndex() and SDL_Libretro_InsertDisk(). With the tray ejected, SDL_Libretro_AddDiskImage(lr, path) or SDL_Libretro_AddDiskImage_IO(lr, src, path, closeio) appends an image; path is the name the image is known by. Depending on the core, it gets the image's bytes, or, when it needs a full path, a temp file the stream was written to.

Zip Loading

To enable Zip Loading, link PhysFS by enabling the SDL_LIBRETRO_PHYSFS CMake option, and then let SDL_libretro know it's available with SDL_LIBRETRO_ENABLE_PHYSFS.

#define SDL_LIBRETRO_IMPLEMENTATION
#define SDL_LIBRETRO_ENABLE_PHYSFS
#include "SDL_libretro.h"
// Enables loading games directly from .zip files.
SDL_Libretro_LoadGame(lr, "game.zip");

Cores that set block_extract in their .info file, or that list zip as a supported extension, get the raw .zip instead of the content inside it. Cores that need a file path, but read it from the OS rather than the libretro VFS, get the whole archive extracted to a temp directory, so content like a .cue sheet finds its tracks; it's removed when the game unloads.

Config

SDL_Libretro_InitConfig(lr, org, app) or SDL_Libretro_InitConfigFile(lr, file) loads settings from an .ini file and saves them back when the context is destroyed. Besides each core's options and the keyboard and gamepad bindings, it stores volume, username, audiolatency, fitmode, scalemode, savedirectory, systemdirectory, coredirectory, coreassetsdirectory, rewindenabled, rewindframes, rewindinterval, rewindmemorylimit, mouseport, language and filebrowserdirectory.

Menu

To enable the in-app menu, enable the SDL_LIBRETRO_MENU CMake option (linking the SDL_libretro_menu target), and let SDL_libretro know it's available with SDL_LIBRETRO_ENABLE_MENU.

The menu reports what it does through SDL events (see the Events section below). Applications can also add their own entries with SDL_Libretro_AddMenuButton() and SDL_Libretro_AddMenuCheckbox(), and jump straight to a page with SDL_Libretro_OpenMenuPath(menu, "Settings/Audio & Video").

Besides loading games and core options, the menu rebinds Player 1's keyboard (Settings > Keyboard) and each player's gamepad buttons and analog-to-digital D-Pad (Settings > Gamepad, or SDL_Libretro_SetGamepadMapping() and SDL_Libretro_SetAnalogToDigital() from code), swaps disks for multi-disk games (Disks), and shows core-reported progress messages.

#define SDL_LIBRETRO_IMPLEMENTATION
#define SDL_LIBRETRO_ENABLE_MENU
#include "SDL_libretro.h"
// For each event...
if (!SDL_Libretro_HandleMenuEvent(menu, &event)) {
}
// Each frame...
}
SDL_Libretro_Render(lr, renderer, NULL);
SDL_RenderPresent(renderer);
bool SDL_Libretro_IsMenuOpen(const SDL_LibretroMenu *menu)
Checks whether the menu is currently shown.
struct SDL_LibretroMenu SDL_LibretroMenu
An in-app menu built on nuklear_console.
void SDL_Libretro_UpdateMenu(SDL_LibretroMenu *menu)
Processes input and lays out the menu for this frame.
void SDL_Libretro_RenderMenu(SDL_LibretroMenu *menu)
Draws the menu laid out by SDL_Libretro_UpdateMenu() to the renderer.
SDL_LibretroMenu * SDL_Libretro_CreateMenu(SDL_Libretro *lr)
Creates the in-app menu for a libretro context.
bool SDL_Libretro_SetRenderer(SDL_Libretro *lr, SDL_Renderer *renderer)
Set the renderer the libretro context draws into.
bool SDL_Libretro_HandleMenuEvent(SDL_LibretroMenu *menu, const SDL_Event *event)
Routes an event to the menu; call this for every event, before SDL_Libretro_HandleEvent().

Events

SDL_libretro reports what happens through the SDL event queue, as SDL_UserEvents based at SDL_EVENT_LIBRETRO. event->user.data1 is always the SDL_Libretro* that sent the event.

Every SDL_libretro event falls between SDL_EVENT_LIBRETRO and SDL_EVENT_LIBRETRO_LAST, a fixed range in the SDL user events. The first SDL_Libretro_Create() reserves it through SDL_RegisterEvents(), so events the application registers afterwards never collide with it; events registered before then should stay below SDL_EVENT_LIBRETRO, or a warning is logged.

  • Environment commands the core calls that SDL_libretro doesn't handle itself arrive as SDL_EVENT_LIBRETRO_ENVIRONMENT(RETRO_ENVIRONMENT_*), for example SDL_EVENT_LIBRETRO_ENVIRONMENT(RETRO_ENVIRONMENT_GET_LOCATION_INTERFACE). Experimental and private commands get event types of their own, so they don't collide with plain commands that share their number, and SDL_LIBRETRO_ENVIRONMENT_COMMAND(event->type) turns the type back into the command. Every environment event falls between SDL_EVENT_LIBRETRO and SDL_EVENT_LIBRETRO_ENVIRONMENT_LAST. The data pointer the core passed rides along in event->user.data2.
  • SDL_EVENT_LIBRETRO_CORE_LOADED / SDL_EVENT_LIBRETRO_GAME_LOADED: A core or game finished loading, whether directly or through the menu. data2 is the core or game name.
  • SDL_EVENT_LIBRETRO_CORE_UNLOADED / SDL_EVENT_LIBRETRO_GAME_UNLOADED: The core or game was unloaded; unloading a core reports the game first.
  • SDL_EVENT_LIBRETRO_SHUTDOWN: The core requested shutdown, mirroring SDL_Libretro_ShouldQuit().
  • SDL_EVENT_LIBRETRO_GEOMETRY_CHANGED: The video size, aspect ratio, or timing changed mid-game; query SDL_Libretro_GetSize() and friends for the new values.
  • SDL_EVENT_LIBRETRO_MENU_OPENED / SDL_EVENT_LIBRETRO_MENU_CLOSED: The menu became visible (the game pauses) or was dismissed (the game resumes). data2 is the SDL_LibretroMenu*.
while (SDL_PollEvent(&event)) {
switch (event.type) {
SDL_Log("Loaded: %s", (const char*)event.user.data2);
break;
case SDL_EVENT_LIBRETRO_ENVIRONMENT(RETRO_ENVIRONMENT_GET_LOCATION_INTERFACE):
SDL_Log("Core asked for location services");
break;
}
}
#define SDL_EVENT_LIBRETRO_ENVIRONMENT(cmd)
The SDL event type a RETRO_ENVIRONMENT_* command arrives as, when SDL_libretro leaves the command to ...
#define SDL_EVENT_LIBRETRO_GAME_LOADED
A game finished loading, whether directly or through the menu; data2 is the game name,...

An environment command's data2 pointer is only valid while the core waits inside the environment call, which has already returned by the time the event is polled. To implement an environment command, handle the event from an SDL_AddEventWatch() callback instead — watches run synchronously while the core waits. Set event->user.code to a non-zero value there to tell the core the command succeeded; a claimed command is consumed by the watch and doesn't also reach the event queue, while earlier unclaimed copies of the same command stay queued. An SDL_SetEventFilter() filter can claim a command the same way, and may return false to drop the event.

static bool SDLCALL MyEventWatch(void* userdata, SDL_Event* event) {
if (event->type == SDL_EVENT_LIBRETRO_ENVIRONMENT(RETRO_ENVIRONMENT_GET_CAMERA_INTERFACE)) {
struct retro_camera_callback* camera = event->user.data2;
// ... fill in the camera interface ...
event->user.code = 1; // Tell the core the command succeeded.
}
return true;
}
SDL_AddEventWatch(MyEventWatch, NULL);

Build

git clone --recurse-submodules https://github.com/RobLoach/SDL_libretro.git
cd SDL_libretro
mkdir build && cd build
cmake ..
cmake --build .

To build a release instead, download its SDL_libretro-<version>.tar.gz, which includes the git submodules. GitHub's generated "Source code" archives leave them out.

An SDL3 already installed on the system is used when found (point CMAKE_PREFIX_PATH at it if it's somewhere CMake doesn't look). Otherwise, SDL3 is fetched and built via CMake FetchContent.

Emscripten

With the Emscripten SDK activated, configure through emcmake:

emcmake cmake -B build-web
cmake --build build-web

Embedding on the Web

The Emscripten build embeds into any webpage with SDL_libretro_embed.js. Serve the build output (SDL_libretro.js, SDL_libretro.wasm and SDL_libretro.data) together with SDL_libretro_embed.js, include the script, and point it at an element on the page:

<div id="player" style="width: 640px; height: 480px"></div>
<script src="SDL_libretro_embed.js"></script>
<script>
SDL_libretro.embed('#player');
</script>

SDL_libretro.embed(target, options) accepts a CSS selector or an element, creates the canvas inside it, and starts the emulator. Options:

  • script: URL of the Emscripten-generated script (default SDL_libretro.js)
  • arguments: Argument list for SDL_libretro, e.g. ‘[’/cores/core.wasm', '/game.rom'] -pixelated: Keep upscaled pixels crisp instead of smoothed (defaulttrue) -onReady: Called with the embed handle once the runtime is initialized -onError: Called with anError` when the script fails to load

See example/SDL_libretro_embed.html for a complete page. The Emscripten build copies both files next to its output, so the build directory is directly servable. One embed can run per page.

Configuration

Use macros before SDL_LIBRETRO_IMPLEMENTATION to change how SDL_Libretro behaves.

  • SDL_LIBRETRO_MAX_USERS: The number of controller ports (default 4)
  • SDL_LIBRETRO_GET_TICKS_NS(): The nanosecond clock that paces SDL_Libretro_Update() and the frame-time callback (default SDL_GetTicksNS()), e.g. to substitute a fake clock in tests
  • SDL_LIBRETRO_AUDIO_DEFAULT_LATENCY_MS: The default audio latency threshold (default 100)
  • SDL_LIBRETRO_AUDIO_DEFAULT_SAMPLE_RATE: The rate used when a core reports no sample rate (default 48000)
  • SDL_LIBRETRO_AUDIO_DRC_MAX_DELTA: How hard dynamic rate control may bend the audio pitch (default 0.005)
  • SDL_LIBRETRO_MIC_DEFAULT_RATE: The default microphone sample rate (default 44100)
  • SDL_LIBRETRO_REWIND_DEFAULT_MAX_BYTES: The default rewind memory limit (default 256 MiB)
  • SDL_LIBRETRO_ENABLE_REWIND_DELTA: Enable the XOR delta between rewind frames to reduce memory at the expense of performance
  • SDL_LIBRETRO_ENABLE_PHYSFS: Enable .zip loading with PhysFS
  • SDL_LIBRETRO_PHYSFS_MOUNT_POINT: The PhysFS mount point archives are mounted at (default "game")
  • SDL_LIBRETRO_ENABLE_MENU: Enable the nuklear_console menu
  • SDL_LIBRETRO_MENU_TOGGLE_KEY: The key that toggles the menu (default SDLK_F1)
  • SDL_LIBRETRO_MENU_FONT_HEIGHT: Base menu font height in pixels (default 13)
  • SDL_LIBRETRO_MENU_DEFAULT_STYLE: The initial menu theme (default SDL_LIBRETRO_MENU_STYLE_CATPPUCCIN_MOCHA)
  • NK_CONSOLE_FILE_NO_SDL_NATIVE_DIALOG: Load Game with the menu's own file browser instead of the OS file dialog. Gamepads, and systems where the OS dialog can't open, use the file browser either way
  • SDL_LIBRETRO_DISABLE_PHYSFS / SDL_LIBRETRO_DISABLE_MENU: Turn PhysFS or the menu back off, even when enabled
  • SDL_LIBRETRO_DISABLE_VFS: Don't provide the SDL-backed libretro VFS, e.g. when implementing your own (PhysFS requires it)
  • SDL_LIBRETRO_NO_INI_IMPLEMENTATION, SDL_LIBRETRO_PHYSFS_NO_SDL_PHYSFS_IMPLEMENTATION, SDL_LIBRETRO_MENU_NO_NK_IMPLEMENTATION: Skip compiling the bundled SDL_ini, SDL_PhysFS or Nuklear implementations when they're already compiled elsewhere
  • SDL_LIBRETRO_MENU_NUKLEAR_H, SDL_LIBRETRO_MENU_NUKLEAR_SDL3_RENDERER_H, SDL_LIBRETRO_MENU_NUKLEAR_GAMEPAD_H, SDL_LIBRETRO_MENU_NUKLEAR_CONSOLE_H: Include paths for the menu's Nuklear headers

Limitations

  • Only one SDL_Libretro context can have a core loaded at a time, as libretro callbacks reach it through a single global. The API isn't thread-safe; use it from the main thread.
  • SRAM is only loaded and saved automatically with SDL_Libretro_SetSRAMAutoSave(), and then only when the game loads and unloads, so a crash loses progress since then (#182). Otherwise, call SDL_Libretro_LoadSRAM() and SDL_Libretro_SaveSRAM() from the application.
  • Web builds don't persist saves between visits (#222).
  • Hardware rendering (RETRO_ENVIRONMENT_SET_HW_RENDER) is unsupported, so cores need software rendering (#14).

Dependencies

Development

Use clang-format to apply coding standards.

clang-format -i include/SDL_libre*

License

[zlib/libpng](LICENSE)