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