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
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>>;
}
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)
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: 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)
}
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)
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.
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:
| Step | What | Effort |
|---|---|---|
| 1 | Implement SdiCore (13 methods) | ~200 LOC |
| 2 | Implement InputBackend (1 method) | ~50 LOC |
| 3 | Create main loop: init, poll input, tick core, swap buffers | ~30 LOC |
| 4 | Ship it — all 16 apps, browser, terminal work | — |
| 5 | Optional: add NetworkBackend for online features | ~100 LOC |
| 6 | Optional: add AudioBackend for music/radio | ~150 LOC |
| 7 | Optional: override SdiBackend extensions for HW accel | varies |
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
| Metric | Count |
|---|---|
| Backend traits | 5 (SdiCore, SdiBackend, Input, Network, Audio) |
| SdiCore required methods | 13 |
| SdiBackend extension traits | 8 |
| SdiBackend optional methods | 39 |
| Backend implementations | 4 (+ 1 kernel PRX) |
| Shared rasterization functions | 8 (triangle, circle, rounded_rect, glyph, etc.) |
| Trait definition file | oasis-types/src/backend/ (6 files) |
| Minimum LOC for new backend | ~280 |
| Core crates using traits | all 30 (zero platform-specific code) |