A libretro frontend built with raylib. Ships a single-header C library (core API in include/raylib-libretro.h) plus a full frontend app (bin/raylib-libretro.c). C99, license zlib/libpng.
Headers in include/ are stb-style. Define the implementation macro once, in exactly one translation unit:
#define RAYLIB_LIBRETRO_IMPLEMENTATION
#include "raylib-libretro.h"Each opt-in layer has its own macro — RAYLIB_LIBRETRO_MENU_IMPLEMENTATION, ..._PHYSFS_IMPLEMENTATION, ..._SHADERS_IMPLEMENTATION, ..._TOUCH_IMPLEMENTATION, ..._LOGO_IMPLEMENTATION, ..._ANDROID_IMPLEMENTATION. The VFS layer is pulled in automatically by raylib-libretro.h. A frontend composes only the layers it needs; the example uses just the core header. Guards: include RAYLIB_LIBRETRO_*_H, implementation RAYLIB_LIBRETRO_*_IMPLEMENTATION / ..._IMPLEMENTATION_ONCE.
The core library keeps all its state in one file-static global, static LibretroData LIBRETRO (raylib-libretro.h), with non-zero defaults — no context object is threaded through calls, matching raylib's design. Frontend layers follow the same shape with their own file-static globals (e.g. menu, LibretroPhysFS).
Cores are shared libraries (.so/.dll/.dylib/.wasm) loaded at runtime via dylib.h from libretro-common, using the LoadLibretroMethod(S) macro. PeekLibretroCoreInfo() opens a core just far enough to read retro_get_system_info() (no full init); InitLibretro() does the full load.
The menu builds its core list (ScanLibretroCoreDirectory) from sibling <core>.info files (bundled from the vendor/libretro-core-info submodule) — fast, no dlopen. A core binary with no matching .info is probed directly via PeekLibretroCoreInfo().
The .info fields (supported_extensions, needs_fullpath, supports_no_game) are pre-load hints for menu filtering only — never a hard gate. Authoritative values come from the core's runtime retro_get_system_info() / RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE once loaded, and the two can diverge: e.g. genesis_plus_gx declares needs_fullpath="true" globally (for SegaCD) yet loads cartridge ROMs from memory. Filtering a core out on a coarse .info flag is how zipped Genesis games got wrongly rejected — let the loader make the final per-content decision.
GPU-rendered cores (Beetle PSX HW, Mupen64Plus-Next, Flycast, etc.) negotiate via RETRO_ENVIRONMENT_SET_HW_RENDER. The frontend shares raylib's single GL context with the core — no second/shared/threaded context.
State lives in LIBRETRO.core.hwRender (enabled, active, FBO, lifecycle flags). The gate is hwRender.enabled (set when SET_HW_RENDER succeeds); every HW branch is guarded by it, leaving the software path byte-for-byte untouched.
Per-frame flow (all on the main thread inside LibretroTick, outside BeginDrawing/EndDrawing):
- Flush raylib's batch (
rlDrawRenderBatchActive), bind the frontend-owned FBO. retro_run— the core callsget_current_framebuffer(returns FBO id) andget_proc_address(returnsrlGetProcAddress), renders, then callsvideo_refresh(RETRO_HW_FRAME_BUFFER_VALID, w, h, 0).- Post-run: restore GL state (FBO, viewport, blend, depth, scissor, VAO, sampler objects, pixel-store params) through raylib setters + raw GL so the
RLGLcache stays consistent. Draw()blitsLIBRETRO.core.texture(aliased to the FBO color attachment) with a vertical flip forbottom_left_origincores.
FBO is sized to max_width × max_height from retro_get_system_av_info, built with raylib's LoadRenderTexture (color + depth) so rlgl owns the GL objects and teardown is a plain UnloadRenderTexture. No stencil buffer is provided — cores that request one fall back to depth-only, which keeps the FBO complete across every supported GL/GLES config.
The context lifecycle is funneled through three helpers so every recreation path stays consistent: hw_FireContextReset / hw_FireContextDestroy (bind the FBO, call the core callback, restore the prior target) and hw_RebuildLibretroVideo (destroy → new FBO → reset). context_reset fires once after InitLibretroVideo; context_destroy fires in UnloadLibretroGame before FBO teardown. When a core grows its max geometry via SET_SYSTEM_AV_INFO (e.g. an internal-resolution option), the FBO is rebuilt through hw_RebuildLibretroVideo and the context lifecycle re-fires so the core rebinds to the new framebuffer; a base-dimension or SET_GEOMETRY change is only a smaller visible crop within the existing FBO and never rebuilds it. The public ResetLibretroVideo() wraps the same rebuild for platform glue that detects a genuine GL-context re-creation — but raylib preserves the context across a normal Android pause/resume (it detaches/re-attaches the same EGL context), so it is not called there.
Accepted context types depend on the build: desktop GL 3.3 (OPENGL/OPENGL_CORE ≤ 3.3), GL 4.3 with opt-in rebuild, GLES2. GLES3 requests are accepted when the ES3 build is active but the preferred type advertised via GET_PREFERRED_HW_RENDER is always GLES2 — wasm cores don't have a GLES3 path. Vulkan/D3D cores are rejected cleanly. Test core in tests/test_opengl/.
The core VFS layer (raylib-libretro-vfs.h) exposes three nullable global function-pointer hooks — raylib_libretro_vfs_alt_load_file_data / _stat / _load_dir_files. When set, VFS read/stat/listdir operations consult them before the real filesystem and fall back if they return not-found. This is the bridge that lets a needs_fullpath core read content that lives inside a mounted archive: the PhysFS layer (raylib-libretro-physfs.h) installs PhysFS-backed implementations in InitLibretroPhysFS() (and clears them in CloseLibretroPhysFS()), so when such a core fopens a /game/... virtual path, the VFS serves it from the zip. The core layer stays PhysFS-free; the dependency points one way (frontend → core globals).
git submodule update --init # submodules are required
mkdir build && cd build && cmake .. && cmake --build .Submodules pull raylib, libretro-common, the Nuklear UI stack (nuklear_console, nuklear_gamepad, raylib-nuklear), PhysFS + raylib-physfs, c-vector, raylib-app, and vendor/libretro-core-info. A bundled core whose .info is missing from that submodule is a hard CMake error (bin/CMakeLists.txt → copy_core_info), since a core with no .info would silently never appear in the menu.
Linux deps: xorg-dev, libglu1-mesa-dev. CMake 3.11+. CMake first tries find_package(raylib QUIET), then falls back to building vendor/raylib with a trimmed feature set.
CMake options: BUILD_RAYLIB_LIBRETRO_EXAMPLE (ON), BUILD_RAYLIB_LIBRETRO (ON), BUILD_RAYLIB_LIBRETRO_TEST_CORES (ON, skipped on Web).
Targets:
raylib-libretro-h— INTERFACE target (headers / include path)raylib-libretro-static— static lib (bundles the libretro-common.cfiles it needs)raylib-libretro— the full frontend appraylib-libretro-basic— the minimal example
Tests.yml(push / PR tomaster): Linux only —ubuntu-latest, GCC,cmake -DCMAKE_BUILD_TYPE=Debug. Windows and Web jobs are present but commented out.Release.yml(on release): builds and packages Linux x64, Linux ARM (ubuntu-24.04-arm), and Windows (windows-latest, MSVC).- No unit tests anywhere — CI only validates that things compile/package. PRs are gated on Linux/GCC, but keep code MSVC-clean since releases build Windows.
- Language: C99 (not C++).
- Public API:
PascalCaseverbs —InitLibretro,DrawLibretro,GetLibretroTexture. - Internal helpers:
Libretro-prefixed —LibretroLogger, etc. - All header functions are
static. - Errors: return
bool; log withTraceLog(LOG_ERROR, ...). - Memory: raylib's
MemAlloc/MemFree, never rawmalloc/free. Prefer raylib built-ins (TextCopy,IsFileExtension,LoadFileData, …) over<string.h>/stdio. - File headers: raylib-style
/**** … ****/banners.
Core library:
include/raylib-libretro.h— core loading (dylib), environment callbacks, pixel-format mapping, audio ring buffer, VFS wiringinclude/raylib-libretro-vfs.h— libretro VFS interface implementation, backed by raylib file I/Oinclude/raylib-libretro-config.h—rlconfig: the INI /.infoparser used for settings and core metadata
Frontend layers (opt-in):
include/raylib-libretro-menu.h— full in-app UI (Nuklear / nuklear_console): core scanning, core picker, game-load orchestration, settingsinclude/raylib-libretro-physfs.h— PhysFS-backed, zip-aware game loader (mounts archives at/game)include/raylib-libretro-shaders.h— GLSL retro shadersinclude/raylib-libretro-touch.h— on-screen / touch controlsinclude/raylib-libretro-android.h— Android JNI glue (file picker, intents); guarded by__ANDROID__include/raylib-libretro-styles.h,-logo.h— menu theme + embedded logo image
Apps & data:
example/raylib-libretro-basic.c— canonical minimal usage; reference for the expected API flowbin/raylib-libretro.c— full app usingraylib-applifecycle callbackstests/test_opengl/test_opengl_libretro.c— minimal GL 3.3 core that draws a spinning triangle; smoke-tests the HW render path end-to-endvendor/libretro-core-info/— submodule of.infocore metadata; CMake copies the relevant ones next to built coresTASKS.md— planned features; check here before implementing new work