Another project
1use crate::camera::{ViewportExtent, ViewportPx};
2use crate::pick::{PickId, PickIndex, Picker};
3use crate::snapshot::{SnapshotFrame, Style};
4use crate::{RenderError, Result};
5
6pub(crate) const COLOR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
7pub(crate) const PICK_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R32Uint;
8pub(crate) const BYTES_PER_PIXEL: u32 = 4;
9pub(crate) const PICK_BYTES_PER_PIXEL: u32 = 4;
10const MSAA_SAMPLE_COUNT: u32 = 4;
11
12#[derive(Copy, Clone, Debug, PartialEq, Eq)]
13pub enum AdapterPolicy {
14 Platform,
15 Software,
16}
17
18#[derive(Copy, Clone, Debug, PartialEq, Eq)]
19pub struct BackendTag(wgpu::Backend);
20
21impl BackendTag {
22 #[cfg(test)]
23 pub(crate) const fn from_backend(backend: wgpu::Backend) -> Self {
24 Self(backend)
25 }
26
27 #[must_use]
28 pub fn backend(self) -> wgpu::Backend {
29 self.0
30 }
31}
32
33impl core::fmt::Display for BackendTag {
34 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
35 write!(f, "{:?}", self.0)
36 }
37}
38
39#[derive(Clone, Debug)]
40pub struct Capabilities {
41 adapter_limits: wgpu::Limits,
42 backend: BackendTag,
43 adapter_name: String,
44 msaa_x4: bool,
45}
46
47impl Capabilities {
48 pub(crate) fn probe(adapter: &wgpu::Adapter) -> Self {
49 let info = adapter.get_info();
50 let color = adapter.get_texture_format_features(COLOR_FORMAT).flags;
51 let depth = adapter
52 .get_texture_format_features(crate::pipelines::solid::DEPTH_FORMAT)
53 .flags;
54 Self {
55 adapter_limits: adapter.limits(),
56 backend: BackendTag(info.backend),
57 adapter_name: info.name,
58 msaa_x4: msaa_x4_supported(color, depth),
59 }
60 }
61
62 #[must_use]
63 pub fn adapter_limits(&self) -> &wgpu::Limits {
64 &self.adapter_limits
65 }
66
67 #[must_use]
68 pub fn backend(&self) -> BackendTag {
69 self.backend
70 }
71
72 #[must_use]
73 pub fn adapter_name(&self) -> &str {
74 &self.adapter_name
75 }
76
77 #[must_use]
78 pub fn supports_msaa_x4(&self) -> bool {
79 self.msaa_x4
80 }
81}
82
83fn msaa_x4_supported(
84 color: wgpu::TextureFormatFeatureFlags,
85 depth: wgpu::TextureFormatFeatureFlags,
86) -> bool {
87 color.sample_count_supported(MSAA_SAMPLE_COUNT)
88 && depth.sample_count_supported(MSAA_SAMPLE_COUNT)
89}
90
91pub struct Gpu {
92 device: wgpu::Device,
93 queue: wgpu::Queue,
94 capabilities: Capabilities,
95}
96
97impl Gpu {
98 pub(crate) fn from_parts(
99 device: wgpu::Device,
100 queue: wgpu::Queue,
101 capabilities: Capabilities,
102 ) -> Self {
103 Self {
104 device,
105 queue,
106 capabilities,
107 }
108 }
109
110 #[must_use]
111 pub fn device(&self) -> &wgpu::Device {
112 &self.device
113 }
114
115 #[must_use]
116 pub fn queue(&self) -> &wgpu::Queue {
117 &self.queue
118 }
119
120 #[must_use]
121 pub fn capabilities(&self) -> &Capabilities {
122 &self.capabilities
123 }
124}
125
126pub struct OffscreenContext {
127 gpu: Gpu,
128 color: wgpu::Texture,
129 pick: wgpu::Texture,
130 depth: wgpu::Texture,
131 pick_staging: wgpu::Buffer,
132 extent: ViewportExtent,
133}
134
135fn require_nonzero(extent: ViewportExtent) -> Result<()> {
136 if extent.width().value() == 0 || extent.height().value() == 0 {
137 return Err(RenderError::ZeroExtent);
138 }
139 Ok(())
140}
141
142fn offscreen_color_texture(device: &wgpu::Device, extent: ViewportExtent) -> wgpu::Texture {
143 device.create_texture(&wgpu::TextureDescriptor {
144 label: Some("bone-render:offscreen-color"),
145 size: texture_size(extent),
146 mip_level_count: 1,
147 sample_count: 1,
148 dimension: wgpu::TextureDimension::D2,
149 format: COLOR_FORMAT,
150 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
151 view_formats: &[],
152 })
153}
154
155fn offscreen_pick_texture(device: &wgpu::Device, extent: ViewportExtent) -> wgpu::Texture {
156 device.create_texture(&wgpu::TextureDescriptor {
157 label: Some("bone-render:offscreen-pick"),
158 size: texture_size(extent),
159 mip_level_count: 1,
160 sample_count: 1,
161 dimension: wgpu::TextureDimension::D2,
162 format: PICK_FORMAT,
163 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
164 view_formats: &[],
165 })
166}
167
168impl OffscreenContext {
169 pub async fn new(extent: ViewportExtent, policy: AdapterPolicy) -> Result<Self> {
170 require_nonzero(extent)?;
171 let instance =
172 wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
173 let adapter = instance
174 .request_adapter(&wgpu::RequestAdapterOptions {
175 power_preference: wgpu::PowerPreference::LowPower,
176 force_fallback_adapter: matches!(policy, AdapterPolicy::Software),
177 compatible_surface: None,
178 })
179 .await?;
180 let (device, queue) = adapter
181 .request_device(&wgpu::DeviceDescriptor {
182 label: Some("bone-render:offscreen"),
183 required_features: wgpu::Features::empty(),
184 required_limits: wgpu::Limits::downlevel_defaults(),
185 experimental_features: wgpu::ExperimentalFeatures::default(),
186 memory_hints: wgpu::MemoryHints::default(),
187 trace: wgpu::Trace::Off,
188 })
189 .await?;
190 let capabilities = Capabilities::probe(&adapter);
191 let color = offscreen_color_texture(&device, extent);
192 let pick = offscreen_pick_texture(&device, extent);
193 let depth = crate::depth_texture(&device, extent);
194 let pick_staging = device.create_buffer(&wgpu::BufferDescriptor {
195 label: Some("bone-render:pick-readback"),
196 size: u64::from(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT),
197 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
198 mapped_at_creation: false,
199 });
200 Ok(Self {
201 gpu: Gpu {
202 device,
203 queue,
204 capabilities,
205 },
206 color,
207 pick,
208 depth,
209 pick_staging,
210 extent,
211 })
212 }
213
214 pub fn resize(&mut self, extent: ViewportExtent) -> Result<()> {
215 require_nonzero(extent)?;
216 self.color = offscreen_color_texture(&self.gpu.device, extent);
217 self.pick = offscreen_pick_texture(&self.gpu.device, extent);
218 self.depth = crate::depth_texture(&self.gpu.device, extent);
219 self.extent = extent;
220 Ok(())
221 }
222
223 pub fn render_passes<F>(&self, build_passes: F)
224 where
225 F: FnOnce(
226 &mut wgpu::CommandEncoder,
227 &wgpu::TextureView,
228 &wgpu::TextureView,
229 &wgpu::TextureView,
230 ),
231 {
232 let mut encoder = self
233 .gpu
234 .device
235 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
236 label: Some("bone-render:offscreen-frame"),
237 });
238 let color_view = self.color_view();
239 let pick_view = self.pick_view();
240 let depth_view = self
241 .depth
242 .create_view(&wgpu::TextureViewDescriptor::default());
243 build_passes(&mut encoder, &color_view, &pick_view, &depth_view);
244 self.gpu.queue.submit(Some(encoder.finish()));
245 }
246
247 pub fn capture(&self) -> Result<SnapshotFrame> {
248 self.render(|_, _, _| {})
249 }
250
251 #[must_use]
252 pub fn gpu(&self) -> &Gpu {
253 &self.gpu
254 }
255
256 #[must_use]
257 pub fn extent(&self) -> ViewportExtent {
258 self.extent
259 }
260
261 #[must_use]
262 pub const fn color_format(&self) -> wgpu::TextureFormat {
263 COLOR_FORMAT
264 }
265
266 #[must_use]
267 pub fn color_view(&self) -> wgpu::TextureView {
268 self.color
269 .create_view(&wgpu::TextureViewDescriptor::default())
270 }
271
272 #[must_use]
273 pub fn pick_view(&self) -> wgpu::TextureView {
274 self.pick
275 .create_view(&wgpu::TextureViewDescriptor::default())
276 }
277
278 #[must_use]
279 pub fn picker(&self, index: PickIndex) -> Picker<'_> {
280 Picker::new(
281 &self.gpu,
282 &self.pick,
283 &self.pick_staging,
284 self.extent,
285 index,
286 )
287 }
288
289 pub fn render_clear(&self, style: &Style) -> Result<SnapshotFrame> {
290 self.render(|encoder, color_view, pick_view| {
291 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
292 label: Some("bone-render:clear"),
293 color_attachments: &[
294 Some(wgpu::RenderPassColorAttachment {
295 view: color_view,
296 resolve_target: None,
297 depth_slice: None,
298 ops: wgpu::Operations {
299 load: wgpu::LoadOp::Clear(style.background().into()),
300 store: wgpu::StoreOp::Store,
301 },
302 }),
303 Some(wgpu::RenderPassColorAttachment {
304 view: pick_view,
305 resolve_target: None,
306 depth_slice: None,
307 ops: wgpu::Operations {
308 load: wgpu::LoadOp::Clear(pick_clear_color()),
309 store: wgpu::StoreOp::Store,
310 },
311 }),
312 ],
313 depth_stencil_attachment: None,
314 timestamp_writes: None,
315 occlusion_query_set: None,
316 multiview_mask: None,
317 });
318 })
319 }
320
321 pub(crate) fn render<F>(&self, mut build_passes: F) -> Result<SnapshotFrame>
322 where
323 F: FnMut(&mut wgpu::CommandEncoder, &wgpu::TextureView, &wgpu::TextureView),
324 {
325 let mut encoder = self
326 .gpu
327 .device
328 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
329 label: Some("bone-render:encoder"),
330 });
331 let color_view = self.color_view();
332 let pick_view = self.pick_view();
333 build_passes(&mut encoder, &color_view, &pick_view);
334 let padded_bpr = padded_bytes_per_row(self.extent.width());
335 let staging = self.gpu.device.create_buffer(&wgpu::BufferDescriptor {
336 label: Some("bone-render:readback"),
337 size: u64::from(padded_bpr) * u64::from(self.extent.height().value()),
338 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
339 mapped_at_creation: false,
340 });
341 encoder.copy_texture_to_buffer(
342 wgpu::TexelCopyTextureInfo {
343 texture: &self.color,
344 mip_level: 0,
345 origin: wgpu::Origin3d::ZERO,
346 aspect: wgpu::TextureAspect::All,
347 },
348 wgpu::TexelCopyBufferInfo {
349 buffer: &staging,
350 layout: wgpu::TexelCopyBufferLayout {
351 offset: 0,
352 bytes_per_row: Some(padded_bpr),
353 rows_per_image: Some(self.extent.height().value()),
354 },
355 },
356 texture_size(self.extent),
357 );
358 self.gpu.queue.submit(Some(encoder.finish()));
359 let rgba = read_staging(&self.gpu.device, &staging, self.extent, padded_bpr)?;
360 Ok(SnapshotFrame::new(
361 self.extent,
362 rgba,
363 self.gpu.capabilities.backend(),
364 ))
365 }
366}
367
368pub(crate) fn clear_pick_attachment(
369 encoder: &mut wgpu::CommandEncoder,
370 pick_view: &wgpu::TextureView,
371) {
372 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
373 label: Some("bone-render:pick-clear"),
374 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
375 view: pick_view,
376 resolve_target: None,
377 depth_slice: None,
378 ops: wgpu::Operations {
379 load: wgpu::LoadOp::Clear(pick_clear_color()),
380 store: wgpu::StoreOp::Store,
381 },
382 })],
383 depth_stencil_attachment: None,
384 timestamp_writes: None,
385 occlusion_query_set: None,
386 multiview_mask: None,
387 });
388}
389
390pub(crate) fn pick_clear_color() -> wgpu::Color {
391 wgpu::Color {
392 r: f64::from(PickId::NONE.raw()),
393 g: 0.0,
394 b: 0.0,
395 a: 0.0,
396 }
397}
398
399fn texture_size(extent: ViewportExtent) -> wgpu::Extent3d {
400 wgpu::Extent3d {
401 width: extent.width().value(),
402 height: extent.height().value(),
403 depth_or_array_layers: 1,
404 }
405}
406
407pub(crate) fn padded_bytes_per_row(width: ViewportPx) -> u32 {
408 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
409 let w = width.value();
410 debug_assert!(
411 w <= u32::MAX / BYTES_PER_PIXEL,
412 "viewport width too large for padded row calc"
413 );
414 let raw = w * BYTES_PER_PIXEL;
415 raw.div_ceil(align) * align
416}
417
418fn read_staging(
419 device: &wgpu::Device,
420 buffer: &wgpu::Buffer,
421 extent: ViewportExtent,
422 padded_bpr: u32,
423) -> Result<Vec<u8>> {
424 let slice = buffer.slice(..);
425 let (tx, rx) =
426 std::sync::mpsc::sync_channel::<core::result::Result<(), wgpu::BufferAsyncError>>(1);
427 slice.map_async(wgpu::MapMode::Read, move |res| {
428 let _ = tx.send(res);
429 });
430 device
431 .poll(wgpu::PollType::wait_indefinitely())
432 .map_err(RenderError::Poll)?;
433 match rx.try_recv() {
434 Ok(Ok(())) => {}
435 Ok(Err(e)) => return Err(RenderError::Map(e)),
436 Err(_) => return Err(RenderError::MapMissing),
437 }
438 let row_bytes = extent.width().value() * BYTES_PER_PIXEL;
439 let rgba: Vec<u8> = {
440 let view = slice.get_mapped_range();
441 (0..extent.height().value())
442 .flat_map(|y| {
443 let start = (y * padded_bpr) as usize;
444 let end = start + row_bytes as usize;
445 view[start..end].iter().copied()
446 })
447 .collect()
448 };
449 buffer.unmap();
450 Ok(rgba)
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 #[test]
458 fn msaa_x4_requires_both_color_and_depth_support() {
459 let yes = wgpu::TextureFormatFeatureFlags::MULTISAMPLE_X4;
460 let no = wgpu::TextureFormatFeatureFlags::empty();
461 assert!(msaa_x4_supported(yes, yes));
462 assert!(!msaa_x4_supported(yes, no));
463 assert!(!msaa_x4_supported(no, yes));
464 assert!(!msaa_x4_supported(no, no));
465 }
466}