Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

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}