1use 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
24pub fn set_shared_device(device: wgpu::Device, queue: wgpu::Queue) {
29 #[cfg(not(target_arch = "wasm32"))]
30 {
31 if let Ok(mut g) = shared_slot().lock() {
32 *g = Some((device, queue));
33 }
34 }
35 #[cfg(target_arch = "wasm32")]
36 {
37 SHARED_DEVICE_WASM.with(|c| *c.borrow_mut() = Some((device, queue)));
38 }
39}
40pub fn shared_device() -> Option<(wgpu::Device, wgpu::Queue)> {
41 #[cfg(not(target_arch = "wasm32"))]
42 {
43 shared_slot().lock().ok().and_then(|g| g.clone())
44 }
45 #[cfg(target_arch = "wasm32")]
46 {
47 SHARED_DEVICE_WASM.with(|c| c.borrow().clone())
48 }
49}
50pub fn clear_shared_device() {
51 #[cfg(not(target_arch = "wasm32"))]
52 {
53 if let Ok(mut g) = shared_slot().lock() {
54 *g = None;
55 }
56 }
57 #[cfg(target_arch = "wasm32")]
58 {
59 SHARED_DEVICE_WASM.with(|c| *c.borrow_mut() = None);
60 }
61}
62
63pub struct OffscreenRenderer {
79 renderer: WgpuSceneRenderer,
80 texture: wgpu::Texture,
81 view: wgpu::TextureView,
82 readback: wgpu::Buffer,
83 width: u32,
84 height: u32,
85 padded_bytes_per_row: u32,
86}
87
88impl OffscreenRenderer {
89 pub async fn new(width: u32, height: u32, msaa: u32) -> Result<Self> {
93 let width = width.max(1);
94 let height = height.max(1);
95 let instance = if cfg!(target_arch = "wasm32") {
96 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
97 desc.backends = wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL;
98 wgpu::util::new_instance_with_webgpu_detection(desc).await
99 } else {
100 wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle())
101 };
102 let adapter = instance
103 .request_adapter(&wgpu::RequestAdapterOptions {
104 power_preference: wgpu::PowerPreference::HighPerformance,
105 compatible_surface: None,
106 ..Default::default()
107 })
108 .await?;
109 let format = TextureFormat::Rgba8UnormSrgb;
110 let msaa = crate::pick_surface_msaa(&adapter, format, msaa);
111 let (device, queue) = adapter
112 .request_device(&wgpu::DeviceDescriptor {
113 label: Some("repose-offscreen"),
114 required_features: wgpu::Features::empty(),
115 required_limits: wgpu::Limits::default(),
116 memory_hints: wgpu::MemoryHints::Performance,
117 experimental_features: wgpu::ExperimentalFeatures::default(),
118 trace: wgpu::Trace::Off,
119 })
120 .await?;
121 let renderer = WgpuSceneRenderer::from_device(device, queue, format, msaa);
122 Self::from_renderer(renderer, width, height)
123 }
124
125 pub fn new_blocking(width: u32, height: u32, msaa: u32) -> Result<Self> {
128 pollster::block_on(Self::new(width, height, msaa))
129 }
130
131 pub fn from_device(
136 device: wgpu::Device,
137 queue: wgpu::Queue,
138 width: u32,
139 height: u32,
140 msaa: u32,
141 ) -> Result<Self> {
142 let width = width.max(1);
143 let height = height.max(1);
144 let format = TextureFormat::Rgba8UnormSrgb;
145 let renderer = WgpuSceneRenderer::from_device(device, queue, format, msaa.max(1));
146 Self::from_renderer(renderer, width, height)
147 }
148
149 pub fn from_device_with_adapter(
152 device: wgpu::Device,
153 queue: wgpu::Queue,
154 adapter: &wgpu::Adapter,
155 width: u32,
156 height: u32,
157 msaa: u32,
158 ) -> Result<Self> {
159 let width = width.max(1);
160 let height = height.max(1);
161 let format = TextureFormat::Rgba8UnormSrgb;
162 let msaa = crate::pick_surface_msaa(adapter, format, msaa);
163 let renderer = WgpuSceneRenderer::from_device(device, queue, format, msaa);
164 Self::from_renderer(renderer, width, height)
165 }
166
167 pub fn from_renderer(mut renderer: WgpuSceneRenderer, width: u32, height: u32) -> Result<Self> {
170 let width = width.max(1);
171 let height = height.max(1);
172 renderer.resize(width, height);
173 let texture = renderer.device.create_texture(&wgpu::TextureDescriptor {
174 label: Some("repose-offscreen-tex"),
175 size: wgpu::Extent3d {
176 width,
177 height,
178 depth_or_array_layers: 1,
179 },
180 mip_level_count: 1,
181 sample_count: 1,
182 dimension: wgpu::TextureDimension::D2,
183 format: TextureFormat::Rgba8UnormSrgb,
184 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
185 view_formats: &[],
186 });
187 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
188 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
189 let unpadded = width * 4;
190 let padded = unpadded.div_ceil(align) * align;
191 let buf_size = (padded * height) as u64;
192 let readback = renderer.device.create_buffer(&wgpu::BufferDescriptor {
193 label: Some("repose-offscreen-readback"),
194 size: buf_size,
195 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
196 mapped_at_creation: false,
197 });
198 Ok(Self {
199 renderer,
200 texture,
201 view,
202 readback,
203 width,
204 height,
205 padded_bytes_per_row: padded,
206 })
207 }
208
209 fn encode_rgba(&mut self, scene: &Scene, clear: Option<[f64; 4]>) -> wgpu::CommandBuffer {
210 let mut encoder =
211 self.renderer
212 .device
213 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
214 label: Some("repose-offscreen-encoder"),
215 });
216 self.renderer
217 .render_scene_to_encoder(scene, &mut encoder, &self.view, clear);
218 encoder.copy_texture_to_buffer(
219 wgpu::TexelCopyTextureInfo {
220 texture: &self.texture,
221 mip_level: 0,
222 origin: wgpu::Origin3d::ZERO,
223 aspect: wgpu::TextureAspect::All,
224 },
225 wgpu::TexelCopyBufferInfo {
226 buffer: &self.readback,
227 layout: wgpu::TexelCopyBufferLayout {
228 offset: 0,
229 bytes_per_row: Some(self.padded_bytes_per_row),
230 rows_per_image: Some(self.height),
231 },
232 },
233 wgpu::Extent3d {
234 width: self.width,
235 height: self.height,
236 depth_or_array_layers: 1,
237 },
238 );
239 encoder.finish()
240 }
241
242 pub fn render_rgba(&mut self, scene: &Scene, clear: Option<[f64; 4]>) -> Result<Vec<u8>> {
247 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
249 debug_assert!(
250 web_workers::web::has_block_support(),
251 "render_rgba (blocking) called on wasm main thread; use render_rgba_async"
252 );
253 let cmd = self.encode_rgba(scene, clear);
254 self.renderer.queue.submit(Some(cmd));
255 let slice = self.readback.slice(..);
256 let (tx, rx) = web_workers::sync::mpsc::channel();
257 slice.map_async(wgpu::MapMode::Read, move |r| {
258 let _ = tx.send_sync(r);
259 });
260 self.renderer.device.poll(PollType::wait_indefinitely())?;
261 rx.recv_sync()??;
262 let mapped = slice.get_mapped_range()?;
263 let out = strip_padding(&mapped, self.width, self.height, self.padded_bytes_per_row);
264 drop(mapped);
265 self.readback.unmap();
266 Ok(out)
267 }
268
269 #[cfg(target_arch = "wasm32")]
270 pub async fn render_rgba_async(
271 &mut self,
272 scene: &Scene,
273 clear: Option<[f64; 4]>,
274 ) -> Result<Vec<u8>> {
275 let cmd = self.encode_rgba(scene, clear);
276 self.renderer.queue.submit(Some(cmd));
277 let slice = self.readback.slice(..);
278 let (tx, rx) = web_workers::sync::mpsc::channel();
279 slice.map_async(wgpu::MapMode::Read, {
280 let tx = tx.clone();
281 move |r| {
282 let _ = tx.send_sync(r);
283 }
284 });
285 let start = web_workers::sync::Instant::now();
286 loop {
287 let _ = self.renderer.device.poll(PollType::Poll);
288 if let Ok(r) = rx.try_recv() {
289 r?;
290 break;
291 }
292 if start.elapsed().as_secs() > 30 {
293 anyhow::bail!("offscreen readback timed out after 30s (map callback never fired)");
294 }
295 web_workers::web::yield_now_async(web_workers::web::YieldTime::UserVisible).await;
296 }
297 let mapped = slice.get_mapped_range()?;
298 let out = strip_padding(&*mapped, self.width, self.height, self.padded_bytes_per_row);
299 drop(mapped);
300 self.readback.unmap();
301 Ok(out)
302 }
303
304 pub fn renderer_mut(&mut self) -> &mut WgpuSceneRenderer {
305 &mut self.renderer
306 }
307 pub fn renderer(&self) -> &WgpuSceneRenderer {
308 &self.renderer
309 }
310
311 pub fn width(&self) -> u32 {
313 self.width
314 }
315
316 pub fn height(&self) -> u32 {
318 self.height
319 }
320
321 pub fn ensure_size(&mut self, width: u32, height: u32) -> Result<()> {
325 let width = width.max(1);
326 let height = height.max(1);
327 if self.width == width && self.height == height {
328 return Ok(());
329 }
330 self.renderer.resize(width, height);
331 let texture = self
332 .renderer
333 .device
334 .create_texture(&wgpu::TextureDescriptor {
335 label: Some("repose-offscreen-tex"),
336 size: wgpu::Extent3d {
337 width,
338 height,
339 depth_or_array_layers: 1,
340 },
341 mip_level_count: 1,
342 sample_count: 1,
343 dimension: wgpu::TextureDimension::D2,
344 format: TextureFormat::Rgba8UnormSrgb,
345 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
346 view_formats: &[],
347 });
348 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
349 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
350 let padded = (width * 4).div_ceil(align) * align;
351 let readback = self.renderer.device.create_buffer(&wgpu::BufferDescriptor {
352 label: Some("repose-offscreen-readback"),
353 size: (padded * height) as u64,
354 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
355 mapped_at_creation: false,
356 });
357 self.texture = texture;
358 self.view = view;
359 self.readback = readback;
360 self.width = width;
361 self.height = height;
362 self.padded_bytes_per_row = padded;
363 Ok(())
364 }
365
366 pub async fn render_rgba_unified(
367 &mut self,
368 scene: &Scene,
369 clear: Option<[f64; 4]>,
370 ) -> Result<Vec<u8>> {
371 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
372 {
373 if web_workers::web::has_block_support() {
374 return self.render_rgba(scene, clear);
375 } else {
376 return self.render_rgba_async(scene, clear).await;
377 }
378 }
379 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
380 {
381 self.render_rgba(scene, clear)
382 }
383 }
384}
385
386fn strip_padding(mapped: &[u8], width: u32, height: u32, padded: u32) -> Vec<u8> {
387 let unpadded = width * 4;
388 let mut out = Vec::with_capacity((width * height * 4) as usize);
389 for y in 0..height {
390 let start = (y * padded) as usize;
391 let end = start + unpadded as usize;
392 out.extend_from_slice(&mapped[start..end]);
393 }
394 out
395}
396
397pub fn map_buffer_blocking(slice: &wgpu::BufferSlice<'_>, device: &wgpu::Device) -> Result<()> {
398 let (tx, rx) = web_workers::sync::mpsc::channel();
399 slice.map_async(wgpu::MapMode::Read, move |r| {
400 let _ = tx.send_sync(r);
401 });
402 device.poll(PollType::wait_indefinitely())?;
403 rx.recv_sync()??;
404 Ok(())
405}
406
407#[cfg(target_arch = "wasm32")]
408pub async fn map_buffer_async(slice: &wgpu::BufferSlice<'_>, device: &wgpu::Device) -> Result<()> {
409 let (tx, rx) = web_workers::sync::mpsc::channel();
410 slice.map_async(wgpu::MapMode::Read, {
411 let tx = tx.clone();
412 move |r| {
413 let _ = tx.send_sync(r);
414 }
415 });
416 let start = web_workers::sync::Instant::now();
417 loop {
418 let _ = device.poll(PollType::Poll);
419 if let Ok(r) = rx.try_recv() {
420 r?;
421 break;
422 }
423 if start.elapsed().as_secs() > 30 {
424 anyhow::bail!("map_buffer timed out after 30s (map callback never fired)");
425 }
426 web_workers::web::yield_now_async(web_workers::web::YieldTime::UserVisible).await;
427 }
428 Ok(())
429}
430
431pub async fn map_buffer_unified(
432 slice: &wgpu::BufferSlice<'_>,
433 device: &wgpu::Device,
434) -> Result<()> {
435 #[cfg(all(target_family = "wasm", target_os = "unknown"))]
436 {
437 if web_workers::web::has_block_support() {
438 return map_buffer_blocking(slice, device);
439 } else {
440 return map_buffer_async(slice, device).await;
441 }
442 }
443 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
444 {
445 map_buffer_blocking(slice, device)
446 }
447}