← Developer's Journal

Anatomy of the Backend Trait System

How five traits, 13 required methods, 39 optional primitives, and 8 extension traits let one Rust codebase draw to SDL3, Canvas 2D, PSP hardware, and a software framebuffer — without a single #[cfg] in application code.

The Central Idea

Every pixel OASIS_OS draws, every button press it reads, every audio sample it plays, and every TCP byte it sends passes through one of five backend traits defined in oasis-types/src/backend/. Application code — the browser engine, 32 UI widgets, 90+ terminal commands, 18 skins, the window manager — never touches platform APIs. It accepts a &mut dyn SdiBackend and draws to it.

This isn't a theoretical pattern. The same compiled crate graph runs on four radically different hosts today: an SDL3 desktop window, a browser tab via WebAssembly, a 333 MHz MIPS handheld from 2004, and inside Unreal Engine 5 as a dynamic texture.

Trait Architecture Overview

Five Backend Traits

SdiCore            13 required methods — the minimum viable backend
  ↑ extends
SdiBackend         39 optional accelerated primitives, split into:
  ├─ SdiShapes       fill/stroke: rounded_rect, circle, line, triangle, polygon
  ├─ SdiGradients    linear_gradient, radial_gradient, fill_rect_gradient
  ├─ SdiAlpha        set_global_alpha, fill_rect_alpha
  ├─ SdiText         draw_text_styled (bold/italic), draw_text_wrapped
  ├─ SdiTextures     blit_rotated, blit_tinted, blit_region, blit_scaled
  ├─ SdiClipTransform push/pop_clip, push/pop_translate
  ├─ SdiVector       begin_path, move_to, line_to, arc, bezier, fill, stroke
  └─ SdiBatch        begin_batch, end_batch, submit_rect_batch, submit_text_batch

InputBackend       poll() → Vec<InputEvent>
NetworkBackend     listen, accept, connect → NetworkStream (read/write/close)
AudioBackend       load_track, play, pause, stop, set_volume, feed_pcm
The complete platform abstraction boundary lives in oasis-types/src/backend/

SdiCore: The 13 Methods Every Backend Must Implement

SdiCore is the rendering contract. If you can implement these 13 methods, OASIS_OS will run on your platform. No more, no less.

pub trait SdiCore {
    // Lifecycle
    fn init(&mut self, width: u32, height: u32) -> Result<()>;
    fn shutdown(&mut self) -> Result<()>;

    // Frame cycle
    fn clear(&mut self, color: Color) -> Result<()>;
    fn swap_buffers(&mut self) -> Result<()>;

    // Primitives
    fn fill_rect(&mut self, x: i32, y: i32, w: u32, h: u32, color: Color) -> Result<()>;
    fn blit(&mut self, tex: TextureId, x: i32, y: i32, w: u32, h: u32) -> Result<()>;

    // Textures
    fn load_texture(&mut self, w: u32, h: u32, rgba: &[u8]) -> Result<TextureId>;
    fn destroy_texture(&mut self, tex: TextureId) -> Result<()>;

    // Text
    fn draw_text(&mut self, text: &str, x: i32, y: i32, size: u16, color: Color) -> Result<()>;
    fn measure_text(&self, text: &str, size: u16) -> u32;

    // Clipping
    fn set_clip_rect(&mut self, x: i32, y: i32, w: u32, h: u32) -> Result<()>;
    fn reset_clip_rect(&mut self) -> Result<()>;

    // Readback
    fn read_pixels(&self, x: i32, y: i32, w: u32, h: u32) -> Result<Vec<u8>>;
}
13 methods = one complete rendering backend

Every widget in oasis-ui, every CSS box painted by oasis-browser, every line of terminal output — they all reduce to calls against this interface. fill_rect draws backgrounds and borders. blit draws images and textures. draw_text renders text. That's the entire rendering vocabulary at the core level.

SdiBackend: Progressive Capability via Extension Traits

SdiBackend extends SdiCore with 39 optional methods. Each has a software fallback default implementation — if a backend doesn't override fill_circle, the default uses the midpoint circle algorithm from oasis-types::rasterize. If it doesn't override draw_text_styled, the default draws faux-bold via double-strike and faux-italic via pixel skew.

This means a new backend starts working with just 13 methods. Hardware acceleration is added incrementally:

Backend Capability Matrix

                      SDL3    WASM    UE5     PSP
SdiCore (13 req)      ✔       ✔       ✔       ✔
SdiShapes             HW      Canvas  SW      GU
SdiGradients          HW      Canvas  SW      GU
SdiAlpha              HW      Canvas  SW      GU
SdiText (styled)      bitmap  cached  bitmap  atlas+TTF
SdiTextures           HW      Canvas  SW      GU+DMA
SdiClipTransform      stack   Canvas  stack   GU
SdiVector             SW      Canvas  SW      —
SdiBatch              —      —      —      GU list

HW = hardware-accelerated, SW = software fallback, GU = PSP Graphics Unit
Canvas = browser Canvas 2D API, — = uses default (no-op or software)
Each backend implements what its platform accelerates, falls back for the rest

How a Frame Renders

Here's the call flow for a single frame, from the backend's main loop through core rendering and back:

Frame Rendering Flow

Backend main loop (SDL/WASM/PSP/UE5)
  │
  ├─ backend.clear(bg_color)
  │
  ├─ oasis-core::tick(&mut backend, &mut sdi, &mut state)
  │   │
  │   ├─ dashboard.update_sdi(&mut sdi, &theme)  // creates/updates named objects
  │   ├─ active_app.update_sdi(&mut sdi, &theme)
  │   ├─ status_bar.update_sdi(&mut sdi, &theme)
  │   └─ sdi.draw(&mut backend)                   // iterates objects by z-order
  │       │
  │       ├─ sdi.draw_base_layer(&mut backend)      // non-overlay objects
  │       │   └─ for each visible object:
  │       │       ├─ backend.fill_rect(...)      // or fill_rounded_rect
  │       │       ├─ backend.blit(texture, ...)  // if textured
  │       │       └─ backend.draw_text(...)      // if has text
  │       └─ sdi.draw_overlay_layer(&mut backend)    // bars, toasts, cursors
  │
  └─ backend.swap_buffers()
The SDI registry mediates between application state and backend draw calls

The SDI Registry: Named Object Scene Graph

Between the backend traits and application code sits the SDI (Scene Display Interface) registry — a flat collection of named, positionable, blittable objects with z-ordering. It's deliberately simple: no DOM, no layout engine, no retained-mode GUI framework.

Each frame, applications update SDI objects (position, text, color, visibility), and the registry draws them in z-order via the backend. This decouples application state from rendering:

SDI Object Properties

SdiObject {
    name:            String,       // unique identifier
    x, y:            i32,          // position in virtual coords
    w, h:            u32,          // dimensions
    z:               i32,          // draw order (lower = behind)
    color:           Color,        // fill color (RGBA)
    alpha:           u8,           // object-level opacity
    visible:         bool,         // skip if false
    texture:         Option<TextureId>,
    text:            Option<String>,
    text_color:      Color,
    font_size:       u16,
    border_radius:   Option<u16>,  // rounded corners
    gradient_top:    Option<Color>, // vertical gradient
    gradient_bottom: Option<Color>,
    shadow_level:    Option<u8>,   // elevation shadow (0-3)
    stroke_width:    Option<u16>,  // outline
    stroke_color:    Option<Color>,
    overlay:         bool,         // drawn in overlay pass (on top)
}
~108 SDI objects for a typical dashboard page (9 per icon × 12 icons)

The registry supports two-pass rendering: draw_base_layer() for normal objects and draw_overlay_layer() for status bars, toasts, and cursors. On the PSP, this split is critical — drawing all ~108 objects every frame drops FPS from 60 to 30, so the dashboard only redraws the base layer when state changes.

How Each Backend Implements the Traits

SDL3 Desktop

The richest implementation. Uses SDL3's GPU-accelerated 2D renderer for all SdiCore operations. Texture lifetimes are erased via transmute (with a carefully ordered Drop impl to ensure textures are destroyed before the texture creator). Shapes use a mix of SDL draw calls and software rasterization. Compiles SDL3 from source via the build-from-source feature.

WebAssembly

Renders to an HTML Canvas 2D context. Shapes map directly to Canvas path operations (arc(), fillRect()). Glyph rendering uses an LRU cache of pre-rendered Canvas elements for performance. Input comes from DOM event listeners translated to InputEvents. Audio uses the Web Audio API.

PSP (sceGu)

The most constrained implementation: 333 MHz, 32 MB RAM, 480×272 display. Uses the PSP's sceGu hardware graphics unit for fill, blit, and shape operations via display lists. Textures must be power-of-2, 16-byte aligned, with a VRAM stride of 512px. Text rendering combines a RAM-based bitmap font atlas with system TrueType fonts via psp::font. Threading uses lock-free SpscQueues and atomics to avoid priority inversion on the single-core CPU.

Unreal Engine 5

A pure software RGBA framebuffer. Every fill_rect, draw_text, and blit writes directly into a Vec<u8> pixel buffer. The host calls oasis_get_buffer() via the C-ABI FFI layer to read the pixels back and upload them as a dynamic UE5 texture. Shapes use the shared PixelSink trait from oasis-types::rasterize.

The Compilation Firewall

Dependency Direction

  oasis-types   defines backend traits
       │
       ▼
  oasis-ui      uses &mut dyn SdiBackend
  oasis-browser uses &mut dyn SdiBackend
  oasis-terminal uses &mut dyn SdiBackend
  oasis-core    uses &mut dyn SdiBackend
       │
       ▼
  oasis-backend-sdl  implements SdiBackend (← leaf node)
  oasis-backend-wasm implements SdiBackend (← leaf node)
  oasis-backend-ue5  implements SdiBackend (← leaf node)
  oasis-backend-psp  implements SdiBackend (← leaf node)
Backends depend on core. Core never depends on backends.

This is the key architectural insight: backend crates are leaf nodes. They depend on core; core never depends on them. Adding a sixth backend (Android, iOS, Linux framebuffer) requires zero changes to any existing crate. You implement the 13 SdiCore methods, wire up input polling, and the entire OS runs.

Shared Rasterization Infrastructure

Backends don't start from scratch. oasis-types provides shared algorithms: rasterize_circle(), rasterize_triangle(), rasterize_rounded_rect() via the PixelSink trait, plus for_each_glyph_pixel() for bitmap font rendering, perpendicular_normal_f32() for thick lines, and radial_extent() for stroke circles. The UE5 backend's entire shape rendering is built on these shared primitives.

Input, Network, and Audio Traits

InputBackend

A single method: poll_events() → Vec<InputEvent>. Each backend translates its native input (SDL events, DOM events, PSP controller, UE5 callbacks) into a unified InputEvent enum: button presses, analog sticks, mouse movement, text input, and touch events. The core processes these identically regardless of source.

NetworkBackend

TCP client/server with listen(), accept(), and connect(). Returns Box<dyn NetworkStream> with read/write/close. An optional tls_provider() method enables TLS — desktop uses rustls, PSP uses embedded-tls (pure Rust TLS 1.3 on MIPS). The remote terminal, FTP transfer, and browser HTTP fetching all use this trait.

AudioBackend

Track-based audio with load_track(), play(), pause(), stop(), and set_volume(). Extended with load_streaming() and feed_pcm_f32() for real-time audio (video playback, internet radio). SDL uses SDL3 audio, WASM uses Web Audio, PSP uses sceAudiocodec hardware decode with psp::audio::AudioChannel.

Implementing a New Backend

Here's what it takes to bring OASIS_OS to a new platform:

StepWhatEffort
1Implement SdiCore (13 methods)~200 LOC
2Implement InputBackend (1 method)~50 LOC
3Create main loop: init, poll input, tick core, swap buffers~30 LOC
4Ship it — all 16 apps, browser, terminal work
5Optional: add NetworkBackend for online features~100 LOC
6Optional: add AudioBackend for music/radio~150 LOC
7Optional: override SdiBackend extensions for HW accelvaries

The UE5 backend is the proof: its SdiCore implementation is ~250 lines of pixel buffer writes, and it runs the full OS including the browser engine.

Numbers

MetricCount
Backend traits5 (SdiCore, SdiBackend, Input, Network, Audio)
SdiCore required methods13
SdiBackend extension traits8
SdiBackend optional methods39
Backend implementations4 (+ 1 kernel PRX)
Shared rasterization functions8 (triangle, circle, rounded_rect, glyph, etc.)
Trait definition fileoasis-types/src/backend/ (6 files)
Minimum LOC for new backend~280
Core crates using traitsall 30 (zero platform-specific code)