From cb69a936acb8d7bd81e3fa0cf2e5f73ea21653a4 Mon Sep 17 00:00:00 2001 From: Lewis Date: Fri, 22 May 2026 16:41:50 +0300 Subject: [PATCH] fix: frame budget test & better clippy allow reasoning Lewis: May this revision serve well! --- crates/bone-app/src/shell.rs | 8 +- crates/bone-document/src/sketch/mod.rs | 10 +- crates/bone-render/src/camera.rs | 10 +- crates/bone-render/src/diff.rs | 6 +- crates/bone-render/src/pick.rs | 10 +- crates/bone-render/src/pipelines/arc.rs | 25 ++- crates/bone-render/src/pipelines/glyph.rs | 13 +- crates/bone-render/src/pipelines/grid.rs | 6 +- crates/bone-render/src/pipelines/lines.rs | 25 ++- crates/bone-render/src/pipelines/mod.rs | 6 +- crates/bone-render/src/pipelines/text.rs | 10 +- crates/bone-render/src/snapshot.rs | 11 +- crates/bone-render/tests/frame_budget.rs | 208 ++++++++++++++++++++++ crates/bone-render/tests/picker.rs | 6 +- crates/bone-ui/src/layout/geometry.rs | 5 +- crates/bone-ui/src/text/raster/sdf.rs | 21 ++- crates/bone-ui/src/theme/color.rs | 10 +- crates/bone-ui/src/theme/typography.rs | 5 +- crates/bone-ui/src/widgets/ribbon.rs | 8 +- crates/bone-ui/src/widgets/slider.rs | 15 +- crates/bone-ui/src/widgets/toolbar.rs | 22 +-- justfile | 16 ++ 22 files changed, 390 insertions(+), 66 deletions(-) create mode 100644 crates/bone-render/tests/frame_budget.rs diff --git a/crates/bone-app/src/shell.rs b/crates/bone-app/src/shell.rs index e127be0..f6df013 100644 --- a/crates/bone-app/src/shell.rs +++ b/crates/bone-app/src/shell.rs @@ -3985,10 +3985,10 @@ mod tests { let size = layout_size(1600.0, 900.0); let ltr = render_with_locale(size, Locale::EnUs); let rtl = render_with_locale(size, Locale::ArXb); - let ltr_rect = label_rect(<r.paints, key) - .unwrap_or_else(|| panic!("ltr paint missing for {key}")); - let rtl_rect = label_rect(&rtl.paints, key) - .unwrap_or_else(|| panic!("rtl paint missing for {key}")); + let ltr_rect = + label_rect(<r.paints, key).unwrap_or_else(|| panic!("ltr paint missing for {key}")); + let rtl_rect = + label_rect(&rtl.paints, key).unwrap_or_else(|| panic!("rtl paint missing for {key}")); let half = size.width.value() * 0.5; assert!( ltr_rect.origin.x.value() < half, diff --git a/crates/bone-document/src/sketch/mod.rs b/crates/bone-document/src/sketch/mod.rs index 2e6fd5e..0eef878 100644 --- a/crates/bone-document/src/sketch/mod.rs +++ b/crates/bone-document/src/sketch/mod.rs @@ -1707,13 +1707,9 @@ mod tests { _ => panic!("expected entity outcomes"), }) .collect(); - let Ok((mut sketch, EditOutcome::Entity(line_id))) = - sketch.apply(SketchEdit::AddEntity(SketchEntity::line( - entity_ids[0], - entity_ids[1], - false, - ))) - else { + let Ok((mut sketch, EditOutcome::Entity(line_id))) = sketch.apply(SketchEdit::AddEntity( + SketchEntity::line(entity_ids[0], entity_ids[1], false), + )) else { panic!("line should add cleanly"); }; let entities = Arc::make_mut(&mut sketch.entities); diff --git a/crates/bone-render/src/camera.rs b/crates/bone-render/src/camera.rs index a66d042..d85940b 100644 --- a/crates/bone-render/src/camera.rs +++ b/crates/bone-render/src/camera.rs @@ -144,7 +144,10 @@ impl Camera2 { } #[must_use] - #[allow(clippy::cast_possible_truncation)] + #[allow( + clippy::cast_possible_truncation, + reason = "mm coordinates fit f32 mantissa at CAD scales" + )] pub fn clip_from_world_mm(self) -> [f32; 16] { let (sx, sy) = self.scale(); let (px, py) = self.pan_mm.coords_mm(); @@ -154,7 +157,10 @@ impl Camera2 { } #[must_use] - #[allow(clippy::cast_possible_truncation)] + #[allow( + clippy::cast_possible_truncation, + reason = "mm coordinates fit f32 mantissa at CAD scales" + )] pub fn world_mm_from_clip(self) -> [f32; 16] { let (sx, sy) = self.scale(); let inv_x = 1.0 / sx; diff --git a/crates/bone-render/src/diff.rs b/crates/bone-render/src/diff.rs index 6812272..88b4f25 100644 --- a/crates/bone-render/src/diff.rs +++ b/crates/bone-render/src/diff.rs @@ -20,7 +20,11 @@ impl PixelDiffThreshold { } #[must_use] - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + #[allow( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + reason = "value is clamped to [0, 1] before scaling to u8" + )] pub fn as_u8(self) -> u8 { (self.0.clamp(0.0, 1.0) * 255.0).round() as u8 } diff --git a/crates/bone-render/src/pick.rs b/crates/bone-render/src/pick.rs index db1d484..7b23dc8 100644 --- a/crates/bone-render/src/pick.rs +++ b/crates/bone-render/src/pick.rs @@ -104,7 +104,10 @@ impl PickId { } #[must_use] - #[allow(clippy::cast_possible_truncation)] + #[allow( + clippy::cast_possible_truncation, + reason = "right-shift by TAG_SHIFT (28) leaves the high nibble in the low 4 bits; value is in 0..16 so u8 fits" + )] pub const fn tag(self) -> Option { EntityKindTag::from_bits((self.0 >> Self::TAG_SHIFT) as u8) } @@ -557,7 +560,10 @@ impl PickIndex { } } -#[allow(clippy::cast_possible_truncation)] +#[allow( + clippy::cast_possible_truncation, + reason = "high 32 bits are masked off before the u64-to-u32 cast, so the low 32 bits fit u32 exactly" +)] fn slot_index(key: K) -> Result { let slot = (key.data().as_ffi() & 0xFFFF_FFFF_u64) as u32; if slot > PickId::INDEX_MASK { diff --git a/crates/bone-render/src/pipelines/arc.rs b/crates/bone-render/src/pipelines/arc.rs index 08c32f3..53994e3 100644 --- a/crates/bone-render/src/pipelines/arc.rs +++ b/crates/bone-render/src/pipelines/arc.rs @@ -229,7 +229,10 @@ fn build_instances( .collect() } -#[allow(clippy::cast_possible_truncation)] +#[allow( + clippy::cast_possible_truncation, + reason = "mm coordinates and radians fit f32 mantissa at CAD scales" +)] fn arc_instance(arc: SceneArc, style: &Style) -> ArcInstance { let (cx, cy) = arc.center().coords_mm(); let radius_mm = arc.radius().get::() as f32; @@ -259,7 +262,10 @@ fn arc_instance(arc: SceneArc, style: &Style) -> ArcInstance { } } -#[allow(clippy::cast_possible_truncation)] +#[allow( + clippy::cast_possible_truncation, + reason = "mm coordinates and radians fit f32 mantissa at CAD scales" +)] fn circle_instance(circle: SceneCircle, style: &Style) -> ArcInstance { let (cx, cy) = circle.center().coords_mm(); let radius_mm = circle.radius().get::() as f32; @@ -281,7 +287,10 @@ fn circle_instance(circle: SceneCircle, style: &Style) -> ArcInstance { } } -#[allow(clippy::cast_possible_truncation)] +#[allow( + clippy::cast_possible_truncation, + reason = "mm coordinates and radians fit f32 mantissa at CAD scales" +)] fn preview_circle_instance(circle: PreviewCircle, style: &Style) -> ArcInstance { let (cx, cy) = circle.center.coords_mm(); let radius_mm = circle.radius.get::() as f32; @@ -298,7 +307,10 @@ fn preview_circle_instance(circle: PreviewCircle, style: &Style) -> ArcInstance } } -#[allow(clippy::cast_possible_truncation)] +#[allow( + clippy::cast_possible_truncation, + reason = "mm coordinates and radians fit f32 mantissa at CAD scales" +)] fn preview_arc_instance(arc: PreviewArc, style: &Style) -> ArcInstance { let (cx, cy) = arc.center.coords_mm(); let radius_mm = arc.radius.get::() as f32; @@ -319,7 +331,10 @@ fn preview_arc_instance(arc: PreviewArc, style: &Style) -> ArcInstance { } } -#[allow(clippy::cast_possible_truncation)] +#[allow( + clippy::cast_possible_truncation, + reason = "mm coordinates and radians fit f32 mantissa at CAD scales" +)] fn arc_aabb_offsets_mm( center: Point2, radius: Length, diff --git a/crates/bone-render/src/pipelines/glyph.rs b/crates/bone-render/src/pipelines/glyph.rs index 76007d1..f66f8e1 100644 --- a/crates/bone-render/src/pipelines/glyph.rs +++ b/crates/bone-render/src/pipelines/glyph.rs @@ -258,7 +258,10 @@ fn create_pipeline( }) } -#[allow(clippy::cast_possible_truncation)] +#[allow( + clippy::cast_possible_truncation, + reason = "mm coordinates fit f32 mantissa at CAD scales" +)] fn build_instance(glyph: SceneRelationGlyph) -> GlyphInstance { let (ax, ay) = glyph.anchor_mm().coords_mm(); let (dx, dy) = glyph.offset_dir().coords_mm(); @@ -270,7 +273,10 @@ fn build_instance(glyph: SceneRelationGlyph) -> GlyphInstance { } } -#[allow(clippy::cast_possible_truncation)] +#[allow( + clippy::cast_possible_truncation, + reason = "mm coordinates fit f32 mantissa at CAD scales" +)] fn build_uniform(camera: Camera2, glyphs: GlyphStyle) -> GlyphUniform { GlyphUniform { clip_from_world: camera.clip_from_world_mm(), @@ -362,7 +368,8 @@ mod tests { #[allow( clippy::cast_possible_truncation, clippy::cast_precision_loss, - clippy::cast_sign_loss + clippy::cast_sign_loss, + reason = "atlas tile coords fit f32 mantissa and coverage is clamped to [0, 1] before u8 scaling" )] fn pixel_rgba(x: u32, y: u32) -> [u8; 4] { let col = x / TILE_SIDE; diff --git a/crates/bone-render/src/pipelines/grid.rs b/crates/bone-render/src/pipelines/grid.rs index c9b4a52..3c10290 100644 --- a/crates/bone-render/src/pipelines/grid.rs +++ b/crates/bone-render/src/pipelines/grid.rs @@ -151,7 +151,11 @@ impl core::fmt::Debug for GridPipeline { } } -#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] +#[allow( + clippy::cast_possible_truncation, + clippy::cast_precision_loss, + reason = "viewport extents and grid spacing fit f32 mantissa" +)] fn build_uniform(camera: Camera2, spacing: GridSpacing, style: &Style) -> GridUniform { let grid = style.grid(); let extent = camera.extent(); diff --git a/crates/bone-render/src/pipelines/lines.rs b/crates/bone-render/src/pipelines/lines.rs index 924acc8..7820d0a 100644 --- a/crates/bone-render/src/pipelines/lines.rs +++ b/crates/bone-render/src/pipelines/lines.rs @@ -226,7 +226,10 @@ fn build_instances( .collect() } -#[allow(clippy::cast_possible_truncation)] +#[allow( + clippy::cast_possible_truncation, + reason = "mm coordinates fit f32 mantissa at CAD scales" +)] fn line_instance(line: SceneLine, style: &Style) -> LineInstance { let (ax, ay) = line.a().coords_mm(); let (bx, by) = line.b().coords_mm(); @@ -244,7 +247,10 @@ fn line_instance(line: SceneLine, style: &Style) -> LineInstance { } } -#[allow(clippy::cast_possible_truncation)] +#[allow( + clippy::cast_possible_truncation, + reason = "mm coordinates fit f32 mantissa at CAD scales" +)] fn point_instance(point: ScenePoint, style: &Style) -> LineInstance { let (x, y) = point.at().coords_mm(); let xy = [x as f32, y as f32]; @@ -257,7 +263,10 @@ fn point_instance(point: ScenePoint, style: &Style) -> LineInstance { } } -#[allow(clippy::cast_possible_truncation)] +#[allow( + clippy::cast_possible_truncation, + reason = "mm coordinates fit f32 mantissa at CAD scales" +)] fn preview_line_instance(a: Point2, b: Point2, style: &Style) -> LineInstance { let (ax, ay) = a.coords_mm(); let (bx, by) = b.coords_mm(); @@ -270,7 +279,10 @@ fn preview_line_instance(a: Point2, b: Point2, style: &Style) -> LineInstance { } } -#[allow(clippy::cast_possible_truncation)] +#[allow( + clippy::cast_possible_truncation, + reason = "mm coordinates fit f32 mantissa at CAD scales" +)] fn preview_point_instance(at: Point2, style: &Style) -> LineInstance { let (x, y) = at.coords_mm(); let xy = [x as f32, y as f32]; @@ -285,7 +297,10 @@ fn preview_point_instance(at: Point2, style: &Style) -> LineInstance { const SNAP_RADIUS_RATIO: f32 = 1.8; -#[allow(clippy::cast_possible_truncation)] +#[allow( + clippy::cast_possible_truncation, + reason = "mm coordinates fit f32 mantissa at CAD scales" +)] fn preview_snap_instance(at: Point2, style: &Style) -> LineInstance { let (x, y) = at.coords_mm(); let xy = [x as f32, y as f32]; diff --git a/crates/bone-render/src/pipelines/mod.rs b/crates/bone-render/src/pipelines/mod.rs index 0cc05df..da820eb 100644 --- a/crates/bone-render/src/pipelines/mod.rs +++ b/crates/bone-render/src/pipelines/mod.rs @@ -34,7 +34,11 @@ pub(crate) const FRAME_UNIFORM_SIZE: u64 = core::mem::size_of::() pub(crate) const CONSTRUCTION_BIT: u32 = 1; -#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] +#[allow( + clippy::cast_possible_truncation, + clippy::cast_precision_loss, + reason = "zoom factor and stroke widths fit f32 mantissa at CAD scales" +)] pub(crate) fn build_frame_uniform(camera: Camera2, style: &Style) -> FrameUniform { let strokes = style.strokes(); FrameUniform { diff --git a/crates/bone-render/src/pipelines/text.rs b/crates/bone-render/src/pipelines/text.rs index 0b75ffd..e6444a8 100644 --- a/crates/bone-render/src/pipelines/text.rs +++ b/crates/bone-render/src/pipelines/text.rs @@ -289,7 +289,10 @@ fn create_pipeline( }) } -#[allow(clippy::cast_possible_truncation)] +#[allow( + clippy::cast_possible_truncation, + reason = "mm coordinates and pixel offsets fit f32 mantissa at CAD scales" +)] fn build_uniform(camera: Camera2, text: TextStyle) -> TextUniform { TextUniform { clip_from_world: camera.clip_from_world_mm(), @@ -301,7 +304,10 @@ fn build_uniform(camera: Camera2, text: TextStyle) -> TextUniform { } } -#[allow(clippy::cast_possible_truncation)] +#[allow( + clippy::cast_possible_truncation, + reason = "mm coordinates and pixel offsets fit f32 mantissa at CAD scales" +)] fn assemble_geometry( scene: &SketchScene, cache: &HashMap, diff --git a/crates/bone-render/src/snapshot.rs b/crates/bone-render/src/snapshot.rs index e26c3b4..7baf4d0 100644 --- a/crates/bone-render/src/snapshot.rs +++ b/crates/bone-render/src/snapshot.rs @@ -32,7 +32,10 @@ impl ClearColor { } #[must_use] - #[allow(clippy::cast_possible_truncation)] + #[allow( + clippy::cast_possible_truncation, + reason = "clear-color channels are bounded [0, 1]" + )] pub fn to_rgba_array(self) -> [f32; 4] { [self.r as f32, self.g as f32, self.b as f32, self.a as f32] } @@ -49,7 +52,11 @@ impl From for wgpu::Color { } } -#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] +#[allow( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + reason = "value is clamped to [0, 1] before scaling to u8" +)] fn channel_to_u8(value: f64) -> u8 { (value.clamp(0.0, 1.0) * 255.0).round() as u8 } diff --git a/crates/bone-render/tests/frame_budget.rs b/crates/bone-render/tests/frame_budget.rs new file mode 100644 index 0000000..84574a3 --- /dev/null +++ b/crates/bone-render/tests/frame_budget.rs @@ -0,0 +1,208 @@ +use bone_document::{ + DimensionKind, EditOutcome, Sketch, SketchDimension, SketchEdit, SketchEntity, SketchRelation, +}; +use bone_render::{Camera2, OffscreenContext, SketchRenderer, SketchScene, Style}; +use bone_types::{ + BudgetCeiling, Point2, Point3, SketchEntityId, SketchPlaneBasis, Tolerance, UnitVec3, +}; +use std::time::{Duration, Instant}; +use uom::si::f64::Length as UomLength; +use uom::si::length::millimeter; + +mod common; + +use common::{extent_square as extent, make_context}; + +fn xy_plane() -> SketchPlaneBasis { + let Ok(basis) = SketchPlaneBasis::new( + Point3::origin(), + UnitVec3::x_axis(), + UnitVec3::y_axis(), + Tolerance::new(1e-9), + ) else { + panic!("xy plane basis is orthogonal"); + }; + basis +} + +fn add_point(s: Sketch, x: f64, y: f64) -> (Sketch, SketchEntityId) { + let Ok((next, EditOutcome::Entity(id))) = s.apply(SketchEdit::AddEntity(SketchEntity::point( + Point2::from_mm(x, y), + ))) else { + panic!("add point"); + }; + (next, id) +} + +fn add_line(s: Sketch, a: SketchEntityId, b: SketchEntityId) -> (Sketch, SketchEntityId) { + let Ok((next, EditOutcome::Entity(id))) = + s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false))) + else { + panic!("add line"); + }; + (next, id) +} + +fn add_circle(s: Sketch, center: SketchEntityId, radius_mm: f64) -> Sketch { + let Ok((next, _)) = s.apply(SketchEdit::AddEntity(SketchEntity::circle( + center, + UomLength::new::(radius_mm), + false, + ))) else { + panic!("add circle"); + }; + next +} + +fn add_arc( + s: Sketch, + center: SketchEntityId, + start: SketchEntityId, + end: SketchEntityId, +) -> Sketch { + let Ok((next, _)) = s.apply(SketchEdit::AddEntity(SketchEntity::arc( + center, start, end, false, + ))) else { + panic!("add arc"); + }; + next +} + +fn add_relation(s: Sketch, r: SketchRelation) -> Sketch { + let Ok((next, _)) = s.apply(SketchEdit::AddRelation(r)) else { + panic!("add relation"); + }; + next +} + +fn add_linear_dim(s: Sketch, a: SketchEntityId, b: SketchEntityId, value_mm: f64) -> Sketch { + let Ok((next, _)) = s.apply(SketchEdit::AddDimension(SketchDimension::Linear { + a, + b, + value: UomLength::new::(value_mm), + kind: DimensionKind::Driving, + })) else { + panic!("add linear dimension"); + }; + next +} + +fn reference_sketch() -> (Sketch, SketchEntityId) { + let s = Sketch::new(xy_plane()); + let (s, p0) = add_point(s, 0.0, 0.0); + let (s, p1) = add_point(s, 10.0, 0.0); + let (s, p2) = add_point(s, 10.0, 5.0); + let (s, p3) = add_point(s, 0.0, 5.0); + let (s, e_bottom) = add_line(s, p0, p1); + let (s, e_right) = add_line(s, p1, p2); + let (s, e_top) = add_line(s, p2, p3); + let (s, e_left) = add_line(s, p3, p0); + let s = add_relation(s, SketchRelation::Horizontal(e_bottom)); + let s = add_relation(s, SketchRelation::Horizontal(e_top)); + let s = add_relation(s, SketchRelation::Vertical(e_right)); + let s = add_relation(s, SketchRelation::Vertical(e_left)); + let s = add_relation(s, SketchRelation::Fix(p0)); + let s = add_linear_dim(s, p0, p1, 10.0); + let s = add_circle(s, p0, 5.0); + let (s, arc_center) = add_point(s, -12.0, 0.0); + let (s, arc_start) = add_point(s, -8.0, 0.0); + let (s, arc_end) = add_point(s, -12.0, 4.0); + let s = add_arc(s, arc_center, arc_start, arc_end); + (s, p2) +} + +#[allow( + clippy::too_many_arguments, + reason = "stepper bundles per-frame pipeline state alongside per-step drag inputs" +)] +fn drag_resolve_render( + current: &Sketch, + dragged: SketchEntityId, + target: Point2, + solver_budget: BudgetCeiling, + renderer: &mut SketchRenderer, + ctx: &OffscreenContext, + camera: Camera2, + style: &Style, +) -> (Sketch, Duration) { + let started = Instant::now(); + let Ok(next) = current.solve_with_drag(dragged, target, solver_budget) else { + panic!("drag must converge inside solver budget"); + }; + let Ok(scene) = SketchScene::extract(&next) else { + panic!("scene extract must succeed for solved sketch"); + }; + let Ok(_frame) = renderer.render(ctx, &scene, camera, style) else { + panic!("render must succeed for solved sketch"); + }; + (next, started.elapsed()) +} + +const DRAG_STEPS: u32 = 16; + +#[test] +#[cfg_attr( + debug_assertions, + ignore = "frame budget assertions are only meaningful in release builds" +)] +fn drag_resolve_plus_render_fits_frame_budget_on_reference_sketch() { + let (sketch, dragged) = reference_sketch(); + let Ok(sketch) = sketch.solve() else { + panic!("reference sketch must solve at rest"); + }; + let size = extent(256); + let ctx = make_context(size); + let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format()); + let camera = Camera2::new(size); + let style = Style::default(); + let frame_budget = Duration::from_millis(16); + let worst_ceiling = frame_budget * 2; + let solver_budget = BudgetCeiling::new(frame_budget / 2_u32); + let warmup_target = Point2::from_mm(10.0, 4.0); + let (warmed, _warmup_elapsed) = drag_resolve_render( + &sketch, + dragged, + warmup_target, + solver_budget, + &mut renderer, + &ctx, + camera, + &style, + ); + let (_final_sketch, durations) = (0..DRAG_STEPS).fold( + (warmed, Vec::::new()), + |(current, durations), i| { + let target = Point2::from_mm(10.0, 5.0 + 0.1 * f64::from(i)); + let (next, elapsed) = drag_resolve_render( + ¤t, + dragged, + target, + solver_budget, + &mut renderer, + &ctx, + camera, + &style, + ); + let next_durations = durations.into_iter().chain([elapsed]).collect(); + (next, next_durations) + }, + ); + let sorted = { + let mut v = durations.clone(); + v.sort(); + v + }; + let median = sorted[sorted.len() / 2]; + let Some(&worst) = sorted.last() else { + panic!("drag loop produced zero samples"); + }; + assert!( + median <= frame_budget, + "median drag+render step {median:?} exceeds {frame_budget:?} frame budget; samples {durations:?}", + ); + assert!( + worst <= worst_ceiling, + "worst drag+render step {worst:?} exceeds {worst_ceiling:?} relaxed ceiling; samples {durations:?}", + ); + println!("median {median:?}, worst {worst:?}"); +} diff --git a/crates/bone-render/tests/picker.rs b/crates/bone-render/tests/picker.rs index 8307e5a..c89c7a5 100644 --- a/crates/bone-render/tests/picker.rs +++ b/crates/bone-render/tests/picker.rs @@ -71,7 +71,11 @@ fn add_arc( (next, id) } -#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] +#[allow( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + reason = "fixture coordinates are bounded within the small test viewport" +)] fn world_to_px(x_mm: f32, y_mm: f32) -> PickQuery { let x_px = (CENTER_PX + x_mm * PX_PER_MM).round() as u32; let y_px = (CENTER_PX - y_mm * PX_PER_MM).round() as u32; diff --git a/crates/bone-ui/src/layout/geometry.rs b/crates/bone-ui/src/layout/geometry.rs index 726c513..7612344 100644 --- a/crates/bone-ui/src/layout/geometry.rs +++ b/crates/bone-ui/src/layout/geometry.rs @@ -215,8 +215,9 @@ impl LayoutRect { super::axis::LayoutDirection::Rtl => { let container_left = container.origin.x.value(); let container_right = container_left + container.size.width.value(); - let mirrored_x = - container_right - (self.origin.x.value() - container_left) - self.size.width.value(); + let mirrored_x = container_right + - (self.origin.x.value() - container_left) + - self.size.width.value(); Self::new( LayoutPos::new(LayoutPx::new(mirrored_x), self.origin.y), self.size, diff --git a/crates/bone-ui/src/text/raster/sdf.rs b/crates/bone-ui/src/text/raster/sdf.rs index cedbc0a..a4ad9af 100644 --- a/crates/bone-ui/src/text/raster/sdf.rs +++ b/crates/bone-ui/src/text/raster/sdf.rs @@ -238,7 +238,10 @@ struct GlyphTile { bearing_top: f32, } -#[allow(clippy::cast_precision_loss)] +#[allow( + clippy::cast_precision_loss, + reason = "atlas tile dimensions fit f32 mantissa" +)] fn rasterise_glyph_sdf( font: &FontRef<'_>, glyph: u16, @@ -318,7 +321,10 @@ fn build_padded_mask(coverage: &[u8], inner_w: u32, inner_h: u32, pad: u32) -> V const FAR: i32 = i32::MAX / 4; -#[allow(clippy::cast_precision_loss)] +#[allow( + clippy::cast_precision_loss, + reason = "atlas dimensions fit f32 mantissa" +)] fn compute_sdf_bytes(mask: &[bool], width: u32, height: u32, spread: u32) -> Vec { let outside = distance_field_to(mask, width, height, true); let inside = distance_field_to(mask, width, height, false); @@ -329,7 +335,11 @@ fn compute_sdf_bytes(mask: &[bool], width: u32, height: u32, spread: u32) -> Vec let di = (inside[i] as f32).sqrt(); let signed = if mask[i] { -di } else { do_ }; let normalised = ((signed / spread_f) * 0.5 + 0.5).clamp(0.0, 1.0); - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + #[allow( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + reason = "normalised is clamped to [0, 1] before scaling to u8" + )] let byte = (normalised * 255.0 + 0.5) as u8; byte }) @@ -491,7 +501,10 @@ fn blit_tile(dst: &mut [u8], extent: u32, placed: PlacedTile, tile: &GlyphTile) }); } -#[allow(clippy::cast_precision_loss)] +#[allow( + clippy::cast_precision_loss, + reason = "atlas extents and tile dimensions fit f32 mantissa" +)] fn atlas_entry(params: SdfAtlasParams, placed: PlacedTile, tile: &GlyphTile) -> AtlasEntry { let extent = params.atlas_extent as f32; let uv_min = [placed.x as f32 / extent, placed.y as f32 / extent]; diff --git a/crates/bone-ui/src/theme/color.rs b/crates/bone-ui/src/theme/color.rs index aa73f7b..76d744f 100644 --- a/crates/bone-ui/src/theme/color.rs +++ b/crates/bone-ui/src/theme/color.rs @@ -72,7 +72,10 @@ impl Color { #[must_use] pub(in crate::theme) fn from_srgb_u8(r: u8, g: u8, b: u8) -> Self { - #[allow(clippy::disallowed_methods)] + #[allow( + clippy::disallowed_methods, + reason = "the theme color constructor is the chosen entry point for raw palette construction" + )] let lin: LinSrgb = Srgb::new( f32::from(r) / 255.0, f32::from(g) / 255.0, @@ -166,7 +169,10 @@ const SRGB_GAMUT_EPS: f32 = 1.0e-4; const GAMUT_BISECT_STEPS: u32 = 24; fn oklch_to_linsrgb_raw(l: f32, c: f32, h_degrees: f32) -> [f32; 3] { - #[allow(clippy::disallowed_methods)] + #[allow( + clippy::disallowed_methods, + reason = "the theme color constructor is the chosen entry point for raw palette construction" + )] let lin = LinSrgb::::from_color_unclamped(Oklch::::new(l, c, h_degrees)); [lin.red, lin.green, lin.blue] } diff --git a/crates/bone-ui/src/theme/typography.rs b/crates/bone-ui/src/theme/typography.rs index 3a7827e..80a89fe 100644 --- a/crates/bone-ui/src/theme/typography.rs +++ b/crates/bone-ui/src/theme/typography.rs @@ -39,7 +39,10 @@ macro_rules! length_newtype { } #[must_use] - #[allow(clippy::cast_possible_truncation)] + #[allow( + clippy::cast_possible_truncation, + reason = "px sizes fit f32 mantissa at typography scales" + )] pub fn as_px_f32(self) -> f32 { length_to_px(self.0) as f32 } diff --git a/crates/bone-ui/src/widgets/ribbon.rs b/crates/bone-ui/src/widgets/ribbon.rs index a41a349..b860493 100644 --- a/crates/bone-ui/src/widgets/ribbon.rs +++ b/crates/bone-ui/src/widgets/ribbon.rs @@ -268,7 +268,13 @@ fn render_groups( .iter() .map(|r| r.mirror_horizontally_within(body_rect, direction)) .collect(); - paint.extend(group_dividers(&raw_layouts, body_rect, group_gap, direction, ctx)); + paint.extend(group_dividers( + &raw_layouts, + body_rect, + group_gap, + direction, + ctx, + )); groups .iter() .zip(layouts.iter()) diff --git a/crates/bone-ui/src/widgets/slider.rs b/crates/bone-ui/src/widgets/slider.rs index 7ae04e2..7793385 100644 --- a/crates/bone-ui/src/widgets/slider.rs +++ b/crates/bone-ui/src/widgets/slider.rs @@ -300,7 +300,10 @@ fn build_paint( LayoutSize::new(rect.size.width, track_height), ); let unit = value.to_unit(range).clamp(0.0, 1.0); - #[allow(clippy::cast_possible_truncation)] + #[allow( + clippy::cast_possible_truncation, + reason = "unit is clamped to [0, 1] before f32 narrowing" + )] let unit_f32 = unit as f32; let filled_width = LayoutPx::new(rect.size.width.value() * unit_f32); let filled_rect = LayoutRect::new( @@ -398,7 +401,10 @@ impl SliderScalar for f32 { f64::from((self - range.min) / (range.max - range.min)).clamp(0.0, 1.0) } - #[allow(clippy::cast_possible_truncation)] + #[allow( + clippy::cast_possible_truncation, + reason = "unit is clamped to [0, 1] before f32 narrowing" + )] fn from_unit(unit: f64, range: SliderRange) -> Self { range.min + (range.max - range.min) * unit.clamp(0.0, 1.0) as f32 } @@ -408,7 +414,10 @@ impl SliderScalar for f32 { } fn step_by(self, step: SliderStep, sign: i32) -> Self { - #[allow(clippy::cast_precision_loss)] + #[allow( + clippy::cast_precision_loss, + reason = "sign values fit f32 mantissa exactly" + )] let mul = sign as f32; self + step.value() * mul } diff --git a/crates/bone-ui/src/widgets/toolbar.rs b/crates/bone-ui/src/widgets/toolbar.rs index 619fd2b..0ade926 100644 --- a/crates/bone-ui/src/widgets/toolbar.rs +++ b/crates/bone-ui/src/widgets/toolbar.rs @@ -201,13 +201,8 @@ pub fn show_toolbar(ctx: &mut FrameCtx<'_>, toolbar: Toolbar<'_>) -> ToolbarResp overflow_toggled = result.activated; if cfg.open && plan.hidden_count > 0 { let hidden_items = &items[items.len() - plan.hidden_count..]; - let popup = render_overflow_popup( - ctx, - chevron_rect, - hidden_items, - item_size, - orientation, - ); + let popup = + render_overflow_popup(ctx, chevron_rect, hidden_items, item_size, orientation); popover_paint.extend(popup.paint); popup_consumed_click = popup.consumed_click; if let Some(act) = popup.activated @@ -633,9 +628,7 @@ fn total_extent(items: &[ToolbarItem], item_size: LayoutPx, gap: LayoutPx) -> f3 items .iter() .enumerate() - .map(|(i, it)| { - item_extent(it, item_size) + if i == 0 { 0.0 } else { gap.value() } - }) + .map(|(i, it)| item_extent(it, item_size) + if i == 0 { 0.0 } else { gap.value() }) .sum() } @@ -702,9 +695,7 @@ fn single_item_rect( mod tests { use std::sync::Arc; - use super::{ - Toolbar, ToolbarItem, ToolbarOverflowConfig, overflow_chevron_id, show_toolbar, - }; + use super::{Toolbar, ToolbarItem, ToolbarOverflowConfig, overflow_chevron_id, show_toolbar}; use crate::focus::FocusManager; use crate::frame::FrameCtx; use crate::hit_test::{HitFrame, HitState, resolve}; @@ -907,10 +898,7 @@ mod tests { #[test] fn clicking_disabled_popup_item_consumes_click_but_does_not_activate() { - let items: Vec = items(6) - .into_iter() - .map(|it| it.disabled(true)) - .collect(); + let items: Vec = items(6).into_iter().map(|it| it.disabled(true)).collect(); let rect = LayoutRect::new( LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO), LayoutSize::new(LayoutPx::new(100.0), LayoutPx::new(28.0)), diff --git a/justfile b/justfile index 041e8b7..f9a0d40 100644 --- a/justfile +++ b/justfile @@ -35,3 +35,19 @@ fmt-check: cargo fmt --all -- --check lint: fmt-check clippy + +determinism: + @find crates -name '*.snap.new' -delete + @echo "run 1 / 2 (debug)" + CARGO_INCREMENTAL=0 INSTA_UPDATE=no cargo test --workspace --all-features --locked --quiet + @if find crates -name '*.snap.new' -print -quit | grep -q .; then echo "snapshot drift in run 1" >&2; exit 1; fi + @echo "run 2 / 2 (debug)" + CARGO_INCREMENTAL=0 INSTA_UPDATE=no cargo test --workspace --all-features --locked --quiet + @if find crates -name '*.snap.new' -print -quit | grep -q .; then echo "snapshot drift in run 2" >&2; exit 1; fi + @echo "run 1 / 2 (release)" + CARGO_INCREMENTAL=0 INSTA_UPDATE=no cargo test --workspace --all-features --locked --release --quiet + @if find crates -name '*.snap.new' -print -quit | grep -q .; then echo "snapshot drift in release run 1" >&2; exit 1; fi + @echo "run 2 / 2 (release)" + CARGO_INCREMENTAL=0 INSTA_UPDATE=no cargo test --workspace --all-features --locked --release --quiet + @if find crates -name '*.snap.new' -print -quit | grep -q .; then echo "snapshot drift in release run 2" >&2; exit 1; fi + @echo "determinism: clean across two debug + two release runs" -- 2.51.2