Skip to main content

rlx_runtime/
browser.rs

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