Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hello World Core

This chapter builds the first core: a no-content dynamic library that paints a blue 320x240 software frame at 60 FPS and submits silent stereo audio at 48 kHz.

Cargo Setup

Configure the crate as a cdylib. A libretro frontend loads a dynamic library; cargo run is not the execution model.

[package]
name = "hello-libretro"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies]
libretro = { package = "libretro-core", version = "0.1" }

Inside this workspace, the example uses a path dependency instead:

libretro = { package = "libretro-core", path = "../../crates/libretro-core" }

Core Code

The reusable shape is the same as examples/software-libretro: keep frame and audio buffers in core state, use one content contract helper, and submit one video frame plus one audio batch from run.

use libretro::{
    ContentContract, Core, Environment, GameInfo, Runtime, SystemAvInfo,
    SystemInfo, fixed_system_av_info, silent_stereo_frames_for_video_frame,
};

const WIDTH: u32 = 320;
const HEIGHT: u32 = 240;
const FPS_HZ: u32 = 60;
const SAMPLE_RATE_HZ: u32 = 48_000;
const BLUE_0RGB1555: u16 = 0x001f;

struct HelloCore {
    frame: Vec<u16>,
    silence: Vec<[i16; 2]>,
}

impl Default for HelloCore {
    fn default() -> Self {
        Self {
            frame: vec![BLUE_0RGB1555; (WIDTH * HEIGHT) as usize],
            silence: silent_stereo_frames_for_video_frame(SAMPLE_RATE_HZ, FPS_HZ),
        }
    }
}

impl Core for HelloCore {
    fn system_info(&self) -> SystemInfo {
        let mut info = SystemInfo::new("hello-libretro", env!("CARGO_PKG_VERSION"));
        content_contract().apply_to_system_info(&mut info);
        info
    }

    fn av_info(&self) -> SystemAvInfo {
        fixed_system_av_info(WIDTH, HEIGHT, FPS_HZ as f64, SAMPLE_RATE_HZ as f64)
    }

    fn on_set_environment(&mut self, env: &mut Environment<'_>) {
        let _ = content_contract().register_environment(env);
    }

    fn load_game(&mut self, _game: Option<GameInfo<'_>>, _runtime: &mut Runtime<'_>) -> bool {
        true
    }

    fn run(&mut self, runtime: &mut Runtime<'_>) {
        runtime.poll_input();
        let pitch = WIDTH as usize * core::mem::size_of::<u16>();
        let _ = runtime.video_refresh_frame_with_audio(
            &self.frame,
            WIDTH,
            HEIGHT,
            pitch,
            &self.silence,
        );
    }
}

fn content_contract() -> ContentContract {
    ContentContract::new("").with_support_no_game(true)
}

libretro::export_core!(HelloCore::default());

ContentContract::new("").with_support_no_game(true) describes a core that can start without content. Required-content cores use extensions such as "bin|dat" and return false from load_game when required content is absent or invalid.

The software frame uses libretro’s default 0RGB1555 format. That keeps hello world small, but a real software renderer may choose another pixel format explicitly during load_game.

Audio batches are &[[i16; 2]]: one element is one stereo frame, left then right. At 48,000 Hz and 60 FPS, this core submits 800 silent stereo frames per video frame.

Build And Load

Build the core:

cargo build --release

On Linux, the output for the example is a dynamic library such as target/release/libhello_libretro.so. Other platforms use their normal dynamic library suffixes, such as .dylib or .dll.

Load the library in a libretro frontend without content. The expected result is a solid blue 320x240 frame and silence.