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

Frontend Services

Frontend services are optional capabilities a core discovers at runtime. Each one is a typed handle acquired from Environment (or Runtime); if the frontend does not expose the service, the accessor returns None and the core should degrade gracefully rather than fail loudly.

let Some(rumble) = runtime.environment().rumble_interface() else {
    return; // frontend has no rumble; skip haptics this frame
};
rumble.set_state(InputPort::from(0), RumbleEffect::Strong, RumbleStrength::max());

This chapter groups services by what they do. Event-shaped callbacks (keyboard, audio callback, camera frames, location lifecycle, frame time) are registered through CoreEventConfig instead — see Input and Events.

Logging and Messages

Logger is the universal output for debug/info/warn/error lines. Acquire it from Environment or Runtime; it has no is_available because the frontend always exposes a log target (defaulting to stderr if needed).

runtime.logger().info("starting frame");
runtime.logger().warn(format!("fallback path: {reason}"));

For user-visible overlays, two helpers exist on Runtime and Environment:

let _ = runtime.set_message("Save complete", 120); // text, frames

let mut env = runtime.environment();
let _ = env.set_message_ext(
    ExtendedMessage::new("Compiling shaders…")
        .with_duration_millis(2_000)
        .with_target(MessageTarget::Osd)
        .with_kind(MessageKind::Progress)
        .with_progress(MessageProgress::Percent(50)),
);

See Environment and Core Options for the full ExtendedMessage shape.

Rumble

if let Some(rumble) = env.rumble_interface() {
    rumble.set_state(InputPort::from(0), RumbleEffect::Strong, RumbleStrength::max());
    rumble.set_state(InputPort::from(0), RumbleEffect::Weak, RumbleStrength::new(0x4000));
}

RumbleEffect is Strong or Weak. RumbleStrength is a u16 newtype with new, off, and max constructors. The port is the same player slot used by joypad polling — see Input.

LED

if let Some(led) = env.led_interface() {
    let _ = led.set_state(LedIndex::new(0), LedState::On);
}

LedIndex::new(i32) selects which LED to toggle and LedState is On or Off. The mapping is frontend-specific.

Sensors

Per-port accelerometer, gyroscope, and illuminance sampling. Enable each sensor explicitly at the rate you want, poll the typed inputs each frame, disable when done.

if let Some(sensors) = env.sensor_interface() {
    let port = InputPort::from(0);
    let _ = sensors.enable(port, Sensor::Accelerometer, SensorRateHz::new(60));

    let ax = sensors.input(port, SensorInput::AccelerometerX); // Option<f32>
    let ay = sensors.input(port, SensorInput::AccelerometerY);
    let az = sensors.input(port, SensorInput::AccelerometerZ);

    let _ = sensors.disable(port, Sensor::Accelerometer);
}

Sensor selects which physical sensor to drive. SensorInput selects which axis to read. Reads return Option<f32>; None means the sensor is not enabled or not available.

Microphone

Microphone access is RAII-wrapped. Open the interface, open a stream, read samples while enabled, drop the handle when done.

let Some(interface) = env.microphone_interface() else { return; };
let Some(mut mic) = interface.open_default() else { return; };

mic.set_enabled(true);
let mut buffer = [0i16; 512];
match mic.read_samples(&mut buffer) {
    Ok(count) => self.consume_audio(&buffer[..count]),
    Err(MicrophoneReadError::Unavailable) => self.disable_microphone(),
    Err(_) => {}
}

Use interface.open(MicrophoneParams::new(rate)) for a specific sample rate. The Microphone handle is !Send; keep it on the core struct only while you actually use it.

MIDI

Byte-level MIDI in/out with microsecond delta tracking:

if let Some(midi) = env.midi_interface() {
    if midi.input_enabled() {
        while let Some(byte) = midi.read_byte() {
            self.midi_buffer.push(byte);
        }
    }
    if midi.output_enabled() {
        let _ = midi.write_byte(0x90, MidiDeltaMicros::from(0)); // note on, no delay
        let _ = midi.flush();
    }
}

midi.is_available() is a const check and useful for early-out before calling other methods.

Location

GPS-style position polling.

if let Some(location) = env.location_interface() {
    let _ = location.set_interval(
        LocationIntervalMillis::from(1_000),
        LocationIntervalMeters::from(5),
    );
    let _ = location.start();

    if let Some(pos) = location.position() {
        // pos.latitude, pos.longitude, pos.horizontal_accuracy, ...
    }

    let _ = location.stop();
}

Lifecycle callbacks (location_initialized, location_deinitialized) are event-shaped — see Input and Events.

Camera

Camera control acquires the interface with the desired delivery mode (raw framebuffer or OpenGL texture):

let Some(camera) = env.camera_interface(CameraRequest::raw_framebuffer()) else {
    return;
};

if camera.capabilities().contains(CameraCapability::RawFramebuffer) {
    let _ = camera.start();
    // Frame delivery arrives through the camera_raw_frame event listener.
}

CameraRequest::open_gl_texture() selects texture delivery and pairs with the camera_texture_frame event. See Input and Events for the matching frame-callback shapes.

Performance Counters

PerfInterface exposes wall clock, tick counter, CPU feature flags, and pinned counter management. See Diagnostics and Performance for the full walkthrough; here is the shape:

let perf = runtime.environment().perf_interface();
if let Some(perf) = perf {
    let now_us = perf.time_micros();           // Option<PerfTimeMicros>
    let features = perf.cpu_features();        // Option<CpuFeatures>
    if features.map(|f| f.contains(CpuFeature::Avx2)).unwrap_or(false) {
        self.use_simd_path();
    }
}

Netplay Packets

When a core wants to participate in netplay, it installs a packet handler through Environment and receives a NetpacketSession from the wrapper:

fn on_netpacket_receive(&mut self, session: &NetpacketSession, packet: Netpacket<'_>) {
    let from = packet.client_id();
    self.apply_remote_input(from, packet.data());
}

fn run(&mut self, runtime: &mut Runtime<'_>) {
    if let Some(session) = self.netpacket_session.as_ref() {
        session.send(
            NetpacketTarget::Broadcast,
            NetpacketFlags::reliable(),
            &self.local_input_packet(),
        );
        session.flush(NetpacketTarget::Broadcast);
    }
}

NetpacketTarget is Client(NetplayClientId) or Broadcast. NetpacketFlags is built with reliable(), unreliable(), and unsequenced() constructors that can be combined.

Frontend State Queries

These are not “services” in the discovery sense, but they are how cores adapt to frontend mode. All return Option<...>:

let mut env = runtime.environment();

let lang = env.language();           // localized strings
let user = env.username();           // player profile
let fast = env.fastforwarding();     // skip non-essential work
let throttle = env.throttle_state(); // explicit target rate
let power = env.device_power();      // battery-aware policies
let av = env.audio_video_enable();   // selective frame skip

Use them to choose conservative paths when the frontend is fast-forwarding or running on low battery, and to load locale-aware UI strings.