Another project
0

Configure Feed

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

feat(render): surface depth, msaa probe, extrude budget

Lewis: May this revision serve well! <lu5a@proton.me>

author
Lewis
date (Jun 7, 2026, 11:30 AM +0300) commit 5dfb25dc parent 4aa6001e change-id tktvvqpu
+260 -11
+1 -1
crates/bone-app/src/main.rs
··· 1254 1254 renderer.prepare(scene, style); 1255 1255 let pre_present = || scheduler.window().pre_present_notify(); 1256 1256 surface.render( 1257 - |encoder, color, pick| { 1257 + |encoder, color, pick, _depth| { 1258 1258 renderer.encode_passes( 1259 1259 encoder, 1260 1260 RenderTargets::new(color, pick),
+1
crates/bone-render/Cargo.toml
··· 24 24 [dev-dependencies] 25 25 pollster = { workspace = true } 26 26 proptest = { workspace = true } 27 + wgpu = { workspace = true } 27 28 28 29 [lints] 29 30 workspace = true
+2 -1
crates/bone-render/src/camera3.rs
··· 706 706 ) else { 707 707 panic!("camera is non-degenerate"); 708 708 }; 709 - let Ok(rolled) = roll_about_view(front, extent(), vp(200.0, 128.0), vp(128.0, 200.0)) else { 709 + let Ok(rolled) = roll_about_view(front, extent(), vp(200.0, 128.0), vp(128.0, 200.0)) 710 + else { 710 711 panic!("a roll drag transforms the camera"); 711 712 }; 712 713 let (ux, uy, uz) = rolled.up().components();
+35
crates/bone-render/src/gpu.rs
··· 7 7 pub(crate) const PICK_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R32Uint; 8 8 pub(crate) const BYTES_PER_PIXEL: u32 = 4; 9 9 pub(crate) const PICK_BYTES_PER_PIXEL: u32 = 4; 10 + const MSAA_SAMPLE_COUNT: u32 = 4; 10 11 11 12 #[derive(Copy, Clone, Debug, PartialEq, Eq)] 12 13 pub struct BackendTag(wgpu::Backend); ··· 34 35 adapter_limits: wgpu::Limits, 35 36 backend: BackendTag, 36 37 adapter_name: String, 38 + msaa_x4: bool, 37 39 } 38 40 39 41 impl Capabilities { 40 42 pub(crate) fn probe(adapter: &wgpu::Adapter) -> Self { 41 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; 42 48 Self { 43 49 adapter_limits: adapter.limits(), 44 50 backend: BackendTag(info.backend), 45 51 adapter_name: info.name, 52 + msaa_x4: msaa_x4_supported(color, depth), 46 53 } 47 54 } 48 55 ··· 60 67 pub fn adapter_name(&self) -> &str { 61 68 &self.adapter_name 62 69 } 70 + 71 + #[must_use] 72 + pub fn supports_msaa_x4(&self) -> bool { 73 + self.msaa_x4 74 + } 75 + } 76 + 77 + fn 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) 63 83 } 64 84 65 85 pub struct Gpu { ··· 370 390 buffer.unmap(); 371 391 Ok(rgba) 372 392 } 393 + 394 + #[cfg(test)] 395 + mod 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 + }
+1 -1
crates/bone-render/src/lib.rs
··· 399 399 const HIDDEN_DASH_PERIOD_PX: f32 = 6.0; 400 400 const HIDDEN_DASH_ON_RATIO: f32 = 0.5; 401 401 402 - fn depth_texture(device: &wgpu::Device, extent: ViewportExtent) -> wgpu::Texture { 402 + pub(crate) fn depth_texture(device: &wgpu::Device, extent: ViewportExtent) -> wgpu::Texture { 403 403 device.create_texture(&wgpu::TextureDescriptor { 404 404 label: Some("bone-render:solid-depth"), 405 405 size: wgpu::Extent3d {
+1 -4
crates/bone-render/src/navigate.rs
··· 215 215 camera().eye(), 216 216 "a horizontal orbit drag must move the eye" 217 217 ); 218 - let rotated = nav 219 - .orbit_rotation() 220 - .angle() 221 - .get::<uom::si::angle::radian>(); 218 + let rotated = nav.orbit_rotation().angle().get::<uom::si::angle::radian>(); 222 219 assert!( 223 220 rotated.abs() > 1e-3, 224 221 "the orbit state accumulates the drag rotation: {rotated}"
+14 -2
crates/bone-render/src/surface.rs
··· 26 26 config: wgpu::SurfaceConfiguration, 27 27 pick: wgpu::Texture, 28 28 pick_staging: wgpu::Buffer, 29 + depth: wgpu::Texture, 29 30 extent: ViewportExtent, 30 31 reconfigure_pending: bool, 31 32 } ··· 101 102 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, 102 103 mapped_at_creation: false, 103 104 }); 105 + let depth = crate::depth_texture(&device, extent); 104 106 Ok(Self { 105 107 gpu: Gpu::from_parts(device, queue, capabilities), 106 108 surface, 107 109 config, 108 110 pick, 109 111 pick_staging, 112 + depth, 110 113 extent, 111 114 reconfigure_pending: false, 112 115 }) ··· 147 150 self.config.height = extent.height().value(); 148 151 self.surface.configure(self.gpu.device(), &self.config); 149 152 self.pick = create_pick_texture(self.gpu.device(), extent); 153 + self.depth = crate::depth_texture(self.gpu.device(), extent); 150 154 self.reconfigure_pending = false; 151 155 } 152 156 153 157 pub fn render<F, G>(&mut self, build_passes: F, pre_present: G) 154 158 where 155 - F: FnOnce(&mut wgpu::CommandEncoder, &wgpu::TextureView, &wgpu::TextureView), 159 + F: FnOnce( 160 + &mut wgpu::CommandEncoder, 161 + &wgpu::TextureView, 162 + &wgpu::TextureView, 163 + &wgpu::TextureView, 164 + ), 156 165 G: FnOnce(), 157 166 { 158 167 let Some(frame) = self.acquire_frame() else { ··· 164 173 let pick_view = self 165 174 .pick 166 175 .create_view(&wgpu::TextureViewDescriptor::default()); 176 + let depth_view = self 177 + .depth 178 + .create_view(&wgpu::TextureViewDescriptor::default()); 167 179 let mut encoder = 168 180 self.gpu 169 181 .device() 170 182 .create_command_encoder(&wgpu::CommandEncoderDescriptor { 171 183 label: Some("bone-render:surface-encoder"), 172 184 }); 173 - build_passes(&mut encoder, &color_view, &pick_view); 185 + build_passes(&mut encoder, &color_view, &pick_view, &depth_view); 174 186 self.gpu.queue().submit(Some(encoder.finish())); 175 187 pre_present(); 176 188 frame.present();
+35
crates/bone-render/tests/capabilities.rs
··· 1 + mod common; 2 + 3 + use common::{extent_square, make_context}; 4 + 5 + const RENDER_COLOR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm; 6 + const RENDER_DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; 7 + 8 + #[test] 9 + fn msaa_x4_probe_matches_independent_color_and_depth_query() { 10 + let ctx = make_context(extent_square(16)); 11 + let probed = ctx.gpu().capabilities().supports_msaa_x4(); 12 + 13 + let instance = 14 + wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env()); 15 + let Ok(adapter) = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { 16 + power_preference: wgpu::PowerPreference::LowPower, 17 + force_fallback_adapter: false, 18 + compatible_surface: None, 19 + })) else { 20 + panic!("cross-check adapter request must match the offscreen adapter"); 21 + }; 22 + let color = adapter 23 + .get_texture_format_features(RENDER_COLOR_FORMAT) 24 + .flags; 25 + let depth = adapter 26 + .get_texture_format_features(RENDER_DEPTH_FORMAT) 27 + .flags; 28 + let expected = color.supported_sample_counts().contains(&4) 29 + && depth.supported_sample_counts().contains(&4); 30 + 31 + assert_eq!( 32 + probed, expected, 33 + "supports_msaa_x4 must AND msaa x4 across the color and depth render formats", 34 + ); 35 + }
+170 -2
crates/bone-render/tests/frame_budget.rs
··· 1 1 use bone_document::{ 2 2 DimensionKind, EditOutcome, Sketch, SketchDimension, SketchEdit, SketchEntity, SketchRelation, 3 + evaluate_extrude, evaluate_sketch, 3 4 }; 4 - use bone_render::{Camera2, OffscreenContext, SketchRenderer, SketchScene, Style}; 5 + use bone_kernel::{ 6 + ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, ExtrudeSense, MergeResult, 7 + }; 8 + use bone_render::{ 9 + Camera2, OffscreenContext, SketchRenderer, SketchScene, SolidRenderer, SolidScene, Style, 10 + }; 5 11 use bone_types::{ 6 - BudgetCeiling, Point2, Point3, SketchEntityId, SketchPlaneBasis, Tolerance, UnitVec3, 12 + Aabb3, AngleTolerance, BudgetCeiling, Camera3, ChordHeightTolerance, FeatureId, Point2, Point3, 13 + PositiveLength, Projection, SketchEntityId, SketchId, SketchPlaneBasis, Tolerance, UnitVec3, 7 14 }; 15 + use slotmap::{Key, SlotMap}; 8 16 use std::time::{Duration, Instant}; 9 17 use uom::si::f64::Length as UomLength; 10 18 use uom::si::length::millimeter; 19 + 20 + const TOLERANCE: Tolerance = Tolerance::new(1.0e-9); 21 + const CHORD: f64 = 0.05; 22 + const ANGLE: f64 = 0.2; 23 + const BASE_DEPTH_MM: f64 = 4.0; 24 + const DEPTH_STEP_MM: f64 = 0.5; 11 25 12 26 mod common; 13 27 ··· 206 220 ); 207 221 println!("median {median:?}, worst {worst:?}"); 208 222 } 223 + 224 + fn reference_rectangle() -> Sketch { 225 + let s = Sketch::new(xy_plane()); 226 + let (s, p0) = add_point(s, 0.0, 0.0); 227 + let (s, p1) = add_point(s, 10.0, 0.0); 228 + let (s, p2) = add_point(s, 10.0, 5.0); 229 + let (s, p3) = add_point(s, 0.0, 5.0); 230 + let (s, e_bottom) = add_line(s, p0, p1); 231 + let (s, e_right) = add_line(s, p1, p2); 232 + let (s, e_top) = add_line(s, p2, p3); 233 + let (s, e_left) = add_line(s, p3, p0); 234 + let s = add_relation(s, SketchRelation::Horizontal(e_bottom)); 235 + let s = add_relation(s, SketchRelation::Horizontal(e_top)); 236 + let s = add_relation(s, SketchRelation::Vertical(e_right)); 237 + let s = add_relation(s, SketchRelation::Vertical(e_left)); 238 + let s = add_relation(s, SketchRelation::Fix(p0)); 239 + add_linear_dim(s, p0, p1, 10.0) 240 + } 241 + 242 + fn blind(depth_mm: f64) -> ExtrudeFeature { 243 + let Ok(depth) = PositiveLength::new(UomLength::new::<millimeter>(depth_mm)) else { 244 + panic!("{depth_mm} mm is a positive length"); 245 + }; 246 + ExtrudeFeature { 247 + sketch: SketchId::null(), 248 + direction: ExtrudeDirection::Normal { 249 + sense: ExtrudeSense::Forward, 250 + }, 251 + end_condition: ExtrudeEndCondition::Blind { depth }, 252 + draft: None, 253 + thin_wall: None, 254 + merge_result: MergeResult::Merge, 255 + } 256 + } 257 + 258 + fn direction(x: f64, y: f64, z: f64) -> UnitVec3 { 259 + let Ok(unit) = UnitVec3::try_from_components(x, y, z, TOLERANCE) else { 260 + panic!("({x}, {y}, {z}) is a nonzero direction"); 261 + }; 262 + unit 263 + } 264 + 265 + fn framed(aabb: Aabb3, from: UnitVec3, up: UnitVec3) -> Camera3 { 266 + let center = aabb.center(); 267 + let span = 0.5 * aabb.extent().norm_mm(); 268 + let eye = center + from.into_vec(UomLength::new::<millimeter>(span * 3.0)); 269 + let Ok(projection) = Projection::orthographic(UomLength::new::<millimeter>(span * 1.2)) else { 270 + panic!("half height is positive"); 271 + }; 272 + let Ok(camera) = Camera3::new(eye, center, up, projection) else { 273 + panic!("camera is non-degenerate"); 274 + }; 275 + camera 276 + } 277 + 278 + fn frame_slab(sketch: &Sketch, extrude: FeatureId, depth_mm: f64) -> Camera3 { 279 + let evaluated = evaluate_sketch(sketch); 280 + let extruded = evaluate_extrude(extrude, &evaluated, &blind(depth_mm)); 281 + let Some(solid) = extruded.solid() else { 282 + panic!("the reference rectangle extrudes into a slab"); 283 + }; 284 + let Some(aabb) = solid.bounding_box() else { 285 + panic!("the slab has a bounding box"); 286 + }; 287 + framed(aabb, direction(1.0, 1.0, 1.0), UnitVec3::z_axis()) 288 + } 289 + 290 + struct SolidStage<'a> { 291 + renderer: &'a mut SolidRenderer, 292 + ctx: &'a OffscreenContext, 293 + camera: Camera3, 294 + style: &'a Style, 295 + } 296 + 297 + impl SolidStage<'_> { 298 + fn step(&mut self, sketch: &Sketch, extrude: FeatureId, depth_mm: f64) -> Duration { 299 + let started = Instant::now(); 300 + let evaluated = evaluate_sketch(sketch); 301 + let extruded = evaluate_extrude(extrude, &evaluated, &blind(depth_mm)); 302 + let Some(solid) = extruded.solid() else { 303 + panic!("the slab evaluates at {depth_mm} mm"); 304 + }; 305 + let Ok(mesh) = solid.tessellate( 306 + ChordHeightTolerance::from_mm(CHORD), 307 + AngleTolerance::from_radians(ANGLE), 308 + ) else { 309 + panic!("the slab tessellates at {depth_mm} mm"); 310 + }; 311 + let Ok(scene) = SolidScene::from_mesh(&mesh) else { 312 + panic!("the slab mesh packs face pick ids"); 313 + }; 314 + let Ok(_frame) = self 315 + .renderer 316 + .render(self.ctx, &scene, self.camera, self.style) 317 + else { 318 + panic!("solid render must succeed at {depth_mm} mm"); 319 + }; 320 + started.elapsed() 321 + } 322 + } 323 + 324 + #[test] 325 + #[cfg_attr( 326 + debug_assertions, 327 + ignore = "frame budget assertions are only meaningful in release builds" 328 + )] 329 + fn drag_resolve_plus_render_fits_frame_budget_on_reference_extrude() { 330 + let rectangle = reference_rectangle(); 331 + let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key(); 332 + let extrude = features.insert(()); 333 + let size = extent(256); 334 + let ctx = make_context(size); 335 + let mut renderer = SolidRenderer::new(ctx.gpu(), ctx.color_format()); 336 + let style = Style::default(); 337 + let max_depth = BASE_DEPTH_MM + DEPTH_STEP_MM * f64::from(DRAG_STEPS - 1); 338 + let camera = frame_slab(&rectangle, extrude, max_depth); 339 + let mut stage = SolidStage { 340 + renderer: &mut renderer, 341 + ctx: &ctx, 342 + camera, 343 + style: &style, 344 + }; 345 + let _warmup = stage.step(&rectangle, extrude, BASE_DEPTH_MM); 346 + let durations: Vec<Duration> = (0..DRAG_STEPS) 347 + .map(|i| { 348 + let depth_mm = BASE_DEPTH_MM + DEPTH_STEP_MM * f64::from(i); 349 + stage.step(&rectangle, extrude, depth_mm) 350 + }) 351 + .collect(); 352 + let sorted = { 353 + let mut v = durations.clone(); 354 + v.sort(); 355 + v 356 + }; 357 + let median = sorted[sorted.len() / 2]; 358 + let Some(&worst) = sorted.last() else { 359 + panic!("drag loop produced zero samples"); 360 + }; 361 + let budget = BudgetCeiling::FRAME_16MS.duration(); 362 + let worst_ceiling = budget * 2; 363 + if std::env::var("BONE_FRAME_BUDGET_REPORT").is_ok() { 364 + println!( 365 + "reference extrude drag: median {median:?}, worst {worst:?}, samples {durations:?}" 366 + ); 367 + } 368 + assert!( 369 + median <= budget, 370 + "median solve+extrude+tessellate+render step {median:?} exceeds {budget:?} frame budget; samples {durations:?}", 371 + ); 372 + assert!( 373 + worst <= worst_ceiling, 374 + "worst solve+extrude+tessellate+render step {worst:?} exceeds {worst_ceiling:?} relaxed ceiling; samples {durations:?}", 375 + ); 376 + }