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