Skip to main content

oxiui_render_soft/
backend_switch.rs

1//! Runtime CPU/GPU backend switching via the shared [`oxiui_core::paint::DrawList`] format.
2//!
3//! Both [`SoftBackend`] and the GPU path (`oxiui-render-wgpu::WgpuBackend`) consume
4//! the same [`oxiui_core::paint::DrawList`] from [`oxiui_core`].  This module provides a thin
5//! [`DynBackend`] enum that wraps either backend so application code can choose
6//! the rendering path at runtime — for example, falling back to the CPU path in
7//! CI / headless environments where a GPU adapter is unavailable.
8//!
9//! # Feature gate
10//!
11//! This module is only compiled when the `wgpu-compat` feature is enabled.
12//! Without the feature, no runtime switching overhead is incurred.
13//!
14//! # Example
15//!
16//! ```ignore
17//! use oxiui_render_soft::backend_switch::{DynBackend, BackendKind};
18//!
19//! // Prefer GPU, fall back to CPU.
20//! let backend = DynBackend::prefer_gpu(1920, 1080)
21//!     .unwrap_or_else(|| DynBackend::soft(1920, 1080));
22//!
23//! let kind = backend.kind();
24//! println!("active backend: {kind:?}");
25//! ```
26
27use oxiui_core::{
28    geometry::Size,
29    paint::{DrawList, RenderBackend},
30    UiError,
31};
32
33use crate::SoftBackend;
34
35#[cfg(feature = "wgpu-compat")]
36use oxiui_render_wgpu::WgpuBackend;
37
38// ---------------------------------------------------------------------------
39// BackendKind discriminant
40// ---------------------------------------------------------------------------
41
42/// Identifies which backend variant is active inside a [`DynBackend`].
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum BackendKind {
45    /// CPU framebuffer — headless / ffi-audit / no-GPU path.
46    Soft,
47    /// GPU path via wgpu (Vulkan / Metal / DX12 / WebGPU).
48    #[cfg(feature = "wgpu-compat")]
49    Wgpu,
50}
51
52// ---------------------------------------------------------------------------
53// DynBackend
54// ---------------------------------------------------------------------------
55
56/// A runtime-polymorphic render backend that implements [`RenderBackend`].
57///
58/// Wraps either [`SoftBackend`] or `WgpuBackend` (behind `wgpu-compat` feature)
59/// and forwards all [`RenderBackend`] method calls to the active variant.
60/// The CPU and GPU paths share the same [`DrawList`] command format (defined in
61/// `oxiui_core`), so callers do not need to change anything when the active
62/// backend changes.
63///
64/// Construct via [`DynBackend::soft`] or — when the `wgpu-compat` feature is
65/// active — via `DynBackend::wgpu`.
66pub enum DynBackend {
67    /// CPU software rasteriser.
68    Soft(Box<SoftBackend>),
69    /// wgpu GPU rasteriser.
70    #[cfg(feature = "wgpu-compat")]
71    Wgpu(Box<WgpuBackend>),
72}
73
74impl DynBackend {
75    /// Wrap a [`SoftBackend`] as the CPU path.
76    pub fn soft(width: u32, height: u32) -> Self {
77        DynBackend::Soft(Box::new(SoftBackend::new(width, height)))
78    }
79
80    /// Wrap a [`WgpuBackend`] as the GPU path.
81    ///
82    /// Only available when the `wgpu-compat` feature is enabled.
83    #[cfg(feature = "wgpu-compat")]
84    pub fn wgpu(backend: WgpuBackend) -> Self {
85        DynBackend::Wgpu(Box::new(backend))
86    }
87
88    /// Return the active [`BackendKind`].
89    pub fn kind(&self) -> BackendKind {
90        match self {
91            DynBackend::Soft(_) => BackendKind::Soft,
92            #[cfg(feature = "wgpu-compat")]
93            DynBackend::Wgpu(_) => BackendKind::Wgpu,
94        }
95    }
96
97    /// Return `true` if the active backend is the CPU soft rasteriser.
98    pub fn is_soft(&self) -> bool {
99        matches!(self, DynBackend::Soft(_))
100    }
101
102    /// Return `true` if the active backend is the wgpu GPU rasteriser.
103    #[cfg(feature = "wgpu-compat")]
104    pub fn is_wgpu(&self) -> bool {
105        matches!(self, DynBackend::Wgpu(_))
106    }
107
108    /// Borrow the inner [`SoftBackend`], or `None` if the GPU path is active.
109    pub fn as_soft(&self) -> Option<&SoftBackend> {
110        match self {
111            DynBackend::Soft(b) => Some(b.as_ref()),
112            #[cfg(feature = "wgpu-compat")]
113            _ => None,
114        }
115    }
116
117    /// Mutably borrow the inner [`SoftBackend`], or `None` if the GPU path is active.
118    pub fn as_soft_mut(&mut self) -> Option<&mut SoftBackend> {
119        match self {
120            DynBackend::Soft(b) => Some(b.as_mut()),
121            #[cfg(feature = "wgpu-compat")]
122            _ => None,
123        }
124    }
125}
126
127impl RenderBackend for DynBackend {
128    fn execute(&mut self, list: &DrawList) -> Result<(), UiError> {
129        match self {
130            DynBackend::Soft(b) => b.execute(list),
131            #[cfg(feature = "wgpu-compat")]
132            DynBackend::Wgpu(b) => b.execute(list),
133        }
134    }
135
136    fn surface_size(&self) -> Size {
137        match self {
138            DynBackend::Soft(b) => b.surface_size(),
139            #[cfg(feature = "wgpu-compat")]
140            DynBackend::Wgpu(b) => b.surface_size(),
141        }
142    }
143
144    fn supports_blur(&self) -> bool {
145        match self {
146            DynBackend::Soft(b) => b.supports_blur(),
147            #[cfg(feature = "wgpu-compat")]
148            DynBackend::Wgpu(b) => b.supports_blur(),
149        }
150    }
151
152    fn supports_gradients(&self) -> bool {
153        match self {
154            DynBackend::Soft(b) => b.supports_gradients(),
155            #[cfg(feature = "wgpu-compat")]
156            DynBackend::Wgpu(b) => b.supports_gradients(),
157        }
158    }
159
160    fn supports_paths(&self) -> bool {
161        match self {
162            DynBackend::Soft(b) => b.supports_paths(),
163            #[cfg(feature = "wgpu-compat")]
164            DynBackend::Wgpu(b) => b.supports_paths(),
165        }
166    }
167
168    fn supports_images(&self) -> bool {
169        match self {
170            DynBackend::Soft(b) => b.supports_images(),
171            #[cfg(feature = "wgpu-compat")]
172            DynBackend::Wgpu(b) => b.supports_images(),
173        }
174    }
175
176    fn supports_text(&self) -> bool {
177        match self {
178            DynBackend::Soft(b) => b.supports_text(),
179            #[cfg(feature = "wgpu-compat")]
180            DynBackend::Wgpu(b) => b.supports_text(),
181        }
182    }
183
184    fn supports_blend_modes(&self) -> bool {
185        match self {
186            DynBackend::Soft(b) => b.supports_blend_modes(),
187            #[cfg(feature = "wgpu-compat")]
188            DynBackend::Wgpu(b) => b.supports_blend_modes(),
189        }
190    }
191
192    fn supports_backdrop_blur(&self) -> bool {
193        match self {
194            DynBackend::Soft(b) => b.supports_backdrop_blur(),
195            #[cfg(feature = "wgpu-compat")]
196            DynBackend::Wgpu(b) => b.supports_backdrop_blur(),
197        }
198    }
199}
200
201// ---------------------------------------------------------------------------
202// Tests
203// ---------------------------------------------------------------------------
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn dyn_backend_soft_kind() {
211        let b = DynBackend::soft(64, 64);
212        assert_eq!(b.kind(), BackendKind::Soft);
213        assert!(b.is_soft());
214        assert_eq!(b.surface_size(), Size::new(64.0, 64.0));
215    }
216
217    #[test]
218    fn dyn_backend_soft_supports_capabilities() {
219        let b = DynBackend::soft(32, 32);
220        // SoftBackend always reports true for these.
221        assert!(b.supports_blur());
222        assert!(b.supports_gradients());
223        assert!(b.supports_paths());
224        assert!(b.supports_images());
225    }
226
227    #[test]
228    fn dyn_backend_soft_execute_fill_rect() {
229        use oxiui_core::geometry::{Rect, Size};
230        use oxiui_core::paint::DrawList;
231        use oxiui_core::Color;
232
233        let mut b = DynBackend::soft(100, 100);
234        let mut list = DrawList::new();
235        list.push_rect(Rect::new(10.0, 10.0, 20.0, 20.0), Color(255, 0, 0, 255));
236        let result = b.execute(&list);
237        assert!(result.is_ok(), "execute must not fail: {result:?}");
238        // Verify that the center pixel is red.
239        let fb_pixel = b.as_soft().and_then(|s| s.frame().get_rgba(20, 20));
240        if let Some((r, _, _, _)) = fb_pixel {
241            assert!(r > 200, "center pixel should be red: r={r}");
242        }
243        // Suppress unused import warning.
244        let _ = Size::new(0.0, 0.0);
245    }
246
247    #[test]
248    fn dyn_backend_as_soft_mut() {
249        let mut b = DynBackend::soft(10, 10);
250        let soft = b.as_soft_mut();
251        assert!(soft.is_some());
252    }
253}