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) {
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 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}