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 = 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 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}