Skip to main content

rlx_runtime/
browser.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna. GPL-3.0-only.
3
4//! Browser runtime — unified WebGPU (async) + WebGL fallback + CPU.
5//!
6//! Call [`init_webgpu`] once at startup on wasm, then use [`BrowserSession`] to
7//! compile and run graphs through the same backend registry as native code.
8
9use crate::browser_support;
10use crate::compiled::CompiledGraph;
11use crate::device_ext::{self, BROWSER_DEVICE_PRIORITY};
12use crate::session::Session;
13use rlx_driver::Device;
14use rlx_ir::Graph;
15
16/// Error from browser compile/run preflight.
17#[derive(Debug, Clone)]
18pub struct BrowserError(pub String);
19
20impl std::fmt::Display for BrowserError {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        write!(f, "{}", self.0)
23    }
24}
25
26impl std::error::Error for BrowserError {}
27
28impl From<String> for BrowserError {
29    fn from(s: String) -> Self {
30        Self(s)
31    }
32}
33
34/// Initialize the WebGPU device on wasm. Idempotent; returns `true` when an
35/// adapter is available. No-op on native (uses blocking device discovery).
36#[cfg(all(feature = "gpu", target_arch = "wasm32"))]
37pub async fn init_webgpu() -> bool {
38    rlx_wgpu::device::init_wgpu_device().await
39}
40
41#[cfg(not(all(feature = "gpu", target_arch = "wasm32")))]
42pub async fn init_webgpu() -> bool {
43    #[cfg(feature = "gpu")]
44    {
45        rlx_wgpu::is_available()
46    }
47    #[cfg(not(feature = "gpu"))]
48    {
49        false
50    }
51}
52
53/// Re-export browser device priority for bindings.
54pub use device_ext::BROWSER_DEVICE_PRIORITY as browser_device_priority;
55
56/// Available browser backends in priority order.
57pub fn available_browser_devices() -> Vec<Device> {
58    device_ext::available_browser_devices()
59}
60
61/// Highest-priority live browser backend.
62pub fn preferred_browser_device() -> Option<Device> {
63    device_ext::preferred_browser_device()
64}
65
66/// Is `graph` runnable on any available browser backend?
67pub fn supports_browser_graph(graph: &Graph) -> bool {
68    browser_support::select_browser_device_for_graph(graph).is_some()
69}
70
71/// Diagnostic when no browser backend can run `graph`.
72pub fn browser_block_reason(graph: &Graph) -> Option<String> {
73    if let Some(reason) = browser_support::collective_block_reason(graph) {
74        return Some(reason);
75    }
76    for &device in BROWSER_DEVICE_PRIORITY {
77        if !device_ext::is_available(device) {
78            continue;
79        }
80        if let Some((_, op)) = browser_support::first_browser_unsupported_op(graph, device) {
81            return Some(format!(
82                "op {op:?} not supported on browser backend {device:?}"
83            ));
84        }
85        if device_ext::supports_graph(device, graph) {
86            return None;
87        }
88    }
89    Some("no browser backend available — enable `webgpu` and/or `opengl` features".into())
90}
91
92/// Session targeting a browser backend (`WebGpu` preferred, then `OpenGl`, then `Cpu`).
93pub struct BrowserSession {
94    device: Device,
95}
96
97impl BrowserSession {
98    /// Use the highest-priority available browser backend.
99    pub fn new_preferred() -> Result<Self, BrowserError> {
100        let device = preferred_browser_device().ok_or_else(|| {
101            BrowserError(
102                "no browser backend available — call init_webgpu() and/or enable opengl feature"
103                    .into(),
104            )
105        })?;
106        Ok(Self { device })
107    }
108
109    /// Target a specific browser backend.
110    pub fn new(device: Device) -> Result<Self, BrowserError> {
111        if !BROWSER_DEVICE_PRIORITY.contains(&device) {
112            return Err(BrowserError(format!(
113                "{device:?} is not a browser backend (use WebGpu, OpenGl, or Cpu)"
114            )));
115        }
116        if !device_ext::is_available(device) {
117            return Err(BrowserError(format!(
118                "browser backend {device:?} is not available"
119            )));
120        }
121        Ok(Self { device })
122    }
123
124    /// Compile for a specific graph, auto-selecting WebGPU vs WebGL fallback.
125    pub fn compile_for_graph(graph: &Graph) -> Result<BrowserCompiledGraph, BrowserError> {
126        if let Some(reason) = browser_support::collective_block_reason(graph) {
127            return Err(BrowserError(reason));
128        }
129        let device = browser_support::select_browser_device_for_graph(graph).ok_or_else(|| {
130            browser_block_reason(graph)
131                .unwrap_or_else(|| "graph not supported on any browser backend".into())
132        })?;
133        BrowserSession { device }.compile(graph.clone())
134    }
135
136    pub fn device(&self) -> Device {
137        self.device
138    }
139
140    /// Compile `graph` on the selected browser backend.
141    pub fn compile(&self, graph: Graph) -> Result<BrowserCompiledGraph, BrowserError> {
142        if let Some(reason) = browser_support::collective_block_reason(&graph) {
143            return Err(BrowserError(reason));
144        }
145        if !device_ext::supports_graph(self.device, &graph) {
146            return Err(BrowserError(browser_block_reason(&graph).unwrap_or_else(
147                || format!("graph not supported on {:?}", self.device),
148            )));
149        }
150        let compiled = Session::new(self.device).compile(graph);
151        Ok(BrowserCompiledGraph {
152            inner: compiled,
153            device: self.device,
154        })
155    }
156}
157
158/// A graph compiled for a browser backend.
159pub struct BrowserCompiledGraph {
160    inner: CompiledGraph,
161    device: Device,
162}
163
164impl BrowserCompiledGraph {
165    pub fn device(&self) -> Device {
166        self.device
167    }
168
169    pub fn set_param(&mut self, name: &str, data: &[f32]) {
170        self.inner.set_param(name, data);
171    }
172
173    /// Synchronous run (WebGL, CPU, or native WebGPU).
174    pub fn run(&mut self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
175        self.inner.run(inputs)
176    }
177
178    /// Async run on wasm WebGPU; sync fallback for WebGL/CPU.
179    #[cfg(target_arch = "wasm32")]
180    pub async fn run_async(
181        &mut self,
182        inputs: &[(&str, &[f32])],
183    ) -> Result<Vec<Vec<f32>>, BrowserError> {
184        match self.device {
185            Device::WebGpu | Device::Gpu => {
186                self.inner.run_async(inputs).await.map_err(BrowserError)
187            }
188            _ => Ok(self.inner.run(inputs)),
189        }
190    }
191
192    #[cfg(not(target_arch = "wasm32"))]
193    pub async fn run_async(
194        &mut self,
195        inputs: &[(&str, &[f32])],
196    ) -> Result<Vec<Vec<f32>>, BrowserError> {
197        Ok(self.inner.run(inputs))
198    }
199}