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