Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/LiveSplit/livesplit-core/llms.txt

Use this file to discover all available pages before exploring further.

livesplit-core provides multiple built-in renderers. All of them operate on a LayoutState produced by calling layout.state(&timer.snapshot(), &mut image_cache). The state is a plain data snapshot — renderers consume it but never hold a reference to the live Layout or Timer.

The Rendering Pipeline

Timer snapshot  →  LayoutState  →  Renderer  →  pixels / SVG / canvas
  1. Call timer.snapshot() to capture the current timer state.
  2. Call layout.state(&snapshot, &mut image_cache) to produce a LayoutState.
  3. Pass LayoutState and ImageCache to the renderer of your choice.

Software Renderer

Cargo feature: software-rendering The software renderer rasterizes the layout entirely on the CPU using tiny-skia. It is surprisingly fast and can be considered the default rendering backend. Output is an RGBA8 pixel buffer — 4 bytes per pixel in the order red, green, blue, alpha, each an unsigned 8-bit integer.

Rust Example

use livesplit_core::{
    Layout, Timer,
    rendering::software::BorrowedRenderer,
    settings::ImageCache,
};

let mut image_cache = ImageCache::new();

// Produce the layout state from the current timer snapshot.
let state = layout.state(&timer.snapshot(), &mut image_cache);

// Allocate a pixel buffer: width × height × 4 bytes (RGBA8).
let width: u32 = 300;
let height: u32 = 500;
let stride = width; // pixels per row (no over-allocation padding)
let mut buffer = vec![0u8; (height * stride * 4) as usize];

// Create the renderer (reuse across frames — it caches scene state).
let mut renderer = BorrowedRenderer::new();

// Render into the buffer.
// Returns Some([new_width, new_height]: [f32; 2]) if the ideal layout size changed — a hint only.
renderer.render(
    &state,
    &image_cache,
    &mut buffer,
    [width, height],
    stride,
    false, // force_redraw: set true if the buffer contents were replaced externally
);

// `buffer` now contains RGBA8 pixels ready to blit to a window or texture.

Stride and Over-Allocation

Some frameworks (e.g. OpenGL, Metal, Direct3D) over-allocate image buffers so that each row is aligned to a power-of-two boundary. For example, a 100×50 image might be backed by a 128×64 buffer. In that case:
  • Pass the real width (100) and height (50) as the render dimensions.
  • Pass the allocated row width (128) as the stride.
The renderer writes only width pixels per row but advances stride pixels between rows.

C API

// Allocate the renderer (reuse across frames).
SoftwareRenderer *renderer = SoftwareRenderer_new();

// Render — data must be at least stride * height * 4 bytes.
SoftwareRenderer_render(
    renderer,
    layout_state,
    image_cache,
    data,       // *mut u8 — RGBA8 buffer
    width,      // u32 — actual pixel width
    height,     // u32 — actual pixel height
    stride,     // u32 — pixels per row in the underlying buffer
    false       // force_redraw
);

SoftwareRenderer_drop(renderer);

SVG Renderer

Cargo feature: svg-rendering The SVG renderer produces a vector image as an SVG string. It is well suited for server-side generation and embedding in HTML documents.
SVG rendering is ideal for server-side generation and web display without a canvas element.

Rust Example

use livesplit_core::{
    rendering::svg::Renderer as SvgRenderer,
    settings::ImageCache,
};

let mut image_cache = ImageCache::new();
let state = layout.state(&timer.snapshot(), &mut image_cache);

let mut renderer = SvgRenderer::new();
let mut svg_output = String::new();

// Returns Ok(Some([new_width, new_height])) if the ideal size changed.
renderer
    .render(&mut svg_output, &state, &image_cache, [300.0, 500.0])
    .expect("SVG rendering failed");

// `svg_output` now holds a complete SVG document.
// Write it to a file or embed it directly in HTML.
println!("{}", svg_output);
The [width, height] argument is in logical units. The SVG viewBox is set to 0 0 width height.

Web Rendering (WASM)

Cargo feature: web-rendering When targeting WebAssembly in a browser, the web renderer draws directly to an HTML <canvas> element using the browser’s 2D Canvas API. It relies on web-sys and wasm-bindgen for browser integration.
Web rendering is the recommended approach for browser-based timer UIs built with the WebAssembly target.
The web-rendering feature implies wasm-web, which enables wasm-bindgen support throughout the crate (hotkeys, async, etc.).

Image Cache

ImageCache manages decoded images referenced by the layout — segment icons, game artwork, and backgrounds.
  • Pass a mutable reference to the same ImageCache instance to every call to layout.state().
  • The cache is also passed to the renderer so it can look up images by handle.
  • When the image-shrinking feature is enabled (included in the default feature set), images are automatically resized to a reasonable display size to keep memory usage low.
// Create once and reuse across the entire application lifetime.
let mut image_cache = ImageCache::new();

loop {
    // Both layout.state() and the renderer receive the same cache.
    let state = layout.state(&timer.snapshot(), &mut image_cache);
    renderer.render(&state, &image_cache, &mut buffer, [width, height], stride, false);
}

Custom Renderers

If none of the built-in renderers fit your target (e.g. a proprietary GPU API or a terminal UI), you can build a custom renderer by consuming the LayoutState tree directly. LayoutState is a tree of component states. Each component’s state is a plain data struct:
ComponentState Type
TimerTimerComponentState
SplitsSplitsComponentState
TitleTitleComponentState
Detailed TimerDetailedTimerComponentState
Key-Value (Previous Segment, Sum of Best, etc.)KeyValueComponentState
GraphGraphComponentState
TextTextComponentState
All state types derive serde::Serialize, so you can call layout.state_as_json() to get a JSON snapshot for use in any web or scripting UI:
// Serialize the entire layout state to JSON.
let json = layout.state_as_json(&timer.snapshot(), &mut image_cache);
// Send `json` over a WebSocket, IPC pipe, or REST response.
This approach is used by many livesplit-core integrations that render the timer using a web technology (React, Vue, Svelte) consuming JSON from a native host process.

Build docs developers (and LLMs) love