Skip to main content

runmat_runtime/builtins/plotting/core/
context.rs

1//! Shared plotting context registry for zero-copy rendering.
2//!
3//! This module tracks the GPU device/queue exported by the active acceleration
4//! provider so plotting backends (native GUI or wasm/web) can reuse the same
5//! `wgpu::Device` without creating duplicate adapters. Call
6//! [`ensure_context_from_provider`] or [`install_wgpu_context`] once a provider
7//! is initialized; subsequent callers can query [`shared_wgpu_context`] to
8//! determine whether zero-copy rendering is possible.
9
10use runmat_accelerate_api::{AccelContextHandle, AccelContextKind, WgpuContextHandle};
11#[cfg(target_arch = "wasm32")]
12use runmat_thread_local::runmat_thread_local;
13#[cfg(target_arch = "wasm32")]
14use std::cell::RefCell;
15#[cfg(not(target_arch = "wasm32"))]
16use std::sync::{OnceLock, RwLock};
17
18use crate::{build_runtime_error, BuiltinResult, RuntimeError};
19
20/// Process-wide WGPU context exported by the acceleration provider (if any).
21#[cfg(not(target_arch = "wasm32"))]
22static SHARED_WGPU_CONTEXT: OnceLock<RwLock<Option<WgpuContextHandle>>> = OnceLock::new();
23
24#[cfg(target_arch = "wasm32")]
25runmat_thread_local! {
26    static SHARED_WGPU_CONTEXT: RefCell<Option<WgpuContextHandle>> = RefCell::new(None);
27}
28
29#[cfg(not(target_arch = "wasm32"))]
30fn shared_context_slot() -> &'static RwLock<Option<WgpuContextHandle>> {
31    SHARED_WGPU_CONTEXT.get_or_init(|| RwLock::new(None))
32}
33
34/// Returns the cached shared WGPU context, if one has been installed.
35pub fn shared_wgpu_context() -> Option<WgpuContextHandle> {
36    #[cfg(not(target_arch = "wasm32"))]
37    {
38        shared_context_slot()
39            .read()
40            .ok()
41            .and_then(|context| context.clone())
42    }
43    #[cfg(target_arch = "wasm32")]
44    {
45        SHARED_WGPU_CONTEXT.with(|cell| cell.borrow().clone())
46    }
47}
48
49/// Install a shared context that was exported out-of-band (e.g. from a host
50/// application that already called `export_context`).
51pub fn install_wgpu_context(context: &WgpuContextHandle) {
52    #[cfg(not(target_arch = "wasm32"))]
53    {
54        if let Ok(mut slot) = shared_context_slot().write() {
55            *slot = Some(context.clone());
56        }
57    }
58    #[cfg(target_arch = "wasm32")]
59    {
60        SHARED_WGPU_CONTEXT.with(|cell| {
61            *cell.borrow_mut() = Some(context.clone());
62        });
63    }
64    propagate_to_plot_crate(context);
65}
66
67/// Refresh the shared context from the active acceleration provider, falling
68/// back to the most recently installed context when no provider is active.
69pub fn ensure_context_from_provider() -> BuiltinResult<WgpuContextHandle> {
70    // Ask the active provider first on every call. Providers can be replaced
71    // during a process lifetime, and a buffer created by the new provider must
72    // never be submitted through a device cached from the previous one.
73    if let Some(handle) = runmat_accelerate_api::export_context(AccelContextKind::Plotting) {
74        return match handle {
75            AccelContextHandle::Wgpu(ctx) => {
76                install_wgpu_context(&ctx);
77                Ok(ctx)
78            }
79        };
80    }
81
82    if let Some(ctx) = shared_wgpu_context() {
83        return Ok(ctx);
84    }
85
86    Err(context_error(
87        "plotting context unavailable (GPU provider did not export a shared device)",
88    ))
89}
90
91fn context_error(message: impl Into<String>) -> RuntimeError {
92    build_runtime_error(message)
93        .with_identifier("RunMat:plot:ContextUnavailable")
94        .build()
95}
96
97fn propagate_to_plot_crate(context: &WgpuContextHandle) {
98    // Make the shared device available to the `runmat-plot` crate.
99    //
100    // This is required for zero-copy GPU-resident plotting:
101    // - plot builtins export provider buffers
102    // - plot GPU packers need an Arc<Device/Queue> to build vertex buffers
103    //
104    // Note: `runmat-plot` owns its own global context store; we install into it whenever
105    // a provider exports a plotting-capable WGPU context.
106    #[cfg(any(
107        feature = "gui",
108        feature = "plot-core",
109        all(target_arch = "wasm32", feature = "plot-web")
110    ))]
111    {
112        use runmat_plot::context::{
113            install_shared_wgpu_context as install_plot_context, SharedWgpuContext,
114        };
115        use runmat_plot::gpu::tuning as plot_tuning;
116        install_plot_context(SharedWgpuContext {
117            instance: context.instance.clone(),
118            device: context.device.clone(),
119            queue: context.queue.clone(),
120            adapter: context.adapter.clone(),
121            adapter_info: context.adapter_info.clone(),
122            limits: context.limits.clone(),
123            features: context.features,
124        });
125        if let Some(wg) = runmat_accelerate_api::workgroup_size_hint() {
126            plot_tuning::set_effective_workgroup_size(wg);
127        }
128    }
129
130    #[cfg(not(any(feature = "gui", all(target_arch = "wasm32", feature = "plot-web"))))]
131    {
132        let _ = context;
133    }
134}
135
136#[cfg(all(test, feature = "plot-core"))]
137pub(crate) mod tests {
138    use super::*;
139    use pollster::FutureExt;
140    use std::sync::Arc;
141
142    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
143    #[test]
144    fn install_context_propagates_to_plot_crate() {
145        if std::env::var("RUNMAT_PLOT_SKIP_GPU_TESTS").is_ok() {
146            return;
147        }
148        let instance = Arc::new(wgpu::Instance::default());
149        let adapter = match instance
150            .request_adapter(&wgpu::RequestAdapterOptions {
151                power_preference: wgpu::PowerPreference::HighPerformance,
152                compatible_surface: None,
153                force_fallback_adapter: false,
154            })
155            .block_on()
156        {
157            Some(adapter) => adapter,
158            None => return,
159        };
160        let adapter_info = adapter.get_info();
161        let adapter_limits = adapter.limits();
162        let adapter_features = adapter.features();
163        let adapter = Arc::new(adapter);
164        let (device, queue) = match adapter
165            .request_device(
166                &wgpu::DeviceDescriptor {
167                    label: Some("plotting-context-test-device"),
168                    required_features: adapter_features,
169                    required_limits: adapter_limits.clone(),
170                },
171                None,
172            )
173            .block_on()
174        {
175            Ok(pair) => pair,
176            Err(_) => return,
177        };
178        let handle = WgpuContextHandle {
179            instance: instance.clone(),
180            device: Arc::new(device),
181            queue: Arc::new(queue),
182            adapter: adapter.clone(),
183            adapter_info,
184            limits: adapter_limits.clone(),
185            features: adapter_features,
186        };
187        install_wgpu_context(&handle);
188        assert!(shared_wgpu_context().is_some());
189        assert!(runmat_plot::shared_wgpu_context().is_some());
190    }
191}