Skip to main content

repose_render_wgpu/
offscreen.rs

1//! Unified offscreen helper (native/android/wasm).
2
3use anyhow::Result;
4use wgpu::{PollType, TextureFormat};
5
6use crate::WgpuSceneRenderer;
7use repose_core::Scene;
8#[cfg(not(target_arch = "wasm32"))]
9use std::sync::{Mutex, OnceLock};
10
11#[cfg(not(target_arch = "wasm32"))]
12static SHARED_DEVICE: OnceLock<Mutex<Option<(wgpu::Device, wgpu::Queue)>>> = OnceLock::new();
13
14#[cfg(not(target_arch = "wasm32"))]
15fn shared_slot() -> &'static Mutex<Option<(wgpu::Device, wgpu::Queue)>> {
16    SHARED_DEVICE.get_or_init(|| Mutex::new(None))
17}
18
19#[cfg(target_arch = "wasm32")]
20thread_local! {
21    static SHARED_DEVICE_WASM: std::cell::RefCell<Option<(wgpu::Device, wgpu::Queue)>> = std::cell::RefCell::new(None);
22}
23
24/// Publish live Device/Queue for shared offscreen.
25pub fn set_shared_device(device: wgpu::Device, queue: wgpu::Queue) {
26    #[cfg(not(target_arch = "wasm32"))]
27    {
28        if let Ok(mut g) = shared_slot().lock() {
29            *g = Some((device, queue));
30        }
31    }
32    #[cfg(target_arch = "wasm32")]
33    {
34        SHARED_DEVICE_WASM.with(|c| *c.borrow_mut() = Some((device, queue)));
35    }
36}
37pub fn shared_device() -> Option<(wgpu::Device, wgpu::Queue)> {
38    #[cfg(not(target_arch = "wasm32"))]
39    {
40        shared_slot().lock().ok().and_then(|g| g.clone())
41    }
42    #[cfg(target_arch = "wasm32")]
43    {
44        SHARED_DEVICE_WASM.with(|c| c.borrow().clone())
45    }
46}
47pub fn clear_shared_device() {
48    #[cfg(not(target_arch = "wasm32"))]
49    {
50        if let Ok(mut g) = shared_slot().lock() {
51            *g = None;
52        }
53    }
54    #[cfg(target_arch = "wasm32")]
55    {
56        SHARED_DEVICE_WASM.with(|c| *c.borrow_mut() = None);
57    }
58}
59
60pub struct OffscreenRenderer {
61    renderer: WgpuSceneRenderer,
62    texture: wgpu::Texture,
63    view: wgpu::TextureView,
64    readback: wgpu::Buffer,
65    width: u32,
66    height: u32,
67    padded_bytes_per_row: u32,
68}
69
70impl OffscreenRenderer {
71    pub async fn new(width: u32, height: u32, msaa: u32) -> Result<Self> {
72        let width = width.max(1);
73        let height = height.max(1);
74        let instance = if cfg!(target_arch = "wasm32") {
75            let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
76            desc.backends = wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL;
77            wgpu::util::new_instance_with_webgpu_detection(desc).await
78        } else {
79            wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle())
80        };
81        let adapter = instance
82            .request_adapter(&wgpu::RequestAdapterOptions {
83                power_preference: wgpu::PowerPreference::HighPerformance,
84                compatible_surface: None,
85                ..Default::default()
86            })
87            .await?;
88        let format = TextureFormat::Rgba8UnormSrgb;
89        let msaa = crate::pick_surface_msaa(&adapter, format, msaa);
90        let (device, queue) = adapter
91            .request_device(&wgpu::DeviceDescriptor {
92                label: Some("repose-offscreen"),
93                required_features: wgpu::Features::empty(),
94                required_limits: wgpu::Limits::default(),
95                memory_hints: wgpu::MemoryHints::Performance,
96                experimental_features: wgpu::ExperimentalFeatures::default(),
97                trace: wgpu::Trace::Off,
98            })
99            .await?;
100        let renderer = WgpuSceneRenderer::from_device(device, queue, format, msaa);
101        Self::from_renderer(renderer, width, height)
102    }
103
104    pub fn new_blocking(width: u32, height: u32, msaa: u32) -> Result<Self> {
105        pollster::block_on(Self::new(width, height, msaa))
106    }
107
108    /// Shared-device: reuse Device/Queue, no Adapter.
109    pub fn from_device(
110        device: wgpu::Device,
111        queue: wgpu::Queue,
112        width: u32,
113        height: u32,
114        msaa: u32,
115    ) -> Result<Self> {
116        let width = width.max(1);
117        let height = height.max(1);
118        let format = TextureFormat::Rgba8UnormSrgb;
119        let renderer = WgpuSceneRenderer::from_device(device, queue, format, msaa.max(1));
120        Self::from_renderer(renderer, width, height)
121    }
122
123    pub fn from_device_with_adapter(
124        device: wgpu::Device,
125        queue: wgpu::Queue,
126        adapter: &wgpu::Adapter,
127        width: u32,
128        height: u32,
129        msaa: u32,
130    ) -> Result<Self> {
131        let width = width.max(1);
132        let height = height.max(1);
133        let format = TextureFormat::Rgba8UnormSrgb;
134        let msaa = crate::pick_surface_msaa(adapter, format, msaa);
135        let renderer = WgpuSceneRenderer::from_device(device, queue, format, msaa);
136        Self::from_renderer(renderer, width, height)
137    }
138
139    pub fn from_renderer(mut renderer: WgpuSceneRenderer, width: u32, height: u32) -> Result<Self> {
140        renderer.resize(width, height);
141        let texture = renderer.device.create_texture(&wgpu::TextureDescriptor {
142            label: Some("repose-offscreen-tex"),
143            size: wgpu::Extent3d {
144                width,
145                height,
146                depth_or_array_layers: 1,
147            },
148            mip_level_count: 1,
149            sample_count: 1,
150            dimension: wgpu::TextureDimension::D2,
151            format: TextureFormat::Rgba8UnormSrgb,
152            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
153            view_formats: &[],
154        });
155        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
156        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
157        let unpadded = width * 4;
158        let padded = unpadded.div_ceil(align) * align;
159        let buf_size = (padded * height) as u64;
160        let readback = renderer.device.create_buffer(&wgpu::BufferDescriptor {
161            label: Some("repose-offscreen-readback"),
162            size: buf_size,
163            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
164            mapped_at_creation: false,
165        });
166        Ok(Self {
167            renderer,
168            texture,
169            view,
170            readback,
171            width,
172            height,
173            padded_bytes_per_row: padded,
174        })
175    }
176
177    fn encode_rgba(&mut self, scene: &Scene, clear: Option<[f64; 4]>) -> wgpu::CommandBuffer {
178        let mut encoder =
179            self.renderer
180                .device
181                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
182                    label: Some("repose-offscreen-encoder"),
183                });
184        self.renderer
185            .render_scene_to_encoder(scene, &mut encoder, &self.view, clear);
186        encoder.copy_texture_to_buffer(
187            wgpu::TexelCopyTextureInfo {
188                texture: &self.texture,
189                mip_level: 0,
190                origin: wgpu::Origin3d::ZERO,
191                aspect: wgpu::TextureAspect::All,
192            },
193            wgpu::TexelCopyBufferInfo {
194                buffer: &self.readback,
195                layout: wgpu::TexelCopyBufferLayout {
196                    offset: 0,
197                    bytes_per_row: Some(self.padded_bytes_per_row),
198                    rows_per_image: Some(self.height),
199                },
200            },
201            wgpu::Extent3d {
202                width: self.width,
203                height: self.height,
204                depth_or_array_layers: 1,
205            },
206        );
207        encoder.finish()
208    }
209
210    pub fn render_rgba(&mut self, scene: &Scene, clear: Option<[f64; 4]>) -> Result<Vec<u8>> {
211        let cmd = self.encode_rgba(scene, clear);
212        self.renderer.queue.submit(Some(cmd));
213        let slice = self.readback.slice(..);
214        let (tx, rx) = web_workers::sync::mpsc::channel();
215        slice.map_async(wgpu::MapMode::Read, move |r| {
216            let _ = tx.send_sync(r);
217        });
218        self.renderer.device.poll(PollType::wait_indefinitely())?;
219        rx.recv_sync()??;
220        let mapped = slice.get_mapped_range()?;
221        let out = strip_padding(&*mapped, self.width, self.height, self.padded_bytes_per_row);
222        drop(mapped);
223        self.readback.unmap();
224        Ok(out)
225    }
226
227    #[cfg(target_arch = "wasm32")]
228    pub async fn render_rgba_async(
229        &mut self,
230        scene: &Scene,
231        clear: Option<[f64; 4]>,
232    ) -> Result<Vec<u8>> {
233        let cmd = self.encode_rgba(scene, clear);
234        self.renderer.queue.submit(Some(cmd));
235        let slice = self.readback.slice(..);
236        let (tx, rx) = web_workers::sync::mpsc::channel();
237        slice.map_async(wgpu::MapMode::Read, {
238            let tx = tx.clone();
239            move |r| {
240                let _ = tx.send_sync(r);
241            }
242        });
243        loop {
244            let _ = self.renderer.device.poll(PollType::Poll);
245            if let Ok(r) = rx.try_recv() {
246                r?;
247                break;
248            }
249            web_workers::web::yield_now_async(web_workers::web::YieldTime::UserVisible).await;
250        }
251        let mapped = slice.get_mapped_range()?;
252        let out = strip_padding(&*mapped, self.width, self.height, self.padded_bytes_per_row);
253        drop(mapped);
254        self.readback.unmap();
255        Ok(out)
256    }
257
258    pub fn renderer_mut(&mut self) -> &mut WgpuSceneRenderer {
259        &mut self.renderer
260    }
261    pub fn renderer(&self) -> &WgpuSceneRenderer {
262        &self.renderer
263    }
264
265    pub fn ensure_size(&mut self, width: u32, height: u32) -> Result<()> {
266        let width = width.max(1);
267        let height = height.max(1);
268        if self.width == width && self.height == height {
269            return Ok(());
270        }
271        self.renderer.resize(width, height);
272        let texture = self
273            .renderer
274            .device
275            .create_texture(&wgpu::TextureDescriptor {
276                label: Some("repose-offscreen-tex"),
277                size: wgpu::Extent3d {
278                    width,
279                    height,
280                    depth_or_array_layers: 1,
281                },
282                mip_level_count: 1,
283                sample_count: 1,
284                dimension: wgpu::TextureDimension::D2,
285                format: TextureFormat::Rgba8UnormSrgb,
286                usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
287                view_formats: &[],
288            });
289        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
290        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
291        let padded = (width * 4).div_ceil(align) * align;
292        let readback = self.renderer.device.create_buffer(&wgpu::BufferDescriptor {
293            label: Some("repose-offscreen-readback"),
294            size: (padded * height) as u64,
295            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
296            mapped_at_creation: false,
297        });
298        self.texture = texture;
299        self.view = view;
300        self.readback = readback;
301        self.width = width;
302        self.height = height;
303        self.padded_bytes_per_row = padded;
304        Ok(())
305    }
306
307    pub async fn render_rgba_unified(
308        &mut self,
309        scene: &Scene,
310        clear: Option<[f64; 4]>,
311    ) -> Result<Vec<u8>> {
312        #[cfg(all(target_family = "wasm", target_os = "unknown"))]
313        {
314            if web_workers::web::has_block_support() {
315                return self.render_rgba(scene, clear);
316            } else {
317                return self.render_rgba_async(scene, clear).await;
318            }
319        }
320        #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
321        {
322            self.render_rgba(scene, clear)
323        }
324    }
325}
326
327fn strip_padding(mapped: &[u8], width: u32, height: u32, padded: u32) -> Vec<u8> {
328    let unpadded = width * 4;
329    let mut out = Vec::with_capacity((width * height * 4) as usize);
330    for y in 0..height {
331        let start = (y * padded) as usize;
332        let end = start + unpadded as usize;
333        out.extend_from_slice(&mapped[start..end]);
334    }
335    out
336}
337
338pub fn map_buffer_blocking(slice: &wgpu::BufferSlice<'_>, device: &wgpu::Device) -> Result<()> {
339    let (tx, rx) = web_workers::sync::mpsc::channel();
340    slice.map_async(wgpu::MapMode::Read, move |r| {
341        let _ = tx.send_sync(r);
342    });
343    device.poll(PollType::wait_indefinitely())?;
344    rx.recv_sync()??;
345    Ok(())
346}
347
348#[cfg(target_arch = "wasm32")]
349pub async fn map_buffer_async(slice: &wgpu::BufferSlice<'_>, device: &wgpu::Device) -> Result<()> {
350    let (tx, rx) = web_workers::sync::mpsc::channel();
351    slice.map_async(wgpu::MapMode::Read, {
352        let tx = tx.clone();
353        move |r| {
354            let _ = tx.send_sync(r);
355        }
356    });
357    loop {
358        let _ = device.poll(PollType::Poll);
359        if let Ok(r) = rx.try_recv() {
360            r?;
361            break;
362        }
363        web_workers::web::yield_now_async(web_workers::web::YieldTime::UserVisible).await;
364    }
365    Ok(())
366}
367
368pub async fn map_buffer_unified(
369    slice: &wgpu::BufferSlice<'_>,
370    device: &wgpu::Device,
371) -> Result<()> {
372    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
373    {
374        if web_workers::web::has_block_support() {
375            return map_buffer_blocking(slice, device);
376        } else {
377            return map_buffer_async(slice, device).await;
378        }
379    }
380    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
381    {
382        map_buffer_blocking(slice, device)
383    }
384}