From 4c277bc2f95ce0c8afbd890461c1a1cbc7675daf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 14 May 2026 14:53:42 +0300 Subject: [PATCH 01/76] Factor pipeline stages into own functions --- core/src/render.rs | 74 ++++++++++++++++++++++++++++++----------- core/src/render/clip.rs | 6 ++++ 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/core/src/render.rs b/core/src/render.rs index f6d8c6f0..b1b19a9e 100644 --- a/core/src/render.rs +++ b/core/src/render.rs @@ -137,7 +137,7 @@ pub fn render( shader: &Shd, uniform: Uni, to_screen: Mat4, - mut target: &mut impl Target, + target: &mut impl Target, ctx: &Context, ) where Prim: Render + Clone, @@ -155,38 +155,40 @@ pub fn render( stats.verts.i = verts.len(); // 1. Vertex shader: transform vertices to clip space - let verts: Vec<_> = verts - // verts is borrowed, can't consume - .iter() - // TODO Pass vertex as ref to shader - .cloned() - .map(|v| shader.shade_vertex(v, uniform)) - .map(ClipVert::new) - .collect(); + let verts = vertex_transform(shader, uniform, verts); // 2. Primitive assembly: map vertex indices to actual vertices - let prims: Vec<_> = prims - .iter() - .map(|tri| Prim::inline(tri.clone(), &verts)) - // Collect needed because clip takes a slice... - .collect(); + let prims: Vec<_> = primitive_assembly(prims, &verts); // 3. Clipping: clip against the view frustum - // TODO capacity is just a heuristic, should retain vector between calls somehow - let mut clipped = Vec::with_capacity(prims.len() / 2); + let mut clipped = Vec::new(); view_frustum::clip(&prims[..], &mut clipped); - // Optional depth sorting for use case such as transparency + // Optional depth sorting for use cases such as transparency if let Some(d) = ctx.depth_sort { depth_sort::(&mut clipped, d); } - // For each primitive in the view frustum: + // 4. Rasterize: Turn visible primitives to fragments + stats += rasterize::(clipped, shader, to_screen, target, ctx); + + *ctx.stats.borrow_mut() += stats.finish(); +} + +fn rasterize, Shd: FragmentShader, Var: Vary>( + clipped: Vec, + shader: &Shd, + to_screen: Mat4, + mut target: &mut impl Target, + ctx: &Context, +) -> Stats { + let mut stats = Stats::new(); for prim in clipped { // Transform to screen space let prim = Prim::to_screen(prim, &to_screen); // Back/frontface culling - // TODO This could also be done earlier, before or as part of clipping + // TODO This could also be done earlier, before or as part of clipping, + // but determining back/frontface is more difficult before z-div if ctx.face_cull(Prim::is_backface(&prim)) { continue; } @@ -203,7 +205,39 @@ pub fn render( .rasterize(scanline, shader, ctx); }); } - *ctx.stats.borrow_mut() += stats.finish(); + stats +} + +#[inline] +fn primitive_assembly + Clone, Var: Vary>( + prims: &[Prim], + verts: &[ClipVert], +) -> Vec { + prims + .iter() + .cloned() + .map(|prim| Prim::inline(prim, &verts)) + // Collect needed because clip takes a slice... + .collect() +} + +#[inline] +fn vertex_transform( + shader: &Shd, + uniform: Uni, + verts: &[Vtx], +) -> Vec> +where + Shd: VertexShader>, +{ + verts + // verts is borrowed, can't consume + .iter() + // TODO Pass vertex as ref to shader + .cloned() + .map(|v| shader.shade_vertex(v, uniform)) + .map(ClipVert::new) + .collect() } fn depth_sort, V: Vary>(prims: &mut [P::Clip], d: DepthSort) { diff --git a/core/src/render/clip.rs b/core/src/render/clip.rs index 2bac65a4..46d9e29b 100644 --- a/core/src/render/clip.rs +++ b/core/src/render/clip.rs @@ -313,6 +313,9 @@ impl Clip for [Edge>] { type Item = Edge>; fn clip(&self, planes: &[ClipPlane], out: &mut Vec) { + // TODO capacity is just a heuristic, should retain vector between calls somehow + out.reserve(self.len() / 2); + 'lines: for edge @ Edge(a, b) in self { let both_outside = a.outcode & b.outcode != 0; let neither_outside = a.outcode | b.outcode == 0; @@ -356,6 +359,9 @@ impl Clip for [Tri>] { fn clip(&self, planes: &[ClipPlane], out: &mut Vec) { debug_assert!(out.is_empty()); + // TODO capacity is just a heuristic, should retain vector between calls somehow + out.reserve(self.len() / 2); + // Avoid unnecessary allocations by reusing these let mut verts_in = Vec::with_capacity(10); let mut verts_out = Vec::with_capacity(10); From 1b7652179d62a0987395a01b7ced05743d4f0a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sun, 1 Dec 2024 17:33:22 +0200 Subject: [PATCH 02/76] Add support for light sources Directional, point, and spot lights are implemented. --- README.md | 4 +- core/src/math.rs | 1 + core/src/render.rs | 2 + core/src/render/cam.rs | 13 ++-- core/src/render/light.rs | 132 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 7 deletions(-) create mode 100644 core/src/render/light.rs diff --git a/README.md b/README.md index 2e264210..23a96e73 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ for custom allocators is planned in order to make `alloc` optional as well. * Type-tagged affine and linear transforms and projections * Perspective-correct texture mapping * Triangle mesh data structure and a library of shapes +* Point, directional, and spotlight support * Cubic Bézier, Hermite, Catmull–Rom, and B-splines * Simple random number generation and distributions * Simple text rendering with bitmap fonts @@ -68,8 +69,7 @@ for custom allocators is planned in order to make `alloc` optional as well. ## In progress * Different camera types -* Builtin light source support -* Spherical etc UV mapping +* Spherical etc. UV mapping * Procedural noise generation * Terminal frontend with ncurses * Cube mapping and skyboxes diff --git a/core/src/math.rs b/core/src/math.rs index 20e20b67..c9a03172 100644 --- a/core/src/math.rs +++ b/core/src/math.rs @@ -150,6 +150,7 @@ pub fn lerp(t: f32, from: T, to: T) -> T { /// ``` #[inline] pub fn inv_lerp(t: f32, min: f32, max: f32) -> f32 { + debug_assert!(!min.approx_eq(&max)); (t - min) / (max - min) } diff --git a/core/src/render.rs b/core/src/render.rs index b1b19a9e..90afd5e4 100644 --- a/core/src/render.rs +++ b/core/src/render.rs @@ -26,6 +26,7 @@ pub(super) mod re_exports { cam::Camera, clip::Clip, ctx::Context, + light::Light, raster::Frag, shader::{FragmentShader, VertexShader}, stats::Stats, @@ -41,6 +42,7 @@ pub mod cam; pub mod clip; pub mod ctx; pub mod debug; +pub mod light; pub mod prim; pub mod raster; pub mod scene; diff --git a/core/src/render/cam.rs b/core/src/render/cam.rs index 145e57bd..2db3d047 100644 --- a/core/src/render/cam.rs +++ b/core/src/render/cam.rs @@ -215,11 +215,15 @@ impl Camera { } } +// TODO Should probably pass view and projection matrices separately +pub type CameraUni<'a, B, Uni> = (&'a ProjMat3, Uni); + impl Camera { - /// Returns the camera matrix. + /// Returns the camera (view, eye) matrix. pub fn world_to_view(&self) -> Mat4 { self.transform.world_to_view() } + /// Returns the inverse camera matrix. pub fn view_to_world(&self) -> Mat4 { self.world_to_view().inverse() @@ -244,15 +248,14 @@ impl Camera { ) where Prim: Render + Clone, [::Clip]: Clip, - Shd: for<'a> Shader, Uni)>, + Shd: for<'a> Shader>, { - let tf = to_world.then(&self.world_to_project()); - + let to_proj = to_world.then(&self.world_to_project()); super::render( prims.as_ref(), verts.as_ref(), shader, - (&tf, uniform), + (&to_proj, uniform), self.viewport, target, ctx, diff --git a/core/src/render/light.rs b/core/src/render/light.rs new file mode 100644 index 00000000..1f61cda7 --- /dev/null +++ b/core/src/render/light.rs @@ -0,0 +1,132 @@ +//! Light sources + +use core::fmt::{self, Debug, Formatter}; + +use crate::math::{Color3f, Mat4, Point3, Vec3, color::gray, inv_lerp}; + +/// A light source. +#[derive(Copy, Clone, PartialEq)] +pub struct Light { + pub color: Color3f, + pub kind: Kind, + pub falloff: u8, +} + +#[derive(Copy, Clone, PartialEq)] +pub enum Kind { + /// A light source "at infinity", so that the light rays arrive + /// approximately parallel and the direction of the light source + /// is the same for every point. For example the sun or the moon. + Directional(Vec3), + /// A light source radiating omnidirectionally from a single point. + Point(Point3), + /// A light source radiating from a point in a cone shape. + Spot { + pos: Point3, + dir: Vec3, + radii: (f32, f32), + }, +} + +impl Light { + /// Creates a new light source of the given color and kind. + pub fn new(color: Color3f, mut kind: Kind) -> Self { + match &mut kind { + Kind::Directional(dir) => *dir = dir.normalize(), + Kind::Spot { dir, .. } => *dir = dir.normalize(), + _ => {} + }; + Self { color, kind, ..Self::default() } + } + + /// Returns the normalized direction vector from a point to `self`. + #[inline] + pub fn direction(&self, pt: Point3) -> Vec3 { + match self.kind { + Kind::Point(pos) => (pos - pt).normalize_approx(), + Kind::Directional(dir) => dir, + Kind::Spot { pos, .. } => (pos - pt).normalize_approx(), + } + } + + #[inline] + pub fn eval(&self, pt: Point3) -> (Color3f, Vec3) { + let pt_dir = self.direction(pt); + let color = match self.kind { + Kind::Point(_) => self.color, + Kind::Directional(_) => self.color, + Kind::Spot { dir, radii, .. } => { + let dot = pt_dir.dot(&dir); + let (r0, r1) = (1.0 - radii.0, 1.0 - radii.1); + if dot > r0 { + self.color + } else if dot > r1 { + let t = inv_lerp(dot, r1, r0); // ok: r0 != r1 + self.color * t + } else { + gray(0.0) + } + } + }; + (color, pt_dir) + } + + pub fn transform(&self, mat: &Mat4) -> Light { + let Self { color, kind, falloff } = *self; + let kind = match kind { + Kind::Point(pos) => Kind::Point(mat.apply(&pos)), + Kind::Directional(dir) => Kind::Directional(mat.apply(&dir)), + Kind::Spot { pos, dir, radii } => Kind::Spot { + pos: mat.apply(&pos), + dir: mat.apply(&dir), + radii, + }, + }; + Light { kind, color, falloff } + } +} + +// Ugh, manual impls to avoid B: Default bound on the types... + +impl Debug for Light { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("Light") + .field("kind", &self.kind) + .field("color", &self.color) + .field("falloff", &self.falloff) + .finish() + } +} + +impl Default for Light { + fn default() -> Self { + Self { + color: gray(1.0), + kind: Kind::default(), + falloff: 0, + } + } +} + +impl Debug for Kind { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Kind::Directional(dir) => { + f.debug_tuple("Directional").field(&dir).finish() + } + Kind::Point(pt) => f.debug_tuple("Point").field(&pt).finish(), + Kind::Spot { pos, dir, radii } => f + .debug_struct("Spot") + .field("pos", &pos) + .field("dir", &dir) + .field("radii", radii) + .finish(), + } + } +} + +impl Default for Kind { + fn default() -> Self { + Self::Directional(Vec3::Y) + } +} From 23249db739f05c4b8cbe5024d2c420012b5f9d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Mon, 2 Dec 2024 00:46:25 +0200 Subject: [PATCH 03/76] Add uniform parameter also to fragment shader This allows passing light, material, etc. information to the fragment shader. --- benches/e2e.rs | 19 ++++++++------- core/examples/hello_tri.rs | 4 +-- core/src/render.rs | 22 ++++++++++++----- core/src/render/debug.rs | 8 ++++-- core/src/render/shader.rs | 24 +++++++++--------- core/src/render/target.rs | 50 +++++++++++++++++++++++--------------- core/tests/rendering.rs | 2 +- demos/src/bin/crates.rs | 4 +-- demos/src/bin/curses.rs | 15 +++++------- demos/src/bin/hello.rs | 2 +- demos/src/bin/solids.rs | 2 +- demos/src/bin/sprites.rs | 7 +++--- demos/src/bin/square.rs | 2 +- demos/wasm/src/triangle.rs | 2 +- 14 files changed, 93 insertions(+), 70 deletions(-) diff --git a/benches/e2e.rs b/benches/e2e.rs index d295bb12..d6a2c8dc 100644 --- a/benches/e2e.rs +++ b/benches/e2e.rs @@ -2,14 +2,15 @@ use divan::Bencher; -use retrofire_core::geom::{Normal3, Vertex3, tri, vertex}; -use retrofire_core::math::{ - Color3f, Color4, Color4f, ProjMat3, perspective, pt2, pt3, rgb, rgba, - translate, viewport, +use retrofire_core::{ + geom::{Normal3, Vertex3, tri, vertex}, + math::{ + Color3f, Color4, Color4f, ProjMat3, perspective, pt2, pt3, rgb, rgba, + translate, viewport, + }, + render::{Context, Frag, Model, debug::dir_to_rgb, render, shader}, + util::{buf::Buf2, pnm}, }; -use retrofire_core::render::debug::dir_to_rgb; -use retrofire_core::render::{Context, Frag, Model, render, shader}; -use retrofire_core::util::{buf::Buf2, pnm}; use retrofire_geom::solids::{Build, Sphere}; #[cfg(false)] @@ -28,7 +29,7 @@ fn triangle(b: Bencher, n: u32) { |v: Vertex3, mvp: &ProjMat3| { vertex(mvp.apply(&v.pos), v.attrib) }, - |frag: Frag>| frag.var.to_color4(), + |frag: Frag>, _: &_| frag.var.to_color4(), ); let dims @ (w, h) = (640, 480); @@ -72,7 +73,7 @@ fn sphere(b: Bencher, res: u32) { |v: Vertex3, mvp: &ProjMat3| { vertex(mvp.apply(&v.pos), dir_to_rgb(v.attrib)) }, - |frag: Frag| frag.var.to_color4(), + |frag: Frag, _: &_| frag.var.to_color4(), ); let dims @ (w, h) = (640, 480); diff --git a/core/examples/hello_tri.rs b/core/examples/hello_tri.rs index 5d1af8a3..fcfe000d 100644 --- a/core/examples/hello_tri.rs +++ b/core/examples/hello_tri.rs @@ -17,7 +17,7 @@ fn main() { // Interpolate vertex colors in linear color space vertex(mvp.apply(&v.pos), v.attrib.to_linear()) }, - |frag: Frag>| frag.var.to_srgb().to_color4(), + |frag: Frag>, _| frag.var.to_srgb().to_color4(), ); #[cfg(not(feature = "fp"))] let shader = shader::new( @@ -26,7 +26,7 @@ fn main() { // Interpolate vertex colors in normal sRGB color space vertex(mvp.apply(&v.pos), v.attrib) }, - |frag: Frag>| frag.var.to_color4(), + |frag: Frag>, _| frag.var.to_color4(), ); let dims @ (w, h) = (640, 480); diff --git a/core/src/render.rs b/core/src/render.rs index 90afd5e4..20588b05 100644 --- a/core/src/render.rs +++ b/core/src/render.rs @@ -123,12 +123,13 @@ pub type NdcToScreen = RealToReal<3, Ndc, Screen>; /// Alias for combined vertex+fragment shader types pub trait Shader: - VertexShader> + FragmentShader + VertexShader> + + FragmentShader { } impl Shader for S where S: VertexShader> - + FragmentShader + + FragmentShader { } @@ -172,18 +173,27 @@ pub fn render( } // 4. Rasterize: Turn visible primitives to fragments - stats += rasterize::(clipped, shader, to_screen, target, ctx); + stats += rasterize::( + clipped, shader, uniform, to_screen, target, ctx, + ); *ctx.stats.borrow_mut() += stats.finish(); } -fn rasterize, Shd: FragmentShader, Var: Vary>( +fn rasterize( clipped: Vec, shader: &Shd, + uniform: Uni, to_screen: Mat4, mut target: &mut impl Target, ctx: &Context, -) -> Stats { +) -> Stats +where + Prim: Render, + Shd: FragmentShader, + Var: Vary, + Uni: Copy, +{ let mut stats = Stats::new(); for prim in clipped { // Transform to screen space @@ -204,7 +214,7 @@ fn rasterize, Shd: FragmentShader, Var: Vary>( // Convert to fragments, shade, and draw to target stats.frags += target .deref_mut() - .rasterize(scanline, shader, ctx); + .rasterize(scanline, shader, uniform, ctx); }); } stats diff --git a/core/src/render/debug.rs b/core/src/render/debug.rs index 4ce276ce..32b9991f 100644 --- a/core/src/render/debug.rs +++ b/core/src/render/debug.rs @@ -30,8 +30,12 @@ impl<'a, B> VertexShader, &'a ProjMat3> for Shader { } } -impl FragmentShader for Shader { - fn shade_fragment(&self, f: Frag) -> Option { +impl<'a, B> FragmentShader> for Shader { + fn shade_fragment( + &self, + f: Frag, + _: &'a ProjMat3, + ) -> Option { Some(f.var.to_color4()) } } diff --git a/core/src/render/shader.rs b/core/src/render/shader.rs index b2560654..c7e7b759 100644 --- a/core/src/render/shader.rs +++ b/core/src/render/shader.rs @@ -45,13 +45,13 @@ pub trait VertexShader { /// /// # Type parameters /// * `Var`: The varying of the input fragment. -pub trait FragmentShader { +pub trait FragmentShader { /// Computes the color of `frag`. Returns either `Some(color)`, or `None` /// if the fragment should be discarded. /// /// # Panics /// `shade_fragment` should never panic. - fn shade_fragment(&self, frag: Frag) -> Option; + fn shade_fragment(&self, frag: Frag, uniform: Uni) -> Option; } impl VertexShader for F @@ -66,21 +66,21 @@ where } } -impl FragmentShader for F +impl FragmentShader for F where - F: Fn(Frag) -> Out, + F: Fn(Frag, Uni) -> Out, Out: Into>, { #[inline] - fn shade_fragment(&self, frag: Frag) -> Option { - self(frag).into() + fn shade_fragment(&self, frag: Frag, uniform: Uni) -> Option { + self(frag, uniform).into() } } pub fn new(vs: Vs, fs: Fs) -> Shader where Vs: VertexShader>, - Fs: FragmentShader, + Fs: FragmentShader, { Shader::new(vs, fs) } @@ -98,7 +98,7 @@ impl Shader { pub const fn new(vs: Vs, fs: Fs) -> Self where Vs: VertexShader>, - Fs: FragmentShader, + Fs: FragmentShader, { Self { vertex_shader: vs, @@ -119,12 +119,12 @@ where } } -impl FragmentShader for Shader +impl FragmentShader for Shader where - Fs: FragmentShader, + Fs: FragmentShader, { #[inline] - fn shade_fragment(&self, frag: Frag) -> Option { - self.fragment_shader.shade_fragment(frag) + fn shade_fragment(&self, frag: Frag, uni: Uni) -> Option { + self.fragment_shader.shade_fragment(frag, uni) } } diff --git a/core/src/render/target.rs b/core/src/render/target.rs index 6316d778..1822346c 100644 --- a/core/src/render/target.rs +++ b/core/src/render/target.rs @@ -19,15 +19,17 @@ pub trait Target { /// Writes a single scanline into `self`. /// /// Returns count of fragments input and output. - fn rasterize( + fn rasterize( &mut self, scanline: Scanline, frag_shader: &Fs, + uniform: U, ctx: &Context, ) -> Throughput where V: Vary, - Fs: FragmentShader; + U: Copy, + Fs: FragmentShader; } /// Framebuffer, combining a color (pixel) buffer and a depth buffer. @@ -59,24 +61,26 @@ impl AsMutSlice2 for Colorbuf { impl Target for &mut T { #[inline] - fn rasterize>( + fn rasterize>( &mut self, sl: Scanline, fs: &Fs, + uni: U, ctx: &Context, ) -> Throughput { - (*self).rasterize(sl, fs, ctx) + (*self).rasterize(sl, fs, uni, ctx) } } impl Target for &RefCell { #[inline] - fn rasterize>( + fn rasterize>( &mut self, sl: Scanline, fs: &Fs, + uni: U, ctx: &Context, ) -> Throughput { - RefCell::borrow_mut(self).rasterize(sl, fs, ctx) + RefCell::borrow_mut(self).rasterize(sl, fs, uni, ctx) } } @@ -88,14 +92,15 @@ where { /// Rasterizes `scanline` into this framebuffer. #[inline] - fn rasterize>( + fn rasterize>( &mut self, sl: Scanline, fs: &Fs, + uni: U, ctx: &Context, ) -> Throughput { let Self { color_buf, depth_buf } = self; - rasterize_fb(color_buf, depth_buf, sl, fs, Color4::into_pixel, ctx) + rasterize_fb(color_buf, depth_buf, sl, fs, uni, Color4::into_pixel, ctx) } } @@ -107,44 +112,48 @@ where /// Rasterizes `scanline` into this `u32` color buffer. /// Does no z-buffering. #[inline] - fn rasterize>( + fn rasterize>( &mut self, sl: Scanline, fs: &Fs, + uni: U, ctx: &Context, ) -> Throughput { - rasterize(&mut self.buf, sl, fs, Color4::into_pixel, ctx) + rasterize(&mut self.buf, sl, fs, uni, Color4::into_pixel, ctx) } } impl Target for Buf2 { #[inline] - fn rasterize>( + fn rasterize>( &mut self, sl: Scanline, fs: &Fs, + uni: U, ctx: &Context, ) -> Throughput { - rasterize(self, sl, fs, |c| c, ctx) + rasterize(self, sl, fs, uni, |c| c, ctx) } } impl Target for Buf2 { #[inline] - fn rasterize>( + fn rasterize>( &mut self, sl: Scanline, fs: &Fs, + uni: U, ctx: &Context, ) -> Throughput { - rasterize(self, sl, fs, |c| c.to_rgb(), ctx) + rasterize(self, sl, fs, uni, |c| c.to_rgb(), ctx) } } -pub fn rasterize( +pub fn rasterize( buf: &mut B, mut sl: Scanline, - fs: &impl FragmentShader, + fs: &impl FragmentShader, + uni: U, mut conv: impl FnMut(Color4) -> B::Elem, ctx: &Context, ) -> Throughput { @@ -157,7 +166,7 @@ pub fn rasterize( sl.fragments() .zip(cbuf_span) .for_each(|(frag, curr_col)| { - if let Some(new_col) = fs.shade_fragment(frag) + if let Some(new_col) = fs.shade_fragment(frag, uni) && ctx.color_write { io.o += 1; @@ -167,11 +176,12 @@ pub fn rasterize( io } -pub fn rasterize_fb( +pub fn rasterize_fb( cbuf: &mut B, zbuf: &mut impl AsMutSlice2, mut sl: Scanline, - fs: &impl FragmentShader, + fs: &impl FragmentShader, + uni: U, mut conv: impl FnMut(Color4) -> B::Elem, ctx: &Context, ) -> Throughput { @@ -189,7 +199,7 @@ pub fn rasterize_fb( let new_z = frag.pos.z(); if ctx.depth_test(new_z, *curr_z) - && let Some(new_col) = fs.shade_fragment(frag) + && let Some(new_col) = fs.shade_fragment(frag, uni) { if ctx.color_write { io.o += 1; diff --git a/core/tests/rendering.rs b/core/tests/rendering.rs index 1aefbcf7..bbdc5205 100644 --- a/core/tests/rendering.rs +++ b/core/tests/rendering.rs @@ -26,7 +26,7 @@ fn textured_quad() { |v: Vertex3<_>, mvp: &ProjMat3| { vertex(mvp.apply(&v.pos), v.attrib) }, - |frag: Frag<_>| SamplerClamp.sample(&checker, frag.var), + |frag: Frag<_>, _| SamplerClamp.sample(&checker, frag.var), ); let (w, h) = (256, 256); diff --git a/demos/src/bin/crates.rs b/demos/src/bin/crates.rs index d9f652db..45a8d475 100644 --- a/demos/src/bin/crates.rs +++ b/demos/src/bin/crates.rs @@ -30,7 +30,7 @@ fn main() { let floor_shader = shader::new( |v: Vertex3<_>, mvp: &ProjMat3<_>| vertex(mvp.apply(&v.pos), v.attrib), - |frag: Frag| { + |frag: Frag, _: &_| { let even_odd = (frag.var.x() > 0.5) ^ (frag.var.y() > 0.5); gray(if even_odd { 0.8 } else { 0.1 }).to_color4() }, @@ -39,7 +39,7 @@ fn main() { |v: Vertex3<(Normal3, TexCoord)>, mvp: &ProjMat3<_>| { vertex(mvp.apply(&v.pos), v.attrib) }, - |frag: Frag<(Normal3, TexCoord)>| { + |frag: Frag<(Normal3, TexCoord)>, _: &_| { let (n, uv) = frag.var; let kd = lerp(n.dot(&light_dir).max(0.0), 0.4, 1.0); let col = SamplerClamp.sample(&tex, uv); diff --git a/demos/src/bin/curses.rs b/demos/src/bin/curses.rs index b3180849..b6ab2cab 100644 --- a/demos/src/bin/curses.rs +++ b/demos/src/bin/curses.rs @@ -52,8 +52,8 @@ fn main() { |v: Vertex3<_>, mvp: &ProjMat3| { vertex(mvp.apply(&v.pos), v.attrib) }, - |frag: Frag| { - let [x, y, z] = (frag.var / 2.0 + splat(0.5)).0; + |frag: Frag, _: &_| { + let [x, y, z] = (frag.var * 0.5 + splat(0.5)).0; rgb(x, y, z).to_color4() }, ); @@ -106,23 +106,20 @@ fn main() { } impl Target for Win { - fn rasterize( + fn rasterize>( &mut self, mut sc: Scanline, fs: &Fs, + uni: U, _ctx: &Context, - ) -> Throughput - where - V: Vary, - Fs: FragmentShader, - { + ) -> Throughput { let w = sc.xs.len(); let y = sc.y; self.0.mv(y as i32, sc.xs.start as i32); for frag in sc.fragments() { - let Some(col) = fs.shade_fragment(frag) else { + let Some(col) = fs.shade_fragment(frag, uni) else { continue; }; let [r, g, b, _] = col.0.map(|c| c as u32); diff --git a/demos/src/bin/hello.rs b/demos/src/bin/hello.rs index 605f5fc0..d52283ca 100644 --- a/demos/src/bin/hello.rs +++ b/demos/src/bin/hello.rs @@ -34,7 +34,7 @@ fn main() { |v: Vertex<_, _>, mvp: &ProjMat3| { vertex(mvp.apply(&v.pos), v.attrib) }, - |frag: Frag| text.sample(frag.var).to_rgba(), + |frag: Frag, _: &_| text.sample(frag.var).to_rgba(), ); let vp: ProjMat3 = translate(vec3(0.0, 0.0, 15.0)) diff --git a/demos/src/bin/solids.rs b/demos/src/bin/solids.rs index e1a7255a..8ab3e5a3 100644 --- a/demos/src/bin/solids.rs +++ b/demos/src/bin/solids.rs @@ -77,7 +77,7 @@ fn main() { vertex(mvp.apply(&v.pos), col) } - fn frag_shader(f: Frag) -> Color4 { + fn frag_shader(f: Frag, _: Uniform) -> Color4 { f.var.to_color4() } diff --git a/demos/src/bin/sprites.rs b/demos/src/bin/sprites.rs index 2892bcc3..1b3836e4 100644 --- a/demos/src/bin/sprites.rs +++ b/demos/src/bin/sprites.rs @@ -38,12 +38,12 @@ fn main() { let shader = shader::new( |v: Vertex3>, - (mv, proj): (&Mat4, &ProjMat3)| { + (mv, proj): &(Mat4, ProjMat3)| { let vertex_pos = 0.008 * v.attrib.to_vec3().to(); // Model->View let view_pos = mv.apply(&v.pos) + vertex_pos; vertex(proj.apply(&view_pos), v.attrib) }, - |frag: Frag>| { + |frag: Frag>, _: & _| { let d2 = frag.var.len_sqr(); (d2 < 1.0).then(|| { let col = gray(1.0) - d2 * rgb(0.25, 0.5, 1.0); @@ -66,11 +66,12 @@ fn main() { .to() .then(&cam.world_to_view()); + let uniform = (modelview, cam.project); render( &tris, &verts, &shader, - (&modelview, &cam.project), + &uniform, cam.viewport, &mut frame.buf, frame.ctx, diff --git a/demos/src/bin/square.rs b/demos/src/bin/square.rs index 2bc53df8..e0529494 100644 --- a/demos/src/bin/square.rs +++ b/demos/src/bin/square.rs @@ -36,7 +36,7 @@ fn main() { let shader = shader::new( |v: Vertex3<_>, mvp: &ProjMat3<_>| vertex(mvp.apply(&v.pos), v.attrib), - |frag: Frag<_>| SamplerClamp.sample(&checker, frag.var), + |frag: Frag<_>, _: &_| SamplerClamp.sample(&checker, frag.var), ); let (w, h) = win.dims; diff --git a/demos/wasm/src/triangle.rs b/demos/wasm/src/triangle.rs index d2d20734..98a52e48 100644 --- a/demos/wasm/src/triangle.rs +++ b/demos/wasm/src/triangle.rs @@ -37,7 +37,7 @@ pub fn start() { let sh = Shader::new( |v: Vertex3, _| vertex(mvp.apply(&v.pos), v.attrib), - |f: Frag| f.var.to_color4(), + |f: Frag, _| f.var.to_color4(), ); render([tri(0, 1, 2)], vs, &sh, (), vp, &mut frame.buf, frame.ctx); From b59e267390d8ebe48634f469c6b2862041a75327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Tue, 12 May 2026 15:37:15 +0300 Subject: [PATCH 04/76] Impl Lerp for arrays --- core/src/math.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/src/math.rs b/core/src/math.rs index c9a03172..e9b4e0dd 100644 --- a/core/src/math.rs +++ b/core/src/math.rs @@ -199,6 +199,12 @@ where } } +impl Lerp for [T; N] { + fn lerp(&self, other: &Self, t: f32) -> Self { + core::array::from_fn(|i| self[i].lerp(&other[i], t)) + } +} + impl Lerp for () { fn lerp(&self, _: &Self, _: f32) {} } From 3aa86d2e04d2e3ef9cf0a102144060819a1c7a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Tue, 27 Aug 2024 14:52:25 +0300 Subject: [PATCH 05/76] Add support for 2D and 3D Perlin noise generation --- core/src/math.rs | 1 + core/src/math/noise.rs | 482 ++++++++++++++++++++++++++++++++++++++++ core/src/math/spline.rs | 16 +- 3 files changed, 497 insertions(+), 2 deletions(-) create mode 100644 core/src/math/noise.rs diff --git a/core/src/math.rs b/core/src/math.rs index e9b4e0dd..dfd0153a 100644 --- a/core/src/math.rs +++ b/core/src/math.rs @@ -77,6 +77,7 @@ pub mod color; pub mod float; pub mod grad; pub mod mat; +pub mod noise; pub mod param; pub mod point; pub mod rand; diff --git a/core/src/math/noise.rs b/core/src/math/noise.rs new file mode 100644 index 00000000..51439a32 --- /dev/null +++ b/core/src/math/noise.rs @@ -0,0 +1,482 @@ +//! Procedural noise generation. +//! +//! This module implements two- and three-dimensional Perlin noise. + +use core::{array::from_fn, cell::Cell}; + +use super::{ + Lerp, Point, Point2, Point3, Vec2, Vec3, Vector, lerp, pt3, space::Real, + splat, spline::smoothstep_unit, vec2, vec3, +}; + +/// 2D-dimensional Perlin noise generator. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Perlin2 { + pub seed: u8, + + // Cache the grid points and gradients because usually several + // consecutive evaluations are likely to hit the same grid square + cache: Cell, +} + +/// 3-dimensional Perlin noise generator. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Perlin3 { + pub seed: u8, + + // Cache the grid points and gradients because usually several + // consecutive evaluations are likely to hit the same grid square + cache: Cell, +} + +#[derive(Copy, Clone, Debug, PartialEq)] +struct GridCell { + pts: [Point<[f32; DIM], Real>; N], + grads: [Vector<[f32; DIM], Real>; N], +} + +type GridCell2 = GridCell<2, 4>; +type GridCell3 = GridCell<3, 8>; + +impl Perlin2 { + pub fn new() -> Self { + Self::default() + } + + pub fn with_seed(seed: u8) -> Self { + Self { seed, ..Self::default() } + } + + /// Returns the Perlin noise value corresponding to a 2D point. + #[inline] + pub fn eval(&self, pt: Point2) -> f32 { + let GridCell { pts, grads } = self.grid_cell(pt); + + // Get the delta vectors from pt to the grid points + let deltas = pts.map(|p| pt - p); + + // Compute the dot products between gradients and deltas + let dots = from_fn(|i| grads[i].dot(&deltas[i])); + + // Smooth the interpolation variables + let tu = deltas[0].map(smoothstep_unit).0; + // Interpolate the final noise value at pt + bilerp(tu, dots) + } + + /// Returns the Perlin gradient vector corresponding to a 2D point. + /// + /// The vector is computed by taking the gradient vectors of all four + /// surrounding grid points and smoothly interpolating between them. + #[inline] + pub fn gradient(&self, pt: Point2) -> Vec2 { + let GridCell { pts, grads } = self.grid_cell(pt); + let tu = (pt - pts[0]).map(smoothstep_unit).0; + bilerp(tu, grads) + } + + #[inline] + fn grid_cell(&self, pt: Point2) -> GridCell2 { + use super::float::f32; + let pt0 = pt.map(f32::floor); + + let mut cached = self.cache.get(); + // Update cache if we're not in the same grid cell + if cached.pts[0] != pt0 { + // Find the four integer-coordinate points around pt + cached.pts = Self::grid_pts(pt0); + // Get the gradient vectors at the grid points + cached.grads = cached.pts.map(|p| self.grad(p)); + self.cache.set(cached); + } + cached + } + + /// Returns the four integer-coordinate grid points around a point. + #[inline] + fn grid_pts(pt0: Point2) -> [Point2; 4] { + [pt0, pt0 + Vec2::X, pt0 + Vec2::Y, pt0 + Vec2::X + Vec2::Y] + } + + /// Returns the gradient vector at a grid point. + #[inline] + fn grad(&self, pt: Point2) -> Vec2 { + let hash = PERM[pt.y() as i32 as u8 as usize]; + let hash = PERM[(pt.x() as i32 as u8).wrapping_add(hash) as usize]; + let hash = PERM[self.seed.wrapping_add(hash) as usize]; + GRADS_2[hash as usize & 0x7] + } +} +/// Gradient vectors for 2D noise. +static GRADS_2: [Vec2; 8] = const { + const A: f32 = 1.237; + const B: f32 = 0.513; + [ + vec2(A, B), + vec2(B, A), + vec2(-B, A), + vec2(-A, B), + vec2(-A, -B), + vec2(-B, -A), + vec2(B, -A), + vec2(A, -B), + ] +}; + +impl Perlin3 { + /// Returns the Perlin noise value corresponding to a 3D point. + // #[inline] Benchmarks appear to indicate that this reduces perf + pub fn eval(&self, pt: Point3) -> f32 { + let GridCell { pts, grads } = self.grid_cell(pt); + + // Get the delta vectors from pt to the grid points + let deltas = pts.map(|p| pt - p); + + // Compute the dot products between gradients and delts + let dots: [f32; 8] = from_fn(|i| grads[i].dot(&deltas[i])); + + // Smooth the interpolation variables + let tuv = deltas[0].map(smoothstep_unit).0; + + // Interpolate the final noise value at pt + trilerp(tuv, dots) + } + + /// Returns the Perlin gradient vector corresponding to a 3D point. + #[inline] + pub fn gradient(&self, pt: Point3) -> Vec3 { + let GridCell { pts, grads } = self.grid_cell(pt); + + // Smooth the interpolation variables + let tuv = (pt - pts[0]).map(smoothstep_unit).0; + // Interpolate the gradient at pt + trilerp(tuv, grads) + } + + #[inline] + fn grid_cell(&self, pt: Point3) -> GridCell3 { + use super::float::f32; + let pt0 = pt.map(f32::floor); + + let mut cached = self.cache.get(); + // Update cache if we're not in the same grid cell + if cached.pts[0] != pt0 { + // Find the four integer-coordinate points around pt + cached.pts = Self::grid_pts(pt0); + // Get the gradient vectors at the grid points + cached.grads = cached.pts.map(Self::grad); + self.cache.set(cached); + } + cached + } + + #[inline] + #[rustfmt::skip] + fn grid_pts(pt0: Point3) -> [Point3; 8] { + // + // 011 +--------------+ 111 + // / | / | + // / | / | + // 010 +--------------+ 110 | + // | | | | + // | 001 +--------|-----+ 101 + // | / | / y z + // | / | / | / + // 000 +--------------+ 100 O--- x + // + let [x0, y0, z0] = pt0.0; + let [x1, y1, z1] = (pt0 + splat(1.0)).0; + [ + pt3(x0, y0, z0), pt3(x0, y1, z0), pt3(x0, y0, z1), pt3(x0, y1, z1), + pt3(x1, y0, z0), pt3(x1, y1, z0), pt3(x1, y0, z1), pt3(x1, y1, z1), + ] + } + + #[inline] + fn grad(pt: Point3) -> Vec3 { + let [x, y, z] = pt.0; + let perm = perm(x as i32 + perm(y as i32 + perm(z as i32))); + GRADS_3[perm as usize & 0xF] + } +} +/// Gradient vectors for 3D noise. +static GRADS_3: [Vec3; 16] = [ + // YZ plane + vec3(0.0, -1.0, -1.0), + vec3(0.0, -1.0, 1.0), + vec3(0.0, 1.0, -1.0), + vec3(0.0, 1.0, 1.0), + // XZ plane + vec3(-1.0, 0.0, -1.0), + vec3(-1.0, 0.0, 1.0), + vec3(1.0, 0.0, -1.0), + vec3(1.0, 0.0, 1.0), + // XY plane + vec3(-1.0, -1.0, 0.0), + vec3(-1.0, 1.0, 0.0), + vec3(1.0, -1.0, 0.0), + vec3(1.0, 1.0, 0.0), + // Pad to power of two + vec3(1.0, 1.0, 0.0), + vec3(-1.0, 1.0, 0.0), + vec3(0.0, -1.0, 1.0), + vec3(0.0, -1.0, -1.0), +]; + +#[inline] +fn bilerp([t, u]: [f32; 2], [x00, x01, x10, x11]: [T; 4]) -> T { + lerp(t, x00, x01).lerp(&lerp(t, x10, x11), u) +} + +#[inline] +fn trilerp( + [t, u, v]: [f32; 3], + [v000, v001, v010, v011, v100, v101, v110, v111]: [V; 8], +) -> V { + bilerp( + [u, v], + [v000, v001, v010, v011].lerp(&[v100, v101, v110, v111], t), + ) +} + +#[inline] +fn perm(x: i32) -> i32 { + PERM[(x & 0xFF) as usize] as i32 +} + +/// Permutation table for calculating a pseudo-random index for each grid point. +#[rustfmt::skip] +static PERM: [u8; 256] = [ + 156, 2, 157, 90, 75, 199, 55, 167, 62, 92, 101, 253, 66, 134, 113, 83, + 1, 136, 78, 106, 254, 105, 248, 176, 234, 5, 195, 226, 49, 71, 87, 44, + 122, 94, 219, 140, 72, 159, 237, 212, 8, 162, 200, 124, 125, 69, 165, 74, + 245, 42, 89, 216, 158, 108, 238, 184, 217, 73, 126, 210, 14, 111, 19, 188, + 186, 45, 38, 223, 35, 112, 214, 26, 145, 95, 99, 193, 250, 189, 152, 182, + 166, 247, 148, 213, 168, 70, 96, 249, 127, 132, 4, 137, 41, 60, 102, 28, + 27, 240, 227, 155, 211, 230, 9, 80, 178, 3, 68, 153, 143, 84, 179, 181, + 12, 97, 103, 16, 225, 146, 63, 82, 203, 175, 163, 147, 11, 116, 185, 215, + 57, 120, 208, 129, 115, 198, 37, 201, 39, 98, 20, 183, 56, 118, 109, 142, + 138, 65, 117, 114, 160, 25, 43, 191, 204, 161, 22, 251, 139, 79, 131, 231, + 76, 0, 205, 206, 244, 51, 174, 13, 110, 85, 209, 77, 64, 53, 48, 221, + 133, 93, 224, 24, 33, 164, 23, 47, 171, 128, 243, 18, 52, 119, 149, 100, + 246, 233, 31, 192, 252, 190, 15, 172, 91, 229, 144, 54, 61, 58, 220, 36, + 222, 29, 50, 88, 121, 173, 232, 194, 239, 197, 32, 180, 107, 46, 7, 130, + 169, 81, 218, 67, 21, 170, 187, 59, 86, 235, 154, 123, 150, 177, 135, 228, + 104, 242, 6, 151, 255, 34, 30, 141, 202, 196, 236, 207, 241, 40, 17, 10 +]; + +impl Default for GridCell +where + [f32; DIM]: Default, +{ + fn default() -> Self { + Self { + pts: [Point::new([f32::NAN; DIM]); N], + grads: [Vector::default(); N], + } + } +} + +#[cfg(test)] +mod tests { + use alloc::string::String; + use core::fmt::Write; + + use crate::math::{pt2, pt3}; + + use super::*; + + #[derive(PartialEq, Debug)] + struct Stats { + total: f32, + avg: f32, + std: f32, + min: f32, + max: f32, + } + impl Stats { + fn new() -> Self { + Self { + total: 0.0, + avg: 0.0, + std: 0.0, + min: f32::MAX, + max: f32::MIN, + } + } + fn cum(&mut self, v: f32) { + self.total += 1.0; + self.avg += v; + self.std += v * v; + self.min = self.min.min(v); + self.max = self.max.max(v); + } + fn finish(self) -> Self { + Self { + avg: self.avg / self.total, + std: (self.std / self.total).sqrt(), + ..self + } + } + } + + #[test] + fn perlin2_statistics() { + let count = 1000u32; + let scale = 10.0; + let mut stats = Stats::new(); + let p = Perlin2::default(); + for i in 0..count { + for j in 0..count { + let pt = pt2(i as f32 / scale, j as f32 / scale); + let v = p.eval(pt); + stats.cum(v); + } + } + let stats = stats.finish(); + + assert_eq!( + stats, + Stats { + total: 1000000.0, + avg: -0.00024930664, + std: 0.28139114, + min: -0.8853002, + max: 0.8853002 + } + ); + } + #[test] + fn perlin3_statistics() { + let count = 100u32; + let scale = 10.0; + let mut stats = Stats::new(); + let p = Perlin3::default(); + for i in 0..count { + for j in 0..count { + for k in 0..count { + let pt = pt3(i, j, k).map(|c| c as f32 / scale); + let v = p.eval(pt); + stats.cum(v); + } + } + } + let stats = stats.finish(); + assert_eq!( + stats, + Stats { + total: 1000000.0, + avg: 0.0003959533, + std: 0.24879986, + min: -0.8853568, + max: 0.87813747 + } + ); + } + + const PALETTE: &[u8] = b" ..,:;=+*odO#%@WW"; + + #[test] + fn perlin2_pattern() { + #[rustfmt::skip] + static EXPECTED: &str = "\ +++++++++++++++++++oooooo+++:::,,,;;;++++++;;;;;; +oooooodddOOOOOOddddddooo+++;;;,,,,,,;;;======+++ +ooo***ddd###%%%OOOooo++++++===::::::;;;***oooddd ++++===***dddOOO***;;;;;;++++++===;;;+++ddd###OOO ++++;;;;;;++++++;;;,,,:::+++***+++===+++dddOOOooo ++++;;;:::::::::,,,,,,===oooddd***===+++oooooo=== ++++===:::,,,,,,:::===oooOOOOOO***===+++ooo***::: +++++++;;;:::;;;+++***ooodddddd+++===+++dddooo=== ++++***+++===+++oooooo+++++++++;;;;;;+++dddOOOooo +oooOOOdddooooooddd***===;;;;;;;;;:::;;;oooOOO### +OOO%%%###OOOOOOddd+++;;;;;;++++++===;;;===ddd%%% +ddd######OOOddd***;;;;;;+++dddOOOooo;;;;;;+++ddd ++++ooooooooo+++;;;,,,:::+++OOO%%%OOO+++;;;;;;+++ +;;;======;;;,,,......:::+++ddd###ddd+++===:::::: +;;;===;;;,,,......:::+++ooooooooo***++++++;;;,,, ++++***+++:::,,,;;;+++oooooo+++;;;;;;+++******=== +"; + const SIZE: usize = 16; + const SCALE: f32 = 4.0; + + let mut actual = String::new(); + let p = Perlin2::default(); + for i in 0..SIZE { + for j in 0..SIZE { + let pt = pt2(i as f32 / SCALE, j as f32 / SCALE); + let val = p.eval(pt) * 0.5 + 0.5; + + let ch = PALETTE[(val * PALETTE.len() as f32) as usize]; + _ = write!(actual, "{0}{0}{0}", ch as char); + } + _ = writeln!(actual); + } + assert_eq!(&actual, EXPECTED); + } + #[test] + fn perlin3_pattern() { + #[rustfmt::skip] + static EXPECTED: &str = "\ ++++++++++ooo+++...+++### +###***+++++++++===+++OOO ++++;;;+++;;;+++ooo+++;;; +...,,,+++;;;;;;===+++*** ++++++++++++++++;;;+++### +###%%%#########ooo+++=== ++++ooo+++++++++ooo+++;;; +;;;===+++===+++***++++++ + +ddd***===ooo***,,,+++### +###***===+++***;;;===ddd +===,,,;;;===ooo***;;;::: +...,,,+++============+++ +:::===ooo+++;;;:::===ddd +oooOOOdddOOOOOOooo+++;;; ++++***;;;***oooOOO***::: +++++++***+++***ooo***=== + +###***+++ooo+++...+++### +OOO+++===***+++,,,===ooo +;;;,,,;;;***ooo===;;;;;; + ...+++******+++++++++ +...;;;ooo+++;;;;;;;;;+++ ++++++++++oooooo***===,,, +++++++;;;ooooooOOOooo;;; +ooo*********+++******=== + +ddd+++***ooo===...+++### +***===+++ooo;;;...===ooo +;;;;;;===OOO***,,,====== +...,,,+++OOOOOOoooooo+++ +::::::***+++===+++;;;;;; +===::::::******+++;;;,,, +++++++===OOO***oooooo=== +ddd*********;;;===+++;;; + +"; + + const SIZE: usize = 8; + const SCALE: f32 = 2.0; + + let mut actual = String::new(); + let p = Perlin3::default(); + + for k in 0..4 { + for i in 0..SIZE { + for j in 0..SIZE { + let pt = + pt3(i as f32 / SCALE, j as f32 / SCALE, k as f32 / 4.0); + let val = p.eval(pt) * 0.5 + 0.5; + + let ch = PALETTE[(val * PALETTE.len() as f32) as usize]; + _ = write!(actual, "{0}{0}{0}", ch as char); + } + _ = writeln!(actual); + } + _ = writeln!(actual); + } + + assert_eq!(actual, EXPECTED); + } +} diff --git a/core/src/math/spline.rs b/core/src/math/spline.rs index b784bedf..d3e1e5e5 100644 --- a/core/src/math/spline.rs +++ b/core/src/math/spline.rs @@ -94,15 +94,27 @@ pub struct Euclidean(Spl, Vec<(f32, f32)>); /// /// Returns 0 for all `t` <= 0 and 1 for all `t` >= 1. Has a continuous /// first derivative. +#[inline] pub fn smoothstep(t: f32) -> f32 { - step(t, &0.0, &1.0, |t| t * t * (3.0 - 2.0 * t)) + step(t, &0.0, &1.0, smoothstep_unit) } /// Even smoother version of [`smoothstep`]. /// /// Has continuous first and second derivatives. +#[inline] pub fn smootherstep(t: f32) -> f32 { - step(t, &0.0, &1.0, |t| t * t * t * (10.0 + t * (6.0 * t - 15.0))) + step(t, &0.0, &1.0, smootherstep_unit) +} + +#[inline] +pub(crate) fn smoothstep_unit(t: f32) -> f32 { + t * t * (3.0 - 2.0 * t) +} + +#[inline] +pub(crate) fn smootherstep_unit(t: f32) -> f32 { + t * t * t * (10.0 + t * (6.0 * t - 15.0)) } /// Helper for defining step functions. From 266e99390ed3f3e85d295157802aee59b71cbbe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Tue, 12 May 2026 16:12:14 +0300 Subject: [PATCH 06/76] Add noise benchmarks --- Cargo.toml | 4 ++++ benches/noise.rs | 57 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 benches/noise.rs diff --git a/Cargo.toml b/Cargo.toml index 1627252d..9175a441 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,3 +104,7 @@ harness = false [[bench]] name = "vec" harness = false + +[[bench]] +name = "noise" +harness = false diff --git a/benches/noise.rs b/benches/noise.rs new file mode 100644 index 00000000..c27ff05b --- /dev/null +++ b/benches/noise.rs @@ -0,0 +1,57 @@ +//! Noise generation benchmarks. + +use core::hint::black_box; + +use divan::Bencher; + +use retrofire_core::math::{ + noise::{Perlin2, Perlin3}, + pt2, pt3, +}; + +const SIZES: [u32; 5] = [1, 4, 16, 64, 256]; + +#[divan::bench(args = SIZES)] +fn perlin2(b: Bencher, sz: u32) { + let noise = Perlin2::default(); + + b.counter(sz * sz).bench_local(|| { + for i in 0..sz { + for j in 0..sz { + black_box(noise.eval(pt2(i as f32, j as f32) / 64.0)); + } + } + }); +} + +#[divan::bench(args = SIZES)] +fn cache_hits(b: Bencher, sz: u32) { + let noise = Perlin2::default(); + + b.counter(sz * sz) + .counter(256 * 256u32) + .bench_local(|| { + for i in 0..256 { + for j in 0..256 { + black_box(noise.eval(pt2(i as f32, j as f32) / sz as f32)); + } + } + }); +} + +#[divan::bench(args = SIZES)] +fn perlin3(b: Bencher, sz: u32) { + let noise = Perlin3::default(); + + b.counter(sz * sz).bench_local(|| { + for i in 0..sz { + for j in 0..sz { + black_box(noise.eval(pt3(i as f32, j as f32, 0.0) / 64.0)); + } + } + }); +} + +fn main() { + divan::main() +} From 3ae52255ff7fdbfdbc6c21068029c3fa1c6a3f21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Wed, 13 May 2026 18:11:58 +0300 Subject: [PATCH 07/76] Simplify bench counters --- benches/fill.rs | 8 ++++---- benches/isect.rs | 27 +++++++++++++-------------- benches/vec.rs | 6 +++--- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/benches/fill.rs b/benches/fill.rs index 2bee07fa..4ff7c603 100644 --- a/benches/fill.rs +++ b/benches/fill.rs @@ -2,7 +2,7 @@ use core::iter::zip; -use divan::{Bencher, counter::ItemsCount}; +use divan::Bencher; use retrofire_core::{ geom::{Tri, vertex}, @@ -23,7 +23,7 @@ fn flat(b: Bencher, sz: f32) { let mut buf: Buf2 = Buf2::new((1024, 1024)); b.with_inputs(|| VERTS.map(|p| vertex(p * sz, ()))) - .input_counter(move |vs| ItemsCount::new(Tri(*vs).area() as usize)) + .input_counter(move |vs| Tri(*vs).area() as usize) .bench_local_values(|vs| { tri_fill(vs, |sl| { buf[sl.y][sl.xs].fill(gray(0xCC)); @@ -43,7 +43,7 @@ fn gouraud(b: Bencher, sz: f32) { vertex(VERTS[2] * sz, rgb(0.2, 0.3, 1.0)), ] }) - .input_counter(move |vs| ItemsCount::new(Tri(*vs).area() as usize)) + .input_counter(move |vs| Tri(*vs).area() as usize) .bench_local_values(|vs| { tri_fill(vs, |sl| { let y = sl.y; @@ -78,7 +78,7 @@ fn texture(b: Bencher, sz: f32) { vertex(VERTS[2] * sz, uv(0.0, 4.0)), ] }) - .input_counter(move |vs| ItemsCount::new(Tri(*vs).area() as usize)) + .input_counter(move |vs| Tri(*vs).area() as usize) .bench_local_values(|vs| { tri_fill(vs, |sl| { let y = sl.y; diff --git a/benches/isect.rs b/benches/isect.rs index 7f23b7e5..028376af 100644 --- a/benches/isect.rs +++ b/benches/isect.rs @@ -2,12 +2,11 @@ use core::hint::black_box; -use divan::{Bencher, counter::ItemsCount}; +use divan::Bencher; use retrofire::core::{ geom::{Plane3, Ray, Sphere}, - math::rand::*, - math::{Point3, degs, pt3, spherical, vec3}, + math::{Point3, degs, pt3, rand::*, spherical, vec3}, render::scene::BBox, }; use retrofire::geom::Intersect; @@ -25,7 +24,7 @@ fn ray_plane_hit(b: Bencher) { let v = (splat(-1.0)..splat(0.0)).sample(&mut rng); Ray(pt3(0.0, 10.0, 0.0), 100.0 * (v - vec3(1.0, 1.0, 1.0))) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| ray.intersect(&black_box(plane))); } #[divan::bench] @@ -40,7 +39,7 @@ fn ray_plane_miss(b: Bencher) { let v = (splat(0.0)..splat(1.0)).sample(&mut rng); Ray(pt3(0.0, 10.0, 0.0), 100.0 * v) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| ray.intersect(&black_box(plane))); } #[divan::bench] @@ -55,7 +54,7 @@ fn ray_plane_mixed(b: Bencher) { let v = VectorsInUnitBall.sample(&mut rng); Ray(pt3(0.0, 10.0, 0.0), 100.0 * v) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| ray.intersect(&black_box(plane))); } @@ -68,7 +67,7 @@ fn ray_bbox_hit(b: Bencher) { let v = VectorsInUnitBall.sample(&mut rng); Ray(v.to_pt(), 100.0 * v) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| { assert!(ray.intersect(&black_box(bbox)).is_some()) }); @@ -84,7 +83,7 @@ fn ray_bbox_hit_2(b: Bencher) { let v = (min..max).sample(&mut rng); Ray(pt3(0.0, 2.0, 0.0), v.to_cart()) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| { assert!(ray.intersect(&black_box(bbox)).is_some()) }); @@ -99,7 +98,7 @@ fn ray_bbox_inside(b: Bencher) { let dir = VectorsInUnitBall.sample(&mut rng); Ray(pt, dir) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| { assert!(ray.intersect(&black_box(bbox)).is_some()) }); @@ -116,7 +115,7 @@ fn ray_bbox_miss(b: Bencher) { let v = (min..max).sample(&mut rng); Ray(pt3(0.0, 3.0, 0.0), v.to_cart()) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| { assert!(ray.intersect(&black_box(bbox)).is_none()) }); @@ -134,7 +133,7 @@ fn ray_bbox_mixed(b: Bencher) { let dir = (p..q).sample(&mut rng); Ray(2.0 * orig, 100.0 * dir.to_vec()) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| ray.intersect(&black_box(bbox))); } @@ -150,7 +149,7 @@ fn ray_sphere_miss(b: Bencher) { let v = (min..max).sample(&mut rng); Ray(pt3(0.0, 3.0, 0.0), v.to_cart()) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| { let ip = ray.intersect(&black_box(sphere)); assert!(ip.is_none()); @@ -170,7 +169,7 @@ fn ray_sphere_hit(b: Bencher) { .sample(&mut rng); Ray(pt3(0.0, 2.0f32.sqrt(), 0.0), v.to_cart()) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| { let ip = ray.intersect(&black_box(sphere)); assert!(ip.is_some()); @@ -188,7 +187,7 @@ fn ray_sphere_mixed(b: Bencher) { let v = VectorsInUnitBall.sample(&mut rng); Ray(pt3(0.0, 2.0, 0.0), v) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| ray.intersect(&black_box(sphere))); } diff --git a/benches/vec.rs b/benches/vec.rs index 177eb04b..d6e7409b 100644 --- a/benches/vec.rs +++ b/benches/vec.rs @@ -1,7 +1,7 @@ //! Triangle clipping benchmarks. use divan::Bencher; -use divan::counter::ItemsCount; + use retrofire_core::{ math::rand::{DefaultRng, Distrib}, math::{Vec3, splat}, @@ -13,7 +13,7 @@ fn normalize_exact(b: Bencher) { let vecs = splat(-1e6)..splat(1e6); b.with_inputs(|| vecs.sample(rng)) - .input_counter(|_| ItemsCount::new(1u32)) + .counter(1u32) .bench_local_values(|v: Vec3| v.normalize()); } @@ -23,7 +23,7 @@ fn normalize_approx(b: Bencher) { let vecs = splat(-1e6)..splat(1e6); b.with_inputs(|| vecs.sample(rng)) - .input_counter(|_| ItemsCount::new(1u32)) + .counter(1u32) .bench_local_values(|v: Vec3| v.normalize_approx()); } From 5d7af02cda4112796f71ac23c0dc36bef0a952c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 14 May 2026 21:34:33 +0300 Subject: [PATCH 08/76] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 23a96e73..ec161b17 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ for custom allocators is planned in order to make `alloc` optional as well. * Reading and writing pnm image files * Reading and writing Wavefront .obj files * Minifb, SDL2, and Wasm frontends +* Procedural noise generation * Forever emoji-free README and docs * Forever LLM-free code @@ -70,7 +71,6 @@ for custom allocators is planned in order to make `alloc` optional as well. * Different camera types * Spherical etc. UV mapping -* Procedural noise generation * Terminal frontend with ncurses * Cube mapping and skyboxes From 6334596dbe77046e20d9d196cc23a22402eb6ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 14 May 2026 18:19:27 +0300 Subject: [PATCH 09/76] Add wallclock time stats --- core/src/render.rs | 11 ++- core/src/render/stats.rs | 172 ++++++++++++++++++++++----------------- front/src/minifb.rs | 3 +- front/src/sdl2.rs | 5 +- 4 files changed, 111 insertions(+), 80 deletions(-) diff --git a/core/src/render.rs b/core/src/render.rs index 20588b05..2668f6fb 100644 --- a/core/src/render.rs +++ b/core/src/render.rs @@ -7,6 +7,8 @@ use alloc::vec::Vec; use core::{fmt::Debug, ops::DerefMut}; +#[cfg(feature = "std")] +use std::time::Instant; use crate::geom::Vertex; use crate::math::{ @@ -152,10 +154,12 @@ pub fn render( let verts = verts.as_ref(); let prims = prims.as_ref(); - let mut stats = Stats::start(); + let mut stats = Stats::new(); stats.calls = 1.0; stats.prims.i = prims.len(); stats.verts.i = verts.len(); + #[cfg(feature = "std")] + let start = Instant::now(); // 1. Vertex shader: transform vertices to clip space let verts = vertex_transform(shader, uniform, verts); @@ -177,6 +181,11 @@ pub fn render( clipped, shader, uniform, to_screen, target, ctx, ); + #[cfg(feature = "std")] + { + stats.render_time = start.elapsed(); + } + *ctx.stats.borrow_mut() += stats.finish(); } diff --git a/core/src/render/stats.rs b/core/src/render/stats.rs index 598c6124..0c6bbcf2 100644 --- a/core/src/render/stats.rs +++ b/core/src/render/stats.rs @@ -4,8 +4,6 @@ use alloc::{format, string::String}; use core::fmt::{self, Display, Formatter}; use core::ops::AddAssign; use core::time::Duration; -#[cfg(feature = "std")] -use std::time::Instant; // // Types @@ -14,8 +12,10 @@ use std::time::Instant; /// Collects and accumulates rendering statistics and performance data. #[derive(Clone, Debug, Default)] pub struct Stats { + /// Wall clock time elapsed. + pub wall_time: Duration, /// Time spent rendering. - pub time: Duration, + pub render_time: Duration, /// Number of render calls issued. pub calls: f32, /// Number of frames rendered. @@ -26,9 +26,6 @@ pub struct Stats { pub prims: Throughput, pub verts: Throughput, pub frags: Throughput, - - #[cfg(feature = "std")] - start: Option, } #[derive(Copy, Clone, Debug, Default)] @@ -48,19 +45,6 @@ impl Stats { pub fn new() -> Self { Self::default() } - /// Creates a `Stats` instance that records the time of its creation. - /// - /// Call [`finish`][Self::finish] to write the elapsed time to `self.time`. - /// Useful for timing frames, rendering calls, etc. - /// - /// Equivalent to [`Stats::new`] if the `std` feature is not enabled. - pub fn start() -> Self { - Self { - #[cfg(feature = "std")] - start: Some(Instant::now()), - ..Self::default() - } - } /// Stops the timer and records the elapsed time to `self.time`. /// @@ -68,50 +52,64 @@ impl Stats { /// the `std` feature is enabled. #[must_use] pub fn finish(self) -> Self { + self + } + + /// Returns the average throughput in items per second. + pub fn per_render_sec(&self) -> Self { + let secs = if self.render_time.is_zero() { + 1.0 + } else { + self.render_time.as_secs_f32() + }; + let [objs, prims, verts, frags] = + self.throughput().map(|stat| stat.per_sec(secs)); Self { - #[cfg(feature = "std")] - time: self.start.map_or(self.time, |st| st.elapsed()), - ..self + render_time: Duration::from_secs(1), + wall_time: self.wall_time.div_f32(secs), + frames: self.frames / secs, + calls: self.calls / secs, + objs, + prims, + verts, + frags, } } - /// Returns the average throughput in items per second. - pub fn per_sec(&self) -> Self { - let secs = if self.time.is_zero() { + pub fn per_wall_sec(&self) -> Self { + let secs = if self.wall_time.is_zero() { 1.0 } else { - self.time.as_secs_f32() + self.wall_time.as_secs_f32() }; let [objs, prims, verts, frags] = self.throughput().map(|stat| stat.per_sec(secs)); Self { + wall_time: Duration::from_secs(1), + render_time: self.render_time.div_f32(secs), frames: self.frames / secs, calls: self.calls / secs, - time: Duration::from_secs(1), objs, prims, verts, frags, - #[cfg(feature = "std")] - start: None, } } /// Returns the average throughput in items per frame. pub fn per_frame(&self) -> Self { - let frames = self.frames.max(1.0); + let frames = self.frames.max(1.0) as u32; let [objs, prims, verts, frags] = self .throughput() .map(|stat| stat.per_frame(frames)); Self { frames: 1.0, - calls: self.calls / frames, - time: self.time.div_f32(frames), + render_time: self.render_time / frames, + wall_time: self.wall_time / frames, + calls: self.calls / frames as f32, objs, prims, verts, frags, - #[cfg(feature = "std")] - start: None, } } @@ -132,7 +130,7 @@ impl Throughput { o: (self.o as f32 / secs) as usize, } } - fn per_frame(&self, frames: f32) -> Self { + fn per_frame(&self, frames: u32) -> Self { Self { i: self.i / frames as usize, o: self.o / frames as usize, @@ -144,33 +142,53 @@ impl Display for Stats { #[rustfmt::skip] #[inline(never)] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let w = f.width().unwrap_or(16); - let per_s = self.per_sec(); + let per_ws = self.per_wall_sec(); + let per_rs = self.per_render_sec(); let per_f = self.per_frame(); - write!(f, - " STATS {:>w$} │ {:>w$} │ {:>w$}\n\ - ────────{empty:─>w$}─┼─{empty:─>w$}─┼─{empty:─>w$}─\n \ - time {:>w$} │ {empty:w$} │ {:>w$}\n \ - calls {:>w$} │ {:>w$.1} │ {:>w$.1}\n \ - frames {:>w$} │ {:>w$.1} │\n\ - ────────{empty:─>w$}─┼─{empty:─>w$}─┼─{empty:─>w$}─\n", - "TOTAL", "PER SEC", "PER FRAME", - human_time(self.time), human_time(per_f.time), - self.calls, per_s.calls, per_f.calls, - self.frames, per_s.frames, - empty = "" + + let ws_per_rs = self.wall_time.div_duration_f32(self.render_time); + + let w = f.width().unwrap_or(14); + let e = ""; + + writeln!(f, + " STATS {:>w$} │ {:>w$} │ {:>w$} │ {:>w$}\n\ + ───────────{e:─>w$}─┼─{e:─>w$}─┼─{e:─>w$}─┼─{e:─>w$}─", + "TOTAL", "PER WALL-SEC", "PER REND-SEC", "PER FRAME" + )?; + writeln!(f, + " wall-time {:>w$} │ {e:w$} │ {:>w$.2} │ {:>w$}", + human_time(self.wall_time), ws_per_rs, + human_time(per_f.wall_time) + )?; + + + let rs_per_ws = 1.0 / ws_per_rs; + + writeln!(f, + " rend-time {:>w$} │ {:>w$.2} │ {e:w$} │ {:>w$}\n \ + calls {:>w$} │ {:>w$.1} │ {:>w$.1} │ {:>w$.1}\n \ + frames {:>w$} │ {:>w$.1} │ {:>w$.1} │\n\ + ───────────{e:─>w$}─┼─{e:─>w$}─┼─{e:─>w$}─┼─{e:─>w$}─", + + human_time(self.render_time), rs_per_ws, human_time(per_f.render_time), + self.calls, per_ws.calls, per_rs.calls, per_f.calls, + self.frames, per_ws.frames, per_rs.frames, + e = "" )?; let labels = ["objs", "prims", "verts", "frags"]; for (i, lbl) in (0..4).zip(labels) { - let [tot, per_s, per_f] = [self, &per_s, &per_f].map(|s| s.throughput()[i]); + let [tot, per_ws, per_rs, per_f] = + [self, &per_ws, &per_rs, &per_f].map(|s| s.throughput()[i]); if f.alternate() { - writeln!(f, " {lbl:6} {tot:#w$} │ {per_s:#w$} │ {per_f:#w$}")?; + writeln!(f, " {lbl:9} {tot:#w$} │ {per_ws:#w$} │ {per_rs:#w$} │ {per_f:#w$}")?; } else { - writeln!(f, " {lbl:6} {tot:w$} │ {per_s:w$} │ {per_f:w$}")?; + writeln!(f, " {lbl:9} {tot:w$} │ {per_ws:w$} │ {per_rs:w$} │ {per_f:w$}")?; } } + Ok(()) } } @@ -197,7 +215,8 @@ impl Display for Throughput { impl AddAssign for Stats { /// Appends the stats of `other` to `self`. fn add_assign(&mut self, other: Self) { - self.time += other.time; + self.wall_time += other.wall_time; + self.render_time += other.render_time; self.calls += other.calls; self.frames += other.frames; for i in 0..4 { @@ -262,44 +281,45 @@ mod tests { let stats = Stats { frames: 1234.0, calls: 5678.0, - time: Duration::from_millis(4321), + wall_time: Duration::from_millis(5432), + render_time: Duration::from_millis(4321), objs, prims, verts, frags, - #[cfg(feature = "std")] - start: None, }; assert_eq!( format!("{stats}"), " \ - STATS TOTAL │ PER SEC │ PER FRAME -─────────────────────────┼──────────────────┼────────────────── - time 4.3s │ │ 3.5ms - calls 5678 │ 1314.0 │ 4.6 - frames 1234 │ 285.6 │ -─────────────────────────┼──────────────────┼────────────────── - objs 12.3k / 4.3k │ 2.9k / 1.0k │ 10 / 3 - prims 24.7k / 8.6k │ 5.7k / 2.0k │ 20 / 7 - verts 37.0k / 13.0k │ 8.6k / 3.0k │ 30 / 10 - frags 49.4k / 17.3k │ 11.4k / 4.0k │ 40 / 14 + STATS TOTAL │ PER WALL-SEC │ PER REND-SEC │ PER FRAME +──────────────────────────┼────────────────┼────────────────┼──────────────── + wall-time 5.4s │ │ 1.26 │ 4.4ms + rend-time 4.3s │ 0.80 │ │ 3.5ms + calls 5678 │ 1045.3 │ 1314.0 │ 4.6 + frames 1234 │ 227.2 │ 285.6 │ +──────────────────────────┼────────────────┼────────────────┼──────────────── + objs 12.3k / 4.3k │ 2.3k / 795 │ 2.9k / 1.0k │ 10 / 3 + prims 24.7k / 8.6k │ 4.5k / 1.6k │ 5.7k / 2.0k │ 20 / 7 + verts 37.0k / 13.0k │ 6.8k / 2.4k │ 8.6k / 3.0k │ 30 / 10 + frags 49.4k / 17.3k │ 9.1k / 3.2k │ 11.4k / 4.0k │ 40 / 14 " ); assert_eq!( format!("{stats:#}"), " \ - STATS TOTAL │ PER SEC │ PER FRAME -─────────────────────────┼──────────────────┼────────────────── - time 4.3s │ │ 3.5ms - calls 5678 │ 1314.0 │ 4.6 - frames 1234 │ 285.6 │ -─────────────────────────┼──────────────────┼────────────────── - objs 35.0% │ 35.0% │ 30.0% - prims 35.0% │ 35.0% │ 35.0% - verts 35.0% │ 35.0% │ 33.3% - frags 35.0% │ 35.0% │ 35.0% + STATS TOTAL │ PER WALL-SEC │ PER REND-SEC │ PER FRAME +──────────────────────────┼────────────────┼────────────────┼──────────────── + wall-time 5.4s │ │ 1.26 │ 4.4ms + rend-time 4.3s │ 0.80 │ │ 3.5ms + calls 5678 │ 1045.3 │ 1314.0 │ 4.6 + frames 1234 │ 227.2 │ 285.6 │ +──────────────────────────┼────────────────┼────────────────┼──────────────── + objs 35.0% │ 35.0% │ 35.0% │ 30.0% + prims 35.0% │ 35.0% │ 35.0% │ 35.0% + verts 35.0% │ 35.0% │ 35.0% │ 33.3% + frags 35.0% │ 35.0% │ 35.0% │ 35.0% " ); } diff --git a/front/src/minifb.rs b/front/src/minifb.rs index 0ec0c42c..c60eec36 100644 --- a/front/src/minifb.rs +++ b/front/src/minifb.rs @@ -150,7 +150,8 @@ impl Window { ctx.stats.borrow_mut().frames += 1.0; } - let stats = ctx.stats.into_inner(); + let mut stats = ctx.stats.into_inner(); + stats.wall_time = start.elapsed(); println!("{stats}"); stats } diff --git a/front/src/sdl2.rs b/front/src/sdl2.rs index 9c57d50e..22ef59dd 100644 --- a/front/src/sdl2.rs +++ b/front/src/sdl2.rs @@ -199,7 +199,7 @@ impl, const N: usize> Window { let mut ctx = self.ctx.clone(); let start = Instant::now(); - let mut last = Instant::now(); + let mut last = start; 'main: loop { self.events.clear(); for e in self.ev_pump.poll_iter() { @@ -242,7 +242,8 @@ impl, const N: usize> Window { break; } } - let stats = ctx.stats.into_inner(); + let mut stats = ctx.stats.into_inner(); + stats.wall_time = start.elapsed(); println!("{stats}"); Ok(stats) } From c2a0d96a8ea6fefb879af2aad666d67cd92a635f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 14 May 2026 18:53:26 +0300 Subject: [PATCH 10/76] Use the xterm 6x6x6 RGB cube in curses instead of 3:3:2 8-bit --- demos/src/bin/curses.rs | 51 +++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/demos/src/bin/curses.rs b/demos/src/bin/curses.rs index b6ab2cab..155531c7 100644 --- a/demos/src/bin/curses.rs +++ b/demos/src/bin/curses.rs @@ -1,5 +1,3 @@ -#![allow(clippy::unusual_byte_groupings)] - use std::time::Instant; use pancurses::*; @@ -7,8 +5,8 @@ use pancurses::*; use re::prelude::*; use re::core::render::{ - Model, ctx::DepthSort::BackToFront, raster::Scanline, render, shader, - stats::Throughput, + Model, ctx::DepthSort::BackToFront, debug::dir_to_rgb, raster::Scanline, + render, shader, stats::Throughput, }; use re::geom::solids::{Build, Torus}; @@ -21,15 +19,13 @@ impl Win { curs_set(0); start_color(); - // Create an RGB 332 palette but keep the eight standard colors - for i in 8..256 { - // Range from 0 to 1000 - let r = (i & 0b111_000_00) * 4; - let g = (i & 0b000_111_00) * 35; - let b = (i & 0b000_000_11) * 330; - - init_color(i, r, g, b); - init_pair(i, i, i); + // Use the standard xterm 8-bit palette: + // 0..8: basic dark + // 8..16: basic bright + // 16..232: 6x6x6 RGB cube + // 232..256: sixteen shades of gray + for i in 16..232 { + init_pair(i, 15, i); } Self(w) } @@ -52,17 +48,14 @@ fn main() { |v: Vertex3<_>, mvp: &ProjMat3| { vertex(mvp.apply(&v.pos), v.attrib) }, - |frag: Frag, _: &_| { - let [x, y, z] = (frag.var * 0.5 + splat(0.5)).0; - rgb(x, y, z).to_color4() - }, + |frag: Frag, _: &_| dir_to_rgb(frag.var).to_color4(), ); let torus = Torus { major_radius: 1.0, minor_radius: 0.3, - major_sectors: 32, - minor_sectors: 16, + major_sectors: 19, + minor_sectors: 13, } .build(); @@ -113,24 +106,18 @@ impl Target for Win { uni: U, _ctx: &Context, ) -> Throughput { - let w = sc.xs.len(); - let y = sc.y; - - self.0.mv(y as i32, sc.xs.start as i32); + self.0.mv(sc.y as i32, sc.xs.start as i32); for frag in sc.fragments() { let Some(col) = fs.shade_fragment(frag, uni) else { continue; }; - let [r, g, b, _] = col.0.map(|c| c as u32); - - let col = (r & 0b111_000_00) - | ((g / 9) & 0b000_111_00) - | ((b / 85) & 0b000_000_11); - - // Avoid the eight standard colors - self.0.addch(COLOR_PAIR(col.max(8) as chtype)); + // Map the RGB to the closest color in the 6x6x6 xterm RGB cube + let [r, g, b, _] = col.0.map(|c| c / 43); + let col = 16 + 36 * r + 6 * g + b; + self.0 + .addch(COLOR_PAIR(col as chtype) | ' ' as chtype); } - Throughput { i: w, o: w } + Throughput { i: sc.xs.len(), o: sc.xs.len() } } } From 11d437d3b1341dcb2d63a54ea938670e9d5e7805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sat, 23 May 2026 20:20:48 +0300 Subject: [PATCH 11/76] Make text rendering easier Adds functions to Text: * render() for simple 2D rendering * batch() for more customized rendering * shader() returning a suitable shader Other improvements: * finish anchor/alignment support * color field to easily set text color --- core/src/render.rs | 1 + core/src/render/text.rs | 118 ++++++++++++++++++++++++++++++++-------- demos/src/bin/hello.rs | 44 +++++++-------- 3 files changed, 117 insertions(+), 46 deletions(-) diff --git a/core/src/render.rs b/core/src/render.rs index 2668f6fb..b2c9082a 100644 --- a/core/src/render.rs +++ b/core/src/render.rs @@ -30,6 +30,7 @@ pub(super) mod re_exports { ctx::Context, light::Light, raster::Frag, + scene::{BBox, Obj}, shader::{FragmentShader, VertexShader}, stats::Stats, target::{Colorbuf, Framebuf, Target}, diff --git a/core/src/render/text.rs b/core/src/render/text.rs index e36aa116..412d93d3 100644 --- a/core/src/render/text.rs +++ b/core/src/render/text.rs @@ -2,22 +2,44 @@ use core::fmt; #[cfg(feature = "std")] use std::io; -use crate::geom::{Mesh, tri, vertex}; -use crate::math::{Color3, Point2, Vec2, pt2, vec2, vec3}; +use crate::geom::{Mesh, Tri, Vertex3, tri, vertex}; +use crate::math::{ + Color3, Color4, Point2, ProjMat3, Vec2, color::gray, orthographic, pt2, + pt3, vec2, vec3, viewport, +}; use crate::util::buf::Buf2; -use super::tex::{Atlas, Layout, SamplerClamp, TexCoord}; +use super::tex::*; +use super::{BBox, Context, Frag, Model, Shader, Target, shader}; /// Text represented as texture-mapped geometry, one quad per glyph. #[derive(Clone)] pub struct Text { pub font: Atlas, pub geom: Mesh, - // TODO Private until fixed - _anchor: Vec2, - cursor: Point2, + pub color: Color3, + pub anchor: Point2, + pub align: Align, + cursor: Point2, } +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub enum Align { + #[default] + TopLeft, + TopCenter, + TopRight, + CenterLeft, + Center, + CenterRight, + BottomLeft, + BottomCenter, + BottomRight, +} + +pub type Batch = + super::Batch, Vertex3, (), Shd, (), Context>; + // // Inherent impls // @@ -28,23 +50,16 @@ impl Text { Self { font, geom: Mesh::default(), - _anchor: Vec2::default(), + color: gray(0xFF), + anchor: Point2::default(), + align: Align::default(), cursor: Point2::default(), } } /// Sets the anchor point of the text. - /// - /// The anchor is a vector that determines how the text is aligned relative - /// to the (local) origin. The default is (0, 0) which places the origin to - /// the top left corner. Use (0.5, 0.5) to center the text vertically and - /// horizontally relative to the origin. - /// - /// Note that this value does not affect how individual lines of text - /// are aligned relative to each other. - // TODO private until fixed - fn _anchor(mut self, x: f32, y: f32) -> Self { - self._anchor = vec2(x, y); + pub fn anchor(mut self, pt: impl Into) -> Self { + self.anchor = pt.into(); self } @@ -55,10 +70,69 @@ impl Text { self.geom.verts.clear(); } - /// Samples the font at `uv`. - pub fn sample(&self, uv: TexCoord) -> Color3 { - // TODO Figure out why coords go out of bounds -> SamplerOnce panics - SamplerClamp.sample(&self.font.texture, uv) + /// Returns a shader for rendering text. + pub fn shader( + &self, + ) -> impl Shader, TexCoord, &ProjMat3> { + shader::new( + |v: Vertex3<_>, tf: &ProjMat3<_>| { + vertex(tf.apply(&v.pos.to()), v.attrib) + }, + |frag: Frag, _| self.sample(frag.var), + ) + } + + /// Renders this text to a render target in 2D. + /// + /// For more customizable rendering, see the [`batch`][Self::batch] function. + pub fn render(&self, target: &mut impl Target) { + let BBox(_lt, rb) = BBox::of(&self.geom); + let [r, b, _] = rb.0; + + use Align::*; + let off = match self.align { + TopLeft => (0.0, 0.0), + TopCenter => (0.5, 0.0), + TopRight => (1.0, 0.0), + CenterLeft => (0.0, 0.5), + Center => (0.5, 0.5), + CenterRight => (1.0, 0.5), + BottomLeft => (0.0, 1.0), + BottomCenter => (0.5, 1.0), + BottomRight => (1.0, 1.0), + }; + let pos = self.anchor - Vec2::from(off) * vec2(r, b); + + let proj: ProjMat3 = + orthographic(pt3(0.0, 0.0, -1.0), pt3(self.cursor.x(), b, 1.0)) + .to(); + let pos = pt2(pos.x() as _, pos.y() as _); + let wh = vec2(r as _, b as _); + let viewport = viewport(pos..pos + wh); + + self.batch() + .uniform(&proj) + .viewport(viewport) + .target(target) + .render(); + } + + /// Returns a `Batch` with the geometry and shader set to render this text. + /// + /// Useful for customized text rendering. + pub fn batch( + &self, + ) -> Batch, TexCoord, &ProjMat3>> { + super::Batch::new() + .mesh(&self.geom) + .shader(self.shader()) + } + + /// Samples the font at a texture coordinate. + #[inline] + fn sample(&self, uv: TexCoord) -> Option { + let col = SamplerClamp.sample(&self.font.texture, uv); + (col != gray(0)).then_some(self.color.to_rgba()) } fn write_char(&mut self, idx: u32) { diff --git a/demos/src/bin/hello.rs b/demos/src/bin/hello.rs index d52283ca..1589ea3e 100644 --- a/demos/src/bin/hello.rs +++ b/demos/src/bin/hello.rs @@ -2,24 +2,25 @@ use std::{env, fmt::Write, ops::ControlFlow::Continue}; use re::prelude::*; +use re::core::math::color::hsl; use re::core::{ - render::{Model, Text, World, render, shader, tex::Atlas, tex::Layout}, - util::pnm::parse_pnm, + render::{Text, World, tex::Atlas, tex::Layout}, + util::pnm::read_pnm, }; - use re_front::{Frame, dims::SVGA_800_600, minifb::Window}; +const FONT: &[u8] = include_bytes!("../../assets/font_16x24.pbm"); + fn main() { - let font = *include_bytes!("../../assets/font_16x24.pbm"); - let font = parse_pnm(font).expect("valid image"); + let font = read_pnm(FONT).expect("valid image"); let font = Atlas::new(Layout::Grid { sub_dims: (16, 24) }, font.into()); - let msg = env::args().nth(1); // Borrow checker... - let msg = msg + let arg = env::args().nth(1); // Borrow checker... + let msg = arg .as_deref() .unwrap_or(" Hello,\nRetrocomputing\n World!"); - let mut text = Text::new(font); + let mut text = Text::new(font.clone()); write!(text, "{msg}").expect("cannot fail"); let mut win = Window::builder() @@ -30,13 +31,6 @@ fn main() { win.ctx.face_cull = None; - let shader = shader::new( - |v: Vertex<_, _>, mvp: &ProjMat3| { - vertex(mvp.apply(&v.pos), v.attrib) - }, - |frag: Frag, _: &_| text.sample(frag.var).to_rgba(), - ); - let vp: ProjMat3 = translate(vec3(0.0, 0.0, 15.0)) .to() .then(&perspective(1.0, 4.0 / 3.0, 0.1..1000.0)); @@ -53,15 +47,17 @@ fn main() { .to() .then(&vp); - render( - &text.geom.faces, - &text.geom.verts, - &shader, - &mvp, - viewport, - &mut frame.buf, - frame.ctx, - ); + text.color = hsl(secs / 10.0 % 1.0, 0.8, 0.6) + .to_rgb() + .to_color3(); + + text.batch() + .uniform(&mvp) + .viewport(viewport) + .target(&mut frame.buf) + .context(frame.ctx) + .render(); + Continue(()) }); } From 76dae8217c4010873f77136786f5cccd6f87a568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sat, 23 May 2026 21:04:11 +0300 Subject: [PATCH 12/76] Add Layout::grid convenience constructor --- core/src/render/tex.rs | 5 +++++ demos/src/bin/hello.rs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/src/render/tex.rs b/core/src/render/tex.rs index 762b0726..d62d9a4f 100644 --- a/core/src/render/tex.rs +++ b/core/src/render/tex.rs @@ -146,6 +146,11 @@ impl Atlas { Self { layout, texture } } + /// Creates a texture atlas with a grid layout. + pub fn grid(sub_dims: Dims, texture: Texture>) -> Self { + Self::new(Layout::Grid { sub_dims }, texture) + } + /// Returns the top-left and bottom-right pixel coordinates /// of the sub-texture with index `i`. fn rect(&self, i: u32) -> [Point2u; 2] { diff --git a/demos/src/bin/hello.rs b/demos/src/bin/hello.rs index 1589ea3e..f582d220 100644 --- a/demos/src/bin/hello.rs +++ b/demos/src/bin/hello.rs @@ -13,7 +13,7 @@ const FONT: &[u8] = include_bytes!("../../assets/font_16x24.pbm"); fn main() { let font = read_pnm(FONT).expect("valid image"); - let font = Atlas::new(Layout::Grid { sub_dims: (16, 24) }, font.into()); + let font = Atlas::grid((16, 24), font.into()); let arg = env::args().nth(1); // Borrow checker... let msg = arg From 8b57bd9664c9958d8fe24874fdb63bf505f2044f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sat, 23 May 2026 21:21:22 +0300 Subject: [PATCH 13/76] Include a 6x10 font in front, add a font to load it --- front/assets/font_6x10.pbm | Bin 0 -> 1930 bytes front/src/lib.rs | 14 +++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 front/assets/font_6x10.pbm diff --git a/front/assets/font_6x10.pbm b/front/assets/font_6x10.pbm new file mode 100644 index 0000000000000000000000000000000000000000..e889aa4c977461d2390633a2eaa6e072c7dc59c3 GIT binary patch literal 1930 zcma)-O>7fK6vthKLj?IaaH!i@IA@A1TpASQfGa_zmZ}o!2}Mod&=a%<7r7R=$*QqL z4ybS^h|sE{)W+*Nh$PyMsHLG|w3_+( zKhN*Y%zLwY;Ej#Dc6JAMZr=!QYgr9Dqff%FWwloWuuDBct?by`M=pfz{g<>GhfnQO zNG4^Mx<}sbK6O7wGC4bS$z@r~d~GiWHFe?CK3U|0;7bT!bjRkvtzBCkw~o&sKiNR! zaFd=6wmEao64d=&i=B}w8nV4Ge*XHI@!!*`1%vl551O;b&?bIyCH7s>WaE~Y?!MR_ zz1&Y(X)DjhN0vk57h=pC6k9Af7y=vMNvP@5alGJq|96*{L@iO3TrnS;u`GeHpbm6F z{TPfp#AWl=2VvvxVenf(O^Zmz-2?!dEsVY^35Cqran$+uv^op|d94|1@ZV9p8-3h% zSo7mdg^bA$th^$iTIbgesq9oi@%F9Oxsagimt}n$FlS~IKKfv76DC=d`oun3? zOPH#{05Fhkpy06Wjn4SCkM{9mf3#kZ70!uTE`BT53t6QDd9j$5>tphSz3S*`**+MZ z?Xby3{ve%m9otTIHCqM#T*GI_8kZc&a5|^FN87ibcFrLr@uXXwOI6e_INeUl}Wo|*(F z8;h(Z)Rp5iZ9&aKZ~KYmj91CXeq7`P^Cgl>2)($Duv>Bw@TG*UY7DV#Ljb(3`XM|L zL8+_Mg18pMnZC$ntR*N6~ReJz{MAS+jMDoh#V(`)3aG6w{f)A`Kog-?`H zw+1$A_;H-o7oEP*p5XqKD@5ca+^;?e<@YgzO@2mS*)917_P%_;vG2_7759c7?a|PF zWp}J?;H}Pz<=V%@KnzdG_0#0%>4I9IntENr=YP59=g&PqzT{c;^m;lT8IoIdHvYF~ z@y!jWgTp2qa$LO#>tG6e=*`-&=Xv}~r@1(1wzxL<)e!22+8z(u*V?6rU&LDMo1Wn? z;6deM;>CIG(4EoSodihwPU3sgX4eS-XH#JC+srgJeK^mE?u$&0nY?gR*_2DA Atlas { + let font = read_pnm(FONT_6X10).expect("font statically included"); + Atlas::grid((6, 10), font.into()) +} + /// Per-frame state. The window run method passes an instance of `Frame` /// to the callback function on every iteration of the main loop. pub struct Frame<'a, Win, Buf> { From b080f462734b60ff9c12b7aab5100e06181dcf76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sat, 23 May 2026 23:10:07 +0300 Subject: [PATCH 14/76] Add fps display to minifb and sdl2 frontends --- demos/src/bin/crates.rs | 5 +++-- front/src/minifb.rs | 13 +++++++++++-- front/src/sdl2.rs | 17 +++++++++++++---- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/demos/src/bin/crates.rs b/demos/src/bin/crates.rs index 45a8d475..cdd9f569 100644 --- a/demos/src/bin/crates.rs +++ b/demos/src/bin/crates.rs @@ -16,6 +16,8 @@ use re::core::util::{pixfmt::Rgba8888, pnm::read_pnm}; use re::front::sdl2::Window; use re::geom::solids::{Build, Cube}; +static CRATE_TEX: &[u8] = include_bytes!("../../assets/crate.ppm"); + fn main() { let mut win = Window::builder() .title("retrofire//crates") @@ -23,8 +25,7 @@ fn main() { .build() .expect("should create window"); - let tex_data = *include_bytes!("../../assets/crate.ppm"); - let tex = Texture::from(read_pnm(&tex_data[..]).expect("data exists")); + let tex = Texture::from(read_pnm(CRATE_TEX).expect("data exists")); let light_dir = vec3(-2.0, 1.0, -4.0).normalize(); diff --git a/front/src/minifb.rs b/front/src/minifb.rs index c60eec36..eee1e3d3 100644 --- a/front/src/minifb.rs +++ b/front/src/minifb.rs @@ -2,6 +2,7 @@ use core::{ cell::RefCell, + fmt::Write, mem::replace, ops::ControlFlow::{self, *}, }; @@ -10,11 +11,11 @@ use std::time::Instant; use minifb::{Key, WindowOptions}; use retrofire_core::{ - render::{Colorbuf, Context, Stats, target}, + render::{Colorbuf, Context, Stats, Text, target}, util::{Dims, buf::Buf2, buf::MutSlice2, pixfmt::Xrgb8888}, }; -use super::{Frame, dims}; +use super::{Frame, dims, font_6x10}; /// A lightweight wrapper of a `minibuf` window. pub struct Window { @@ -124,6 +125,9 @@ impl Window { let mut zbuf = Buf2::new((w, h)); let mut ctx = self.ctx.clone(); + let mut fps = Text::new(font_6x10()); + fps.anchor = (2.0, 2.0).into(); + let start = Instant::now(); let mut last = Instant::now(); loop { @@ -146,6 +150,11 @@ impl Window { if let Break(_) = frame_fn(frame) { break; } + + fps.clear(); + _ = write!(fps, "{:>6.1}", frame.dt.as_secs_f32().recip()); + fps.render(&mut frame.buf); + self.present(cbuf.data_mut()); ctx.stats.borrow_mut().frames += 1.0; diff --git a/front/src/sdl2.rs b/front/src/sdl2.rs index 22ef59dd..72337c69 100644 --- a/front/src/sdl2.rs +++ b/front/src/sdl2.rs @@ -1,5 +1,5 @@ //! Frontend using the `sdl2` crate for window creation and event handling. -use core::{cell::RefCell, fmt, mem::replace, ops::ControlFlow}; +use core::{cell::RefCell, fmt, fmt::Write, mem::replace, ops::ControlFlow}; use std::time::Instant; use sdl2::{ @@ -12,14 +12,14 @@ use sdl2::{ }; use retrofire_core::math::Color4; -use retrofire_core::render::{Colorbuf, Context, Stats, target}; +use retrofire_core::render::{Colorbuf, Context, Stats, Text, target}; use retrofire_core::util::{ Dims, buf::{AsMutSlice2, Buf2, MutSlice2}, pixfmt::{IntoPixel, Rgb565, Rgba4444, Rgba8888}, }; -use super::{Frame, dims}; +use super::{Frame, dims, font_6x10}; /// Helper trait to support different pixel format types. pub trait PixelFmt: Copy + Default { @@ -198,6 +198,9 @@ impl, const N: usize> Window { let mut zbuf = Buf2::new(dims); let mut ctx = self.ctx.clone(); + let mut fps = Text::new(font_6x10()); + fps.anchor = (2.0, 2.0).into(); + let start = Instant::now(); let mut last = start; 'main: loop { @@ -232,7 +235,13 @@ impl, const N: usize> Window { }; frame.clear(); - frame_fn(frame) + let cf = frame_fn(frame); + + fps.clear(); + _ = write!(fps, "{:>6.1}", frame.dt.as_secs_f32().recip()); + fps.render(&mut frame.buf); + + cf })?; self.present(&tex)?; From b25ced712a17e89d8d1f1da6ba95e625bb45c08e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 21 May 2026 23:19:21 +0300 Subject: [PATCH 15/76] Add doc comments and doctests to math::float --- core/src/math/float.rs | 80 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/core/src/math/float.rs b/core/src/math/float.rs index 7f11d18e..7b84bafd 100644 --- a/core/src/math/float.rs +++ b/core/src/math/float.rs @@ -6,6 +6,7 @@ //! it also implements a critical subset of the functions even if none of //! the features is enabled. +/// Floating-point functions delegating to software implementations in `libm`. #[cfg(feature = "libm")] pub mod libm { pub use libm::floorf as floor; @@ -32,6 +33,8 @@ pub mod libm { } } +/// Floating-point functions delegating to approximate implementations +/// in the `micromath` library. #[cfg(feature = "mm")] pub mod mm { use micromath::F32Ext as mm; @@ -104,25 +107,82 @@ pub mod mm { } } +/// Fallback implementations of required floating-point functions +/// used if none of the fp features is enabled. +//#[cfg(not(feature = "fp"))] pub mod fallback { - use crate::math::float::fast_recip_sqrt; + use core::hint::cold_path; /// Returns the largest integer less than or equal to `x`. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::float::fallback::floor; + /// + /// assert_eq!(floor(1.0), 1.0); + /// assert_eq!(floor(1.9), 1.0); + /// + /// assert_eq!(floor(-1.0), -1.0); + /// assert_eq!(floor(-1.1), -2.0); + /// ``` #[inline] pub fn floor(x: f32) -> f32 { - (x as i64 - (x < 0.0) as i64) as f32 + let xi = x as i64 as f32; + if xi > x { xi - 1.0 } else { xi } } + /// Returns the least non-negative remainder of `x` (mod `m`). + /// + /// # Examples + /// ``` + /// use retrofire_core::math::float::fallback::rem_euclid; + /// + /// assert_eq!(rem_euclid(3.0, 4.0), 3.0); + /// assert_eq!(rem_euclid(4.0, 4.0), 0.0); + /// assert_eq!(rem_euclid(5.5, 4.0), 1.5); + /// assert_eq!(rem_euclid(-3.5, 4.0), 0.5); + /// ``` #[inline] pub fn rem_euclid(x: f32, m: f32) -> f32 { let r = x % m; r + if r < 0.0 { m.abs() } else { 0.0 } } - /// Returns the approximate reciprocal of the square root of `x`. + + /// Returns the (approximate) reciprocal square root of a number. + /// + /// If the argument is zero or negative, returns infinity or NaN respectively. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::float::fallback::recip_sqrt; + /// + /// assert_eq!(recip_sqrt(4.0), 0.49999782); + /// assert_eq!(recip_sqrt(9.0), 0.3333327); + /// assert_eq!(recip_sqrt(0.0), f32::INFINITY); + /// ``` #[inline] pub fn recip_sqrt(x: f32) -> f32 { - fast_recip_sqrt(x) + if x > 0.0 { + let y = super::fast_recip_sqrt(x); + return y * (1.5 - 0.5 * x * y * y); + } + cold_path(); + if x == 0.0 { f32::INFINITY } else { f32::NAN } } + + /// Returns the (approximate) square root of a number. + /// + /// If the argument is negative, returns NaN. + /// + /// # Example + /// ``` + /// use retrofire_core::math::float::fallback::sqrt; + /// + /// assert_eq!(sqrt(0.0), 0.0); + /// assert_eq!(sqrt(4.0), 2.0000088); + /// assert_eq!(sqrt(9.0), 3.0000057); + /// assert!(sqrt(-1.0).is_nan()); + /// ``` #[inline] pub fn sqrt(x: f32) -> f32 { 1.0 / recip_sqrt(x) @@ -130,6 +190,17 @@ pub mod fallback { } /// Returns a fast approximation of the reciprocal square root of a number. +/// +/// If the argument is zero or negative, the return value is unspecified. +/// +/// # Example +/// ``` +/// use retrofire_core::math::float::fast_recip_sqrt; +/// +/// assert_eq!(fast_recip_sqrt(4.0), 0.49915406); +/// assert_eq!(fast_recip_sqrt(0.25), 1.9966162); +/// +/// ``` #[inline] pub fn fast_recip_sqrt(x: f32) -> f32 { // https://en.wikipedia.org/wiki/Fast_inverse_square_root @@ -147,6 +218,7 @@ pub type f32 = core::primitive::f32; #[allow(unused)] pub(crate) trait RecipSqrt { + /// Returns the reciprocal square root (1/√x) of a number. fn recip_sqrt(x: Self) -> Self; } From e8d65a3e42c78e24fb569a4c345fe846e65233c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Fri, 22 May 2026 00:57:21 +0300 Subject: [PATCH 16/76] Add doctests to math::approx --- core/src/math/approx.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/core/src/math/approx.rs b/core/src/math/approx.rs index 606b3341..c7a46eb7 100644 --- a/core/src/math/approx.rs +++ b/core/src/math/approx.rs @@ -35,6 +35,17 @@ pub trait ApproxEq { /// /// This means that `self` is either strictly contained in the range /// or approximately equal to one of the endpoints. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::ApproxEq; + /// + /// assert!(1.0.approx_in(0.0..2.0)); + /// assert!((-0.000001).approx_in(0.0..2.0)); + /// assert!(2.000001.approx_in(0.0..2.0)); + /// + /// assert!(!2.001.approx_in(0.0..2.0)); + /// ``` fn approx_in(&self, rg: Range) -> bool where Self: PartialOrd + Sized, @@ -44,6 +55,17 @@ pub trait ApproxEq { } /// Returns whether `self` is less than or approximately equal to a value. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::ApproxEq; + /// + /// assert!(1.0.approx_le(&2.0)); + /// assert!(2.0.approx_le(&2.0)); + /// assert!(2.000001.approx_le(&2.0)); + /// + /// assert!(!2.001.approx_le(&2.0)); + /// ``` fn approx_le(&self, other: &Self) -> bool where Self: PartialOrd, @@ -52,6 +74,19 @@ pub trait ApproxEq { } /// Returns whether `self` is greater than or approximately equal to a value. + /// + /// TODO should be renamed to `approx_ge`! + /// + /// # Examples + /// ``` + /// use retrofire_core::math::ApproxEq; + /// + /// assert!(2.0.approx_gt(&1.0)); + /// assert!(1.0.approx_gt(&1.0)); + /// assert!(0.9999999.approx_gt(&1.0)); + /// + /// assert!(!0.999.approx_gt(&1.0)); + /// ``` fn approx_gt(&self, other: &Self) -> bool where Self: PartialOrd, From 63d5b04555aee7cc2e391d08c7b4c6041a41f579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Fri, 22 May 2026 00:57:42 +0300 Subject: [PATCH 17/76] Add doctests to math::color --- core/src/math/color.rs | 65 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/core/src/math/color.rs b/core/src/math/color.rs index 34dccc7f..ad8c6d90 100644 --- a/core/src/math/color.rs +++ b/core/src/math/color.rs @@ -192,6 +192,15 @@ impl Color { impl Color<[Ch; N], Sp> { /// Returns `self` with each channel mapped with the given function. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::color::rgb; + /// + /// let cyan = rgb(0.2,0.8,1.0); + /// let darker_cyan = cyan.map(|ch| ch * 0.5); + /// assert_eq!(darker_cyan, rgb(0.1, 0.4, 0.5)); + /// ``` #[inline] pub fn map(&self, f: impl FnMut(Ch) -> C) -> Color<[C; N], Sp> where @@ -208,11 +217,13 @@ impl Color<[f32; N], Sp> { /// /// # Examples /// ``` - /// use retrofire_core::math::color::{Color3f, gray, rgb}; - /// let c: Color3f = rgb(-0.1, 0.5, 1.2); + /// use retrofire_core::math::color::*; /// - /// let clamped = c.clamp(&gray(0.0), &gray(1.0)); + /// let out_of_bounds = rgb(-0.1, 0.5, 1.2); + /// + /// let clamped = out_of_bounds.clamp(&gray(0.0), &gray(1.0)); /// assert_eq!(clamped, rgb(0.0, 0.5, 1.0)); + /// ``` // TODO f32 and f64 have inherent clamp methods because they're not Ord. // A generic clamp for Sc: Ord would conflict with this one. There is // currently no clean way to support both floats and impl Ord types. @@ -226,6 +237,14 @@ impl Color<[f32; N], Sp> { impl Color3 { /// Returns `self` as RGBA, with alpha set to 0xFF (fully opaque). + /// + /// # Examples + /// ``` + /// use retrofire_core::math::color::*; + /// + /// let red = rgb(0xFF, 0, 0); + /// assert_eq!(red.to_rgba(), rgba(0xFF, 0, 0, 0xFF)) + /// ``` #[inline] pub const fn to_rgba(self) -> Color4 { let [r, g, b] = self.0; @@ -234,8 +253,16 @@ impl Color3 { /// Returns `self` as floating-point RGB, with channels normalized /// to the range [0, 1]. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::color::rgb; + /// + /// let orange = rgb(0xE0, 0x80, 0); + /// assert_eq!(orange.to_color3f(), rgb(0.875, 0.5, 0.0)); + /// ``` #[inline] - pub fn to_color3f(self) -> Color3f { + pub const fn to_color3f(self) -> Color3f { let [r, g, b] = self.0; rgb(r as f32 / 256.0, g as f32 / 256.0, b as f32 / 256.0) } @@ -291,6 +318,16 @@ impl Color4 { rgb(r, g, b) } + /// Returns `self` as floating-point RGBA, with channels normalized + /// to the range [0, 1]. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::color::rgba; + /// + /// let orange = rgba(0xE0, 0x80, 0, 0x40); + /// assert_eq!(orange.to_color4f(), rgba(0.875, 0.5, 0.0, 0.25)); + /// ``` #[inline] pub const fn to_color4f(self) -> Color4f { let [r, g, b, a] = self.0; @@ -350,6 +387,16 @@ impl Color3f { /// [converted to sRGB][1] right before writing to the output. Conversion, /// however, incurs a small performance penalty. /// + /// # Examples + /// ``` + /// use retrofire_core::math::color::*; + /// + /// let cyan = rgb(0.0, 0.8, 1.0); + /// let linear_cyan: Color3f = [0.0, 0.6120656, 1.0].into(); + /// + /// assert_eq!(cyan.to_linear(), linear_cyan); + /// ``` + /// /// [1]: Color3f::to_srgb() #[cfg(feature = "fp")] #[inline] @@ -457,6 +504,16 @@ impl Color3f { /// before interpolation, and right before writing to the output. /// Conversion, however, incurs a small performance penalty. /// + /// # Examples + /// ``` + /// use retrofire_core::math::color::*; + /// + /// let linear_cyan: Color3f = [0.0, 0.8, 1.0].into(); + /// let gamma_cyan = rgb(0.0, 0.90354544, 1.0); + /// + /// assert_eq!(linear_cyan.to_srgb(), gamma_cyan); + /// ``` + /// /// [1]: Color3f::to_linear() #[cfg(feature = "fp")] #[inline] From 73466198ebfe1e4b03a19124699f0f7c43941826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Fri, 22 May 2026 00:58:20 +0300 Subject: [PATCH 18/76] Improve test coverage in math::mat --- core/src/math/mat.rs | 83 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 12 deletions(-) diff --git a/core/src/math/mat.rs b/core/src/math/mat.rs index 042300b7..ab9b809f 100644 --- a/core/src/math/mat.rs +++ b/core/src/math/mat.rs @@ -392,6 +392,18 @@ impl Mat3 { /// Constructs a matrix from a linear basis. /// /// The basis does not have to be orthonormal. + /// + /// # Examples + /// ``` + /// use retrofire_core::{mat, math::{Mat3, vec2}}; + /// + /// let m = ::from_linear(vec2(0.0, 2.0), vec2(3.0, 0.0)); + /// assert_eq!(m, mat![ + /// 0.0, 3.0, 0.0; + /// 2.0, 0.0, 0.0; + /// 0.0, 0.0, 1.0; + /// ]) + /// ``` pub const fn from_linear(i: Vec2, j: Vec2) -> Self { Self::from_affine(i, j, Point2::origin()) } @@ -399,6 +411,20 @@ impl Mat3 { /// Constructs a matrix from an affine basis, or frame. /// /// The basis does not have to be orthonormal. + /// + /// # Examples + /// ``` + /// use retrofire_core::{mat, math::{Mat3, vec2, pt2}}; + /// + /// let m = ::from_affine( + /// vec2(0.0, 2.0), vec2(3.0, 0.0), pt2(4.0, 5.0)); + /// + /// assert_eq!(m, mat![ + /// 0.0, 3.0, 4.0; + /// 2.0, 0.0, 5.0; + /// 0.0, 0.0, 1.0; + /// ]) + /// ``` pub const fn from_affine( i: Vec2, j: Vec2, @@ -416,13 +442,18 @@ impl Mat3 { /// /// # Examples /// ``` - /// use retrofire_core::assert_approx_eq; - /// use retrofire_core::math::*; + /// use retrofire_core::{mat, math::*}; /// - /// // TODO translate2 does not exist (yet) - /// /*let m = rotate2(degs(90.0)).then(&translate3(1.0, 2.0, 3.0)); - /// let lin = m.linear(); - /// assert_approx_eq!(lin.apply(&pt2(1.0, 0.0, 0.0)), pt2(0.0, 0.0, -1.0));*/ + /// let m: Mat3 = mat![ + /// 2.0, 0.0, 4.0; + /// 0.0, 3.0, 5.0; + /// 0.0, 0.0, 1.0; + /// ]; + /// assert_eq!(m.linear(), mat![ + /// 2.0, 0.0; + /// 0.0, 3.0; + /// ]); + /// ``` pub const fn linear(&self) -> Mat2 { let [r, s, _] = self.0; mat![r[0], r[1]; s[0], s[1]] @@ -432,12 +463,15 @@ impl Mat3 { /// /// # Example /// ``` - /// use retrofire_core::math::*; + /// use retrofire_core::{mat, math::*}; /// - /// // TODO translate2 does not exist (yet) - /// /*let trans = vec2(1.0, 2.0); - /// let m = rotate2(degs(45.0)).then(&translate(trans)); - /// assert_eq!(m.translation(), trans);*/ + /// let m: Mat3 = mat![ + /// 2.0, 0.0, 4.0; + /// 0.0, 3.0, 5.0; + /// 0.0, 0.0, 1.0; + /// ]; + /// assert_eq!(m.translation(), vec2(4.0, 5.0)); + /// ``` pub const fn translation(&self) -> Vec2 { let [r, s, _] = self.0; vec2(r[2], s[2]) @@ -446,8 +480,16 @@ impl Mat3 { /// Returns the translation column vector of `self` as a point. /// /// # Example + /// ``` + /// use retrofire_core::{mat, math::*}; /// - /// TODO + /// let m: Mat3 = mat![ + /// 2.0, 0.0, 4.0; + /// 0.0, 3.0, 5.0; + /// 0.0, 0.0, 1.0; + /// ]; + /// assert_eq!(m.origin(), pt2(4.0, 5.0)); + /// ``` pub const fn origin(&self) -> Point2 { self.translation().to_pt() } @@ -1358,6 +1400,14 @@ mod tests { assert_approx_eq!(m_inv.inverse(), m); } #[test] + fn inverse_of_singular_does_not_exist() { + let singular: Mat2 = mat![ + 1.0, 0.0; + 2.0, 0.0; + ]; + assert_eq!(singular.checked_inverse(), None); + } + #[test] fn composition_of_inverse_is_identity() { let m: Mat2 = [[0.5, 1.5], [1.0, -0.5]].into(); let m_inv: Mat2 = m.inverse(); @@ -1451,6 +1501,15 @@ mod tests { ); } #[test] + fn inverse_of_singular_does_not_exist() { + let singular: Mat3 = mat![ + 1.0, 0.0, 0.0; + 2.0, 0.0, 0.0; + 0.0, 0.0, 1.0; + ]; + assert_eq!(singular.checked_inverse(), None); + } + #[test] fn matrix_composed_with_inverse_is_identity() { let mat: Mat3 = mat![ 1.0, -2.0, 2.0; From 80b4bc271f2293435b2d058214d888dc668cc0d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Fri, 22 May 2026 00:58:41 +0300 Subject: [PATCH 19/76] Improve test coverage in math::vec --- core/src/math/vec.rs | 255 ++++++++++++++++++++++++++++++------------- 1 file changed, 179 insertions(+), 76 deletions(-) diff --git a/core/src/math/vec.rs b/core/src/math/vec.rs index 18bb600f..3d3b8f44 100644 --- a/core/src/math/vec.rs +++ b/core/src/math/vec.rs @@ -256,6 +256,19 @@ impl Vector<[f32; N], Sp> { /// Returns `true` if every component of `self` is finite, `false` otherwise. /// /// See [`f32::is_finite()`]. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::*; + /// + /// let finite: Vec2 = vec2(0.0, 2.0); + /// let has_inf: Vec3 = vec3(0.0, f32::INFINITY, 2.0); + /// let has_nan: Vec3 = vec3(0.0, 1.0, f32::NAN); + /// + /// assert!(finite.is_finite()); + /// assert!(!has_inf.is_finite()); + /// assert!(!has_nan.is_finite()); + /// ``` pub fn is_finite(&self) -> bool { self.0.iter().all(|c| c.is_finite()) } @@ -457,6 +470,14 @@ impl Vector<[Sc; 2], Real<2, B>> { } /// Converts `self` to a `Vec3`, with z set to 0. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::vec::*; + /// + /// let vector: Vec2 = vec2(1.0, 2.0); + /// assert_eq!(vector.to_vec3(), vec3(1.0, 2.0, 0.0)); + /// ``` pub fn to_vec3(self) -> Vector<[Sc; 3], Real<3, B>> where Sc: Linear, @@ -650,6 +671,14 @@ impl Vector<[Sc; 4], Proj3> { } /// Projects `self` to the real plane by dividing by `w`. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::*; + /// + /// let proj = ProjVec3::new([1.0, 2.0, 3.0, 4.0]); + /// assert_eq!(proj.to_real::<()>(), pt3(0.25, 0.5, 0.75)); + /// ``` #[inline] pub fn to_real(&self) -> Point<[Sc; 3], Real<3, B>> where @@ -793,24 +822,61 @@ impl From for Vector<[Sc; N], Sp> { // Vector <-> tuple conversions impl From<(Sc, Sc)> for Vector<[Sc; 2], Sp> { + /// Converts a 2-tuple into a 2-vector. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::vec::*; + /// + /// assert_eq!(::from((1.0, 2.0)), vec2(1.0, 2.0)); + /// ``` #[inline] fn from(xy: (Sc, Sc)) -> Self { Self::new(xy.into()) } } impl From> for (Sc, Sc) { + /// Converts a 2-vector into a 2-tuple. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::vec::*; + /// + /// let vector: Vec2 = vec2(1.0, 2.0); + /// let (x, y) = vector.into(); + /// assert_eq!((x, y), (1.0, 2.0)); + /// ``` #[inline] fn from(v: Vector<[Sc; 2], Sp>) -> Self { v.0.into() } } impl From<(Sc, Sc, Sc)> for Vector<[Sc; 3], Sp> { + /// Converts a 3-tuple into a 3-vector. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::vec::*; + /// + /// let vector: Vec3 = (1.0, 2.0, 3.0).into(); + /// assert_eq!(vector, vec3(1.0, 2.0, 3.0)); + /// ``` #[inline] fn from(xyz: (Sc, Sc, Sc)) -> Self { Self::new(xyz.into()) } } impl From> for (Sc, Sc, Sc) { + /// Converts a 3-vector into a 3-tuple. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::vec::*; + /// + /// let vector: Vec3 = vec3(1.0, 2.0, 3.0); + /// let (x, y, z) = vector.into(); + /// assert_eq!((x, y, z), (1.0, 2.0, 3.0)); + /// ``` #[inline] fn from(v: Vector<[Sc; 3], Sp>) -> Self { v.0.into() @@ -1018,29 +1084,31 @@ mod tests { mod f32 { use super::*; + const X: Vec2 = Vec2::X; + const Y: Vec2 = Vec2::Y; + + const V2: Vec2 = vec2(1.0, -2.0); + const V3: Vec3 = vec3(1.0, -2.0, 3.0); + const V4: Vector<[f32; 4], Real<4>> = vec4(1.0, -2.0, 3.0, -4.0); + #[test] fn length() { assert_approx_eq!(vec2(1.0, 1.0).len(), SQRT_2); assert_approx_eq!(vec2(-3.0, 4.0).len(), 5.0); - assert_approx_eq!(vec3(1.0, -2.0, 3.0).len(), 14.0f32.sqrt()); + assert_approx_eq!(V3.len(), 14.0f32.sqrt()); } #[test] fn length_squared() { - assert_eq!(vec2(1.0, 1.0).len_sqr(), 2.0); - assert_eq!(vec2(-4.0, 3.0).len_sqr(), 25.0); - assert_eq!(vec3(1.0, -2.0, 3.0).len_sqr(), 14.0); + assert_eq!(V2.len_sqr(), 5.0); + assert_eq!(V3.len_sqr(), 14.0); } #[test] fn normalize() { assert_approx_eq!(vec2(3.0, 4.0).normalize(), vec2(0.6, 0.8)); - let sqrt_14 = 14.0f32.sqrt(); - assert_approx_eq!( - vec3(1.0, 2.0, 3.0).normalize(), - vec3(1.0 / sqrt_14, 2.0 / sqrt_14, 3.0 / sqrt_14) - ); + assert_approx_eq!(V3.normalize(), V3.map(|x| x / 14.0f32.sqrt())); } #[test] @@ -1069,50 +1137,58 @@ mod tests { #[test] fn vector_addition() { - assert_eq!(vec2(1.0, 2.0) + vec2(-2.0, 1.0), vec2(-1.0, 3.0)); - assert_eq!( - vec3(1.0, 2.0, 0.0) + vec3(-2.0, 1.0, -1.0), - vec3(-1.0, 3.0, -1.0) - ); + assert_eq!(V2 + vec2(-2.0, 3.0), vec2(-1.0, 1.0)); + let mut v2 = V2; + v2 += vec2(-2.0, 3.0); + assert_eq!(v2, vec2(-1.0, 1.0)); + + assert_eq!(V3 + vec3(-2.0, 1.0, -1.0), vec3(-1.0, -1.0, 2.0)); + let mut v3 = V3; + v3 += vec3(-2.0, 1.0, -1.0); + assert_eq!(v3, vec3(-1.0, -1.0, 2.0)); } #[test] fn scalar_multiplication() { - assert_eq!(vec2(1.0, -2.0) * 0.0, vec2(0.0, 0.0)); - assert_eq!(vec3(1.0, -2.0, 3.0) * 3.0, vec3(3.0, -6.0, 9.0)); - assert_eq!(3.0 * vec3(1.0, -2.0, 3.0), vec3(3.0, -6.0, 9.0)); - assert_eq!( - vec4(1.0, -2.0, 0.0, -3.0) * 3.0, - vec4(3.0, -6.0, 0.0, -9.0) - ); - assert_eq!( - 3.0 * vec4(1.0, -2.0, 0.0, -3.0), - vec4(3.0, -6.0, 0.0, -9.0) - ); + assert_eq!(V2 * 0.0, vec2(0.0, 0.0)); + assert_eq!(V2 * 2.0, vec2(2.0, -4.0)); + assert_eq!(-2.0 * V2, vec2(-2.0, 4.0)); + + assert_eq!(V3 * 3.0, vec3(3.0, -6.0, 9.0)); + assert_eq!(-3.0 * V3, vec3(-3.0, 6.0, -9.0)); + + assert_eq!(V4 * 2.0, vec4(2.0, -4.0, 6.0, -8.0)); + assert_eq!(-2.0 * V4, vec4(-2.0, 4.0, -6.0, 8.0)); } #[test] fn scalar_division() { - assert_eq!(vec2(1.0, -2.0) / 1.0, vec2(1.0, -2.0)); - assert_eq!(vec3(3.0, -6.0, 9.0) / 3.0, vec3(1.0, -2.0, 3.0)); - assert_eq!( - vec4(3.0, -6.0, 0.0, -9.0) / 3.0, - vec4(1.0, -2.0, 0.0, -3.0) - ); + assert_eq!(V2 / 1.0, V2); + assert_eq!(V2 / -2.0, vec2(-0.5, 1.0)); + + assert_eq!(V3 / 0.5, vec3(2.0, -4.0, 6.0)); + + let v4 = vec4(3.0, -6.0, 0.0, -9.0); + assert_eq!(v4 / 3.0, vec4(1.0, -2.0, 0.0, -3.0)); } #[test] fn dot_product() { - assert_eq!(vec2(1.0, -2.0).dot(&vec2(2.0, 3.0)), -4.0); - assert_eq!(vec3(1.0, -2.0, 3.0).dot(&vec3(2.0, 3.0, -1.0)), -7.0); + assert_eq!(V2.dot(&V2), 5.0); + assert_eq!(V2.dot(&V2.perp()), 0.0); + assert_eq!(V2.dot(&vec2(2.0, 3.0)), -4.0); + + assert_eq!(V3.dot(&V3), 14.0); + assert_eq!(V3.dot(&-V3), -14.0); + assert_eq!(V3.dot(&vec3(2.0, 3.0, -1.0)), -7.0); } #[test] fn zero_parallel_to_anything() { // Zero vector is parallel with anything assert!(vec2(0.0, 0.0).is_parallel_to(&vec2(0.0, 0.0))); - assert!(vec2(0.0, 0.0).is_parallel_to(&vec2(1.0, 2.0))); - assert!(vec2(0.0, 0.0).is_parallel_to(&vec2(-1.0, 2.0))); + assert!(vec2(0.0, 0.0).is_parallel_to(&V2)); + assert!(vec2(0.0, 0.0).is_parallel_to(&-V2)); } #[test] @@ -1126,11 +1202,11 @@ mod tests { #[test] fn a_b_parallel_to_ka_kb() { - // (2, -1) is parallel with any (2·k, -1·k) - assert!(vec2(2.0, -1.0).is_parallel_to(&vec2(0.0, 0.0))); - assert!(vec2(2.0, -1.0).is_parallel_to(&vec2(2.0, -1.0))); - assert!(vec2(2.0, -1.0).is_parallel_to(&vec2(-4.0, 2.0))); - assert!(vec2(2.0, -1.0).is_parallel_to(&vec2(1.0, -0.5))); + // (1, -2) is parallel with any (k, -2·k) + assert!(V2.is_parallel_to(&vec2(0.0, 0.0))); + assert!(V2.is_parallel_to(&vec2(-1.0, 2.0))); + assert!(V2.is_parallel_to(&vec2(2.0, -4.0))); + assert!(V2.is_parallel_to(&vec2(-0.125, 0.25))); } #[test] @@ -1145,41 +1221,41 @@ mod tests { #[test] fn indexing() { - let mut v = vec2(1.0, 2.0); - assert_eq!(v[1], 2.0); + let mut v = V2; + assert_eq!(v[1], -2.0); v[0] = 3.0; - assert_eq!(v.0, [3.0, 2.0]); + assert_eq!(v.0, [3.0, -2.0]); - let mut v = vec3(1.0, 2.0, 3.0); - assert_eq!(v[1], 2.0); + let mut v = V3; + assert_eq!(v[1], -2.0); v[2] = 4.0; - assert_eq!(v.0, [1.0, 2.0, 4.0]); + assert_eq!(v.0, [1.0, -2.0, 4.0]); + + let mut v = V4; + assert_eq!(v[2], 3.0); + v[3] = 5.0; + assert_eq!(v.0, [1.0, -2.0, 3.0, 5.0]); } #[test] fn from_array() { - assert_eq!(Vec2::from([1.0, -2.0]), vec2(1.0, -2.0)); - assert_eq!(Vec3::from([1.0, -2.0, 4.0]), vec3(1.0, -2.0, 4.0)); - assert_eq!( - Vector::from([1.0, -2.0, 4.0, -3.0]), - vec4(1.0, -2.0, 4.0, -3.0) - ); + assert_eq!(Vec2::from([1.0, -2.0]), V2); + assert_eq!(Vec3::from([1.0, -2.0, 3.0]), V3); + assert_eq!(Vector::from([1.0, -2.0, 3.0, -4.0]), V4); } #[test] fn perp() { - assert_eq!(Vec2::<()>::zero().perp(), Vec2::zero()); - assert_eq!(Vec2::<()>::X.perp(), Vec2::Y); - assert_eq!(vec2(-0.2, -1.5).perp(), vec2(1.5, -0.2)); + assert_eq!(::zero().perp(), Vec2::zero()); + assert_eq!(X.perp(), Y); + assert_eq!(V2.perp(), vec2(2.0, 1.0)); } #[test] fn perp_dot() { - const X: Vec2 = Vec2::X; - const Y: Vec2 = Vec2::Y; - assert_eq!(X.perp_dot(X), 0.0); assert_eq!(X.perp_dot(Y), 1.0); + assert_eq!(X.perp_dot(-Y), -1.0); assert_eq!((2.0 * Y).perp_dot(3.0 * X), -6.0); } } @@ -1187,53 +1263,80 @@ mod tests { mod i32 { use super::*; + const V2: Vec2i = vec2(1, -2); + const V3: Vec3i = vec3(1, -2, 3); + #[test] fn vector_addition() { - assert_eq!(vec2(1, 2) + vec2(-2, 1), vec2(-1, 3)); - assert_eq!(vec3(1, 2, 0) + vec3(-2, 1, -1), vec3(-1, 3, -1)); + assert_eq!(V2 + vec2(-2, 1), vec2(-1, -1)); + + let mut v2 = V2; + v2 += vec2(2, -3); + assert_eq!(v2, vec2(3, -5)); + + assert_eq!(V3 + vec3(-2, 1, -1), vec3(-1, -1, 2)); + + let mut v3 = V3; + v3 += vec3(2, 3, -4); + assert_eq!(v3, vec3(3, 1, -1)); } #[test] fn vector_subtraction() { - assert_eq!(vec2(1, 2) - vec2(-2, 3), vec2(3, -1)); - assert_eq!(vec3(1, 2, 0) - vec3(-2, 1, 2), vec3(3, 1, -2)); + assert_eq!(V2 - vec2(2, -3), vec2(-1, 1)); + + let mut v2 = V2; + v2 -= vec2(2, -3); + assert_eq!(v2, vec2(-1, 1)); + + assert_eq!(V3 - vec3(-2, 1, 2), vec3(3, -3, 1)); + + let mut v3 = V3; + v3 -= vec3(2, 3, -1); + assert_eq!(v3, vec3(-1, -5, 4)); } #[test] #[allow(clippy::erasing_op)] fn scalar_multiplication() { - assert_eq!(vec2(1, -2) * 0, vec2(0, 0)); + assert_eq!(V2 * 0, vec2(0, 0)); + assert_eq!(V2 * 2, vec2(2, -4)); + assert_eq!(-2 * V2, vec2(-2, 4)); + + let mut v2 = V2; + v2 *= 3; + assert_eq!(v2, vec2(3, -6)); - assert_eq!(vec3(1, -2, 3) * 3, vec3(3, -6, 9)); - assert_eq!(3 * vec3(1, -2, 3), vec3(3, -6, 9)); + assert_eq!(V3 * 3, vec3(3, -6, 9)); + assert_eq!(-3 * V3, vec3(-3, 6, -9)); assert_eq!(vec4(1, -2, 0, -3) * 3, vec4(3, -6, 0, -9)); - assert_eq!(3 * vec4(1, -2, 0, -3), vec4(3, -6, 0, -9)); + assert_eq!(-3 * vec4(1, -2, 0, -3), vec4(-3, 6, 0, 9)); } #[test] fn dot_product() { - assert_eq!(vec2(1, -2).dot(&vec2(2, 3)), -4); - assert_eq!(vec3(1, -2, 3).dot(&vec3(2, 3, -1)), -7); + assert_eq!(V2.dot(&vec2(2, -3)), 8); + assert_eq!(V3.dot(&vec3(2, 3, -1)), -7); } #[test] fn indexing() { - let mut v = vec2(1, 2); - assert_eq!(v[1], 2); + let mut v = V2; + assert_eq!(v[1], -2); v[0] = 3; - assert_eq!(v.0, [3, 2]); + assert_eq!(v.0, [3, -2]); - let mut v = vec3(1, 2, 3); - assert_eq!(v[1], 2); + let mut v = V3; + assert_eq!(v[1], -2); v[2] = 4; - assert_eq!(v.0, [1, 2, 4]); + assert_eq!(v.0, [1, -2, 4]); } #[test] fn from_array() { - assert_eq!(Vec2i::from([1, -2]), vec2(1, -2)); - assert_eq!(Vec3i::from([1, -2, 3]), vec3(1, -2, 3)); + assert_eq!(Vec2i::from([1, -2]), V2); + assert_eq!(Vec3i::from([1, -2, 3]), V3); } } From 6b793537e480f0f5c782a228e652e850b57994c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sun, 24 May 2026 00:06:45 +0300 Subject: [PATCH 20/76] Constify some Line2 and other functions --- core/src/geom/prim.rs | 17 ++++++++++------- core/src/math/float.rs | 2 +- core/src/math/vec.rs | 6 ++++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/core/src/geom/prim.rs b/core/src/geom/prim.rs index 868cb783..d403bfb2 100644 --- a/core/src/geom/prim.rs +++ b/core/src/geom/prim.rs @@ -673,7 +673,7 @@ impl Line2 { /// /// # Panics /// If the vector (a, b) is not unit-length. - pub fn new(a: f32, b: f32, c: f32) -> Self { + pub const fn new(a: f32, b: f32, c: f32) -> Self { // TODO This method can't itself normalize because const assert!((a * a + b * b - 1.0).abs() < 1e-6, "non-unit normal"); Self(Vector::new([a, b, -c])) @@ -684,6 +684,7 @@ impl Line2 { /// # Panics /// If the points coincide. pub fn from_points(p: Point2, q: Point2) -> Self { + // TODO not const due to normalize Edge(p, q).into() } @@ -698,16 +699,18 @@ impl Line2 { /// assert_approx_eq!(slope, 0.5); /// assert_approx_eq!(y_intercept, 1.5); /// ``` - pub fn slope_intercept(&self) -> Option<(f32, f32)> { + pub const fn slope_intercept(&self) -> Option<(f32, f32)> { // ax + by + c = 0 let [a, b, c] = self.coeffs(); - (b != 0.0).then(|| { + if b != 0.0 { // by = -ax - c <=> y = -a/b x - c/b let m = -a / b; // slope let y0 = -c / b; // y intercept - (m, y0) - }) + Some((m, y0)) + } else { + None + } } /// Returns @@ -715,8 +718,8 @@ impl Line2 { vec2(self.0[0], self.0[1]).normalize() } /// Returns the signed distance of `self` from the origin. - pub fn offset(&self) -> f32 { - -self.0[2] + pub const fn offset(&self) -> f32 { + -self.0.0[2] } /// Returns the coefficients [a, b, c] of the line equation ax + by = c. diff --git a/core/src/math/float.rs b/core/src/math/float.rs index 7b84bafd..620ea9a6 100644 --- a/core/src/math/float.rs +++ b/core/src/math/float.rs @@ -202,7 +202,7 @@ pub mod fallback { /// /// ``` #[inline] -pub fn fast_recip_sqrt(x: f32) -> f32 { +pub const fn fast_recip_sqrt(x: f32) -> f32 { // https://en.wikipedia.org/wiki/Fast_inverse_square_root const MAGIC: u32 = 0x5f37_5a86; let mut y = f32::from_bits(MAGIC.saturating_sub(x.to_bits() >> 1)); diff --git a/core/src/math/vec.rs b/core/src/math/vec.rs index 3d3b8f44..a3b7baf0 100644 --- a/core/src/math/vec.rs +++ b/core/src/math/vec.rs @@ -541,8 +541,10 @@ impl Vec2 { /// assert! (v.perp_dot(Vec2::Y) > 0.0, "Y is counter-clockwise from v"); /// ``` #[inline] - pub fn perp_dot(self, other: Self) -> f32 { - self.perp().dot(&other) + pub const fn perp_dot(self, other: Self) -> f32 { + let perp = self.perp(); + // Manual dot to allow const + perp.x() * other.x() + perp.y() * other.y() } /// Returns the angle between `self` and the positive x-axis. From edd8b81d0e0ab6aed31be558a53c180c25c24290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sun, 24 May 2026 00:08:09 +0300 Subject: [PATCH 21/76] Fix some tests that need approx_eq in no_std --- core/src/geom/prim.rs | 2 +- core/src/math/mat.rs | 30 +++++++++++++++--------------- core/src/math/vec.rs | 3 ++- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/core/src/geom/prim.rs b/core/src/geom/prim.rs index d403bfb2..3df451a4 100644 --- a/core/src/geom/prim.rs +++ b/core/src/geom/prim.rs @@ -1084,7 +1084,7 @@ mod tests { assert_eq!(format!("{l:?}"), "Line(x = 0)"); l = Line2::from_points(pt2(2.0, 0.0), pt2(2.0, -1.0)); - assert_eq!(l.coeffs(), [1.0, 0.0, -2.0]); + assert_approx_eq!(l.coeffs(), [1.0, 0.0, -2.0]); assert_eq!(format!("{l:?}"), "Line(x = 2)"); l = Line2::new(1.0, 0.0, 2.0); // x = 2 diff --git a/core/src/math/mat.rs b/core/src/math/mat.rs index ab9b809f..3a61a5a8 100644 --- a/core/src/math/mat.rs +++ b/core/src/math/mat.rs @@ -1768,13 +1768,13 @@ mod tests { let m = orient_y(Y, X); assert_approx_eq!(m.apply(&X), X); - assert_eq!(m.apply(&X.to_pt()), X.to_pt()); + assert_approx_eq!(m.apply(&X.to_pt()), X.to_pt()); - assert_eq!(m.apply(&Y), Y); - assert_eq!(m.apply(&Y.to_pt()), Y.to_pt()); + assert_approx_eq!(m.apply(&Y), Y); + assert_approx_eq!(m.apply(&Y.to_pt()), Y.to_pt()); - assert_eq!(m.apply(&Z), Z); - assert_eq!(m.apply(&Z.to_pt()), Z.to_pt()); + assert_approx_eq!(m.apply(&Z), Z); + assert_approx_eq!(m.apply(&Z.to_pt()), Z.to_pt()); } #[test] @@ -1782,13 +1782,13 @@ mod tests { let m = orient_y(Z, X); assert_approx_eq!(m.apply(&X), X); - assert_eq!(m.apply(&X.to_pt()), X.to_pt()); + assert_approx_eq!(m.apply(&X.to_pt()), X.to_pt()); - assert_eq!(m.apply(&Y), Z); - assert_eq!(m.apply(&Y.to_pt()), Z.to_pt()); + assert_approx_eq!(m.apply(&Y), Z); + assert_approx_eq!(m.apply(&Y.to_pt()), Z.to_pt()); - assert_eq!(m.apply(&Z), -Y); - assert_eq!(m.apply(&Z.to_pt()), (-Y).to_pt()); + assert_approx_eq!(m.apply(&Z), -Y); + assert_approx_eq!(m.apply(&Z.to_pt()), (-Y).to_pt()); } #[test] @@ -1796,13 +1796,13 @@ mod tests { let m = orient_z(Y, X); assert_approx_eq!(m.apply(&X), X); - assert_eq!(m.apply(&X.to_pt()), X.to_pt()); + assert_approx_eq!(m.apply(&X.to_pt()), X.to_pt()); - assert_eq!(m.apply(&Y), -Z); - assert_eq!(m.apply(&Y.to_pt()), (-Z).to_pt()); + assert_approx_eq!(m.apply(&Y), -Z); + assert_approx_eq!(m.apply(&Y.to_pt()), (-Z).to_pt()); - assert_eq!(m.apply(&Z), Y); - assert_eq!(m.apply(&Z.to_pt()), Y.to_pt()); + assert_approx_eq!(m.apply(&Z), Y); + assert_approx_eq!(m.apply(&Z.to_pt()), Y.to_pt()); } #[test] diff --git a/core/src/math/vec.rs b/core/src/math/vec.rs index a3b7baf0..e040891d 100644 --- a/core/src/math/vec.rs +++ b/core/src/math/vec.rs @@ -220,10 +220,11 @@ impl Vector<[f32; N], Sp> { /// # Examples /// ``` /// use retrofire_core::math::{degs, vec3, Vec3}; + /// use retrofire_core::assert_approx_eq; /// /// let a: Vec3 = vec3(0.0, 1.0, 0.0); /// let b: Vec3 = vec3(2.0, 0.0, 3.0); - /// assert_eq!(a.angle(&b), degs(90.0)); + /// assert_approx_eq!(a.angle(&b), degs(90.0)); /// ``` #[cfg(feature = "fp")] #[inline] From 32bcae1d4f7de925b710278c04b4a3f2107e1e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Wed, 17 Jun 2026 22:21:55 +0300 Subject: [PATCH 22/76] Make Inner::data pub(super), remove the accessor methods --- core/src/util/buf.rs | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/core/src/util/buf.rs b/core/src/util/buf.rs index 421308e0..3ba00c22 100644 --- a/core/src/util/buf.rs +++ b/core/src/util/buf.rs @@ -203,13 +203,13 @@ impl Buf2 { /// Returns a view of the backing data of `self`. #[inline] pub fn data(&self) -> &[T] { - self.0.data() + &self.0.data } /// Returns a mutable view of the backing data of `self`. #[inline] pub fn data_mut(&mut self) -> &mut [T] { - self.0.data_mut() + &mut self.0.data } /// Reinterprets `self` as a buffer of different dimensions but same area. @@ -396,7 +396,7 @@ pub mod inner { pub struct Inner { dims: Dims, stride: u32, - data: D, + pub(super) data: D, _pd: PhantomData, } @@ -531,12 +531,6 @@ pub mod inner { Self { dims, stride, data, _pd: PhantomData } } - /// Returns the data of `self` as a linear slice. - #[inline] - pub(super) fn data(&self) -> &[T] { - &self.data - } - /// Borrows `self` as a `Slice2`. #[inline] pub fn as_slice2(&self) -> Slice2<'_, T> { @@ -605,12 +599,6 @@ pub mod inner { MutSlice2(Inner { dims, stride, data, _pd }) } - /// Returns the data of `self` as a single mutable slice. - #[inline] - pub(super) fn data_mut(&mut self) -> &mut [T] { - &mut self.data - } - /// Returns an iterator over the rows of this buffer as `&mut [T]`. /// /// The length of each slice equals [`self.width()`](Self::width). @@ -945,7 +933,7 @@ mod tests { assert_eq!(slice.width(), 3); assert_eq!(slice.height(), 6); assert_eq!(slice.stride(), 10); - assert_eq!(slice.data().len(), 5 * 10 + 3); + assert_eq!(slice.data.len(), 5 * 10 + 3); } #[test] From dbb4953935cb2a646e3d1012cc62531bf8b80b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Wed, 17 Jun 2026 23:05:43 +0300 Subject: [PATCH 23/76] Fix buggy Buf2::reshape(), improve doc comments and doctests --- core/src/util/buf.rs | 74 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 12 deletions(-) diff --git a/core/src/util/buf.rs b/core/src/util/buf.rs index 3ba00c22..37234724 100644 --- a/core/src/util/buf.rs +++ b/core/src/util/buf.rs @@ -20,7 +20,7 @@ use inner::Inner; /// A trait for types that can provide a view of their data as a [`Slice2`]. pub trait AsSlice2 { type Elem; - /// Returns a borrowed `Slice2` view of `Self`. + /// Returns a borrowed `Slice2` view of `self`. fn as_slice2(&self) -> Slice2<'_, Self::Elem>; } @@ -28,7 +28,7 @@ pub trait AsSlice2 { /// as a [`MutSlice2`]. pub trait AsMutSlice2 { type Elem; - /// Returns a mutably borrowed `MutSlice2` view of `Self`. + /// Returns a mutably borrowed `MutSlice2` view of `self`. fn as_mut_slice2(&mut self) -> MutSlice2<'_, Self::Elem>; } @@ -65,7 +65,7 @@ pub struct Buf2(Inner>); /// An immutable rectangular view to a region of a [`Buf2`], another `Slice2`, /// or in general any `&[T]` slice of memory. A two-dimensional analog to `&[T]`. /// -/// A `Slice2` may be non-contiguous: +/// The backing data of a `Slice2` may be non-contiguous: /// ```text /// +------stride-----+ /// | ____w____ | @@ -74,6 +74,9 @@ pub struct Buf2(Inner>); /// | |r2_______| | /// +-----------------+ /// ``` +/// Internally, `Slice2` borrows a contiguous slice of the backing buffer, +/// but disallows access to any elements not within its 2D extents. +/// /// TODO More documentation #[derive(Copy, Clone, Eq, PartialEq)] #[repr(transparent)] @@ -108,8 +111,8 @@ impl Buf2 { /// ``` /// /// # Panics - /// * If `w * h > isize::MAX`, or - /// * if `init` has fewer than `w * h` elements. + /// * If `w` × `h` > `isize::MAX`., or + /// * if `init` has fewer than `w` × `h` elements. pub fn new_from((w, h): Dims, init: I) -> Self where I: IntoIterator, @@ -151,7 +154,7 @@ impl Buf2 { /// ``` /// /// # Panics - /// If `w * h > isize::MAX`. + /// If `w` × `h` > `isize::MAX`. #[inline] pub fn new((w, h): Dims) -> Self where @@ -170,6 +173,9 @@ impl Buf2 { /// /// Does not allocate or call `init_fn` if `w` = 0 or `h` = 0. /// + /// # Panics + /// If `w` × `h` > `isize::MAX`. + /// /// # Examples /// ``` /// use retrofire_core::util::buf::Buf2; @@ -179,9 +185,6 @@ impl Buf2 { /// 10, 11, 12, /// 20, 21, 22]); /// ``` - /// - /// # Panics - /// If `w * h > isize::MAX`. pub fn new_with((w, h): Dims, mut init_fn: F) -> Self where F: FnMut(u32, u32) -> T, @@ -212,11 +215,30 @@ impl Buf2 { &mut self.0.data } - /// Reinterprets `self` as a buffer of different dimensions but same area. + /// Reinterprets `self` as a buffer of different dimensions but the same area. + /// + /// Does not reallocate or move data. /// /// # Panics - /// If `nw` * `nh` != `cw` * `ch` for the new dimensions (`nw`, `nh`) + /// If `nw` × `nh` ≠ `cw` × `ch` for the new dimensions (`nw`, `nh`) /// and current dimensions (`cw`, `ch`). + /// + /// # Examples + /// ``` + /// use retrofire_core::util::buf::Buf2; + /// + /// let mut buf = Buf2::new_from((3, 2), [1, 2, 3, 4, 5, 6]); + /// + /// buf.reshape((2, 3)); + /// + /// assert_eq!(buf.stride(), 2); + /// assert_eq!(buf.width(), 2); + /// assert_eq!(buf.height(), 3); + /// + /// assert_eq!(buf[0], [1, 2]); + /// assert_eq!(buf[1], [3, 4]); + /// assert_eq!(buf[2], [5, 6]); + /// ``` pub fn reshape(&mut self, dims: Dims) { self.0.reshape(dims); } @@ -374,6 +396,30 @@ impl DerefMut for MutSlice2<'_, T> { } } +impl From<&[[T; N]]> for Buf2 { + /// Creates a `Buf2` from a slice of arrays. + /// + /// # Examples + /// ``` + /// use retrofire_core::util::buf::Buf2; + /// + /// let buf = Buf2::from([[1, 2, 3], [4, 5, 6]].as_slice()); + /// + /// assert_eq!(buf.stride(), 3); + /// assert_eq!(buf.width(), 3); + /// assert_eq!(buf.height(), 2); + /// + /// assert_eq!(buf[0], [1, 2, 3]); + /// assert_eq!(buf[1], [4, 5, 6]); + /// ``` + fn from(slice: &[[T; N]]) -> Self { + Self::new_from( + (N as u32, slice.len() as u32), + slice.as_flattened().iter().cloned(), + ) + } +} + pub mod inner { use core::{ fmt::Formatter, @@ -509,7 +555,11 @@ pub mod inner { pub(super) fn reshape(&mut self, dims: Dims) { assert!(self.is_contiguous()); - assert_eq!(dims.0 * dims.1, self.dims.0 * self.dims.1); + assert_eq!( + dims.0 as u64 * dims.1 as u64, + self.dims.0 as u64 * self.dims.1 as u64 + ); + self.stride = dims.0; self.dims = dims; } } From 8921a7fc6d0b7e63c1495803f67f09d0e82b641e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 18 Jun 2026 00:11:05 +0300 Subject: [PATCH 24/76] Add doc comments and doctests to util::pixfmt --- core/src/util/pixfmt.rs | 89 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/core/src/util/pixfmt.rs b/core/src/util/pixfmt.rs index 88113f33..1fa83177 100644 --- a/core/src/util/pixfmt.rs +++ b/core/src/util/pixfmt.rs @@ -42,7 +42,18 @@ pub struct Rgba4444; // Impls for Color3 -impl IntoPixel for Color3 { +impl IntoPixel for Color3 { + /// Converts `self` to a `u32` in 0x00_RR_GG_BB format. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{rgb, Color3}; + /// use retrofire_core::util::pixfmt::{IntoPixel, Xrgb8888}; + /// + /// let color: Color3 = rgb(0x33, 0x66, 0x99); + /// + /// assert_eq!(color.into_pixel_fmt(Xrgb8888), 0x00_33_66_99); + /// ``` #[inline] fn into_pixel(self) -> u32 { let [r, g, b] = self.0; @@ -51,6 +62,17 @@ impl IntoPixel for Color3 { } } impl IntoPixel<[u8; 3], Rgb888> for Color3 { + /// Converts `self` to (0xRR, 0xGG, 0xBB) bytes. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{rgb, Color3}; + /// use retrofire_core::util::pixfmt::{IntoPixel, Rgb888}; + /// + /// let color: Color3 = rgb(0x33, 0x66, 0x99); + /// + /// assert_eq!(color.into_pixel_fmt(Rgb888), [0x33, 0x66, 0x99]); + /// ``` #[inline] fn into_pixel(self) -> [u8; 3] { self.0 @@ -58,6 +80,18 @@ impl IntoPixel<[u8; 3], Rgb888> for Color3 { } impl IntoPixel for Color3 { + /// Converts `self` to a `u16` in 0bRRRRR_GGGGGG_BBBBB format. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{rgb, Color3}; + /// use retrofire_core::util::pixfmt::{IntoPixel, Rgb565}; + /// + /// let color: Color3 = rgb(0x80, 0x40, 0x20); + /// + /// let word_565: u16 = color.into_pixel_fmt(Rgb565); + /// assert_eq!(word_565, 0x8204); // 0b10000_010000_00100 + /// ``` #[inline] fn into_pixel(self) -> u16 { let [r, g, b] = self.0; @@ -89,6 +123,18 @@ where } impl IntoPixel for Color4 { + /// Converts `self` to a `u32` in 0x0RGB format, discarding the value of + /// the alpha channel. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{rgba, Color4}; + /// use retrofire_core::util::pixfmt::{IntoPixel, Xrgb8888}; + /// + /// let color: Color4 = rgba(0x11, 0x22, 0x33, 0x44); + /// + /// assert_eq!(color.into_pixel_fmt(Xrgb8888), 0x00_11_22_33); + /// ``` #[inline] fn into_pixel(self) -> u32 { let [r, g, b, _] = self.0; @@ -97,12 +143,36 @@ impl IntoPixel for Color4 { } } impl IntoPixel<[u8; 4], Rgba8888> for Color4 { + /// Converts `self` to (0xRR, 0xGG, 0xBB, 0xAA) bytes. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{rgba, Color4}; + /// use retrofire_core::util::pixfmt::{IntoPixel, Rgba8888}; + /// + /// let color: Color4 = rgba(0x11, 0x22, 0x33, 0x44); + /// let rgba: [u8; 4] = color.into_pixel_fmt(Rgba8888); + /// + /// assert_eq!(rgba, [0x11, 0x22, 0x33, 0x44]); + /// ``` #[inline] fn into_pixel(self) -> [u8; 4] { self.0 } } impl IntoPixel<[u8; 4], Argb8888> for Color4 { + /// Converts `self` to (0xAA, 0xRR, 0xGG, 0xBB) bytes. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{rgba, Color4}; + /// use retrofire_core::util::pixfmt::{Argb8888, IntoPixel}; + /// + /// let color: Color4 = rgba(0x11, 0x22, 0x33, 0x44); + /// let argb: [u8; 4] = color.into_pixel_fmt(Argb8888); + /// + /// assert_eq!(argb, [0x44, 0x11, 0x22, 0x33]); + /// ``` #[inline] fn into_pixel(self) -> [u8; 4] { let [r, g, b, a] = self.0; @@ -110,6 +180,18 @@ impl IntoPixel<[u8; 4], Argb8888> for Color4 { } } impl IntoPixel<[u8; 4], Bgra8888> for Color4 { + /// Converts `self` to (0xBB, 0xGG, 0xRR, 0xAA) bytes. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{rgba, Color4}; + /// use retrofire_core::util::pixfmt::{Bgra8888, IntoPixel}; + /// + /// let color: Color4 = rgba(0x11, 0x22, 0x33, 0x44); + /// let bgra: [u8; 4] = color.into_pixel_fmt(Bgra8888); + /// + /// assert_eq!(bgra, [0x33, 0x22, 0x11, 0x44]); + /// ``` #[inline] fn into_pixel(self) -> [u8; 4] { let [r, g, b, a] = self.0; @@ -117,12 +199,14 @@ impl IntoPixel<[u8; 4], Bgra8888> for Color4 { } } impl IntoPixel<[u8; 3], Rgb888> for Color4 { + /// Converts `self` to bytes in RGB order, discarding alpha. #[inline] fn into_pixel(self) -> [u8; 3] { [self.r(), self.g(), self.b()] } } impl IntoPixel<[u8; 2], Rgba4444> for Color4 { + /// Converts `self` to two bytes, one nibble (four bits) per channel in RGBA order. #[inline] fn into_pixel(self) -> [u8; 2] { let c: u16 = self.into_pixel_fmt(Rgba4444); @@ -130,6 +214,7 @@ impl IntoPixel<[u8; 2], Rgba4444> for Color4 { } } impl IntoPixel for Color4 { + /// Converts `self` to a `u16`, one nibble (four bits) per channel in RGBA order. #[inline] fn into_pixel(self) -> u16 { let [r, g, b, a] = self.0; @@ -165,7 +250,7 @@ mod tests { #[test] fn color3_to_rgb888() { - let pix: u32 = COL3.into_pixel_fmt(Rgb888); + let pix: u32 = COL3.into_pixel_fmt(Xrgb8888); assert_eq!(pix, 0x00_11_22_33); } From 422018394e180dfdbfb1a2145cf6755eda1a571c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 18 Jun 2026 18:02:35 +0300 Subject: [PATCH 25/76] Convert Dims to a custom struct from an alias --- benches/e2e.rs | 6 +- benches/fill.rs | 16 ++-- core/examples/hello_tri.rs | 8 +- core/src/lib.rs | 1 + core/src/render/cam.rs | 8 +- core/src/render/raster.rs | 4 +- core/src/render/tex.rs | 4 +- core/src/render/text.rs | 12 +-- core/src/util.rs | 11 ++- core/src/util/buf.rs | 174 +++++++++++++++++++------------------ core/src/util/pnm.rs | 35 ++++---- core/tests/rendering.rs | 6 +- demos/src/bin/bezier.rs | 2 +- demos/src/bin/crates.rs | 2 +- demos/src/bin/hello.rs | 6 +- demos/src/bin/solids.rs | 2 +- demos/src/bin/sprites.rs | 4 +- demos/src/bin/square.rs | 4 +- front/src/lib.rs | 62 ++++++------- front/src/minifb.rs | 7 +- front/src/sdl2.rs | 9 +- front/src/wasm.rs | 6 +- 22 files changed, 203 insertions(+), 186 deletions(-) diff --git a/benches/e2e.rs b/benches/e2e.rs index d6a2c8dc..5fbb8aef 100644 --- a/benches/e2e.rs +++ b/benches/e2e.rs @@ -9,7 +9,7 @@ use retrofire_core::{ translate, viewport, }, render::{Context, Frag, Model, debug::dir_to_rgb, render, shader}, - util::{buf::Buf2, pnm}, + util::{Dims, buf::Buf2, pnm}, }; use retrofire_geom::solids::{Build, Sphere}; @@ -32,7 +32,7 @@ fn triangle(b: Bencher, n: u32) { |frag: Frag>, _: &_| frag.var.to_color4(), ); - let dims @ (w, h) = (640, 480); + let dims @ Dims(w, h) = Dims(640, 480); let modelview = translate((0.0, 0.0, 2.0)).to(); let project = perspective(1.0, w as f32 / h as f32, 0.1..1000.0); let viewport = viewport(pt2(0, h)..pt2(w, 0)); @@ -76,7 +76,7 @@ fn sphere(b: Bencher, res: u32) { |frag: Frag, _: &_| frag.var.to_color4(), ); - let dims @ (w, h) = (640, 480); + let dims @ Dims(w, h) = Dims(640, 480); let modelview = translate((0.0, 0.0, 2.0)).to(); let project = perspective(1.0, w as f32 / h as f32, 0.1..1000.0); let viewport = viewport(pt2(0, h)..pt2(w, 0)); diff --git a/benches/fill.rs b/benches/fill.rs index 4ff7c603..cd645c3d 100644 --- a/benches/fill.rs +++ b/benches/fill.rs @@ -10,7 +10,7 @@ use retrofire_core::{ render::{ Texture, raster::ScreenPt, raster::tri_fill, tex::SamplerRepeatPot, uv, }, - util::{buf::Buf2, pnm::save_ppm}, + util::{Dims, buf::Buf2, pnm::save_ppm}, }; const SIZES: [f32; 5] = [4.0, 16.0, 64.0, 256.0, 1024.0]; @@ -20,7 +20,7 @@ const VERTS: [ScreenPt; 3] = #[divan::bench(args = SIZES)] fn flat(b: Bencher, sz: f32) { - let mut buf: Buf2 = Buf2::new((1024, 1024)); + let mut buf: Buf2 = Buf2::new(Dims(1024, 1024)); b.with_inputs(|| VERTS.map(|p| vertex(p * sz, ()))) .input_counter(move |vs| Tri(*vs).area() as usize) @@ -34,7 +34,7 @@ fn flat(b: Bencher, sz: f32) { } #[divan::bench(args = SIZES)] fn gouraud(b: Bencher, sz: f32) { - let mut buf: Buf2 = Buf2::new((1024, 1024)); + let mut buf: Buf2 = Buf2::new(Dims(1024, 1024)); b.with_inputs(|| { [ @@ -56,17 +56,19 @@ fn gouraud(b: Bencher, sz: f32) { }); }); - let buf = - Buf2::new_from((1024, 1024), buf.data().iter().map(|c| c.to_color3())); + let buf = Buf2::new_from( + Dims(1024, 1024), + buf.data().iter().map(|c| c.to_color3()), + ); save_ppm("benches_fill_color.ppm", buf).unwrap(); } #[divan::bench(args = SIZES)] fn texture(b: Bencher, sz: f32) { - let mut buf: Buf2 = Buf2::new((1024, 1024)); + let mut buf: Buf2 = Buf2::new(Dims(1024, 1024)); let tex = Texture::from(Buf2::::new_from( - (2, 2), + Dims(2, 2), [gray(0xFF), gray(0x33), gray(0x33), gray(0xFF)], )); let sampler = SamplerRepeatPot::new(&tex); diff --git a/core/examples/hello_tri.rs b/core/examples/hello_tri.rs index fcfe000d..41e3d197 100644 --- a/core/examples/hello_tri.rs +++ b/core/examples/hello_tri.rs @@ -1,7 +1,5 @@ -use retrofire_core::{ - prelude::*, - render::{Model, render, shader}, -}; +use retrofire_core::prelude::*; +use retrofire_core::render::{Model, render, shader}; fn main() { let verts = [ @@ -29,7 +27,7 @@ fn main() { |frag: Frag>, _| frag.var.to_color4(), ); - let dims @ (w, h) = (640, 480); + let dims @ Dims(w, h) = Dims(640, 480); let modelview = translate((0.0, 0.0, 2.0)).to(); let project = perspective(1.0, w as f32 / h as f32, 0.1..1000.0); let viewport = viewport(pt2(0, h)..pt2(w, 0)); diff --git a/core/src/lib.rs b/core/src/lib.rs index 0a5b3e3c..4eecf430 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -67,6 +67,7 @@ pub mod prelude { }, math::re_exports::*, render::re_exports::*, + util::Dims, util::buf::{AsMutSlice2, AsSlice2, Buf2, MutSlice2, Slice2}, }; } diff --git a/core/src/render/cam.rs b/core/src/render/cam.rs index 2db3d047..59edf86a 100644 --- a/core/src/render/cam.rs +++ b/core/src/render/cam.rs @@ -171,20 +171,20 @@ impl Camera { /// Sets the viewport bounds of this camera. #[must_use] pub fn viewport(self, bounds: impl Into>) -> Self { - let (w, h) = self.dims; - let Rect { left: Some(l), top: Some(t), right: Some(r), bottom: Some(b), - } = bounds.into().intersect(&(0..w, 0..h).into()) + } = bounds + .into() + .intersect(&(0..self.dims.0, 0..self.dims.1).into()) else { unreachable!("bounded ∩ bounded should be bounded") }; Self { - dims: (r.abs_diff(l), b.abs_diff(t)), + dims: Dims(r.abs_diff(l), b.abs_diff(t)), viewport: viewport(pt2(l, t)..pt2(r, b)), ..self } diff --git a/core/src/render/raster.rs b/core/src/render/raster.rs index e7347bad..84f45160 100644 --- a/core/src/render/raster.rs +++ b/core/src/render/raster.rs @@ -315,7 +315,7 @@ mod tests { assert_approx_eq, geom::vertex, math::{point::pt3, vary::Vary, vary::ZDiv}, - util::buf::Buf2, + util::{Dims, buf::Buf2}, }; use super::{Scanline, tri_fill}; @@ -324,7 +324,7 @@ mod tests { #[test] fn shared_edge_should_not_have_gaps_or_overdraw() { - let mut buf = Buf2::new((20, 10)); + let mut buf = Buf2::new(Dims(20, 10)); let verts = [ pt3(8.0, 0.0, 0.0), diff --git a/core/src/render/tex.rs b/core/src/render/tex.rs index d62d9a4f..a7078528 100644 --- a/core/src/render/tex.rs +++ b/core/src/render/tex.rs @@ -155,7 +155,7 @@ impl Atlas { /// of the sub-texture with index `i`. fn rect(&self, i: u32) -> [Point2u; 2] { match self.layout { - Layout::Grid { sub_dims: (sub_w, sub_h) } => { + Layout::Grid { sub_dims: Dims(sub_w, sub_h) } => { let subs_per_row = self.texture.data.width() / sub_w; let top_left = pt2(i % subs_per_row * sub_w, i / subs_per_row * sub_h); @@ -376,7 +376,7 @@ mod tests { #[rustfmt::skip] fn tex() -> Texture> { Texture::from(Buf2::new_from( - (2, 2), vec![ + Dims(2, 2), vec![ rgb(0xFF, 0, 0), rgb(0, 0xFF, 0), rgb(0, 0, 0xFF), diff --git a/core/src/render/text.rs b/core/src/render/text.rs index 412d93d3..6ff5f88a 100644 --- a/core/src/render/text.rs +++ b/core/src/render/text.rs @@ -7,7 +7,7 @@ use crate::math::{ Color3, Color4, Point2, ProjMat3, Vec2, color::gray, orthographic, pt2, pt3, vec2, vec3, viewport, }; -use crate::util::buf::Buf2; +use crate::util::{Dims, buf::Buf2}; use super::tex::*; use super::{BBox, Context, Frag, Model, Shader, Target, shader}; @@ -138,8 +138,8 @@ impl Text { fn write_char(&mut self, idx: u32) { let Self { font, geom, cursor, .. } = self; - let Layout::Grid { sub_dims: (gw, gh) } = font.layout; - let (glyph_w, glyph_h) = (gw as f32, gh as f32); + let Layout::Grid { sub_dims } = font.layout; + let (glyph_w, glyph_h) = (sub_dims.0 as f32, sub_dims.1 as f32); let [tl, tr, bl, br] = font.coords(idx); // TODO doesn't work when the text is written in several pieces, @@ -248,11 +248,11 @@ where num_cols = num_cols.max(row.len() as u32); } if num_rows == 0 || num_cols == 0 { - return Buf2::new((0, 0)); + return Buf2::new(Dims(0, 0)); } - let Layout::Grid { sub_dims: (gw, gh) } = font.layout; - let mut buf = Buf2::new((num_cols * gw, num_rows * gh)); + let Layout::Grid { sub_dims: Dims(gw, gh) } = font.layout; + let mut buf = Buf2::new(Dims(num_cols * gw, num_rows * gh)); let (mut x, mut y) = (0, 0); for row in rows { diff --git a/core/src/util.rs b/core/src/util.rs index db561ea9..5ba62e94 100644 --- a/core/src/util.rs +++ b/core/src/util.rs @@ -5,4 +5,13 @@ pub mod pixfmt; pub mod pnm; pub mod rect; -pub type Dims = (u32, u32); +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct Dims(pub u32, pub u32); + +impl Dims { + pub fn count(&self) -> usize { + (self.0 as u64 * self.1 as u64) + .try_into() + .expect("count should fit in usize") + } +} diff --git a/core/src/util/buf.rs b/core/src/util/buf.rs index 37234724..54821b2f 100644 --- a/core/src/util/buf.rs +++ b/core/src/util/buf.rs @@ -47,14 +47,18 @@ pub trait AsMutSlice2 { /// /// # Examples /// ``` -/// # use retrofire_core::util::buf::Buf2; +/// # use retrofire_core::util::{Dims,buf::Buf2}; /// # use retrofire_core::math::point::pt2; +/// /// // Elements initialized with `Default::default()` -/// let mut buf = Buf2::new((4, 4)); +/// let mut buf = Buf2::new(Dims(4, 4)); +/// /// // Indexing with a 2D point (x, y) yields element at row y, column x: /// buf[pt2(2, 1)] = 123; -/// // Indexing with an usize i yields row with index i as a slice: +/// +/// // Indexing with a usize i yields row with index i as a slice: /// assert_eq!(buf[1], [0, 0, 123, 0]); +/// /// // Thus you can also do this, row first, column second: /// assert_eq!(buf[1][2], 123) /// ``` @@ -92,28 +96,29 @@ pub struct MutSlice2<'a, T>(Inner); // impl Buf2 { - /// Returns a buffer of size `w` × `h`, with elements initialized + /// Returns a buffer of the given dimensions, with elements initialized /// with values yielded by `init`. /// /// The elements are initialized in row-major order. Does not allocate - /// or consume items from `init` if `w` = 0 or `h` = 0. + /// or consume items from `init` if either the width or the height is zero. /// /// # Examples /// ``` - /// use retrofire_core::util::buf::Buf2; + /// use retrofire_core::util::{Dims, buf::Buf2}; /// - /// let buf = Buf2::new_from((3, 3), 1..); + /// let buf = Buf2::new_from(Dims(4, 3), 1..); /// - /// assert_eq!(buf.dims(), (3, 3)); - /// assert_eq!(buf.data(), [1, 2, 3, - /// 4, 5, 6, - /// 7, 8, 9]); + /// assert_eq!(buf.width(), 4); + /// assert_eq!(buf.height(), 3); + /// assert_eq!(buf.data(), [1, 2, 3, 4, + /// 5, 6, 7, 8, + /// 9, 10, 11, 12]); /// ``` /// /// # Panics - /// * If `w` × `h` > `isize::MAX`., or - /// * if `init` has fewer than `w` × `h` elements. - pub fn new_from((w, h): Dims, init: I) -> Self + /// * If width × height > `isize::MAX`., or + /// * if `init` has fewer than width × height elements. + pub fn new_from(Dims(w, h): Dims, init: I) -> Self where I: IntoIterator, { @@ -133,69 +138,71 @@ impl Buf2 { "insufficient items in iterator ({} < {len}", data.len() ); - Self(Inner::new((w, h), w, data)) + Self(Inner::new(Dims(w, h), w, data)) } - /// Returns a buffer of size `w` × `h`, with every element initialized to - /// `T::default()`. + /// Returns a buffer of the given dimensions, with every element initialized + /// to `T::default()`. /// - /// Does not allocate if `w` = 0 or `h` = 0. + /// Does not allocate if either the width or the height is zero. /// /// # Examples /// ``` - /// use retrofire_core::util::buf::Buf2; + /// use retrofire_core::util::{Dims, buf::Buf2}; /// - /// let buf: Buf2 = Buf2::new((3, 3)); + /// let buf: Buf2 = Buf2::new(Dims(4, 3)); /// - /// assert_eq!(buf.dims(), (3, 3)); - /// assert_eq!(buf.data(), [0, 0, 0, - /// 0, 0, 0, - /// 0, 0, 0]); + /// assert_eq!(buf.width(), 4); + /// assert_eq!(buf.height(), 3); + /// assert_eq!(buf.data(), [0, 0, 0, 0, + /// 0, 0, 0, 0, + /// 0, 0, 0, 0]); /// ``` /// /// # Panics - /// If `w` × `h` > `isize::MAX`. + /// If width × height > `isize::MAX`. #[inline] - pub fn new((w, h): Dims) -> Self + pub fn new(dims: Dims) -> Self where T: Default + Clone, { - let data = vec![T::default(); (w * h) as usize]; - Self(Inner::new((w, h), w, data)) + let data = vec![T::default(); dims.count()]; + Self(Inner::new(dims, dims.0, data)) } - /// Returns a buffer of size `w` × `h`, initialized by repeatedly calling - /// the given function. + /// Returns a buffer of the given dimensions, initialized by repeatedly + /// calling the given function. /// /// For each element, `init_fn(x, y)` is invoked, where `x` is the column /// index and `y` the row index of the element being initialized. The /// elements are initialized in row-major order. /// - /// Does not allocate or call `init_fn` if `w` = 0 or `h` = 0. + /// Does not allocate or call `init_fn` if either the width or the height is zero. /// /// # Panics - /// If `w` × `h` > `isize::MAX`. + /// If width × height > `isize::MAX`. /// /// # Examples /// ``` - /// use retrofire_core::util::buf::Buf2; + /// use retrofire_core::util::{Dims, buf::Buf2}; + /// + /// let buf = Buf2::new_with(Dims(4, 3), |x, y| 10 * y + x); /// - /// let buf = Buf2::new_with((3, 3), |x, y| 10 * y + x); - /// assert_eq!(buf.data(), [ 0, 1, 2, - /// 10, 11, 12, - /// 20, 21, 22]); + /// assert_eq!(buf.data(), [ 0, 1, 2, 3, + /// 10, 11, 12, 13, + /// 20, 21, 22, 23]); /// ``` - pub fn new_with((w, h): Dims, mut init_fn: F) -> Self + pub fn new_with(dims: Dims, mut init_fn: F) -> Self where F: FnMut(u32, u32) -> T, { let (mut x, mut y) = (0, 0); Self::new_from( - (w, h), + dims, iter::from_fn(|| { let res = init_fn(x, y); x += 1; - if x == w { + if x == dims.0 { (x, y) = (0, y + 1); } Some(res) @@ -225,11 +232,11 @@ impl Buf2 { /// /// # Examples /// ``` - /// use retrofire_core::util::buf::Buf2; + /// use retrofire_core::util::{Dims, buf::Buf2}; /// - /// let mut buf = Buf2::new_from((3, 2), [1, 2, 3, 4, 5, 6]); + /// let mut buf = Buf2::new_from(Dims(3, 2), [1, 2, 3, 4, 5, 6]); /// - /// buf.reshape((2, 3)); + /// buf.reshape(Dims(2, 3)); /// /// assert_eq!(buf.stride(), 2); /// assert_eq!(buf.width(), 2); @@ -250,9 +257,11 @@ impl<'a, T> Slice2<'a, T> { /// /// # Examples /// ``` - /// # use retrofire_core::util::buf::Slice2; + /// use retrofire_core::util::{Dims, buf::Slice2}; + /// /// let data = &[0, 1, 2, 3, 4, 5, 6]; - /// let slice = Slice2::new((2, 2), 3, data); + /// let slice = Slice2::new(Dims(2, 2), 3, data); + /// /// assert_eq!(&slice[0], &[0, 1]); /// assert_eq!(&slice[1], &[3, 4]); /// ``` @@ -414,7 +423,7 @@ impl From<&[[T; N]]> for Buf2 { /// ``` fn from(slice: &[[T; N]]) -> Self { Self::new_from( - (N as u32, slice.len() as u32), + Dims(N as u32, slice.len() as u32), slice.as_flattened().iter().cloned(), ) } @@ -475,7 +484,7 @@ pub mod inner { /// height is 1, or if it is empty. #[inline] pub fn is_contiguous(&self) -> bool { - let (w, h) = self.dims; + let Dims(w, h) = self.dims; self.stride == w || h <= 1 || w == 0 } /// Returns whether `self` contains no elements. @@ -505,14 +514,13 @@ pub mod inner { /// or `None` if x or y is out of bounds. #[inline] fn to_index_checked(&self, x: u32, y: u32) -> Option { - let (w, h) = self.dims; - (x < w && y < h).then(|| self.to_index(x, y)) + (x < self.dims.0 && y < self.dims.1).then(|| self.to_index(x, y)) } /// Returns the dimensions and linear range corresponding to the rect. #[inline(never)] fn resolve_bounds(&self, rect: &Rect) -> (Dims, Range) { - let (w, h) = self.dims; + let Dims(w, h) = self.dims; let l = rect.left.unwrap_or(0); let t = rect.top.unwrap_or(0); @@ -537,7 +545,7 @@ pub mod inner { // b != 0 because b >= t && b != t self.to_index(r, b - 1) }; - ((r - l, b - t), start..end) + (Dims(r - l, b - t), start..end) } /// A helper for implementing `Debug`. @@ -567,7 +575,7 @@ pub mod inner { #[cold] #[track_caller] #[inline(never)] - fn out_of_bounds((w, h): Dims, x: u32, y: u32) -> ! { + fn out_of_bounds(Dims(w, h): Dims, x: u32, y: u32) -> ! { panic!("position (x={x}, y={y}) out of bounds (0..{w}, 0..{h})",) } @@ -624,7 +632,7 @@ pub mod inner { } } - fn check_preconditions((w, h): Dims, stride: u32, len: usize) { + fn check_preconditions(Dims(w, h): Dims, stride: u32, len: usize) { assert!(w <= stride, "width ({w}) > stride ({stride})"); assert!( h <= 1 || stride as usize <= len, @@ -814,25 +822,25 @@ mod tests { #[test] fn buf_new_from() { - let buf = Buf2::new_from((3, 2), 1..); + let buf = Buf2::new_from(Dims(3, 2), 1..); assert_eq!(buf.data(), &[1, 2, 3, 4, 5, 6]); } #[test] fn buf_new() { - let buf: Buf2 = Buf2::new((3, 2)); + let buf: Buf2 = Buf2::new(Dims(3, 2)); assert_eq!(buf.data(), &[0, 0, 0, 0, 0, 0]); } #[test] fn buf_new_with() { - let buf = Buf2::new_with((3, 2), |x, y| x + y); + let buf = Buf2::new_with(Dims(3, 2), |x, y| x + y); assert_eq!(buf.data(), &[0, 1, 2, 1, 2, 3]); } #[test] fn buf_extents() { - let buf: Buf2<()> = Buf2::new((4, 5)); + let buf: Buf2<()> = Buf2::new(Dims(4, 5)); assert_eq!(buf.width(), 4); assert_eq!(buf.height(), 5); assert_eq!(buf.stride(), 4); @@ -840,7 +848,7 @@ mod tests { #[test] fn buf_index_and_get() { - let buf = Buf2::new_with((4, 5), |x, y| x * 10 + y); + let buf = Buf2::new_with(Dims(4, 5), |x, y| x * 10 + y); assert_eq!(buf[2usize], [2, 12, 22, 32]); @@ -855,7 +863,7 @@ mod tests { #[test] fn buf_index_mut_and_get_mut() { - let mut buf = Buf2::new_with((4, 5), |x, y| x * 10 + y); + let mut buf = Buf2::new_with(Dims(4, 5), |x, y| x * 10 + y); buf[2usize][1] = 123; assert_eq!(buf[2usize], [2, 123, 22, 32]); @@ -872,27 +880,27 @@ mod tests { #[test] #[should_panic = "position (x=4, y=0) out of bounds (0..4, 0..5)"] fn buf_index_x_out_of_bounds_should_panic() { - let buf = Buf2::new((4, 5)); + let buf = Buf2::new(Dims(4, 5)); let _: i32 = buf[[4, 0]]; } #[test] #[should_panic = "position (x=0, y=4) out of bounds (0..5, 0..4)"] fn buf_index_y_out_of_bounds_should_panic() { - let buf = Buf2::new((5, 4)); + let buf = Buf2::new(Dims(5, 4)); let _: i32 = buf[[0, 4]]; } #[test] #[should_panic = "position (x=0, y=5) out of bounds (0..4, 0..5)"] fn buf_index_row_out_of_bounds_should_panic() { - let buf = Buf2::new((4, 5)); + let buf = Buf2::new(Dims(4, 5)); let _: &[i32] = &buf[5usize]; } #[test] fn buf_slice_range_full() { - let buf: Buf2<()> = Buf2::new((4, 5)); + let buf: Buf2<()> = Buf2::new(Dims(4, 5)); let slice = buf.slice(..); assert_eq!(slice.width(), 4); @@ -907,7 +915,7 @@ mod tests { #[test] fn buf_slice_range_inclusive() { - let buf: Buf2<()> = Buf2::new((4, 5)); + let buf: Buf2<()> = Buf2::new(Dims(4, 5)); let slice = buf.slice((1..=3, 0..=3)); assert_eq!(slice.width(), 3); assert_eq!(slice.height(), 4); @@ -916,7 +924,7 @@ mod tests { #[test] fn buf_slice_range_to() { - let buf: Buf2<()> = Buf2::new((4, 5)); + let buf: Buf2<()> = Buf2::new(Dims(4, 5)); let slice = buf.slice((..2, ..4)); assert_eq!(slice.width(), 2); @@ -926,7 +934,7 @@ mod tests { #[test] fn buf_slice_range_from() { - let buf: Buf2<()> = Buf2::new((4, 5)); + let buf: Buf2<()> = Buf2::new(Dims(4, 5)); let slice = buf.slice((3.., 2..)); assert_eq!(slice.width(), 1); @@ -936,7 +944,7 @@ mod tests { #[test] fn buf_slice_empty_range() { - let buf: Buf2<()> = Buf2::new((4, 5)); + let buf: Buf2<()> = Buf2::new(Dims(4, 5)); let empty = buf.slice(pt2(1, 1)..pt2(1, 3)); assert_eq!(empty.width(), 0); @@ -952,32 +960,32 @@ mod tests { #[test] #[should_panic = "range right (5) > width (4)"] fn buf_slice_x_out_of_bounds_should_panic() { - let buf: Buf2<()> = Buf2::new((4, 5)); + let buf: Buf2<()> = Buf2::new(Dims(4, 5)); buf.slice((0..5, 1..3)); } #[test] #[should_panic = "range bottom (6) > height (5)"] fn buf_slice_y_out_of_bounds_should_panic() { - let buf: Buf2<()> = Buf2::new((4, 5)); + let buf: Buf2<()> = Buf2::new(Dims(4, 5)); buf.slice((1..3, 0..6)); } #[test] #[should_panic = "width (4) > stride (3)"] fn slice_stride_less_than_width_should_panic() { - let _ = Slice2::new((4, 4), 3, &[0; 16]); + let _ = Slice2::new(Dims(4, 4), 3, &[0; 16]); } #[test] #[should_panic = "required size (19) > data length (16)"] fn slice_larger_than_data_should_panic() { - let _ = Slice2::new((4, 4), 5, &[0; 16]); + let _ = Slice2::new(Dims(4, 4), 5, &[0; 16]); } #[test] fn slice_extents() { - let buf: Buf2<()> = Buf2::new((10, 10)); + let buf: Buf2<()> = Buf2::new(Dims(10, 10)); let slice = buf.slice((1..4, 2..8)); assert_eq!(slice.width(), 3); @@ -988,7 +996,7 @@ mod tests { #[test] fn slice_contiguity() { - let buf: Buf2<()> = Buf2::new((10, 10)); + let buf: Buf2<()> = Buf2::new(Dims(10, 10)); // Buf2 is always contiguous assert!(buf.is_contiguous()); @@ -1010,7 +1018,7 @@ mod tests { #[test] #[rustfmt::skip] fn slice_fill() { - let mut buf = Buf2::new((5, 4)); + let mut buf = Buf2::new(Dims(5, 4)); let mut slice = buf.slice_mut((2.., 1..3)); slice.fill(1); @@ -1027,7 +1035,7 @@ mod tests { #[test] #[rustfmt::skip] fn slice_fill_with() { - let mut buf = Buf2::new((5, 4)); + let mut buf = Buf2::new(Dims(5, 4)); let mut slice = buf.slice_mut((2.., 1..3)); slice.fill_with(|x, y| x + y); @@ -1044,8 +1052,8 @@ mod tests { #[test] #[rustfmt::skip] fn slice_copy_from() { - let mut dest = Buf2::new((5, 4)); - let src = Buf2::new_with((3, 3), |x, y| x + y); + let mut dest = Buf2::new(Dims(5, 4)); + let src = Buf2::new_with(Dims(3, 3), |x, y| x + y); dest.slice_mut((1..4, 1..)).copy_from(src); @@ -1060,7 +1068,7 @@ mod tests { #[test] fn slice_index() { - let buf = Buf2::new_with((5, 4), |x, y| x * 10 + y); + let buf = Buf2::new_with(Dims(5, 4), |x, y| x * 10 + y); let slice = buf.slice((2.., 1..3)); assert_eq!(slice[[0, 0]], 21); @@ -1073,7 +1081,7 @@ mod tests { #[test] fn slice_index_mut() { - let mut buf = Buf2::new_with((5, 5), |x, y| x * 10 + y); + let mut buf = Buf2::new_with(Dims(5, 5), |x, y| x * 10 + y); let mut slice = buf.slice_mut((2.., 1..3)); slice[[2, 1]] = 123; @@ -1089,7 +1097,7 @@ mod tests { #[test] fn slice_rows() { - let buf = Buf2::new_with((5, 4), |x, y| x * 10 + y); + let buf = Buf2::new_with(Dims(5, 4), |x, y| x * 10 + y); let slice = buf.slice((2..4, 1..)); let mut rows = slice.rows(); @@ -1101,7 +1109,7 @@ mod tests { #[test] fn slice_rows_mut() { - let mut buf = Buf2::new_with((5, 4), |x, y| x * 10 + y); + let mut buf = Buf2::new_with(Dims(5, 4), |x, y| x * 10 + y); let mut slice = buf.slice_mut((2..4, 1..)); let mut rows = slice.rows_mut(); @@ -1116,7 +1124,7 @@ mod tests { fn foo>(buf: T) -> u32 { buf.as_slice2().width() } - let buf = Buf2::new((2, 2)); + let buf = Buf2::new(Dims(2, 2)); let w = foo(&buf); assert_eq!(w, buf.width()); } @@ -1126,7 +1134,7 @@ mod tests { fn foo>(mut buf: T) { buf.as_mut_slice2()[[1, 1]] = 42; } - let mut buf = Buf2::new((2, 2)); + let mut buf = Buf2::new(Dims(2, 2)); foo(&mut buf); assert_eq!(buf[[1, 1]], 42); } diff --git a/core/src/util/pnm.rs b/core/src/util/pnm.rs index 064ec6c8..7f1fe9a6 100644 --- a/core/src/util/pnm.rs +++ b/core/src/util/pnm.rs @@ -264,18 +264,19 @@ impl Header { it.next().ok_or(UnexpectedEnd)?, ]; let format = magic.try_into()?; - let dims = (parse_u16(&mut it)?.into(), parse_u16(&mut it)?.into()); + let w = parse_u16(&mut it)?.into(); + let h = parse_u16(&mut it)?.into(); let max: u16 = match &format { TextBitmap | BinaryBitmap => 1, _ => parse_u16(&mut it)?, }; - Ok(Self { format, dims, max }) + Ok(Self { format, dims: Dims(w, h), max }) } /// Writes `self` to `dest` as a valid PNM header, /// including a trailing newline. #[cfg(feature = "std")] fn write(&self, mut dest: impl io::Write) -> io::Result<()> { - let Self { format, dims: (w, h), max } = *self; + let Self { format, dims: Dims(w, h), max } = *self; let max: &dyn Display = match format { TextBitmap | BinaryBitmap => &"", _ => &max, @@ -399,7 +400,7 @@ mod tests { Header::parse(*b"P6 123\t \n\r321 255 "), Ok(Header { format: BinaryPixmap, - dims: (123, 321), + dims: Dims(123, 321), max: 255, }) ); @@ -411,7 +412,7 @@ mod tests { Header::parse(*b"P6 # foo 42\n 123\n#bar\n#baz\n321 255 "), Ok(Header { format: BinaryPixmap, - dims: (123, 321), + dims: Dims(123, 321), max: 255, }) ); @@ -423,7 +424,7 @@ mod tests { Header::parse(*b"P2 123 456 789"), Ok(Header { format: TextGraymap, - dims: (123, 456), + dims: Dims(123, 456), max: 789, }) ); @@ -435,7 +436,7 @@ mod tests { Header::parse(*b"P3 123 456 789"), Ok(Header { format: TextPixmap, - dims: (123, 456), + dims: Dims(123, 456), max: 789, }) ); @@ -447,7 +448,7 @@ mod tests { Header::parse(*b"P4 123 456 "), Ok(Header { format: BinaryBitmap, - dims: (123, 456), + dims: Dims(123, 456), max: 1, }) ); @@ -459,7 +460,7 @@ mod tests { Header::parse(*b"P5 123 456 789 "), Ok(Header { format: BinaryGraymap, - dims: (123, 456), + dims: Dims(123, 456), max: 789, }) ); @@ -471,7 +472,7 @@ mod tests { Header::parse(*b"P6 123 456 789 "), Ok(Header { format: BinaryPixmap, - dims: (123, 456), + dims: Dims(123, 456), max: 789, }) ); @@ -508,7 +509,7 @@ mod tests { let mut out = Vec::new(); let hdr = Header { format: TextBitmap, - dims: (123, 456), + dims: Dims(123, 456), max: 1, }; hdr.write(&mut out).unwrap(); @@ -521,7 +522,7 @@ mod tests { let mut out = Vec::new(); let hdr = Header { format: BinaryPixmap, - dims: (123, 456), + dims: Dims(123, 456), max: 789, }; hdr.write(&mut out).unwrap(); @@ -549,7 +550,7 @@ mod tests { let buf = parse_pnm(data).unwrap(); - assert_eq!(buf.dims(), (2, 2)); + assert_eq!(buf.dims(), Dims(2, 2)); assert_eq!(buf[[0, 0]], rgb(0, 0, 0)); assert_eq!(buf[[1, 0]], rgb(123, 0, 42)); @@ -562,7 +563,7 @@ mod tests { // 0x69 == 0b0110_1001 let buf = parse_pnm(*b"P4 4 2\n\x69").unwrap(); - assert_eq!(buf.dims(), (4, 2)); + assert_eq!(buf.dims(), Dims(4, 2)); let b = rgb(0u8, 0, 0); let w = rgb(0xFFu8, 0xFF, 0xFF); @@ -575,7 +576,7 @@ mod tests { fn read_pnm_p5() { let buf = parse_pnm(*b"P5 2 2 255\n\x01\x23\x45\x67").unwrap(); - assert_eq!(buf.dims(), (2, 2)); + assert_eq!(buf.dims(), Dims(2, 2)); assert_eq!(buf[0usize], [rgb(0x01, 0x01, 0x01), rgb(0x23, 0x23, 0x23)]); assert_eq!(buf[1usize], [rgb(0x45, 0x45, 0x45), rgb(0x67, 0x67, 0x67)]); @@ -592,7 +593,7 @@ mod tests { ) .unwrap(); - assert_eq!(buf.dims(), (2, 2)); + assert_eq!(buf.dims(), Dims(2, 2)); assert_eq!(buf[0usize], [rgb(0x01, 0x12, 0x23), rgb(0x34, 0x45, 0x56)]); assert_eq!(buf[1usize], [rgb(0x67, 0x78, 0x89), rgb(0x9A, 0xAB, 0xBC)]); @@ -610,7 +611,7 @@ mod tests { ]; let mut out = vec![]; - super::write_ppm(&mut out, Buf2::new_from((2, 2), buf)).unwrap(); + super::write_ppm(&mut out, Buf2::new_from(Dims(2, 2), buf)).unwrap(); assert_eq!( &out, diff --git a/core/tests/rendering.rs b/core/tests/rendering.rs index bbdc5205..7ee10247 100644 --- a/core/tests/rendering.rs +++ b/core/tests/rendering.rs @@ -3,7 +3,7 @@ use retrofire_core::{ prelude::*, render::{Model, render, shader, tex::SamplerClamp}, - util::{self, pixfmt::Xrgb8888, pnm::parse_pnm}, + util::{self, Dims, pixfmt::Xrgb8888, pnm::parse_pnm}, }; const VERTS: [Vertex3; 4] = [ @@ -16,7 +16,7 @@ const FACES: [Tri; 2] = [tri(0, 1, 2), tri(3, 2, 1)]; #[test] fn textured_quad() { - let checker = Texture::from(Buf2::new_with((8, 8), |x, y| { + let checker = Texture::from(Buf2::new_with(Dims(8, 8), |x, y| { let xor = (x ^ y) & 1; // Blue if x == y, dark red otherwise. rgba(0x7F * xor as u8, 0, 0xFF * (1 - xor) as u8, 0) @@ -34,7 +34,7 @@ fn textured_quad() { let viewport = viewport(pt2(0, 0)..pt2(w, h)); let mvp = translate((0.0, 0.0, 1.0)).to().then(&project); - let mut framebuf = Buf2::::new((w, h)); + let mut framebuf = Buf2::::new(Dims(w, h)); let mut ctx = Context::default(); render(FACES, VERTS, &shader, &mvp, viewport, &mut framebuf, &ctx); diff --git a/demos/src/bin/bezier.rs b/demos/src/bin/bezier.rs index 80c1d9d5..e72dd20a 100644 --- a/demos/src/bin/bezier.rs +++ b/demos/src/bin/bezier.rs @@ -11,7 +11,7 @@ use re::core::{ use re::front::{Frame, dims, minifb::Window}; fn main() { - let dims @ (w, h) = dims::SVGA_800_600; + let dims @ Dims(w, h) = dims::SVGA_800_600; let mut win = Window::builder() .title("retrofire//bezier") diff --git a/demos/src/bin/crates.rs b/demos/src/bin/crates.rs index cdd9f569..9005ee60 100644 --- a/demos/src/bin/crates.rs +++ b/demos/src/bin/crates.rs @@ -48,7 +48,7 @@ fn main() { }, ); - let (w, h) = win.dims; + let Dims(w, h) = win.dims; let mut cam = Camera::new(win.dims) .transform(FirstPerson::default()) .viewport((10..w - 10, h - 10..10)) diff --git a/demos/src/bin/hello.rs b/demos/src/bin/hello.rs index f582d220..64142eff 100644 --- a/demos/src/bin/hello.rs +++ b/demos/src/bin/hello.rs @@ -2,9 +2,9 @@ use std::{env, fmt::Write, ops::ControlFlow::Continue}; use re::prelude::*; -use re::core::math::color::hsl; use re::core::{ - render::{Text, World, tex::Atlas, tex::Layout}, + math::color::hsl, + render::{Text, World, tex::Atlas}, util::pnm::read_pnm, }; use re_front::{Frame, dims::SVGA_800_600, minifb::Window}; @@ -13,7 +13,7 @@ const FONT: &[u8] = include_bytes!("../../assets/font_16x24.pbm"); fn main() { let font = read_pnm(FONT).expect("valid image"); - let font = Atlas::grid((16, 24), font.into()); + let font = Atlas::grid(Dims(16, 24), font.into()); let arg = env::args().nth(1); // Borrow checker... let msg = arg diff --git a/demos/src/bin/solids.rs b/demos/src/bin/solids.rs index 8ab3e5a3..de9ed43d 100644 --- a/demos/src/bin/solids.rs +++ b/demos/src/bin/solids.rs @@ -56,7 +56,7 @@ fn main() { win.ctx.color_clear = Some(gray(0x33).to_rgba()); - let (w, h) = win.dims; + let Dims(w, h) = win.dims; let cam = Camera::new(win.dims) .transform(scale((1.0, -1.0, -1.0)).to()) .perspective(Fov::Equiv35mm(28.0), 0.1..1000.0) diff --git a/demos/src/bin/sprites.rs b/demos/src/bin/sprites.rs index 1b3836e4..dc5ce333 100644 --- a/demos/src/bin/sprites.rs +++ b/demos/src/bin/sprites.rs @@ -43,7 +43,7 @@ fn main() { let view_pos = mv.apply(&v.pos) + vertex_pos; vertex(proj.apply(&view_pos), v.attrib) }, - |frag: Frag>, _: & _| { + |frag: Frag>, _: &_| { let d2 = frag.var.len_sqr(); (d2 < 1.0).then(|| { let col = gray(1.0) - d2 * rgb(0.25, 0.5, 1.0); @@ -52,7 +52,7 @@ fn main() { }, ); - let (w, h) = win.dims; + let Dims(w, h) = win.dims; let cam = Camera::new(win.dims) .transform(translate(0.5 * Vec3::Z).to()) .perspective(Fov::FocalRatio(1.0), 1e-2..1e3) diff --git a/demos/src/bin/square.rs b/demos/src/bin/square.rs index e0529494..32b23eae 100644 --- a/demos/src/bin/square.rs +++ b/demos/src/bin/square.rs @@ -29,7 +29,7 @@ fn main() { }; // Texture with a check pattern - let checker = Texture::from(Buf2::new_with((8, 8), |x, y| { + let checker = Texture::from(Buf2::new_with(Dims(8, 8), |x, y| { let xor = (x ^ y) & 1; rgba(xor as u8 * 255, 128, 255 - xor as u8 * 128, 0) })); @@ -39,7 +39,7 @@ fn main() { |frag: Frag<_>, _: &_| SamplerClamp.sample(&checker, frag.var), ); - let (w, h) = win.dims; + let Dims(w, h) = win.dims; let projection = perspective(1.0, w as f32 / h as f32, 0.1..1000.0); let viewport = viewport(pt2(10, 10)..pt2(w - 10, h - 10)); diff --git a/front/src/lib.rs b/front/src/lib.rs index d024ae7e..a101a7a9 100644 --- a/front/src/lib.rs +++ b/front/src/lib.rs @@ -8,7 +8,7 @@ use core::{cell::RefCell, time::Duration}; use retrofire_core::{ math::{Color3, Color4}, render::{Colorbuf, Context, Framebuf, tex::Atlas}, - util::{buf::AsMutSlice2, pixfmt::IntoPixel, pnm::read_pnm}, + util::{Dims, buf::AsMutSlice2, pixfmt::IntoPixel, pnm::read_pnm}, }; #[cfg(feature = "minifb")] @@ -26,7 +26,7 @@ pub static FONT_6X10: &[u8] = include_bytes!("../assets/font_6x10.pbm"); /// Returns a 6x10 bitmap font, e.g. for rendering debug messages. pub fn font_6x10() -> Atlas { let font = read_pnm(FONT_6X10).expect("font statically included"); - Atlas::grid((6, 10), font.into()) + Atlas::grid(Dims(6, 10), font.into()) } /// Per-frame state. The window run method passes an instance of `Frame` @@ -52,49 +52,49 @@ pub mod dims { use retrofire_core::util::Dims; // 5:4 - pub const qSXGA_640_512: Dims = (640, 512); - pub const SXGA_1280_1024: Dims = (1280, 1024); + pub const qSXGA_640_512: Dims = Dims(640, 512); + pub const SXGA_1280_1024: Dims = Dims(1280, 1024); // 4:3 - pub const qVGA_320_240: Dims = (320, 240); - pub const qSVGA_400_300: Dims = (400, 300); - pub const qXGA_512_384: Dims = (512, 384); - pub const VGA_640_480: Dims = (640, 480); - pub const SVGA_800_600: Dims = (800, 600); - pub const XGA_1024_768: Dims = (1024, 768); - pub const QVGA_1280_960: Dims = (1280, 960); - pub const UXGA_1600_1200: Dims = (1600, 1200); - pub const QXGA_2048_1536: Dims = (2048, 1536); + pub const qVGA_320_240: Dims = Dims(320, 240); + pub const qSVGA_400_300: Dims = Dims(400, 300); + pub const qXGA_512_384: Dims = Dims(512, 384); + pub const VGA_640_480: Dims = Dims(640, 480); + pub const SVGA_800_600: Dims = Dims(800, 600); + pub const XGA_1024_768: Dims = Dims(1024, 768); + pub const QVGA_1280_960: Dims = Dims(1280, 960); + pub const UXGA_1600_1200: Dims = Dims(1600, 1200); + pub const QXGA_2048_1536: Dims = Dims(2048, 1536); // 16:10 - pub const CGA_320_200: Dims = (320, 200); + pub const CGA_320_200: Dims = Dims(320, 200); pub const MODE_13H: Dims = CGA_320_200; - pub const QCGA_640_400: Dims = (640, 400); - pub const qWXGA_640_400: Dims = (640, 640); - pub const WXGA_1280_800: Dims = (1280, 800); - pub const WXGAP_1440_900: Dims = (1440, 900); - pub const WSXGAP_1680_1050: Dims = (1680, 1050); - pub const WUXGA_1920_1200: Dims = (1920, 1200); - pub const WQXGA_2560_1600: Dims = (2560, 1600); + pub const QCGA_640_400: Dims = Dims(640, 400); + pub const qWXGA_640_400: Dims = Dims(640, 640); + pub const WXGA_1280_800: Dims = Dims(1280, 800); + pub const WXGAP_1440_900: Dims = Dims(1440, 900); + pub const WSXGAP_1680_1050: Dims = Dims(1680, 1050); + pub const WUXGA_1920_1200: Dims = Dims(1920, 1200); + pub const WQXGA_2560_1600: Dims = Dims(2560, 1600); // 16:9 // 640x360 = "qHD"? // 800x450 = qWSXGA? // 960x540 = qFHD - pub const HD_1280_720: Dims = (1280, 720); - pub const WSXGA_1600_900: Dims = (1600, 900); - pub const FHD_1920_1080: Dims = (1920, 1080); - pub const QHD_2560_1440: Dims = (2560, 1440); - pub const UHD_4K_3840_2160: Dims = (3840, 2160); + pub const HD_1280_720: Dims = Dims(1280, 720); + pub const WSXGA_1600_900: Dims = Dims(1600, 900); + pub const FHD_1920_1080: Dims = Dims(1920, 1080); + pub const QHD_2560_1440: Dims = Dims(2560, 1440); + pub const UHD_4K_3840_2160: Dims = Dims(3840, 2160); // DCI ~17:9 - pub const DCI_2K_2048_1080: Dims = (2048, 1080); - pub const DCI_4K_4096_2160: Dims = (4096, 2160); + pub const DCI_2K_2048_1080: Dims = Dims(2048, 1080); + pub const DCI_4K_4096_2160: Dims = Dims(4096, 2160); // ~21:9 - pub const qUWFHD_1280_540: Dims = (1280, 540); - pub const UWFHD_2560_1080: Dims = (2560, 1080); - pub const UWQHD_3440_1440: Dims = (3440, 1440); + pub const qUWFHD_1280_540: Dims = Dims(1280, 540); + pub const UWFHD_2560_1080: Dims = Dims(2560, 1080); + pub const UWQHD_3440_1440: Dims = Dims(3440, 1440); } impl diff --git a/front/src/minifb.rs b/front/src/minifb.rs index eee1e3d3..a2b8dfa4 100644 --- a/front/src/minifb.rs +++ b/front/src/minifb.rs @@ -103,7 +103,7 @@ impl Window { /// # Panics /// If `fb.len() < self.size.0 * self.size.1`. pub fn present(&mut self, fb: &[u32]) { - let (w, h) = self.dims; + let Dims(w, h) = self.dims; self.imp .update_with_buffer(fb, w as usize, h as usize) .unwrap(); @@ -120,9 +120,8 @@ impl Window { where F: FnMut(&mut Frame>) -> ControlFlow<()>, { - let (w, h) = self.dims; - let mut cbuf = Buf2::new((w, h)); - let mut zbuf = Buf2::new((w, h)); + let mut cbuf = Buf2::new(self.dims); + let mut zbuf = Buf2::new(self.dims); let mut ctx = self.ctx.clone(); let mut fps = Text::new(font_6x10()); diff --git a/front/src/sdl2.rs b/front/src/sdl2.rs index 72337c69..9a18e96b 100644 --- a/front/src/sdl2.rs +++ b/front/src/sdl2.rs @@ -134,10 +134,8 @@ impl<'t, PF: PixelFmt> Builder<'t, PF> { } fn create_window(&self, sdl: &Sdl) -> Result { - let Self { - dims: (w, h), title, fs, hidpi, .. - } = *self; - let mut win = sdl.video()?.window(title, w, h); + let Self { dims, title, fs, hidpi, .. } = *self; + let mut win = sdl.video()?.window(title, dims.0, dims.1); if hidpi { win.allow_highdpi(); } @@ -190,7 +188,8 @@ impl, const N: usize> Window { ) -> ControlFlow<()>, Color4: IntoPixel, { - let dims @ (w, h) = self.canvas.window().drawable_size(); + let (w, h) = self.canvas.window().drawable_size(); + let dims = Dims(w, h); let tc = self.canvas.texture_creator(); let mut tex = tc.create_texture_streaming(PF::SDL_FMT, w, h)?; diff --git a/front/src/wasm.rs b/front/src/wasm.rs index b9f6555f..186e5135 100644 --- a/front/src/wasm.rs +++ b/front/src/wasm.rs @@ -140,15 +140,15 @@ impl Window { web_sys::window()?.document() } - fn create_canvas((w, h): Dims) -> Option { + fn create_canvas(dims: Dims) -> Option { let cvs: HtmlCanvasElement = Self::document()? .create_element("canvas") .ok()? .dyn_into() .ok()?; - cvs.set_width(w); - cvs.set_height(h); + cvs.set_width(dims.0); + cvs.set_height(dims.1); Some(cvs) } From df548fdfdbcf7ea9f24ce72fb8635d88ee797fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 18 Jun 2026 18:12:28 +0300 Subject: [PATCH 26/76] Add re-exports to util --- benches/e2e.rs | 2 +- benches/fill.rs | 2 +- benches/isect.rs | 7 +++---- core/src/lib.rs | 3 +-- core/src/render/cam.rs | 2 +- core/src/render/raster.rs | 2 +- core/src/render/target.rs | 5 +---- core/src/render/tex.rs | 7 ++----- core/src/render/text.rs | 2 +- core/src/util.rs | 14 ++++++++++++-- core/src/util/buf.rs | 14 +++++++------- core/src/util/pnm.rs | 7 ++----- front/src/lib.rs | 2 +- front/src/minifb.rs | 2 +- front/src/sdl2.rs | 5 ++--- 15 files changed, 37 insertions(+), 39 deletions(-) diff --git a/benches/e2e.rs b/benches/e2e.rs index 5fbb8aef..cafc56da 100644 --- a/benches/e2e.rs +++ b/benches/e2e.rs @@ -9,7 +9,7 @@ use retrofire_core::{ translate, viewport, }, render::{Context, Frag, Model, debug::dir_to_rgb, render, shader}, - util::{Dims, buf::Buf2, pnm}, + util::{Buf2, Dims, pnm}, }; use retrofire_geom::solids::{Build, Sphere}; diff --git a/benches/fill.rs b/benches/fill.rs index cd645c3d..ce354ffc 100644 --- a/benches/fill.rs +++ b/benches/fill.rs @@ -10,7 +10,7 @@ use retrofire_core::{ render::{ Texture, raster::ScreenPt, raster::tri_fill, tex::SamplerRepeatPot, uv, }, - util::{Dims, buf::Buf2, pnm::save_ppm}, + util::{Buf2, Dims, pnm::save_ppm}, }; const SIZES: [f32; 5] = [4.0, 16.0, 64.0, 256.0, 1024.0]; diff --git a/benches/isect.rs b/benches/isect.rs index 028376af..9664ac87 100644 --- a/benches/isect.rs +++ b/benches/isect.rs @@ -4,13 +4,12 @@ use core::hint::black_box; use divan::Bencher; -use retrofire::core::{ +use retrofire_core::{ geom::{Plane3, Ray, Sphere}, - math::{Point3, degs, pt3, rand::*, spherical, vec3}, + math::{Point3, degs, pt3, rand::*, spherical, splat, vec3}, render::scene::BBox, }; -use retrofire::geom::Intersect; -use retrofire_core::math::splat; +use retrofire_geom::Intersect; #[divan::bench] fn ray_plane_hit(b: Bencher) { diff --git a/core/src/lib.rs b/core/src/lib.rs index 4eecf430..30483c07 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -67,7 +67,6 @@ pub mod prelude { }, math::re_exports::*, render::re_exports::*, - util::Dims, - util::buf::{AsMutSlice2, AsSlice2, Buf2, MutSlice2, Slice2}, + util::re_exports::*, }; } diff --git a/core/src/render/cam.rs b/core/src/render/cam.rs index 59edf86a..ebda9c82 100644 --- a/core/src/render/cam.rs +++ b/core/src/render/cam.rs @@ -11,7 +11,7 @@ use crate::math::{ Mat4, Point3, ProjMat3, SphericalVec, Vary, orthographic, perspective, pt2, translate, viewport, }; -use crate::util::{Dims, rect::Rect}; +use crate::util::{Dims, Rect}; use super::{Clip, Context, Ndc, Render, Screen, Shader, Target, View, World}; diff --git a/core/src/render/raster.rs b/core/src/render/raster.rs index 84f45160..d0eae461 100644 --- a/core/src/render/raster.rs +++ b/core/src/render/raster.rs @@ -315,7 +315,7 @@ mod tests { assert_approx_eq, geom::vertex, math::{point::pt3, vary::Vary, vary::ZDiv}, - util::{Dims, buf::Buf2}, + util::{Buf2, Dims}, }; use super::{Scanline, tri_fill}; diff --git a/core/src/render/target.rs b/core/src/render/target.rs index 1822346c..e36052e9 100644 --- a/core/src/render/target.rs +++ b/core/src/render/target.rs @@ -7,10 +7,7 @@ use core::cell::RefCell; use crate::math::{Color3, Color4, Vary}; -use crate::util::{ - buf::{AsMutSlice2, Buf2, MutSlice2}, - pixfmt::IntoPixel, -}; +use crate::util::{AsMutSlice2, Buf2, IntoPixel, MutSlice2}; use super::{Context, FragmentShader, raster::Scanline, stats::Throughput}; diff --git a/core/src/render/tex.rs b/core/src/render/tex.rs index a7078528..b77fa4c9 100644 --- a/core/src/render/tex.rs +++ b/core/src/render/tex.rs @@ -2,10 +2,7 @@ use crate::geom::Normal3; use crate::math::{Point2u, Vec2, Vec3, Vector, pt2, splat, vec2}; -use crate::util::{ - Dims, - buf::{AsSlice2, Buf2, Slice2}, -}; +use crate::util::{AsSlice2, Buf2, Dims, Slice2}; /// Basis of the texture space. #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] @@ -369,7 +366,7 @@ mod tests { use alloc::vec; use crate::math::{Color3, Linear, rgb}; - use crate::util::buf::Buf2; + use crate::util::Buf2; use super::*; diff --git a/core/src/render/text.rs b/core/src/render/text.rs index 6ff5f88a..f451cf74 100644 --- a/core/src/render/text.rs +++ b/core/src/render/text.rs @@ -7,7 +7,7 @@ use crate::math::{ Color3, Color4, Point2, ProjMat3, Vec2, color::gray, orthographic, pt2, pt3, vec2, vec3, viewport, }; -use crate::util::{Dims, buf::Buf2}; +use crate::util::{Buf2, Dims}; use super::tex::*; use super::{BBox, Context, Frag, Model, Shader, Target, shader}; diff --git a/core/src/util.rs b/core/src/util.rs index 5ba62e94..0025c700 100644 --- a/core/src/util.rs +++ b/core/src/util.rs @@ -1,9 +1,19 @@ //! Various utility types and functions. -pub mod buf; +mod buf; pub mod pixfmt; pub mod pnm; -pub mod rect; +mod rect; + +pub(super) mod re_exports { + pub use super::{ + Dims, + buf::{AsMutSlice2, AsSlice2, Buf2, MutSlice2, Slice2}, + pixfmt::IntoPixel, + rect::Rect, + }; +} +pub use re_exports::*; #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub struct Dims(pub u32, pub u32); diff --git a/core/src/util/buf.rs b/core/src/util/buf.rs index 54821b2f..5ab24608 100644 --- a/core/src/util/buf.rs +++ b/core/src/util/buf.rs @@ -47,7 +47,7 @@ pub trait AsMutSlice2 { /// /// # Examples /// ``` -/// # use retrofire_core::util::{Dims,buf::Buf2}; +/// # use retrofire_core::util::{Dims, Buf2}; /// # use retrofire_core::math::point::pt2; /// /// // Elements initialized with `Default::default()` @@ -104,7 +104,7 @@ impl Buf2 { /// /// # Examples /// ``` - /// use retrofire_core::util::{Dims, buf::Buf2}; + /// use retrofire_core::util::{Dims, Buf2}; /// /// let buf = Buf2::new_from(Dims(4, 3), 1..); /// @@ -148,7 +148,7 @@ impl Buf2 { /// /// # Examples /// ``` - /// use retrofire_core::util::{Dims, buf::Buf2}; + /// use retrofire_core::util::{Dims, Buf2}; /// /// let buf: Buf2 = Buf2::new(Dims(4, 3)); /// @@ -184,7 +184,7 @@ impl Buf2 { /// /// # Examples /// ``` - /// use retrofire_core::util::{Dims, buf::Buf2}; + /// use retrofire_core::util::{Dims, Buf2}; /// /// let buf = Buf2::new_with(Dims(4, 3), |x, y| 10 * y + x); /// @@ -232,7 +232,7 @@ impl Buf2 { /// /// # Examples /// ``` - /// use retrofire_core::util::{Dims, buf::Buf2}; + /// use retrofire_core::util::{Dims, Buf2}; /// /// let mut buf = Buf2::new_from(Dims(3, 2), [1, 2, 3, 4, 5, 6]); /// @@ -257,7 +257,7 @@ impl<'a, T> Slice2<'a, T> { /// /// # Examples /// ``` - /// use retrofire_core::util::{Dims, buf::Slice2}; + /// use retrofire_core::util::{Dims, Slice2}; /// /// let data = &[0, 1, 2, 3, 4, 5, 6]; /// let slice = Slice2::new(Dims(2, 2), 3, data); @@ -410,7 +410,7 @@ impl From<&[[T; N]]> for Buf2 { /// /// # Examples /// ``` - /// use retrofire_core::util::buf::Buf2; + /// use retrofire_core::util::Buf2; /// /// let buf = Buf2::from([[1, 2, 3], [4, 5, 6]].as_slice()); /// diff --git a/core/src/util/pnm.rs b/core/src/util/pnm.rs index 7f1fe9a6..d99cd7d1 100644 --- a/core/src/util/pnm.rs +++ b/core/src/util/pnm.rs @@ -26,12 +26,9 @@ use std::{ use crate::math::{Color3, color::gray}; -use super::{Dims, buf::Buf2}; #[cfg(feature = "std")] -use super::{ - buf::AsSlice2, - pixfmt::{IntoPixel, Rgb888}, -}; +use super::{AsSlice2, IntoPixel, pixfmt::Rgb888}; +use super::{Buf2, Dims}; use Error::*; use Format::*; diff --git a/front/src/lib.rs b/front/src/lib.rs index a101a7a9..7ecaae1f 100644 --- a/front/src/lib.rs +++ b/front/src/lib.rs @@ -8,7 +8,7 @@ use core::{cell::RefCell, time::Duration}; use retrofire_core::{ math::{Color3, Color4}, render::{Colorbuf, Context, Framebuf, tex::Atlas}, - util::{Dims, buf::AsMutSlice2, pixfmt::IntoPixel, pnm::read_pnm}, + util::{AsMutSlice2, Dims, IntoPixel, pnm::read_pnm}, }; #[cfg(feature = "minifb")] diff --git a/front/src/minifb.rs b/front/src/minifb.rs index a2b8dfa4..7668b71a 100644 --- a/front/src/minifb.rs +++ b/front/src/minifb.rs @@ -12,7 +12,7 @@ use minifb::{Key, WindowOptions}; use retrofire_core::{ render::{Colorbuf, Context, Stats, Text, target}, - util::{Dims, buf::Buf2, buf::MutSlice2, pixfmt::Xrgb8888}, + util::{Buf2, Dims, MutSlice2, pixfmt::Xrgb8888}, }; use super::{Frame, dims, font_6x10}; diff --git a/front/src/sdl2.rs b/front/src/sdl2.rs index 9a18e96b..48ce1082 100644 --- a/front/src/sdl2.rs +++ b/front/src/sdl2.rs @@ -14,9 +14,8 @@ use sdl2::{ use retrofire_core::math::Color4; use retrofire_core::render::{Colorbuf, Context, Stats, Text, target}; use retrofire_core::util::{ - Dims, - buf::{AsMutSlice2, Buf2, MutSlice2}, - pixfmt::{IntoPixel, Rgb565, Rgba4444, Rgba8888}, + AsMutSlice2, Buf2, Dims, IntoPixel, MutSlice2, + pixfmt::{Rgb565, Rgba4444, Rgba8888}, }; use super::{Frame, dims, font_6x10}; From 687c406735c731d2178665ad2a640585d16db83b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 18 Jun 2026 18:25:53 +0300 Subject: [PATCH 27/76] Move resolution constants from front to core::util::dims --- benches/e2e.rs | 6 ++-- core/src/util.rs | 14 ++------- core/src/util/dims.rs | 60 ++++++++++++++++++++++++++++++++++++++ demos/nostd/src/main.rs | 1 - demos/src/bin/bezier.rs | 3 +- demos/src/bin/hello.rs | 6 ++-- demos/wasm/src/triangle.rs | 6 ++-- front/src/lib.rs | 53 --------------------------------- front/src/minifb.rs | 4 +-- front/src/sdl2.rs | 4 +-- front/src/wasm.rs | 9 +++--- 11 files changed, 81 insertions(+), 85 deletions(-) create mode 100644 core/src/util/dims.rs diff --git a/benches/e2e.rs b/benches/e2e.rs index cafc56da..4535ebc3 100644 --- a/benches/e2e.rs +++ b/benches/e2e.rs @@ -9,7 +9,7 @@ use retrofire_core::{ translate, viewport, }, render::{Context, Frag, Model, debug::dir_to_rgb, render, shader}, - util::{Buf2, Dims, pnm}, + util::{Buf2, Dims, dims, pnm}, }; use retrofire_geom::solids::{Build, Sphere}; @@ -32,7 +32,7 @@ fn triangle(b: Bencher, n: u32) { |frag: Frag>, _: &_| frag.var.to_color4(), ); - let dims @ Dims(w, h) = Dims(640, 480); + let dims @ Dims(w, h) = dims::VGA_640_480; let modelview = translate((0.0, 0.0, 2.0)).to(); let project = perspective(1.0, w as f32 / h as f32, 0.1..1000.0); let viewport = viewport(pt2(0, h)..pt2(w, 0)); @@ -76,7 +76,7 @@ fn sphere(b: Bencher, res: u32) { |frag: Frag, _: &_| frag.var.to_color4(), ); - let dims @ Dims(w, h) = Dims(640, 480); + let dims @ Dims(w, h) = dims::VGA_640_480; let modelview = translate((0.0, 0.0, 2.0)).to(); let project = perspective(1.0, w as f32 / h as f32, 0.1..1000.0); let viewport = viewport(pt2(0, h)..pt2(w, 0)); diff --git a/core/src/util.rs b/core/src/util.rs index 0025c700..aa7063c2 100644 --- a/core/src/util.rs +++ b/core/src/util.rs @@ -1,27 +1,17 @@ //! Various utility types and functions. mod buf; +pub mod dims; pub mod pixfmt; pub mod pnm; mod rect; pub(super) mod re_exports { pub use super::{ - Dims, buf::{AsMutSlice2, AsSlice2, Buf2, MutSlice2, Slice2}, + dims::Dims, pixfmt::IntoPixel, rect::Rect, }; } pub use re_exports::*; - -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub struct Dims(pub u32, pub u32); - -impl Dims { - pub fn count(&self) -> usize { - (self.0 as u64 * self.1 as u64) - .try_into() - .expect("count should fit in usize") - } -} diff --git a/core/src/util/dims.rs b/core/src/util/dims.rs new file mode 100644 index 00000000..0ee642cb --- /dev/null +++ b/core/src/util/dims.rs @@ -0,0 +1,60 @@ +// Source for the names: +// https://commons.wikimedia.org/wiki/File:Vector_Video_Standards8.svg + +#![allow(non_upper_case_globals)] + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct Dims(pub u32, pub u32); + +// 5:4 +pub const qSXGA_640_512: Dims = Dims(640, 512); +pub const SXGA_1280_1024: Dims = Dims(1280, 1024); + +// 4:3 +pub const qVGA_320_240: Dims = Dims(320, 240); +pub const qSVGA_400_300: Dims = Dims(400, 300); +pub const qXGA_512_384: Dims = Dims(512, 384); +pub const VGA_640_480: Dims = Dims(640, 480); +pub const SVGA_800_600: Dims = Dims(800, 600); +pub const XGA_1024_768: Dims = Dims(1024, 768); +pub const QVGA_1280_960: Dims = Dims(1280, 960); +pub const UXGA_1600_1200: Dims = Dims(1600, 1200); +pub const QXGA_2048_1536: Dims = Dims(2048, 1536); + +// 16:10 +pub const CGA_320_200: Dims = Dims(320, 200); +pub const MODE_13H: Dims = CGA_320_200; +pub const QCGA_640_400: Dims = Dims(640, 400); +pub const qWXGA_640_400: Dims = Dims(640, 640); +pub const WXGA_1280_800: Dims = Dims(1280, 800); +pub const WXGAP_1440_900: Dims = Dims(1440, 900); +pub const WSXGAP_1680_1050: Dims = Dims(1680, 1050); +pub const WUXGA_1920_1200: Dims = Dims(1920, 1200); +pub const WQXGA_2560_1600: Dims = Dims(2560, 1600); + +// 16:9 +// 640x360 = "qHD"? +// 800x450 = qWSXGA? +// 960x540 = qFHD +pub const HD_1280_720: Dims = Dims(1280, 720); +pub const WSXGA_1600_900: Dims = Dims(1600, 900); +pub const FHD_1920_1080: Dims = Dims(1920, 1080); +pub const QHD_2560_1440: Dims = Dims(2560, 1440); +pub const UHD_4K_3840_2160: Dims = Dims(3840, 2160); + +// DCI ~17:9 +pub const DCI_2K_2048_1080: Dims = Dims(2048, 1080); +pub const DCI_4K_4096_2160: Dims = Dims(4096, 2160); + +// ~21:9 +pub const qUWFHD_1280_540: Dims = Dims(1280, 540); +pub const UWFHD_2560_1080: Dims = Dims(2560, 1080); +pub const UWQHD_3440_1440: Dims = Dims(3440, 1440); + +impl Dims { + pub fn count(&self) -> usize { + (self.0 as u64 * self.1 as u64) + .try_into() + .expect("count should fit in usize") + } +} diff --git a/demos/nostd/src/main.rs b/demos/nostd/src/main.rs index 2486cd57..8ef1294b 100644 --- a/demos/nostd/src/main.rs +++ b/demos/nostd/src/main.rs @@ -10,7 +10,6 @@ use libc::{abort, c_char, c_int, free, malloc, putchar, puts}; use re::prelude::*; -use re::math::mat::ProjMat3; use re::render::{Model, render, shader}; #[global_allocator] diff --git a/demos/src/bin/bezier.rs b/demos/src/bin/bezier.rs index e72dd20a..10d7a57f 100644 --- a/demos/src/bin/bezier.rs +++ b/demos/src/bin/bezier.rs @@ -7,8 +7,9 @@ use re::core::{ math::rand::{Distrib, Uniform, VectorsOnUnitDisk, Xorshift64}, math::spline::approximate, render::raster::line, + util::dims, }; -use re::front::{Frame, dims, minifb::Window}; +use re::front::{Frame, minifb::Window}; fn main() { let dims @ Dims(w, h) = dims::SVGA_800_600; diff --git a/demos/src/bin/hello.rs b/demos/src/bin/hello.rs index 64142eff..2d8ab59c 100644 --- a/demos/src/bin/hello.rs +++ b/demos/src/bin/hello.rs @@ -5,9 +5,9 @@ use re::prelude::*; use re::core::{ math::color::hsl, render::{Text, World, tex::Atlas}, - util::pnm::read_pnm, + util::{dims, pnm::read_pnm}, }; -use re_front::{Frame, dims::SVGA_800_600, minifb::Window}; +use re_front::{Frame, minifb::Window}; const FONT: &[u8] = include_bytes!("../../assets/font_16x24.pbm"); @@ -25,7 +25,7 @@ fn main() { let mut win = Window::builder() .title("retrofire//text") - .dims(SVGA_800_600) + .dims(dims::SVGA_800_600) .build() .unwrap(); diff --git a/demos/wasm/src/triangle.rs b/demos/wasm/src/triangle.rs index 98a52e48..c253e8d8 100644 --- a/demos/wasm/src/triangle.rs +++ b/demos/wasm/src/triangle.rs @@ -5,13 +5,13 @@ use wasm_bindgen::prelude::*; use re::prelude::*; use re::render::{ModelToView, render, shader::Shader}; -use re::util::Dims; -use re_front::{dims::SVGA_800_600, wasm::Window}; +use re::util::{Dims, dims}; +use re_front::wasm::Window; // Entry point from JS #[wasm_bindgen(start)] pub fn start() { - const DIMS: Dims = SVGA_800_600; + const DIMS: Dims = dims::SVGA_800_600; console_error_panic_hook::set_once(); diff --git a/front/src/lib.rs b/front/src/lib.rs index 7ecaae1f..b68d6925 100644 --- a/front/src/lib.rs +++ b/front/src/lib.rs @@ -44,59 +44,6 @@ pub struct Frame<'a, Win, Buf> { pub ctx: &'a mut Context, } -#[allow(non_upper_case_globals)] -pub mod dims { - // Source for the names: - // https://commons.wikimedia.org/wiki/File:Vector_Video_Standards8.svg - - use retrofire_core::util::Dims; - - // 5:4 - pub const qSXGA_640_512: Dims = Dims(640, 512); - pub const SXGA_1280_1024: Dims = Dims(1280, 1024); - - // 4:3 - pub const qVGA_320_240: Dims = Dims(320, 240); - pub const qSVGA_400_300: Dims = Dims(400, 300); - pub const qXGA_512_384: Dims = Dims(512, 384); - pub const VGA_640_480: Dims = Dims(640, 480); - pub const SVGA_800_600: Dims = Dims(800, 600); - pub const XGA_1024_768: Dims = Dims(1024, 768); - pub const QVGA_1280_960: Dims = Dims(1280, 960); - pub const UXGA_1600_1200: Dims = Dims(1600, 1200); - pub const QXGA_2048_1536: Dims = Dims(2048, 1536); - - // 16:10 - pub const CGA_320_200: Dims = Dims(320, 200); - pub const MODE_13H: Dims = CGA_320_200; - pub const QCGA_640_400: Dims = Dims(640, 400); - pub const qWXGA_640_400: Dims = Dims(640, 640); - pub const WXGA_1280_800: Dims = Dims(1280, 800); - pub const WXGAP_1440_900: Dims = Dims(1440, 900); - pub const WSXGAP_1680_1050: Dims = Dims(1680, 1050); - pub const WUXGA_1920_1200: Dims = Dims(1920, 1200); - pub const WQXGA_2560_1600: Dims = Dims(2560, 1600); - - // 16:9 - // 640x360 = "qHD"? - // 800x450 = qWSXGA? - // 960x540 = qFHD - pub const HD_1280_720: Dims = Dims(1280, 720); - pub const WSXGA_1600_900: Dims = Dims(1600, 900); - pub const FHD_1920_1080: Dims = Dims(1920, 1080); - pub const QHD_2560_1440: Dims = Dims(2560, 1440); - pub const UHD_4K_3840_2160: Dims = Dims(3840, 2160); - - // DCI ~17:9 - pub const DCI_2K_2048_1080: Dims = Dims(2048, 1080); - pub const DCI_4K_4096_2160: Dims = Dims(4096, 2160); - - // ~21:9 - pub const qUWFHD_1280_540: Dims = Dims(1280, 540); - pub const UWFHD_2560_1080: Dims = Dims(2560, 1080); - pub const UWQHD_3440_1440: Dims = Dims(3440, 1440); -} - impl Frame<'_, Win, &RefCell, Zbuf>>> where diff --git a/front/src/minifb.rs b/front/src/minifb.rs index 7668b71a..7eac9c4e 100644 --- a/front/src/minifb.rs +++ b/front/src/minifb.rs @@ -12,10 +12,10 @@ use minifb::{Key, WindowOptions}; use retrofire_core::{ render::{Colorbuf, Context, Stats, Text, target}, - util::{Buf2, Dims, MutSlice2, pixfmt::Xrgb8888}, + util::{Buf2, Dims, MutSlice2, dims, pixfmt::Xrgb8888}, }; -use super::{Frame, dims, font_6x10}; +use super::{Frame, font_6x10}; /// A lightweight wrapper of a `minibuf` window. pub struct Window { diff --git a/front/src/sdl2.rs b/front/src/sdl2.rs index 48ce1082..1357a9ee 100644 --- a/front/src/sdl2.rs +++ b/front/src/sdl2.rs @@ -14,11 +14,11 @@ use sdl2::{ use retrofire_core::math::Color4; use retrofire_core::render::{Colorbuf, Context, Stats, Text, target}; use retrofire_core::util::{ - AsMutSlice2, Buf2, Dims, IntoPixel, MutSlice2, + AsMutSlice2, Buf2, Dims, IntoPixel, MutSlice2, dims, pixfmt::{Rgb565, Rgba4444, Rgba8888}, }; -use super::{Frame, dims, font_6x10}; +use super::{Frame, font_6x10}; /// Helper trait to support different pixel format types. pub trait PixelFmt: Copy + Default { diff --git a/front/src/wasm.rs b/front/src/wasm.rs index 186e5135..2411b24c 100644 --- a/front/src/wasm.rs +++ b/front/src/wasm.rs @@ -20,15 +20,14 @@ use web_sys::{ js_sys::{Uint8ClampedArray, Uint32Array}, }; -use crate::{Frame, dims::SVGA_800_600}; - use retrofire_core::{ math::color::rgba, render::{Colorbuf, Context, Stats, target}, - util::buf::{AsMutSlice2, Buf2, MutSlice2}, - util::{Dims, pixfmt::Rgba8888}, + util::{AsMutSlice2, Buf2, Dims, MutSlice2, dims, pixfmt::Rgba8888}, }; +use super::Frame; + #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = console)] @@ -68,7 +67,7 @@ impl Builder { impl Default for Builder { fn default() -> Self { - Self { dims: SVGA_800_600 } + Self { dims: dims::SVGA_800_600 } } } From 337e02cd15f60a230ae0eb068343e9c0fa909a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 14 May 2026 18:52:37 +0300 Subject: [PATCH 28/76] Make stats optional and behind a feature flag The stats printing code has a fairly large binary footprint. Collecting the stats may also have a slight performance impact. --- Cargo.toml | 1 + core/Cargo.toml | 3 ++ core/README.md | 4 +++ core/src/math/vec.rs | 5 +++ core/src/render.rs | 48 +++++++++++++--------------- core/src/render/ctx.rs | 10 +++--- core/src/render/stats.rs | 66 +++++++++++++++++++++++++++++++++------ core/src/render/target.rs | 55 +++++++++++++++++++------------- demos/Cargo.toml | 3 +- demos/README.md | 5 +++ demos/nostd/src/main.rs | 2 +- demos/src/bin/crates.rs | 10 ++++-- demos/src/bin/curses.rs | 22 ++++++++++--- front/Cargo.toml | 1 + front/README.md | 3 +- front/src/minifb.rs | 22 +++++++++---- front/src/sdl2.rs | 36 +++++++++++++++------ front/src/wasm.rs | 2 +- geom/src/isect.rs | 6 ++-- 19 files changed, 213 insertions(+), 91 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9175a441..facfe89a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ clippy.collapsible_if = "allow" [features] default = ["std"] std = ["retrofire-core/std", "retrofire-geom/std"] +stats = ["retrofire-core/stats"] [dependencies] retrofire-core = { version = "0.4.0", path = "core" } diff --git a/core/Cargo.toml b/core/Cargo.toml index e06566cf..86312a8e 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -23,6 +23,7 @@ documentation.workspace = true [features] default = ["std"] + # Use std fp functions, enable I/O and timing support. std = ["fp"] # Use fp functions from the libm crate. @@ -32,6 +33,8 @@ mm = ["fp", "dep:micromath"] # For internal use only. fp = [] +stats = [] + [dependencies] libm = { version = "0.2", optional = true } micromath = { version = "2.1", optional = true } diff --git a/core/README.md b/core/README.md index 3f1f379b..de48f142 100644 --- a/core/README.md +++ b/core/README.md @@ -28,6 +28,10 @@ customizable shaders; with more to come. and transcendental functions. Enabled by default. If this feature is disabled, the crate only depends on `alloc`. +* `stats`: + Enables collection of rendering performance data, with a slight performance + and binary size impact. + * `libm`: Provides software implementations of floating-point functions via the [`libm`](https://crates.io/crates/libm) crate. diff --git a/core/src/math/vec.rs b/core/src/math/vec.rs index e040891d..d4d21f9f 100644 --- a/core/src/math/vec.rs +++ b/core/src/math/vec.rs @@ -593,6 +593,11 @@ where self.0[2] } + #[inline] + pub const fn xy(&self) -> Vector<[Sc; 2], Real<2, B>> { + vec2(self.x(), self.y()) + } + /// Returns the cross product of `self` with `other`. /// /// The result is a vector orthogonal with both input vectors, its length diff --git a/core/src/render.rs b/core/src/render.rs index b2c9082a..5c953983 100644 --- a/core/src/render.rs +++ b/core/src/render.rs @@ -7,8 +7,6 @@ use alloc::vec::Vec; use core::{fmt::Debug, ops::DerefMut}; -#[cfg(feature = "std")] -use std::time::Instant; use crate::geom::Vertex; use crate::math::{ @@ -32,11 +30,13 @@ pub(super) mod re_exports { raster::Frag, scene::{BBox, Obj}, shader::{FragmentShader, VertexShader}, - stats::Stats, target::{Colorbuf, Framebuf, Target}, tex::{TexCoord, Texture, uv}, text::Text, }; + + #[cfg(feature = "stats")] + pub use super::stats::Stats; } pub use re_exports::*; @@ -50,11 +50,13 @@ pub mod prim; pub mod raster; pub mod scene; pub mod shader; -pub mod stats; pub mod target; pub mod tex; pub mod text; +#[cfg(feature = "stats")] +pub mod stats; + /// Renderable geometric primitive. pub trait Render { /// The type of this primitive in clip space @@ -151,16 +153,12 @@ pub fn render( Var: Vary, Shd: Shader, { - // 0. Preparations - let verts = verts.as_ref(); + // 0. Setup let prims = prims.as_ref(); + let verts = verts.as_ref(); - let mut stats = Stats::new(); - stats.calls = 1.0; - stats.prims.i = prims.len(); - stats.verts.i = verts.len(); - #[cfg(feature = "std")] - let start = Instant::now(); + #[cfg(feature = "stats")] + let stats = Stats::start_call(prims.len(), verts.len()); // 1. Vertex shader: transform vertices to clip space let verts = vertex_transform(shader, uniform, verts); @@ -170,7 +168,7 @@ pub fn render( // 3. Clipping: clip against the view frustum let mut clipped = Vec::new(); - view_frustum::clip(&prims[..], &mut clipped); + view_frustum::clip(prims.as_slice(), &mut clipped); // Optional depth sorting for use cases such as transparency if let Some(d) = ctx.depth_sort { @@ -178,16 +176,15 @@ pub fn render( } // 4. Rasterize: Turn visible primitives to fragments - stats += rasterize::( + #[allow(unused_variables)] + let (prims_out, verts_out) = rasterize::( clipped, shader, uniform, to_screen, target, ctx, ); - #[cfg(feature = "std")] + #[cfg(feature = "stats")] { - stats.render_time = start.elapsed(); + *ctx.stats.borrow_mut() += stats.finish_call(prims_out, verts_out); } - - *ctx.stats.borrow_mut() += stats.finish(); } fn rasterize( @@ -197,14 +194,14 @@ fn rasterize( to_screen: Mat4, mut target: &mut impl Target, ctx: &Context, -) -> Stats +) -> (usize, usize) where Prim: Render, Shd: FragmentShader, Var: Vary, Uni: Copy, { - let mut stats = Stats::new(); + let mut out = (0, 0); for prim in clipped { // Transform to screen space let prim = Prim::to_screen(prim, &to_screen); @@ -216,18 +213,17 @@ where } // Log output stats after culling - stats.prims.o += 1; - stats.verts.o += 3; // TODO Get number of verts in prim somehow + out.0 += 1; + out.1 += 3; // TODO Get number of verts in prim somehow // 4. Fragment shader and rasterization + let target = target.deref_mut(); Prim::rasterize(prim, |scanline| { // Convert to fragments, shade, and draw to target - stats.frags += target - .deref_mut() - .rasterize(scanline, shader, uniform, ctx); + target.rasterize(scanline, shader, uniform, ctx); }); } - stats + out } #[inline] diff --git a/core/src/render/ctx.rs b/core/src/render/ctx.rs index f3ed5fcb..f83f02cb 100644 --- a/core/src/render/ctx.rs +++ b/core/src/render/ctx.rs @@ -1,11 +1,9 @@ //! Rendering context and parameters. -use core::{cell::RefCell, cmp::Ordering}; +use core::cmp::Ordering; use crate::math::{Color4, rgba}; -use super::Stats; - /// Context and parameters used by the renderer. #[derive(Clone, Debug)] pub struct Context { @@ -61,7 +59,8 @@ pub struct Context { pub depth_write: bool, /// Collecting rendering statistics. - pub stats: RefCell, + #[cfg(feature = "stats")] + pub stats: core::cell::RefCell, } /// Whether to sort faces front to back or back to front. @@ -121,7 +120,8 @@ impl Default for Context { color_write: true, depth_test: Some(Ordering::Less), depth_write: true, - stats: Default::default(), + #[cfg(feature = "stats")] + stats: super::Stats::new().into(), } } } diff --git a/core/src/render/stats.rs b/core/src/render/stats.rs index 0c6bbcf2..c3882596 100644 --- a/core/src/render/stats.rs +++ b/core/src/render/stats.rs @@ -1,17 +1,25 @@ //! Rendering statistics. use alloc::{format, string::String}; -use core::fmt::{self, Display, Formatter}; -use core::ops::AddAssign; -use core::time::Duration; +use core::{ + fmt::{self, Display, Formatter}, + ops::AddAssign, + time::Duration, +}; + +#[cfg(feature = "std")] +use std::time::Instant; // // Types // /// Collects and accumulates rendering statistics and performance data. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct Stats { + #[cfg(feature = "std")] + pub start: Instant, + /// Wall clock time elapsed. pub wall_time: Duration, /// Time spent rendering. @@ -43,7 +51,35 @@ pub struct Throughput { impl Stats { /// Creates a new zeroed `Stats` instance. pub fn new() -> Self { - Self::default() + Self { + #[cfg(feature = "std")] + start: Instant::now(), + wall_time: Default::default(), + render_time: Default::default(), + calls: 0.0, + frames: 0.0, + objs: Default::default(), + prims: Default::default(), + verts: Default::default(), + frags: Default::default(), + } + } + + pub fn start_call(n_prims: usize, n_verts: usize) -> Self { + Self { + calls: 1.0, + prims: Throughput { i: n_prims, o: 0 }, + verts: Throughput { i: n_verts, o: 0 }, + ..Self::new() + } + } + + pub fn finish(self) -> Self { + Self { + #[cfg(feature = "std")] + wall_time: self.start.elapsed(), + ..self + } } /// Stops the timer and records the elapsed time to `self.time`. @@ -51,8 +87,14 @@ impl Stats { /// No-op if the timer was not running. This method is also no-op unless /// the `std` feature is enabled. #[must_use] - pub fn finish(self) -> Self { - self + pub fn finish_call(mut self, prims_out: usize, verts_out: usize) -> Self { + self.prims.o += prims_out; + self.verts.o += verts_out; + Self { + #[cfg(feature = "std")] + render_time: self.start.elapsed(), + ..self + } } /// Returns the average throughput in items per second. @@ -65,6 +107,8 @@ impl Stats { let [objs, prims, verts, frags] = self.throughput().map(|stat| stat.per_sec(secs)); Self { + #[cfg(feature = "std")] + start: self.start, render_time: Duration::from_secs(1), wall_time: self.wall_time.div_f32(secs), frames: self.frames / secs, @@ -85,6 +129,8 @@ impl Stats { let [objs, prims, verts, frags] = self.throughput().map(|stat| stat.per_sec(secs)); Self { + #[cfg(feature = "std")] + start: self.start, wall_time: Duration::from_secs(1), render_time: self.render_time.div_f32(secs), frames: self.frames / secs, @@ -102,6 +148,8 @@ impl Stats { .throughput() .map(|stat| stat.per_frame(frames)); Self { + #[cfg(feature = "std")] + start: self.start, frames: 1.0, render_time: self.render_time / frames, wall_time: self.wall_time / frames, @@ -267,8 +315,7 @@ fn human_time(d: Duration) -> String { #[cfg(test)] mod tests { - use core::array::from_fn; - use core::time::Duration; + use core::{array::from_fn, time::Duration}; use super::*; @@ -287,6 +334,7 @@ mod tests { prims, verts, frags, + ..Stats::new() }; assert_eq!( diff --git a/core/src/render/target.rs b/core/src/render/target.rs index e36052e9..ab90f7ea 100644 --- a/core/src/render/target.rs +++ b/core/src/render/target.rs @@ -9,24 +9,20 @@ use core::cell::RefCell; use crate::math::{Color3, Color4, Vary}; use crate::util::{AsMutSlice2, Buf2, IntoPixel, MutSlice2}; -use super::{Context, FragmentShader, raster::Scanline, stats::Throughput}; +use super::{Context, FragmentShader, raster::Scanline}; /// Trait for types that can be used as render targets. pub trait Target { /// Writes a single scanline into `self`. /// /// Returns count of fragments input and output. - fn rasterize( + fn rasterize>( &mut self, scanline: Scanline, frag_shader: &Fs, uniform: U, ctx: &Context, - ) -> Throughput - where - V: Vary, - U: Copy, - Fs: FragmentShader; + ); } /// Framebuffer, combining a color (pixel) buffer and a depth buffer. @@ -64,7 +60,7 @@ impl Target for &mut T { fs: &Fs, uni: U, ctx: &Context, - ) -> Throughput { + ) { (*self).rasterize(sl, fs, uni, ctx) } } @@ -76,7 +72,7 @@ impl Target for &RefCell { fs: &Fs, uni: U, ctx: &Context, - ) -> Throughput { + ) { RefCell::borrow_mut(self).rasterize(sl, fs, uni, ctx) } } @@ -95,7 +91,7 @@ where fs: &Fs, uni: U, ctx: &Context, - ) -> Throughput { + ) { let Self { color_buf, depth_buf } = self; rasterize_fb(color_buf, depth_buf, sl, fs, uni, Color4::into_pixel, ctx) } @@ -115,7 +111,7 @@ where fs: &Fs, uni: U, ctx: &Context, - ) -> Throughput { + ) { rasterize(&mut self.buf, sl, fs, uni, Color4::into_pixel, ctx) } } @@ -128,7 +124,7 @@ impl Target for Buf2 { fs: &Fs, uni: U, ctx: &Context, - ) -> Throughput { + ) { rasterize(self, sl, fs, uni, |c| c, ctx) } } @@ -141,7 +137,7 @@ impl Target for Buf2 { fs: &Fs, uni: U, ctx: &Context, - ) -> Throughput { + ) { rasterize(self, sl, fs, uni, |c| c.to_rgb(), ctx) } } @@ -153,11 +149,13 @@ pub fn rasterize( uni: U, mut conv: impl FnMut(Color4) -> B::Elem, ctx: &Context, -) -> Throughput { +) { let x0 = sl.xs.start; let x1 = sl.xs.end.max(x0); - let mut io = Throughput { i: x1 - x0, o: 0 }; + #[cfg(feature = "stats")] + let mut frags_out = 0; + let cbuf_span = &mut buf.as_mut_slice2()[sl.y][x0..x1]; sl.fragments() @@ -166,11 +164,18 @@ pub fn rasterize( if let Some(new_col) = fs.shade_fragment(frag, uni) && ctx.color_write { - io.o += 1; + #[cfg(feature = "stats")] + { + frags_out += 1; + } *curr_col = conv(new_col); } }); - io + #[cfg(feature = "stats")] + { + ctx.stats.borrow_mut().frags += + super::stats::Throughput { i: x1 - x0, o: frags_out }; + }; } pub fn rasterize_fb( @@ -181,13 +186,14 @@ pub fn rasterize_fb( uni: U, mut conv: impl FnMut(Color4) -> B::Elem, ctx: &Context, -) -> Throughput { +) { let x0 = sl.xs.start; let x1 = sl.xs.end.max(x0); let cbuf_span = &mut cbuf.as_mut_slice2()[sl.y][x0..x1]; let zbuf_span = &mut zbuf.as_mut_slice2()[sl.y][x0..x1]; - let mut io = Throughput { i: x1 - x0, o: 0 }; + #[cfg(feature = "stats")] + let mut frags_out = 0; sl.fragments() .zip(cbuf_span) @@ -199,7 +205,10 @@ pub fn rasterize_fb( && let Some(new_col) = fs.shade_fragment(frag, uni) { if ctx.color_write { - io.o += 1; + #[cfg(feature = "stats")] + { + frags_out += 1; + } // TODO Blending should happen here *curr_col = conv(new_col); } @@ -208,5 +217,9 @@ pub fn rasterize_fb( } } }); - io + #[cfg(feature = "stats")] + { + ctx.stats.borrow_mut().frags += + super::stats::Throughput { i: x1 - x0, o: frags_out }; + }; } diff --git a/demos/Cargo.toml b/demos/Cargo.toml index 4e6e3ac5..294e22f2 100644 --- a/demos/Cargo.toml +++ b/demos/Cargo.toml @@ -22,7 +22,7 @@ repository.workspace = true documentation.workspace = true [dependencies] -re = { version = "0.4.0", path = "..", package = "retrofire" } +re = { version = "0.4.0", path = "..", package = "retrofire", features = ["std"], default-features = false } re-front = { version = "0.4.0", path = "../front", package = "retrofire-front" } minifb = { version = "0.27.0", optional = true } @@ -33,6 +33,7 @@ pancurses = { version = "0.17.0", optional = true } default = ["minifb"] minifb = ["dep:minifb", "re-front/minifb"] sdl2 = ["dep:sdl2", "re-front/sdl2"] +stats = ["re-front/stats"] [[bin]] name = "crates" diff --git a/demos/README.md b/demos/README.md index a60be49a..5d1bfac7 100644 --- a/demos/README.md +++ b/demos/README.md @@ -20,11 +20,16 @@ Simple demo programs showcasing [`retrofire`][1] features. * `bezier` : A Bézier curve bouncing around, like in a 90s screensaver. * `crates` : A scene demonstrating a first-person camera and controls. +* `curses` : A colorful torus rendered in the terminal using ncurses. * `hello` : A bouncing message, a custom message on the cmd line. * `solids` : A collection of solid shapes, hit space to switch. * `sprites`: A ball made of a large number of spherical particles. * `square` : A minimal example rendering a textured, transformed quad. +## Crate features + +* `stats`: Enables the collection and display of rendering statistics. + ## License Copyright 2020-2025 Johannes Dahlström. diff --git a/demos/nostd/src/main.rs b/demos/nostd/src/main.rs index 8ef1294b..413509a2 100644 --- a/demos/nostd/src/main.rs +++ b/demos/nostd/src/main.rs @@ -28,7 +28,7 @@ unsafe impl GlobalAlloc for Malloc { #[panic_handler] unsafe fn panic(_info: &PanicInfo) -> ! { - unsafe { abort() } + unsafe { abort(); _info.message(). } } #[unsafe(no_mangle)] diff --git a/demos/src/bin/crates.rs b/demos/src/bin/crates.rs index 9005ee60..fd9c41d5 100644 --- a/demos/src/bin/crates.rs +++ b/demos/src/bin/crates.rs @@ -113,7 +113,10 @@ fn main() { // Crates for Obj { geom, bbox, tf } in &crates { - frame.ctx.stats.borrow_mut().objs.i += 1; + #[cfg(feature = "stats")] + { + frame.ctx.stats.borrow_mut().objs.i += 1; + } let model_to_project = tf.then(world_to_project); @@ -130,7 +133,10 @@ fn main() { .uniform(&model_to_project) .render(); - frame.ctx.stats.borrow_mut().objs.o += 1; + #[cfg(feature = "stats")] + { + frame.ctx.stats.borrow_mut().objs.o += 1; + } } Continue(()) diff --git a/demos/src/bin/curses.rs b/demos/src/bin/curses.rs index 155531c7..c36ed7db 100644 --- a/demos/src/bin/curses.rs +++ b/demos/src/bin/curses.rs @@ -4,9 +4,9 @@ use pancurses::*; use re::prelude::*; +use re::core::render::{Model, render, shader}; use re::core::render::{ - Model, ctx::DepthSort::BackToFront, debug::dir_to_rgb, raster::Scanline, - render, shader, stats::Throughput, + ctx::DepthSort::BackToFront, debug::dir_to_rgb, raster::Scanline, }; use re::geom::solids::{Build, Torus}; @@ -70,7 +70,11 @@ fn main() { win.0.attrset(COLOR_PAIR(0)); win.0.mvprintw(0, 0, "Q to quit"); - ctx.stats.borrow_mut().frames += 1.0; + #[cfg(feature = "stats")] + { + ctx.stats.borrow_mut().frames += 1.0; + } + let t_secs = start.elapsed().as_secs_f32(); let mvp = rotate_x(rads(t_secs)) @@ -96,6 +100,14 @@ fn main() { break; } } + + // Return to normal terminal mode + drop(win); + + #[cfg(feature = "stats")] + { + println!("{}", ctx.stats.into_inner().finish()); + } } impl Target for Win { @@ -105,7 +117,7 @@ impl Target for Win { fs: &Fs, uni: U, _ctx: &Context, - ) -> Throughput { + ) { self.0.mv(sc.y as i32, sc.xs.start as i32); for frag in sc.fragments() { @@ -118,6 +130,6 @@ impl Target for Win { self.0 .addch(COLOR_PAIR(col as chtype) | ' ' as chtype); } - Throughput { i: sc.xs.len(), o: sc.xs.len() } + //Throughput { i: sc.xs.len(), o: sc.xs.len() } } } diff --git a/front/Cargo.toml b/front/Cargo.toml index 792fbb06..57cdf81e 100644 --- a/front/Cargo.toml +++ b/front/Cargo.toml @@ -22,6 +22,7 @@ repository.workspace = true documentation.workspace = true [features] +stats = ["retrofire-core/stats"] wasm = ["dep:wasm-bindgen", "dep:web-sys"] wasm-dev = ["wasm", "dep:console_error_panic_hook"] diff --git a/front/README.md b/front/README.md index d62ea872..aa839235 100644 --- a/front/README.md +++ b/front/README.md @@ -20,7 +20,8 @@ Simple frontends for [`retrofire`][1]. * `minifb`: Enables a frontend using the [`minifb`][2] library. * `sdl2`: Enables a frontend using the [`sdl2`][3] library. -* `wasm` Enables a frontend using WebAssembly and [`wasm-bindgen`][4]. +* `wasm`: Enables a frontend using WebAssembly and [`wasm-bindgen`][4]. +* `stats`: Enables collection and display of performance statistics. All features are disabled by default. diff --git a/front/src/minifb.rs b/front/src/minifb.rs index 7eac9c4e..338e647c 100644 --- a/front/src/minifb.rs +++ b/front/src/minifb.rs @@ -11,12 +11,17 @@ use std::time::Instant; use minifb::{Key, WindowOptions}; use retrofire_core::{ - render::{Colorbuf, Context, Stats, Text, target}, + render::{Colorbuf, Context, Text, target}, util::{Buf2, Dims, MutSlice2, dims, pixfmt::Xrgb8888}, }; use super::{Frame, font_6x10}; +#[cfg(feature = "stats")] +use retrofire_core::render::Stats; +#[cfg(not(feature = "stats"))] +pub type Stats = (); + /// A lightweight wrapper of a `minibuf` window. pub struct Window { /// The wrapped minifb window. @@ -156,12 +161,17 @@ impl Window { self.present(cbuf.data_mut()); - ctx.stats.borrow_mut().frames += 1.0; + #[cfg(feature = "stats")] + { + ctx.stats.borrow_mut().frames += 1.0; + } + } + #[cfg(feature = "stats")] + { + let stats = ctx.stats.into_inner().finish(); + println!("{stats}"); + stats } - let mut stats = ctx.stats.into_inner(); - stats.wall_time = start.elapsed(); - println!("{stats}"); - stats } fn should_quit(&self) -> bool { diff --git a/front/src/sdl2.rs b/front/src/sdl2.rs index 1357a9ee..b34b3ba4 100644 --- a/front/src/sdl2.rs +++ b/front/src/sdl2.rs @@ -11,15 +11,20 @@ use sdl2::{ video::{FullscreenType, Window as SdlWindow, WindowBuildError}, }; -use retrofire_core::math::Color4; -use retrofire_core::render::{Colorbuf, Context, Stats, Text, target}; -use retrofire_core::util::{ - AsMutSlice2, Buf2, Dims, IntoPixel, MutSlice2, dims, - pixfmt::{Rgb565, Rgba4444, Rgba8888}, +use retrofire_core::{ + math::Color4, + render::{Colorbuf, Context, Text, target}, + util::pixfmt::{Rgb565, Rgba4444, Rgba8888}, + util::{AsMutSlice2, Buf2, Dims, IntoPixel, MutSlice2, dims}, }; use super::{Frame, font_6x10}; +#[cfg(feature = "stats")] +use retrofire_core::render::Stats; +#[cfg(not(feature = "stats"))] +pub type Stats = (); + /// Helper trait to support different pixel format types. pub trait PixelFmt: Copy + Default { type Pixel: AsRef<[u8]> + Copy + Sized; @@ -243,16 +248,27 @@ impl, const N: usize> Window { })?; self.present(&tex)?; - ctx.stats.borrow_mut().frames += 1.0; + + #[cfg(feature = "stats")] + { + ctx.stats.borrow_mut().frames += 1.0; + } if cf.is_break() { break; } } - let mut stats = ctx.stats.into_inner(); - stats.wall_time = start.elapsed(); - println!("{stats}"); - Ok(stats) + + #[cfg(feature = "stats")] + { + let stats = ctx.stats.into_inner().finish(); + println!("{stats}"); + Ok(stats) + } + #[cfg(not(feature = "stats"))] + { + Ok(()) + } } } diff --git a/front/src/wasm.rs b/front/src/wasm.rs index 2411b24c..b20e4553 100644 --- a/front/src/wasm.rs +++ b/front/src/wasm.rs @@ -22,7 +22,7 @@ use web_sys::{ use retrofire_core::{ math::color::rgba, - render::{Colorbuf, Context, Stats, target}, + render::{Colorbuf, Context, target}, util::{AsMutSlice2, Buf2, Dims, MutSlice2, dims, pixfmt::Rgba8888}, }; diff --git a/geom/src/isect.rs b/geom/src/isect.rs index df32cea8..5342f15e 100644 --- a/geom/src/isect.rs +++ b/geom/src/isect.rs @@ -169,9 +169,9 @@ impl Intersect> for Ray3 { return None; } - let r_d = vec3(1.0 / dir.x(), 1.0 / dir.y(), 1.0 / dir.z()); - let low = (low - orig) * r_d; - let upp = (upp - orig) * r_d; + let r_dir = dir.map(f32::recip); + let low = (low - orig) * r_dir; + let upp = (upp - orig) * r_dir; let near = low.zip_map(upp, f32::min); let far = low.zip_map(upp, f32::max); From d060b25a89cb6cc3cbc9cc8a626219fc0cda829c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Fri, 19 Jun 2026 17:06:53 +0300 Subject: [PATCH 29/76] Add some convenience methods to Dims --- benches/e2e.rs | 20 ++++++++++---------- core/examples/hello_tri.rs | 28 ++++++++++++++-------------- core/src/render/cam.rs | 5 ++--- core/src/util/dims.rs | 30 ++++++++++++++++++++++++++++++ core/tests/rendering.rs | 8 ++++---- demos/src/bin/hello.rs | 16 +++++++++------- demos/src/bin/square.rs | 6 +++--- demos/wasm/src/triangle.rs | 2 +- 8 files changed, 73 insertions(+), 42 deletions(-) diff --git a/benches/e2e.rs b/benches/e2e.rs index 4535ebc3..ef1b271d 100644 --- a/benches/e2e.rs +++ b/benches/e2e.rs @@ -5,11 +5,11 @@ use divan::Bencher; use retrofire_core::{ geom::{Normal3, Vertex3, tri, vertex}, math::{ - Color3f, Color4, Color4f, ProjMat3, perspective, pt2, pt3, rgb, rgba, + Color3f, Color4, Color4f, ProjMat3, perspective, pt3, rgb, rgba, translate, viewport, }, render::{Context, Frag, Model, debug::dir_to_rgb, render, shader}, - util::{Buf2, Dims, dims, pnm}, + util::{Buf2, dims, pnm}, }; use retrofire_geom::solids::{Build, Sphere}; @@ -32,10 +32,10 @@ fn triangle(b: Bencher, n: u32) { |frag: Frag>, _: &_| frag.var.to_color4(), ); - let dims @ Dims(w, h) = dims::VGA_640_480; + let dims = dims::VGA_640_480; let modelview = translate((0.0, 0.0, 2.0)).to(); - let project = perspective(1.0, w as f32 / h as f32, 0.1..1000.0); - let viewport = viewport(pt2(0, h)..pt2(w, 0)); + let project = perspective(1.0, dims.aspect(), 0.1..1000.0); + let viewport = viewport(dims.into()); let mut framebuf = Buf2::::new(dims); @@ -53,7 +53,7 @@ fn triangle(b: Bencher, n: u32) { } }); - let center_pixel = framebuf[[w / 2, h / 2]]; + let center_pixel = framebuf[[dims.0 / 2, dims.1 / 2]]; assert_eq!(center_pixel, rgba(151, 128, 187, 255)); @@ -76,10 +76,10 @@ fn sphere(b: Bencher, res: u32) { |frag: Frag, _: &_| frag.var.to_color4(), ); - let dims @ Dims(w, h) = dims::VGA_640_480; + let dims = dims::VGA_640_480; let modelview = translate((0.0, 0.0, 2.0)).to(); - let project = perspective(1.0, w as f32 / h as f32, 0.1..1000.0); - let viewport = viewport(pt2(0, h)..pt2(w, 0)); + let project = perspective(1.0, dims.aspect(), 0.1..1000.0); + let viewport = viewport(dims.into()); let mut framebuf = Buf2::::new(dims); @@ -95,7 +95,7 @@ fn sphere(b: Bencher, res: u32) { ); }); - let center_pixel = framebuf[[w / 2, h / 2]]; + let center_pixel = framebuf[[dims.0 / 2, dims.1 / 2]]; assert_eq!(center_pixel, rgba(128, 127, 0, 255)); diff --git a/core/examples/hello_tri.rs b/core/examples/hello_tri.rs index 41e3d197..ec65463d 100644 --- a/core/examples/hello_tri.rs +++ b/core/examples/hello_tri.rs @@ -1,10 +1,11 @@ use retrofire_core::prelude::*; use retrofire_core::render::{Model, render, shader}; +use retrofire_core::util::dims; fn main() { let verts = [ - vertex(pt3(-1.0, 1.0, 0.0), rgb(1.0, 0.0, 0.0)), - vertex(pt3(1.0, 1.0, 0.0), rgb(0.0, 0.8, 0.0)), + vertex(pt3(1.0, 1.0, 0.0), rgb(1.0, 0.2, 0.0)), + vertex(pt3(-1.0, 1.0, 0.0), rgb(0.0, 0.8, 0.2)), vertex(pt3(0.0, -1.0, 0.0), rgb(0.4, 0.4, 1.0)), ]; @@ -27,10 +28,10 @@ fn main() { |frag: Frag>, _| frag.var.to_color4(), ); - let dims @ Dims(w, h) = Dims(640, 480); + let dims = dims::VGA_640_480; let modelview = translate((0.0, 0.0, 2.0)).to(); - let project = perspective(1.0, w as f32 / h as f32, 0.1..1000.0); - let viewport = viewport(pt2(0, h)..pt2(w, 0)); + let project = perspective(1.0, dims.aspect(), 0.1..1000.0); + let viewport = viewport(dims.into()); let mut framebuf = Buf2::::new(dims); @@ -43,17 +44,16 @@ fn main() { &mut framebuf, &Context::default(), ); - - let center_pixel = framebuf[[w / 2, h / 2]]; - - if cfg!(feature = "fp") { - assert_eq!(center_pixel, rgba(151, 128, 187, 255)); - } else { - assert_eq!(center_pixel, rgba(114, 102, 128, 255)); - } #[cfg(feature = "std")] { use retrofire_core::util::pnm; - pnm::save_ppm("triangle.ppm", framebuf).unwrap(); + pnm::save_ppm("triangle.ppm", &framebuf).unwrap(); + } + + let center_pixel = framebuf[[dims.0 / 2, dims.1 / 2]]; + if cfg!(feature = "fp") { + assert_eq!(center_pixel, rgba(152, 130, 187, 255)); + } else { + assert_eq!(center_pixel, rgba(115, 114, 140, 255)); } } diff --git a/core/src/render/cam.rs b/core/src/render/cam.rs index ebda9c82..46e08548 100644 --- a/core/src/render/cam.rs +++ b/core/src/render/cam.rs @@ -149,7 +149,7 @@ impl Camera<()> { pub fn new(dims: Dims) -> Self { Self { dims, - viewport: viewport(pt2(0, 0)..pt2(dims.0, dims.1)), + viewport: viewport(dims.into()), ..Self::default() } } @@ -201,8 +201,7 @@ impl Camera { /// * If `near_far` is an empty range. #[must_use] pub fn perspective(mut self, fov: Fov, near_far: Range) -> Self { - let aspect = self.dims.0 as f32 / self.dims.1 as f32; - + let aspect = self.dims.aspect(); self.project = perspective(fov.focal_ratio(aspect), aspect, near_far); self } diff --git a/core/src/util/dims.rs b/core/src/util/dims.rs index 0ee642cb..b8fa3fa8 100644 --- a/core/src/util/dims.rs +++ b/core/src/util/dims.rs @@ -3,6 +3,13 @@ #![allow(non_upper_case_globals)] +use core::ops::Range; + +use crate::math::{Point2u, pt2}; +use crate::util::Rect; + +/// A width, height tuple for representing 2D buffer or window dimensions, +/// screen resolutions, and similar. #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub struct Dims(pub u32, pub u32); @@ -52,9 +59,32 @@ pub const UWFHD_2560_1080: Dims = Dims(2560, 1080); pub const UWQHD_3440_1440: Dims = Dims(3440, 1440); impl Dims { + /// Returns the number of elements in a buffer of this size. pub fn count(&self) -> usize { (self.0 as u64 * self.1 as u64) .try_into() .expect("count should fit in usize") } + + /// Returns the width-to-height aspect ratio of `self`. + pub fn aspect(&self) -> f32 { + self.0 as f32 / self.1 as f32 + } +} + +impl From for Rect { + fn from(Dims(w, h): Dims) -> Self { + Rect { + left: Some(0), + top: Some(0), + right: Some(w), + bottom: Some(h), + } + } +} + +impl From for Range> { + fn from(Dims(w, h): Dims) -> Self { + pt2(0, 0)..pt2(w, h) + } } diff --git a/core/tests/rendering.rs b/core/tests/rendering.rs index 7ee10247..35694287 100644 --- a/core/tests/rendering.rs +++ b/core/tests/rendering.rs @@ -29,12 +29,12 @@ fn textured_quad() { |frag: Frag<_>, _| SamplerClamp.sample(&checker, frag.var), ); - let (w, h) = (256, 256); - let project = perspective(1.0, 1.0, 0.1..1000.0); - let viewport = viewport(pt2(0, 0)..pt2(w, h)); + let dims = Dims(256, 256); + let project = perspective(1.0, dims.aspect(), 0.1..1000.0); + let viewport = viewport(dims.into()); let mvp = translate((0.0, 0.0, 1.0)).to().then(&project); - let mut framebuf = Buf2::::new(Dims(w, h)); + let mut framebuf = Buf2::::new(dims); let mut ctx = Context::default(); render(FACES, VERTS, &shader, &mvp, viewport, &mut framebuf, &ctx); diff --git a/demos/src/bin/hello.rs b/demos/src/bin/hello.rs index 2d8ab59c..7375039b 100644 --- a/demos/src/bin/hello.rs +++ b/demos/src/bin/hello.rs @@ -31,28 +31,30 @@ fn main() { win.ctx.face_cull = None; - let vp: ProjMat3 = translate(vec3(0.0, 0.0, 15.0)) + let dims = win.dims; + let world_to_project: ProjMat3 = translate(vec3(0.0, 0.0, 15.0)) .to() - .then(&perspective(1.0, 4.0 / 3.0, 0.1..1000.0)); + .then(&perspective(1.0, dims.aspect(), 0.1..1000.0)); - let viewport = viewport(pt2(10, 10)..pt2(790, 590)); + let viewport = viewport(pt2(10, 10)..pt2(dims.0 - 10, dims.1 - 10)); win.run(|frame: &mut Frame<_, _>| { let secs = frame.t.as_secs_f32(); - let mvp = scale(0.1) + let model_to_world = scale(0.1) .then(&translate((-10.0, -5.0, 5.0 * secs.sin()))) .then(&rotate_y(rads(secs * 0.59))) .then(&rotate_z(rads((secs * 1.13).sin()))) - .to() - .then(&vp); + .to(); + + let model_to_project = model_to_world.then(&world_to_project); text.color = hsl(secs / 10.0 % 1.0, 0.8, 0.6) .to_rgb() .to_color3(); text.batch() - .uniform(&mvp) + .uniform(&model_to_project) .viewport(viewport) .target(&mut frame.buf) .context(frame.ctx) diff --git a/demos/src/bin/square.rs b/demos/src/bin/square.rs index 32b23eae..f0ebab5b 100644 --- a/demos/src/bin/square.rs +++ b/demos/src/bin/square.rs @@ -39,9 +39,9 @@ fn main() { |frag: Frag<_>, _: &_| SamplerClamp.sample(&checker, frag.var), ); - let Dims(w, h) = win.dims; - let projection = perspective(1.0, w as f32 / h as f32, 0.1..1000.0); - let viewport = viewport(pt2(10, 10)..pt2(w - 10, h - 10)); + let dims = win.dims; + let projection = perspective(1.0, dims.aspect(), 0.1..1000.0); + let viewport = viewport(pt2(10, 10)..pt2(dims.0 - 10, dims.1 - 10)); win.run(|frame| { let time = frame.t.as_secs_f32(); diff --git a/demos/wasm/src/triangle.rs b/demos/wasm/src/triangle.rs index c253e8d8..00e359f1 100644 --- a/demos/wasm/src/triangle.rs +++ b/demos/wasm/src/triangle.rs @@ -24,7 +24,7 @@ pub fn start() { vertex(pt3(2.0, 2.0, 0.0), rgba(0.2, 0.9, 0.1, 0.8)), ]; - let proj = perspective(1.0, 4.0 / 3.0, 0.1..1000.0); + let proj = perspective(1.0, DIMS.aspect(), 0.1..1000.0); let vp = viewport(pt2(8, 8)..pt2(DIMS.0 - 8, DIMS.1 - 8)); win.run(move |mut frame| { From 91636817679e71e37d97cecd54b10ce448c0832e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Fri, 19 Jun 2026 17:07:25 +0300 Subject: [PATCH 30/76] Add example code to front README --- front/README.md | 60 ++++++++++++++++++++++++++++++++++++++++++++- front/src/minifb.rs | 6 +++-- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/front/README.md b/front/README.md index aa839235..b9fbfcdc 100644 --- a/front/README.md +++ b/front/README.md @@ -12,7 +12,8 @@ # Retrofire-front -Simple frontends for [`retrofire`][1]. +Simple frontends for [`retrofire`][1], managing window creation and basic event +handling. [1]: https://crates.io/crates/retrofire @@ -31,6 +32,63 @@ All features are disabled by default. [4]: https://crates.io/crates/wasm-bindgen +## Example + +```rust +use core::ops::ControlFlow::*; + +use re::core::render::{Model, render, shader}; +use re::front::minifb::{Frame, Window}; +use re::prelude::*; + +fn main() { + let mut win = Window::builder() + .title("retrofire//example") + .build() + .expect("should create window"); + + // Initialize + let triangle = [ + vertex(pt3(-1.0, 0.6, 0.0), rgb(1.0, 0.0, 0.0)), + vertex(pt3(1.0, 0.6, 0.0), rgb(0.0, 0.8, 0.0)), + vertex(pt3(0.0, -1.2, 0.0), rgb(0.4, 0.4, 1.0)), + ]; + let shader = shader::new( + |v: Vertex3, mvp: &ProjMat3| { + vertex(mvp.apply(&v.pos), v.attrib) + }, + |frag: Frag>, _: &_| frag.var.to_color4(), + ); + let Dims(w, h) = win.dims; + let to_view = translate((0.0, 0.0, 2.0)); + let project = perspective(1.0, w as f32 / h as f32, 0.1..1000.0); + let viewport = viewport(pt2(0, h)..pt2(w, 0)); + + // Run the main loop + win.run(|frame: &mut Frame| { + // Handle events + // Automatically quits if window is closed or ESC is pressed + + // Update state + let to_world = rotate_z(rads(frame.t.as_secs_f32())); + + // Render + render( + [tri(0, 1, 2)], + triangle, + &shader, + &to_world.then(&to_view).to().then(&project), + viewport, + &mut frame.buf, + frame.ctx, + ); + + // Returning Break(()) quits the application + Continue(()) + }); +} +``` + ## License Copyright 2020-2025 Johannes Dahlström. diff --git a/front/src/minifb.rs b/front/src/minifb.rs index 338e647c..71c7f7db 100644 --- a/front/src/minifb.rs +++ b/front/src/minifb.rs @@ -15,7 +15,7 @@ use retrofire_core::{ util::{Buf2, Dims, MutSlice2, dims, pixfmt::Xrgb8888}, }; -use super::{Frame, font_6x10}; +use super::font_6x10; #[cfg(feature = "stats")] use retrofire_core::render::Stats; @@ -45,6 +45,8 @@ pub type Framebuf<'a> = target::Framebuf< MutSlice2<'a, f32>, >; +pub type Frame<'a> = super::Frame<'a, Window, &'a RefCell>>; + impl Default for Builder<'_> { fn default() -> Self { Self { @@ -123,7 +125,7 @@ impl Window { /// * the callback returns `ControlFlow::Break`. pub fn run(&mut self, mut frame_fn: F) -> Stats where - F: FnMut(&mut Frame>) -> ControlFlow<()>, + F: FnMut(&mut Frame) -> ControlFlow<()>, { let mut cbuf = Buf2::new(self.dims); let mut zbuf = Buf2::new(self.dims); From 438b80665b0155517a1bde287faff74e070beb4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Mon, 8 Jun 2026 20:51:50 +0300 Subject: [PATCH 31/76] Enable switching object LoD in solids with , and . keys Load/parse obj models in LazyLock to avoid doing it repeatedly --- demos/src/bin/solids.rs | 81 ++++++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/demos/src/bin/solids.rs b/demos/src/bin/solids.rs index de9ed43d..3df3d070 100644 --- a/demos/src/bin/solids.rs +++ b/demos/src/bin/solids.rs @@ -1,4 +1,5 @@ use core::ops::ControlFlow::Continue; +use std::sync::LazyLock; use minifb::{Key, KeyRepeat}; @@ -6,7 +7,7 @@ use re::prelude::*; use re::core::{ geom::{Polyline, Ray}, - math::{ProjMat3, ProjVec3, color::gray, spline::HermiteSpline}, + math::{ProjVec3, color::gray, spline::HermiteSpline}, render::{Model, ModelToWorld, cam::Fov, shader}, }; use re::front::{Frame, minifb::Window}; @@ -47,7 +48,10 @@ impl Carousel { } fn main() { - eprintln!("Press Space to cycle between objects..."); + eprintln!( + "Press to cycle between objects, \ + <.> and <,> to adjust level of detail..." + ); let mut win = Window::builder() .title("retrofire//solids") @@ -83,7 +87,8 @@ fn main() { let shader = shader::new(vtx_shader, frag_shader); - let objects = objects_n(8); + let mut lod = 10; + let mut objects = objects_n(lod); let translate = translate(-3.0 * Vec3::Z); let mut carousel = Carousel::default(); @@ -91,9 +96,18 @@ fn main() { win.run(|frame| { let Frame { t, dt, win, .. } = frame; - // Press Space to trigger carousel animation - if win.imp.is_key_pressed(Key::Space, KeyRepeat::No) { - carousel.start(); + for key in win.imp.get_keys_pressed(KeyRepeat::No) { + match key { + Key::Space => carousel.start(), + + Key::Comma | Key::Period => { + let (num, denom) = + if key == Key::Comma { (3, 4) } else { (4, 3) }; + lod = (lod * num / denom).clamp(3, 50); + objects = objects_n(lod); + } + _ => (), + } } let theta = rads(t.as_secs_f32()); @@ -152,9 +166,9 @@ fn objects_n(res: u32) -> [Mesh; 14] { Torus { major_radius: 0.9, minor_radius: 0.3, major_sectors, minor_sectors }.build(), // Traditional demo models - teapot(), - bunny(), - dragon() + teapot().clone(), + bunny().clone(), + dragon().clone() ] } @@ -180,30 +194,39 @@ fn lathe(secs: u32) -> Mesh { } // Loads the Utah teapot model. -fn teapot() -> Mesh { - static TEAPOT: &[u8] = include_bytes!("../../assets/teapot.obj"); - read_obj(TEAPOT) - .unwrap() - .transform(&scale(0.4).then(&translate(-0.5 * Vec3::Y)).to()) - .build() +fn teapot() -> &'static Mesh { + static TEAPOT: LazyLock> = LazyLock::new(|| { + let obj: &[_] = include_bytes!("../../assets/teapot.obj"); + read_obj::(obj) + .unwrap() + .transform(&scale(0.4).then(&translate(-0.5 * Vec3::Y)).to()) + .build() + }); + &TEAPOT } // Loads the Stanford bunny model. -fn bunny() -> Mesh { - static BUNNY: &[u8] = include_bytes!("../../assets/bunny.obj"); - read_obj::<()>(BUNNY) - .unwrap() - .transform(&scale(0.12).then(&translate(-Vec3::Y)).to()) - .with_vertex_normals() - .build() +fn bunny() -> &'static Mesh { + static BUNNY: LazyLock> = LazyLock::new(|| { + let obj: &[_] = include_bytes!("../../assets/bunny.obj"); + read_obj::<()>(obj) + .unwrap() + .transform(&scale(0.12).then(&translate(-Vec3::Y)).to()) + .with_vertex_normals() + .build() + }); + &BUNNY } // Loads the Stanford dragon model. -fn dragon() -> Mesh { - static DRAGON: &[u8] = include_bytes!("../../assets/dragon.obj"); - read_obj::<()>(DRAGON) - .unwrap() - .with_vertex_normals() - .transform(&scale(0.18).then(&translate(-0.5 * Vec3::Y)).to()) - .build() +fn dragon() -> &'static Mesh { + static DRAGON: LazyLock> = LazyLock::new(|| { + let obj: &[_] = include_bytes!("../../assets/dragon.obj"); + read_obj::<()>(obj) + .unwrap() + .with_vertex_normals() + .transform(&scale(0.18).then(&translate(-0.5 * Vec3::Y)).to()) + .build() + }); + &DRAGON } From 338c8fcf4b2b1b8c634bbe8800724803b4aca7d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sun, 28 Jun 2026 14:58:22 +0300 Subject: [PATCH 32/76] Fix a couple of mm tests that fail with --all-features --- core/src/math/float.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/math/float.rs b/core/src/math/float.rs index 620ea9a6..b081a921 100644 --- a/core/src/math/float.rs +++ b/core/src/math/float.rs @@ -297,9 +297,9 @@ mod tests { assert_approx_eq!(mm::sqrt(9.0), 3.0); assert_eq!(mm::sqrt(16.0), 4.0); assert!(mm::sqrt(-1.0).is_nan()); - assert_approx_eq!(mm::recip_sqrt(9.0), 1.0 / 3.0); + assert_approx_eq!(mm::recip_sqrt(9.0), 1.0 / 3.0, eps = 1e-3); // mm doesn't check for zero, just gives a big number - assert_approx_eq!(mm::recip_sqrt(0.0), 1.9818e19); + assert_approx_eq!(mm::recip_sqrt(0.0), 1.9818029e19); // mm doesn't check for negative, panics due to sub overflow //assert!(mm::recip_sqrt(-1.0).is_nan()); From 089d48768ddf66ecc6e3bc4cd69c34062a36fdba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Mon, 29 Jun 2026 02:05:06 +0300 Subject: [PATCH 33/76] Impl all pixel formats for Color4 and use a blanket impl for Color3 Improve test coverage. --- core/src/util/pixfmt.rs | 202 ++++++++++++++++++++++------------------ 1 file changed, 112 insertions(+), 90 deletions(-) diff --git a/core/src/util/pixfmt.rs b/core/src/util/pixfmt.rs index 1fa83177..d1703d3e 100644 --- a/core/src/util/pixfmt.rs +++ b/core/src/util/pixfmt.rs @@ -16,16 +16,20 @@ pub trait IntoPixel: Sized { } } +// RGB + /// Eight-bit channels in R,G,B order. #[derive(Copy, Clone, Default)] pub struct Rgb888; /// 5,6,5-bit channels in R,G,B order. #[derive(Copy, Clone, Default)] pub struct Rgb565; - /// Eight-bit channels in X,R,G,B order, where X is unused. #[derive(Copy, Clone, Default)] pub struct Xrgb8888; + +// RGBA + /// Eight-bit channels in R,G,B,A order. #[derive(Copy, Clone, Default)] pub struct Rgba8888; @@ -40,10 +44,45 @@ pub struct Bgra8888; #[derive(Copy, Clone, Default)] pub struct Rgba4444; -// Impls for Color3 +/// 5-bit RGB channels and 1-bit alpha. +#[derive(Copy, Clone, Default)] +pub struct Rgba5551; + +// +// IntoPixel Impl for Color3 +// + +impl IntoPixel for Color3 +where + Color4: IntoPixel, +{ + /// Converts `self` to any of the pixel formats implemented for [`Color4`], + /// with alpha set to fully opaque. + #[inline] + fn into_pixel(self) -> T { + self.to_rgba().into_pixel() + } +} + +// +// IntoPixel impls for Color4 +// -impl IntoPixel for Color3 { - /// Converts `self` to a `u32` in 0x00_RR_GG_BB format. +impl IntoPixel for Color4 +where + Self: IntoPixel<[u8; 4], F>, +{ + /// Converts `self` to `u32` using any pixel format for which the equivalent + /// `[u8;4]` conversion exists. + #[inline] + fn into_pixel(self) -> u32 { + // From [0xAA, 0xBB, 0xCC, 0xDD] to 0xAA_BB_CC_DD -> big-endian! + u32::from_be_bytes(self.into_pixel()) + } +} + +impl IntoPixel for Color4 { + /// Converts `self` to a `u32` in 0x00_RR_GG_BB format, discarding alpha. /// /// # Examples /// ``` @@ -56,13 +95,13 @@ impl IntoPixel for Color3 { /// ``` #[inline] fn into_pixel(self) -> u32 { - let [r, g, b] = self.0; + let [r, g, b, _] = self.0; // [0x00, 0xRR, 0xGG, 0xBB] -> 0x00_RR_GG_BB u32::from_be_bytes([0, r, g, b]) } } -impl IntoPixel<[u8; 3], Rgb888> for Color3 { - /// Converts `self` to (0xRR, 0xGG, 0xBB) bytes. +impl IntoPixel<[u8; 3], Rgb888> for Color4 { + /// Converts `self` to [0xRR, 0xGG, 0xBB] bytes, discarding alpha. /// /// # Examples /// ``` @@ -75,12 +114,13 @@ impl IntoPixel<[u8; 3], Rgb888> for Color3 { /// ``` #[inline] fn into_pixel(self) -> [u8; 3] { - self.0 + let [rgb @ .., _] = self.0; + rgb } } - -impl IntoPixel for Color3 { - /// Converts `self` to a `u16` in 0bRRRRR_GGGGGG_BBBBB format. +impl IntoPixel for Color4 { + /// Converts `self` to a `u16` in 0bRRRRR_GGGGGG_BBBBB format, discarding + /// alpha. /// /// # Examples /// ``` @@ -94,56 +134,22 @@ impl IntoPixel for Color3 { /// ``` #[inline] fn into_pixel(self) -> u16 { - let [r, g, b] = self.0; + let [r, g, b, _] = self.0; (r as u16 >> 3 & 0x1F) << 11 | (g as u16 >> 2 & 0x3F) << 5 | (b as u16 >> 3 & 0x1F) } } - -impl IntoPixel<[u8; 2], Rgb565> for Color3 { +impl IntoPixel<[u8; 2], Rgb565> for Color4 { #[inline] fn into_pixel(self) -> [u8; 2] { - let c: u16 = self.into_pixel(); + let c: u16 = self.into_pixel_fmt(Rgb565); c.to_ne_bytes() } } -// Impls for Color4 - -impl IntoPixel for Color4 -where - Self: IntoPixel<[u8; 4], F>, -{ - #[inline] - fn into_pixel(self) -> u32 { - // From [0xAA, 0xBB, 0xCC, 0xDD] to 0xAA_BB_CC_DD -> big-endian! - u32::from_be_bytes(self.into_pixel()) - } -} - -impl IntoPixel for Color4 { - /// Converts `self` to a `u32` in 0x0RGB format, discarding the value of - /// the alpha channel. - /// - /// # Examples - /// ``` - /// use retrofire_core::math::{rgba, Color4}; - /// use retrofire_core::util::pixfmt::{IntoPixel, Xrgb8888}; - /// - /// let color: Color4 = rgba(0x11, 0x22, 0x33, 0x44); - /// - /// assert_eq!(color.into_pixel_fmt(Xrgb8888), 0x00_11_22_33); - /// ``` - #[inline] - fn into_pixel(self) -> u32 { - let [r, g, b, _] = self.0; - // From [0x00, 0xRR, 0xGG, 0xBB] to 0x00_RR_GG_BB -> big-endian! - u32::from_be_bytes([0, r, g, b]) - } -} impl IntoPixel<[u8; 4], Rgba8888> for Color4 { - /// Converts `self` to (0xRR, 0xGG, 0xBB, 0xAA) bytes. + /// Converts `self` to [0xRR, 0xGG, 0xBB, 0xAA] bytes. /// /// # Examples /// ``` @@ -161,7 +167,7 @@ impl IntoPixel<[u8; 4], Rgba8888> for Color4 { } } impl IntoPixel<[u8; 4], Argb8888> for Color4 { - /// Converts `self` to (0xAA, 0xRR, 0xGG, 0xBB) bytes. + /// Converts `self` to [0xAA, 0xRR, 0xGG, 0xBB] bytes. /// /// # Examples /// ``` @@ -180,7 +186,7 @@ impl IntoPixel<[u8; 4], Argb8888> for Color4 { } } impl IntoPixel<[u8; 4], Bgra8888> for Color4 { - /// Converts `self` to (0xBB, 0xGG, 0xRR, 0xAA) bytes. + /// Converts `self` to [0xBB, 0xGG, 0xRR, 0xAA] bytes. /// /// # Examples /// ``` @@ -198,44 +204,26 @@ impl IntoPixel<[u8; 4], Bgra8888> for Color4 { [b, g, r, a] } } -impl IntoPixel<[u8; 3], Rgb888> for Color4 { - /// Converts `self` to bytes in RGB order, discarding alpha. - #[inline] - fn into_pixel(self) -> [u8; 3] { - [self.r(), self.g(), self.b()] - } -} -impl IntoPixel<[u8; 2], Rgba4444> for Color4 { - /// Converts `self` to two bytes, one nibble (four bits) per channel in RGBA order. - #[inline] - fn into_pixel(self) -> [u8; 2] { - let c: u16 = self.into_pixel_fmt(Rgba4444); - c.to_ne_bytes() - } -} + impl IntoPixel for Color4 { - /// Converts `self` to a `u16`, one nibble (four bits) per channel in RGBA order. + /// Converts `self` to a `u16` in 0xRGBA format (four bits per channel). #[inline] fn into_pixel(self) -> u16 { let [r, g, b, a] = self.0; - - // [0xBA, 0xRG] in little-endian (r as u16 >> 4) << 12 | (g as u16 >> 4) << 8 | (b as u16 >> 4) << 4 | (a as u16 >> 4) } } -impl IntoPixel for Color4 { - #[inline] - fn into_pixel(self) -> u16 { - self.to_rgb().into_pixel() - } -} -impl IntoPixel<[u8; 2], Rgb565> for Color4 { +impl IntoPixel<[u8; 2], Rgba4444> for Color4 { + /// Converts `self` to 0xRG and 0xBA bytes (four bits per channel) + /// in native byte order. + /// + /// For example, the result is [0xB, 0xRG] on little-endian systems. #[inline] fn into_pixel(self) -> [u8; 2] { - let c: u16 = self.into_pixel_fmt(Rgb565); + let c: u16 = self.into_pixel_fmt(Rgba4444); c.to_ne_bytes() } } @@ -244,60 +232,94 @@ impl IntoPixel<[u8; 2], Rgb565> for Color4 { #[allow(clippy::unusual_byte_groupings)] mod tests { use super::*; - use crate::math::{color::hex, rgb}; + use crate::math::{color::hex, rgb, rgba}; const COL3: Color3 = hex("#112233"); #[test] fn color3_to_rgb888() { + let pix: [u8; 3] = COL3.into_pixel_fmt(Rgb888); + assert_eq!(pix, [0x11, 0x22, 0x33]); + } + #[test] + fn color3_to_xrgb8888() { let pix: u32 = COL3.into_pixel_fmt(Xrgb8888); assert_eq!(pix, 0x00_11_22_33); } + #[test] + fn color3_to_rgba8888() { + let pix: u32 = COL3.into_pixel_fmt(Rgba8888); + assert_eq!(pix, 0x11_22_33_FF); + } + #[test] + fn color3_to_argb8888() { + let pix: u32 = COL3.into_pixel_fmt(Argb8888); + assert_eq!(pix, 0xFF_11_22_33); + } #[test] fn color3_to_rgb565() { - let pix: u16 = rgb(0x40, 0x20, 0x10).into_pixel(); + let pix: u16 = rgb(0x40, 0x20, 0x10).into_pixel_fmt(Rgb565); assert_eq!(pix, 0b01000_001000_00010_u16); let pix: [u8; 2] = rgb(0x40u8, 0x20, 0x10).into_pixel_fmt(Rgb565); assert_eq!(pix, [0b000_00010, 0b01000_001]); } - const COL4: Color4 = hex("#11223344"); + const COL4: Color4 = hex("#112233AA"); + + #[test] + fn color4_to_rgb888() { + let pix: [u8; 3] = COL4.into_pixel_fmt(Rgb888); + assert_eq!(pix, [0x11, 0x22, 0x33]); + } + #[test] + fn color4_to_xrgb8888() { + let pix: u32 = COL4.into_pixel_fmt(Xrgb8888); + assert_eq!(pix, 0x112233); + } + + #[test] + fn color4_to_rgb565() { + let pix: u16 = rgba(0x40, 0x20, 0x10, 0xAA).into_pixel_fmt(Rgb565); + assert_eq!(pix, 0b01000_001000_00010_u16); + + let pix: [u8; 2] = + rgba(0x40u8, 0x20, 0x10, 0xAA).into_pixel_fmt(Rgb565); + assert_eq!(pix, [0b000_00010, 0b01000_001]); + } #[test] fn color4_to_rgba8888() { let pix: u32 = COL4.into_pixel_fmt(Rgba8888); - assert_eq!(pix, 0x11_22_33_44); + assert_eq!(pix, 0x11_22_33_AA); let pix: [u8; 4] = COL4.into_pixel_fmt(Rgba8888); - assert_eq!(pix, [0x11, 0x22, 0x33, 0x44]); + assert_eq!(pix, [0x11, 0x22, 0x33, 0xAA]); } - #[test] fn color4_to_argb8888() { let pix: u32 = COL4.into_pixel_fmt(Argb8888); - assert_eq!(pix, 0x44_11_22_33); + assert_eq!(pix, 0xAA_11_22_33); let pix: [u8; 4] = COL4.into_pixel_fmt(Argb8888); - assert_eq!(pix, [0x44, 0x11, 0x22, 0x33]); + assert_eq!(pix, [0xAA, 0x11, 0x22, 0x33]); } - #[test] fn color4_to_bgra8888() { let pix: u32 = COL4.into_pixel_fmt(Bgra8888); - assert_eq!(pix, 0x33_22_11_44); + assert_eq!(pix, 0x33_22_11_AA); let pix: [u8; 4] = COL4.into_pixel_fmt(Bgra8888); - assert_eq!(pix, [0x33, 0x22, 0x11, 0x44]); + assert_eq!(pix, [0x33, 0x22, 0x11, 0xAA]); } #[test] fn color4_to_rgba4444() { let pix: [u8; 2] = COL4.into_pixel_fmt(Rgba4444); - assert_eq!(pix, [0x34, 0x12]); + assert_eq!(pix, [0x3A, 0x12]); let pix: u16 = COL4.into_pixel_fmt(Rgba4444); - assert_eq!(pix, 0x1234); + assert_eq!(pix, 0x123A); } } From 15ed08871adc4da26fa723d2721f2da11ad13de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Mon, 3 Nov 2025 12:16:17 +0200 Subject: [PATCH 34/76] Add a couple of new pixel formats and SDL2 frontend support for them * RGB332 * RGB5551 --- core/src/util/pixfmt.rs | 76 +++++++++++++++++++++++++++++++++++++++-- demos/src/bin/crates.rs | 8 +++-- front/src/sdl2.rs | 15 ++++++-- 3 files changed, 92 insertions(+), 7 deletions(-) diff --git a/core/src/util/pixfmt.rs b/core/src/util/pixfmt.rs index d1703d3e..31f06370 100644 --- a/core/src/util/pixfmt.rs +++ b/core/src/util/pixfmt.rs @@ -18,15 +18,19 @@ pub trait IntoPixel: Sized { // RGB +/// Eight-bit channels in X,R,G,B order, where X is unused. +#[derive(Copy, Clone, Default)] +pub struct Xrgb8888; + /// Eight-bit channels in R,G,B order. #[derive(Copy, Clone, Default)] pub struct Rgb888; /// 5,6,5-bit channels in R,G,B order. #[derive(Copy, Clone, Default)] pub struct Rgb565; -/// Eight-bit channels in X,R,G,B order, where X is unused. +/// 3,3,2-bit channels in R,G,B order. #[derive(Copy, Clone, Default)] -pub struct Xrgb8888; +pub struct Rgb332; // RGBA @@ -118,6 +122,7 @@ impl IntoPixel<[u8; 3], Rgb888> for Color4 { rgb } } + impl IntoPixel for Color4 { /// Converts `self` to a `u16` in 0bRRRRR_GGGGGG_BBBBB format, discarding /// alpha. @@ -148,6 +153,23 @@ impl IntoPixel<[u8; 2], Rgb565> for Color4 { } } +impl IntoPixel for Color4 { + /// Packs `self` into a single byte in `0bRRR_GGG_BB` format, discarding + /// alpha. + fn into_pixel(self) -> u8 { + let [r, g, b, _] = self.0; + (r >> 5) << 5 | (g >> 5) << 2 | b >> 6 + } +} +impl IntoPixel<[u8; 1], Rgb332> for Color4 { + /// Packs `self` into a single-byte array in `0bRRR_GGG_BB` format, + /// discarding alpha. + fn into_pixel(self) -> [u8; 1] { + let pix: u8 = self.into_pixel_fmt(Rgb332); + [pix] + } +} + impl IntoPixel<[u8; 4], Rgba8888> for Color4 { /// Converts `self` to [0xRR, 0xGG, 0xBB, 0xAA] bytes. /// @@ -228,6 +250,25 @@ impl IntoPixel<[u8; 2], Rgba4444> for Color4 { } } +impl IntoPixel for Color4 { + /// Packs `self` into a `u16` in a 0bRRRRR_GGGGG_BBBBB_A format. + /// An alpha value of `0xFF` is considered opaque, any other value + /// fully transparent. + fn into_pixel(self) -> u16 { + let [r, g, b, a] = self.0; + (r as u16 >> 3 & 0x1F) << 11 + | (g as u16 >> 3 & 0x1F) << 6 + | (b as u16 >> 3 & 0x1F) << 1 + | (a == 0xFF) as u16 + } +} +impl IntoPixel<[u8; 2], Rgba5551> for Color4 { + fn into_pixel(self) -> [u8; 2] { + let c: u16 = self.into_pixel_fmt(Rgba5551); + c.to_ne_bytes() + } +} + #[cfg(test)] #[allow(clippy::unusual_byte_groupings)] mod tests { @@ -266,6 +307,15 @@ mod tests { assert_eq!(pix, [0b000_00010, 0b01000_001]); } + #[test] + fn color3_to_rgb332() { + let pix: u8 = rgb(0xFF, 0x00, 0xFF).into_pixel(); + assert_eq!(pix, 0b111_000_11, "bits: {pix:b}"); + + let pix: u8 = rgb(0x00, 0x0FF, 0x00).into_pixel(); + assert_eq!(pix, 0b000_111_00, "bits: {pix:b}"); + } + const COL4: Color4 = hex("#112233AA"); #[test] @@ -322,4 +372,26 @@ mod tests { let pix: u16 = COL4.into_pixel_fmt(Rgba4444); assert_eq!(pix, 0x123A); } + + #[test] + fn color4_to_rgba5551_u16() { + let pix: u16 = rgba(0x40u8, 0x20, 0x10, 0).into_pixel_fmt(Rgba5551); + assert_eq!(pix, 0b01000_00100_00010_0_u16, "bits: {pix:b}"); + + let pix: u16 = rgba(0x40u8, 0x20, 0x10, 0x80).into_pixel_fmt(Rgba5551); + assert_eq!(pix, 0b01000_00100_00010_0_u16, "bits: {pix:b}"); + + let pix: u16 = rgba(0x40u8, 0x20, 0x10, 0xFF).into_pixel_fmt(Rgba5551); + assert_eq!(pix, 0b01000_00100_00010_1_u16, "bits: {pix:b}"); + } + + #[test] + fn color4_to_rgba5551_2u8() { + let pix: [u8; 2] = rgba(0x40u8, 0x20, 0x10, 0).into_pixel_fmt(Rgba5551); + assert_eq!(pix, [0b00_00010_0, 0b01000_001]); + + let pix: [u8; 2] = + rgba(0x40u8, 0x20, 0x10, 0xFF).into_pixel_fmt(Rgba5551); + assert_eq!(pix, [0b00_00010_1, 0b01000_001]); + } } diff --git a/demos/src/bin/crates.rs b/demos/src/bin/crates.rs index fd9c41d5..70399e1c 100644 --- a/demos/src/bin/crates.rs +++ b/demos/src/bin/crates.rs @@ -10,18 +10,20 @@ use re::core::render::{ shader, tex::SamplerClamp, }; -// Try also Rgb565 or Rgba4444 -use re::core::util::{pixfmt::Rgba8888, pnm::read_pnm}; +use re::core::util::{pixfmt, pnm::read_pnm}; use re::front::sdl2::Window; use re::geom::solids::{Build, Cube}; static CRATE_TEX: &[u8] = include_bytes!("../../assets/crate.ppm"); +// Try also Rgba4444, Rgb565, Rgb5551, or Rgb332 +const PIXFMT: pixfmt::Rgba8888 = pixfmt::Rgba8888; + fn main() { let mut win = Window::builder() .title("retrofire//crates") - .pixel_fmt(Rgba8888) + .pixel_fmt(PIXFMT) .build() .expect("should create window"); diff --git a/front/src/sdl2.rs b/front/src/sdl2.rs index b34b3ba4..3db78468 100644 --- a/front/src/sdl2.rs +++ b/front/src/sdl2.rs @@ -22,6 +22,8 @@ use super::{Frame, font_6x10}; #[cfg(feature = "stats")] use retrofire_core::render::Stats; +use retrofire_core::util::pixfmt::{Rgb332, Rgba5551}; + #[cfg(not(feature = "stats"))] pub type Stats = (); @@ -280,15 +282,24 @@ impl PixelFmt for Rgba8888 { type Pixel = [u8; 4]; const SDL_FMT: PixelFormatEnum = PixelFormatEnum::RGBA32; } -impl PixelFmt for Rgb565 { +impl PixelFmt for Rgba5551 { type Pixel = [u8; 2]; - const SDL_FMT: PixelFormatEnum = PixelFormatEnum::RGB565; + const SDL_FMT: PixelFormatEnum = PixelFormatEnum::RGBA5551; } impl PixelFmt for Rgba4444 { type Pixel = [u8; 2]; const SDL_FMT: PixelFormatEnum = PixelFormatEnum::RGBA4444; } +impl PixelFmt for Rgb565 { + type Pixel = [u8; 2]; + const SDL_FMT: PixelFormatEnum = PixelFormatEnum::RGB565; +} +impl PixelFmt for Rgb332 { + type Pixel = [u8; 1]; + const SDL_FMT: PixelFormatEnum = PixelFormatEnum::RGB332; +} + impl Default for Builder<'_, PF> { fn default() -> Self { Self { From 8a2ff9e63c54534a90e802912e081fd607699372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Mon, 29 Jun 2026 22:20:19 +0300 Subject: [PATCH 35/76] Fix bug with P4 PBM parsing Rows in P4 are padded to the next byte boundary, so for example a 4x4 image takes 4*8 bits = 4 bytes, not just 2. --- core/src/util/pnm.rs | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/core/src/util/pnm.rs b/core/src/util/pnm.rs index d99cd7d1..40a8374e 100644 --- a/core/src/util/pnm.rs +++ b/core/src/util/pnm.rs @@ -131,11 +131,27 @@ pub fn parse_pnm(input: impl IntoIterator) -> Result> { BinaryGraymap => it // .map(gray) .collect(), - BinaryBitmap => it - .flat_map(|byte| (0..8).rev().map(move |i| (byte >> i) & 1)) - // Conventionally in PBM 0 is white, 1 is black - .map(|bit| gray((1 - bit) * 0xFF)) - .collect(), + BinaryBitmap => { + // In P4, pixel values are packed in bytes, most significant bit first. + // Each row is padded to the next byte boundary, taking ⌈width/8⌉ bytes. + // For example, a 3x3 image takes three bytes, but a 9x1 image + // only takes two. + let w = h.dims.0 as usize; + let mut data = Vec::new(); + let mut it = it.peekable(); + while it.peek().is_some() { + // For each row + (&mut it) + .take(w.div_ceil(8)) + .flat_map(|byte| (0..8).rev().map(move |i| (byte >> i) & 1)) + .take(w) + .for_each(|bit| { + // Conventionally in PBM 0 is white, 1 is black + data.push(gray((1 - bit) * 0xFF)); + }); + } + data + } TextPixmap => { let mut col = [0u8; 3]; (0..3) @@ -557,13 +573,16 @@ mod tests { #[test] fn read_pnm_p4() { - // 0x69 == 0b0110_1001 - let buf = parse_pnm(*b"P4 4 2\n\x69").unwrap(); + // In P4 each row is padded to the next byte boundary + // -> 0110 + // 1001 + // = 0b0110_0000 0b1001_0000 = 0x60 0x90 + let buf = parse_pnm(*b"P4 4 2\n\x60\x90").unwrap(); assert_eq!(buf.dims(), Dims(4, 2)); - let b = rgb(0u8, 0, 0); - let w = rgb(0xFFu8, 0xFF, 0xFF); + let b = gray(0x00_u8); + let w = gray(0xFF_u8); assert_eq!(buf[0usize], [w, b, b, w]); assert_eq!(buf[1usize], [b, w, w, b]); From 52bef34ef8b179dcfeb0da956f56b4ec965e8020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Mon, 29 Jun 2026 22:21:47 +0300 Subject: [PATCH 36/76] Impl Debug for pixel format types --- core/src/util/pixfmt.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/core/src/util/pixfmt.rs b/core/src/util/pixfmt.rs index 31f06370..462e4c49 100644 --- a/core/src/util/pixfmt.rs +++ b/core/src/util/pixfmt.rs @@ -19,41 +19,41 @@ pub trait IntoPixel: Sized { // RGB /// Eight-bit channels in X,R,G,B order, where X is unused. -#[derive(Copy, Clone, Default)] +#[derive(Copy, Clone, Debug, Default)] pub struct Xrgb8888; /// Eight-bit channels in R,G,B order. -#[derive(Copy, Clone, Default)] +#[derive(Copy, Clone, Debug, Default)] pub struct Rgb888; /// 5,6,5-bit channels in R,G,B order. -#[derive(Copy, Clone, Default)] +#[derive(Copy, Clone, Debug, Default)] pub struct Rgb565; /// 3,3,2-bit channels in R,G,B order. -#[derive(Copy, Clone, Default)] +#[derive(Copy, Clone, Debug, Default)] pub struct Rgb332; // RGBA /// Eight-bit channels in R,G,B,A order. -#[derive(Copy, Clone, Default)] +#[derive(Copy, Clone, Debug, Default)] pub struct Rgba8888; /// Eight-bit channels in A,R,G,B order. -#[derive(Copy, Clone, Default)] +#[derive(Copy, Clone, Debug, Default)] pub struct Argb8888; /// Eight-bit channels in B,G,R,A order. -#[derive(Copy, Clone, Default)] +#[derive(Copy, Clone, Debug, Default)] pub struct Bgra8888; /// Four-bit channels in R,G,B,A order. -#[derive(Copy, Clone, Default)] +#[derive(Copy, Clone, Debug, Default)] pub struct Rgba4444; /// 5-bit RGB channels and 1-bit alpha. -#[derive(Copy, Clone, Default)] +#[derive(Copy, Clone, Debug, Default)] pub struct Rgba5551; // -// IntoPixel Impl for Color3 +// IntoPixel impl for Color3 // impl IntoPixel for Color3 From 7ce1347717eef139a9a3abebeb8cee4f6f217967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Tue, 30 Jun 2026 03:35:24 +0300 Subject: [PATCH 37/76] Pass format to into_pixel, remove separate into_pixel_fmt Prepare for addition of indexed formats which store the palette information. --- core/src/render/target.rs | 9 ++- core/src/util/pixfmt.rs | 122 ++++++++++++++++++-------------------- core/src/util/pnm.rs | 6 +- front/src/lib.rs | 8 +-- front/src/sdl2.rs | 2 +- 5 files changed, 72 insertions(+), 75 deletions(-) diff --git a/core/src/render/target.rs b/core/src/render/target.rs index ab90f7ea..652f86ee 100644 --- a/core/src/render/target.rs +++ b/core/src/render/target.rs @@ -80,6 +80,7 @@ impl Target for &RefCell { impl Target for Framebuf, Dep> where Col: AsMutSlice2, + Fmt: Copy, Dep: AsMutSlice2, Color4: IntoPixel, { @@ -93,13 +94,16 @@ where ctx: &Context, ) { let Self { color_buf, depth_buf } = self; - rasterize_fb(color_buf, depth_buf, sl, fs, uni, Color4::into_pixel, ctx) + let fmt = color_buf.fmt; // borrowck... + let conv = |c: Color4| c.into_pixel(fmt); + rasterize_fb(color_buf, depth_buf, sl, fs, uni, conv, ctx) } } impl Target for Colorbuf where Buf: AsMutSlice2, + Fmt: Copy, Color4: IntoPixel, { /// Rasterizes `scanline` into this `u32` color buffer. @@ -112,7 +116,8 @@ where uni: U, ctx: &Context, ) { - rasterize(&mut self.buf, sl, fs, uni, Color4::into_pixel, ctx) + let conv = |c: Color4| c.into_pixel(self.fmt); + rasterize(&mut self.buf, sl, fs, uni, conv, ctx) } } diff --git a/core/src/util/pixfmt.rs b/core/src/util/pixfmt.rs index 462e4c49..fef7ce1c 100644 --- a/core/src/util/pixfmt.rs +++ b/core/src/util/pixfmt.rs @@ -5,15 +5,7 @@ use crate::math::{Color3, Color4}; pub trait IntoPixel: Sized { /// Converts `self` to `T` in format `F`. - fn into_pixel(self) -> T; - - /// Converts `self` to `T`, taking an `F` to help type inference. - /// - /// This can be used to avoid the awkward fully-qualified syntax - /// `IntoPixel::<_, F>::into_pixel(self)`. - fn into_pixel_fmt(self, _: F) -> T { - self.into_pixel() - } + fn into_pixel(self, fmt: F) -> T; } // RGB @@ -63,8 +55,8 @@ where /// Converts `self` to any of the pixel formats implemented for [`Color4`], /// with alpha set to fully opaque. #[inline] - fn into_pixel(self) -> T { - self.to_rgba().into_pixel() + fn into_pixel(self, fmt: F) -> T { + self.to_rgba().into_pixel(fmt) } } @@ -79,9 +71,9 @@ where /// Converts `self` to `u32` using any pixel format for which the equivalent /// `[u8;4]` conversion exists. #[inline] - fn into_pixel(self) -> u32 { + fn into_pixel(self, fmt: F) -> u32 { // From [0xAA, 0xBB, 0xCC, 0xDD] to 0xAA_BB_CC_DD -> big-endian! - u32::from_be_bytes(self.into_pixel()) + u32::from_be_bytes(self.into_pixel(fmt)) } } @@ -95,10 +87,10 @@ impl IntoPixel for Color4 { /// /// let color: Color3 = rgb(0x33, 0x66, 0x99); /// - /// assert_eq!(color.into_pixel_fmt(Xrgb8888), 0x00_33_66_99); + /// assert_eq!(color.into_pixel(Xrgb8888), 0x00_33_66_99); /// ``` #[inline] - fn into_pixel(self) -> u32 { + fn into_pixel(self, _: Xrgb8888) -> u32 { let [r, g, b, _] = self.0; // [0x00, 0xRR, 0xGG, 0xBB] -> 0x00_RR_GG_BB u32::from_be_bytes([0, r, g, b]) @@ -114,10 +106,10 @@ impl IntoPixel<[u8; 3], Rgb888> for Color4 { /// /// let color: Color3 = rgb(0x33, 0x66, 0x99); /// - /// assert_eq!(color.into_pixel_fmt(Rgb888), [0x33, 0x66, 0x99]); + /// assert_eq!(color.into_pixel(Rgb888), [0x33, 0x66, 0x99]); /// ``` #[inline] - fn into_pixel(self) -> [u8; 3] { + fn into_pixel(self, _: Rgb888) -> [u8; 3] { let [rgb @ .., _] = self.0; rgb } @@ -134,11 +126,11 @@ impl IntoPixel for Color4 { /// /// let color: Color3 = rgb(0x80, 0x40, 0x20); /// - /// let word_565: u16 = color.into_pixel_fmt(Rgb565); + /// let word_565: u16 = color.into_pixel(Rgb565); /// assert_eq!(word_565, 0x8204); // 0b10000_010000_00100 /// ``` #[inline] - fn into_pixel(self) -> u16 { + fn into_pixel(self, _: Rgb565) -> u16 { let [r, g, b, _] = self.0; (r as u16 >> 3 & 0x1F) << 11 | (g as u16 >> 2 & 0x3F) << 5 @@ -147,8 +139,8 @@ impl IntoPixel for Color4 { } impl IntoPixel<[u8; 2], Rgb565> for Color4 { #[inline] - fn into_pixel(self) -> [u8; 2] { - let c: u16 = self.into_pixel_fmt(Rgb565); + fn into_pixel(self, _: Rgb565) -> [u8; 2] { + let c: u16 = self.into_pixel(Rgb565); c.to_ne_bytes() } } @@ -156,7 +148,7 @@ impl IntoPixel<[u8; 2], Rgb565> for Color4 { impl IntoPixel for Color4 { /// Packs `self` into a single byte in `0bRRR_GGG_BB` format, discarding /// alpha. - fn into_pixel(self) -> u8 { + fn into_pixel(self, _: Rgb332) -> u8 { let [r, g, b, _] = self.0; (r >> 5) << 5 | (g >> 5) << 2 | b >> 6 } @@ -164,8 +156,8 @@ impl IntoPixel for Color4 { impl IntoPixel<[u8; 1], Rgb332> for Color4 { /// Packs `self` into a single-byte array in `0bRRR_GGG_BB` format, /// discarding alpha. - fn into_pixel(self) -> [u8; 1] { - let pix: u8 = self.into_pixel_fmt(Rgb332); + fn into_pixel(self, _: Rgb332) -> [u8; 1] { + let pix: u8 = self.into_pixel(Rgb332); [pix] } } @@ -179,12 +171,12 @@ impl IntoPixel<[u8; 4], Rgba8888> for Color4 { /// use retrofire_core::util::pixfmt::{IntoPixel, Rgba8888}; /// /// let color: Color4 = rgba(0x11, 0x22, 0x33, 0x44); - /// let rgba: [u8; 4] = color.into_pixel_fmt(Rgba8888); + /// let rgba: [u8; 4] = color.into_pixel(Rgba8888); /// /// assert_eq!(rgba, [0x11, 0x22, 0x33, 0x44]); /// ``` #[inline] - fn into_pixel(self) -> [u8; 4] { + fn into_pixel(self, _: Rgba8888) -> [u8; 4] { self.0 } } @@ -197,12 +189,12 @@ impl IntoPixel<[u8; 4], Argb8888> for Color4 { /// use retrofire_core::util::pixfmt::{Argb8888, IntoPixel}; /// /// let color: Color4 = rgba(0x11, 0x22, 0x33, 0x44); - /// let argb: [u8; 4] = color.into_pixel_fmt(Argb8888); + /// let argb: [u8; 4] = color.into_pixel(Argb8888); /// /// assert_eq!(argb, [0x44, 0x11, 0x22, 0x33]); /// ``` #[inline] - fn into_pixel(self) -> [u8; 4] { + fn into_pixel(self, _: Argb8888) -> [u8; 4] { let [r, g, b, a] = self.0; [a, r, g, b] } @@ -216,12 +208,12 @@ impl IntoPixel<[u8; 4], Bgra8888> for Color4 { /// use retrofire_core::util::pixfmt::{Bgra8888, IntoPixel}; /// /// let color: Color4 = rgba(0x11, 0x22, 0x33, 0x44); - /// let bgra: [u8; 4] = color.into_pixel_fmt(Bgra8888); + /// let bgra: [u8; 4] = color.into_pixel(Bgra8888); /// /// assert_eq!(bgra, [0x33, 0x22, 0x11, 0x44]); /// ``` #[inline] - fn into_pixel(self) -> [u8; 4] { + fn into_pixel(self, _: Bgra8888) -> [u8; 4] { let [r, g, b, a] = self.0; [b, g, r, a] } @@ -230,7 +222,7 @@ impl IntoPixel<[u8; 4], Bgra8888> for Color4 { impl IntoPixel for Color4 { /// Converts `self` to a `u16` in 0xRGBA format (four bits per channel). #[inline] - fn into_pixel(self) -> u16 { + fn into_pixel(self, _: Rgba4444) -> u16 { let [r, g, b, a] = self.0; (r as u16 >> 4) << 12 | (g as u16 >> 4) << 8 @@ -244,8 +236,8 @@ impl IntoPixel<[u8; 2], Rgba4444> for Color4 { /// /// For example, the result is [0xB, 0xRG] on little-endian systems. #[inline] - fn into_pixel(self) -> [u8; 2] { - let c: u16 = self.into_pixel_fmt(Rgba4444); + fn into_pixel(self, _: Rgba4444) -> [u8; 2] { + let c: u16 = self.into_pixel(Rgba4444); c.to_ne_bytes() } } @@ -254,7 +246,7 @@ impl IntoPixel for Color4 { /// Packs `self` into a `u16` in a 0bRRRRR_GGGGG_BBBBB_A format. /// An alpha value of `0xFF` is considered opaque, any other value /// fully transparent. - fn into_pixel(self) -> u16 { + fn into_pixel(self, _: Rgba5551) -> u16 { let [r, g, b, a] = self.0; (r as u16 >> 3 & 0x1F) << 11 | (g as u16 >> 3 & 0x1F) << 6 @@ -263,12 +255,18 @@ impl IntoPixel for Color4 { } } impl IntoPixel<[u8; 2], Rgba5551> for Color4 { - fn into_pixel(self) -> [u8; 2] { - let c: u16 = self.into_pixel_fmt(Rgba5551); + fn into_pixel(self, _: Rgba5551) -> [u8; 2] { + let c: u16 = self.into_pixel(Rgba5551); c.to_ne_bytes() } } +impl IntoPixel> for u8 { + fn into_pixel(self, fmt: Indexed8) -> C { + fmt.0[self as usize] + } +} + #[cfg(test)] #[allow(clippy::unusual_byte_groupings)] mod tests { @@ -279,40 +277,40 @@ mod tests { #[test] fn color3_to_rgb888() { - let pix: [u8; 3] = COL3.into_pixel_fmt(Rgb888); + let pix: [u8; 3] = COL3.into_pixel(Rgb888); assert_eq!(pix, [0x11, 0x22, 0x33]); } #[test] fn color3_to_xrgb8888() { - let pix: u32 = COL3.into_pixel_fmt(Xrgb8888); + let pix: u32 = COL3.into_pixel(Xrgb8888); assert_eq!(pix, 0x00_11_22_33); } #[test] fn color3_to_rgba8888() { - let pix: u32 = COL3.into_pixel_fmt(Rgba8888); + let pix: u32 = COL3.into_pixel(Rgba8888); assert_eq!(pix, 0x11_22_33_FF); } #[test] fn color3_to_argb8888() { - let pix: u32 = COL3.into_pixel_fmt(Argb8888); + let pix: u32 = COL3.into_pixel(Argb8888); assert_eq!(pix, 0xFF_11_22_33); } #[test] fn color3_to_rgb565() { - let pix: u16 = rgb(0x40, 0x20, 0x10).into_pixel_fmt(Rgb565); + let pix: u16 = rgb(0x40, 0x20, 0x10).into_pixel(Rgb565); assert_eq!(pix, 0b01000_001000_00010_u16); - let pix: [u8; 2] = rgb(0x40u8, 0x20, 0x10).into_pixel_fmt(Rgb565); + let pix: [u8; 2] = rgb(0x40u8, 0x20, 0x10).into_pixel(Rgb565); assert_eq!(pix, [0b000_00010, 0b01000_001]); } #[test] fn color3_to_rgb332() { - let pix: u8 = rgb(0xFF, 0x00, 0xFF).into_pixel(); + let pix: u8 = rgb(0xFF, 0x00, 0xFF).into_pixel(Rgb332); assert_eq!(pix, 0b111_000_11, "bits: {pix:b}"); - let pix: u8 = rgb(0x00, 0x0FF, 0x00).into_pixel(); + let pix: u8 = rgb(0x00, 0x0FF, 0x00).into_pixel(Rgb332); assert_eq!(pix, 0b000_111_00, "bits: {pix:b}"); } @@ -320,78 +318,76 @@ mod tests { #[test] fn color4_to_rgb888() { - let pix: [u8; 3] = COL4.into_pixel_fmt(Rgb888); + let pix: [u8; 3] = COL4.into_pixel(Rgb888); assert_eq!(pix, [0x11, 0x22, 0x33]); } #[test] fn color4_to_xrgb8888() { - let pix: u32 = COL4.into_pixel_fmt(Xrgb8888); + let pix: u32 = COL4.into_pixel(Xrgb8888); assert_eq!(pix, 0x112233); } #[test] fn color4_to_rgb565() { - let pix: u16 = rgba(0x40, 0x20, 0x10, 0xAA).into_pixel_fmt(Rgb565); + let pix: u16 = rgba(0x40, 0x20, 0x10, 0xAA).into_pixel(Rgb565); assert_eq!(pix, 0b01000_001000_00010_u16); - let pix: [u8; 2] = - rgba(0x40u8, 0x20, 0x10, 0xAA).into_pixel_fmt(Rgb565); + let pix: [u8; 2] = rgba(0x40u8, 0x20, 0x10, 0xAA).into_pixel(Rgb565); assert_eq!(pix, [0b000_00010, 0b01000_001]); } #[test] fn color4_to_rgba8888() { - let pix: u32 = COL4.into_pixel_fmt(Rgba8888); + let pix: u32 = COL4.into_pixel(Rgba8888); assert_eq!(pix, 0x11_22_33_AA); - let pix: [u8; 4] = COL4.into_pixel_fmt(Rgba8888); + let pix: [u8; 4] = COL4.into_pixel(Rgba8888); assert_eq!(pix, [0x11, 0x22, 0x33, 0xAA]); } #[test] fn color4_to_argb8888() { - let pix: u32 = COL4.into_pixel_fmt(Argb8888); + let pix: u32 = COL4.into_pixel(Argb8888); assert_eq!(pix, 0xAA_11_22_33); - let pix: [u8; 4] = COL4.into_pixel_fmt(Argb8888); + let pix: [u8; 4] = COL4.into_pixel(Argb8888); assert_eq!(pix, [0xAA, 0x11, 0x22, 0x33]); } #[test] fn color4_to_bgra8888() { - let pix: u32 = COL4.into_pixel_fmt(Bgra8888); + let pix: u32 = COL4.into_pixel(Bgra8888); assert_eq!(pix, 0x33_22_11_AA); - let pix: [u8; 4] = COL4.into_pixel_fmt(Bgra8888); + let pix: [u8; 4] = COL4.into_pixel(Bgra8888); assert_eq!(pix, [0x33, 0x22, 0x11, 0xAA]); } #[test] fn color4_to_rgba4444() { - let pix: [u8; 2] = COL4.into_pixel_fmt(Rgba4444); + let pix: [u8; 2] = COL4.into_pixel(Rgba4444); assert_eq!(pix, [0x3A, 0x12]); - let pix: u16 = COL4.into_pixel_fmt(Rgba4444); + let pix: u16 = COL4.into_pixel(Rgba4444); assert_eq!(pix, 0x123A); } #[test] fn color4_to_rgba5551_u16() { - let pix: u16 = rgba(0x40u8, 0x20, 0x10, 0).into_pixel_fmt(Rgba5551); + let pix: u16 = rgba(0x40u8, 0x20, 0x10, 0).into_pixel(Rgba5551); assert_eq!(pix, 0b01000_00100_00010_0_u16, "bits: {pix:b}"); - let pix: u16 = rgba(0x40u8, 0x20, 0x10, 0x80).into_pixel_fmt(Rgba5551); + let pix: u16 = rgba(0x40u8, 0x20, 0x10, 0x80).into_pixel(Rgba5551); assert_eq!(pix, 0b01000_00100_00010_0_u16, "bits: {pix:b}"); - let pix: u16 = rgba(0x40u8, 0x20, 0x10, 0xFF).into_pixel_fmt(Rgba5551); + let pix: u16 = rgba(0x40u8, 0x20, 0x10, 0xFF).into_pixel(Rgba5551); assert_eq!(pix, 0b01000_00100_00010_1_u16, "bits: {pix:b}"); } #[test] fn color4_to_rgba5551_2u8() { - let pix: [u8; 2] = rgba(0x40u8, 0x20, 0x10, 0).into_pixel_fmt(Rgba5551); + let pix: [u8; 2] = rgba(0x40u8, 0x20, 0x10, 0).into_pixel(Rgba5551); assert_eq!(pix, [0b00_00010_0, 0b01000_001]); - let pix: [u8; 2] = - rgba(0x40u8, 0x20, 0x10, 0xFF).into_pixel_fmt(Rgba5551); + let pix: [u8; 2] = rgba(0x40u8, 0x20, 0x10, 0xFF).into_pixel(Rgba5551); assert_eq!(pix, [0b00_00010_1, 0b01000_001]); } } diff --git a/core/src/util/pnm.rs b/core/src/util/pnm.rs index 40a8374e..a965615d 100644 --- a/core/src/util/pnm.rs +++ b/core/src/util/pnm.rs @@ -221,11 +221,9 @@ where } .write(&mut out)?; - // Appease the borrow checker slice - .rows() - .flatten() - .map(|c| c.into_pixel()) + .iter() + .map(|c| c.into_pixel(Rgb888)) .try_for_each(|rgb| out.write_all(&rgb[..])) } diff --git a/front/src/lib.rs b/front/src/lib.rs index b68d6925..ca30b4ca 100644 --- a/front/src/lib.rs +++ b/front/src/lib.rs @@ -57,11 +57,9 @@ where /// is enabled. pub fn clear(&mut self) { if let Some(c) = self.ctx.color_clear { - self.buf - .borrow_mut() - .color_buf - .as_mut_slice2() - .fill(c.into_pixel()); + let cbuf = &mut self.buf.borrow_mut().color_buf; + let fmt = cbuf.fmt; + cbuf.as_mut_slice2().fill(c.into_pixel(fmt)); } if let Some(z) = self.ctx.depth_clear { // Depth buffer contains reciprocal depth values diff --git a/front/src/sdl2.rs b/front/src/sdl2.rs index 3db78468..f98d30e8 100644 --- a/front/src/sdl2.rs +++ b/front/src/sdl2.rs @@ -34,7 +34,7 @@ pub trait PixelFmt: Copy + Default { #[inline] fn encode>(self, color: C) -> Self::Pixel { - color.into_pixel_fmt(self) + color.into_pixel(self) } } From 1c31fe5361b2d15bd59fa3248c3630e257192479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Tue, 30 Jun 2026 03:36:53 +0300 Subject: [PATCH 38/76] Add 256-color indexed pixel format --- core/src/util/pixfmt.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/src/util/pixfmt.rs b/core/src/util/pixfmt.rs index fef7ce1c..6c3aa043 100644 --- a/core/src/util/pixfmt.rs +++ b/core/src/util/pixfmt.rs @@ -44,6 +44,11 @@ pub struct Rgba4444; #[derive(Copy, Clone, Debug, Default)] pub struct Rgba5551; +// Indexed + +#[derive(Copy, Clone)] +pub struct Indexed8(pub [C; 256]); + // // IntoPixel impl for Color3 // From 0665661b730f1762e9a27b163f8245eb07a52476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 2 Jul 2026 12:16:06 +0300 Subject: [PATCH 39/76] Allow Batch to borrow its rendered geometry, avoiding allocations --- core/src/render.rs | 2 +- core/src/render/batch.rs | 91 ++++++++++++++++++++++++++------------- core/src/render/debug.rs | 26 +++++------ core/src/render/text.rs | 17 +++++--- core/triangle.ppm | Bin 921615 -> 921615 bytes demos/src/bin/solids.rs | 4 +- 6 files changed, 89 insertions(+), 51 deletions(-) diff --git a/core/src/render.rs b/core/src/render.rs index 5c953983..395fe598 100644 --- a/core/src/render.rs +++ b/core/src/render.rs @@ -149,7 +149,7 @@ pub fn render( ctx: &Context, ) where Prim: Render + Clone, - [::Clip]: Clip, + [Prim::Clip]: Clip, Var: Vary, Shd: Shader, { diff --git a/core/src/render/batch.rs b/core/src/render/batch.rs index 10da4af9..037895d9 100644 --- a/core/src/render/batch.rs +++ b/core/src/render/batch.rs @@ -28,9 +28,9 @@ use super::{Clip, Context, Ndc, Render, Screen, Shader, Target}; // using the same configuration, or several [instances] of the same geometry. // [instances]: https://en.wikipedia.org/wiki/Geometry_instancing #[derive(Clone, Debug, Default)] -pub struct Batch { - pub prims: Vec, - pub verts: Vec, +pub struct Batch { + pub prims: Prims, + pub verts: Verts, pub uniform: Uni, pub shader: Shd, pub viewport: Mat4, @@ -51,37 +51,33 @@ impl Batch<(), (), (), (), (), Context> { } } -impl Batch { +impl Batch { /// Sets the primitives to be rendered. - /// - /// The primitives are copied into the batch. - pub fn primitives( + pub fn primitives( self, - prims: impl AsRef<[P]>, - ) -> Batch { - let prims = prims.as_ref().to_vec(); + prims: Ps, + ) -> Batch { update!(prims; self verts uniform shader viewport target ctx) } /// Sets the vertices to be rendered. - /// - /// The vertices are cloned into the batch. - // TODO: Allow taking by reference to make cloning Batch cheap - pub fn vertices( + pub fn vertices( self, - verts: impl AsRef<[V]>, - ) -> Batch { - let verts = verts.as_ref().to_vec(); + verts: Vs, + ) -> Batch { update!(verts; self prims uniform shader viewport target ctx) } /// Clones faces and vertices from a mesh to this batch. + /// + /// You can also create a new batch from a moved or borrowed mesh + /// directly using the `From` or `From<&Mesh>` impls. pub fn mesh( self, mesh: &Mesh, - ) -> Batch, Vertex3, Uni, Shd, Tgt, Ctx> { - let prims = mesh.faces.clone(); - let verts = mesh.verts.clone(); + ) -> Batch<&[Tri], &[Vertex3], Uni, Shd, Tgt, Ctx> { + let prims = &mesh.faces; + let verts = &mesh.verts; update!(verts prims; self uniform shader viewport target ctx) } @@ -89,15 +85,20 @@ impl Batch { pub fn uniform( self, uniform: U, - ) -> Batch { + ) -> Batch { update!(uniform; self verts prims shader viewport target ctx) } /// Sets the combined vertex and fragment shader. - pub fn shader>( + pub fn shader( self, shader: S, - ) -> Batch { + ) -> Batch + where + Var: Vary, + S: Shader, + Verts: AsRef<[Vtx]>, + { update!(shader; self verts prims uniform viewport target ctx) } @@ -108,7 +109,7 @@ impl Batch { /// Sets the render target. // TODO what bound for T? - pub fn target(self, target: T) -> Batch { + pub fn target(self, target: T) -> Batch { update!(target; self verts prims uniform shader viewport ctx) } @@ -116,21 +117,25 @@ impl Batch { pub fn context( self, ctx: &Context, - ) -> Batch { + ) -> Batch { update!(ctx; self verts prims uniform shader viewport target) } } -impl Batch { +impl Batch { /// Renders this batch of geometry. #[rustfmt::skip] - pub fn render(&mut self) + pub fn render(&mut self) where Var: Vary, Prim: Render + Clone, Vtx: Clone, + + Prims: AsRef<[Prim]>, + Verts: AsRef<[Vtx]>, + Uni: Copy, - [::Clip]: Clip, + [Prim::Clip]: Clip, Shd: Shader, Tgt: Target, Ctx: Borrow @@ -146,7 +151,9 @@ impl Batch { } } -impl Batch, Vtx, Uni, Shd, Tgt, Ctx> { +impl + Batch>, Vec, Uni, Shd, Tgt, Ctx> +{ pub fn append(&mut self, other: Self) { let Batch { prims, verts, .. } = other; let n = self.verts.len(); @@ -157,7 +164,9 @@ impl Batch, Vtx, Uni, Shd, Tgt, Ctx> { } } -impl Batch, Vtx, Uni, Shd, Tgt, Ctx> { +impl + Batch>, Vec, Uni, Shd, Tgt, Ctx> +{ pub fn append(&mut self, other: Self) { let Batch { prims, verts, .. } = other; let n = self.verts.len(); @@ -167,3 +176,25 @@ impl Batch, Vtx, Uni, Shd, Tgt, Ctx> { self.prims.extend(prims); } } + +// +// Foreign trait impls +// + +impl From> + for Batch>, Vec>, (), (), (), Context> +{ + fn from(m: Mesh) -> Self { + Batch::new().primitives(m.faces).vertices(m.verts) + } +} + +impl<'a, A, B> From<&'a Mesh> + for Batch<&'a [Tri], &'a [Vertex3], (), (), (), Context> +{ + fn from(m: &'a Mesh) -> Self { + Batch::new() + .primitives(m.faces.as_slice()) + .vertices(m.verts.as_slice()) + } +} diff --git a/core/src/render/debug.rs b/core/src/render/debug.rs index 32b9991f..83b9930f 100644 --- a/core/src/render/debug.rs +++ b/core/src/render/debug.rs @@ -1,9 +1,7 @@ //! Routines for drawing wireframe visualizations of geometric objects //! for debugging purposes. Includes normals, bounding boxes, and more. -#[cfg(feature = "fp")] use alloc::vec::Vec; -use core::fmt::Debug; use crate::geom::{Edge, Tri, Vertex, Vertex3, vertex}; use crate::math::{ @@ -40,8 +38,14 @@ impl<'a, B> FragmentShader> for Shader { } } -pub type DbgBatch = - super::Batch, Vertex3, (), Shader, (), Context>; +pub type DbgBatch = super::Batch< + Vec>, + Vec>, + (), + Shader, + (), + Context, +>; /// Returns a color visualizing the direction of a vector. /// @@ -85,15 +89,13 @@ pub fn ray(o: Point3, dir: Vec3) -> DbgBatch { [2, 4], [2, 5], [3, 4], [3, 5], ].map(Edge::from); - DbgBatch::new(&edges, &verts) + DbgBatch::new(edges.to_vec(), verts.to_vec()) } /// Draws a unit-length ray denoting the normal vector of a triangle. /// /// The ray originates from the triangle's centroid. -pub fn face_normal( - tri: &Tri>, -) -> DbgBatch { +pub fn face_normal(tri: &Tri>) -> DbgBatch { ray(tri.centroid(), tri.normal().to()) } @@ -132,7 +134,7 @@ pub fn cuboid(v0: Point3, v1: Point3) -> DbgBatch { [0, 4], [1, 5], [2, 6], [3, 7], ].map(Edge::from); - DbgBatch::new(&edges, &verts) + DbgBatch::new(edges.to_vec(), verts.to_vec()) } /// Draws the smallest axis-aligned box that contains a set of vertices. @@ -156,7 +158,7 @@ pub fn circle(o: Point3, r: f32) -> DbgBatch { let edges: Vec<_> = (0..RES).map(|i| Edge(i, i + 1)).collect(); - DbgBatch::new(&edges, &verts) + DbgBatch::new(edges, verts) } /// Draws a wireframe sphere with the given center and radius. @@ -186,11 +188,11 @@ pub fn sphere(o: Point3, r: f32) -> DbgBatch { }) .collect(); - DbgBatch::new(&edges, &verts) + DbgBatch::new(edges, verts) } impl DbgBatch { - fn new(prims: &[Edge], verts: &[Vertex3]) -> Self { + fn new(prims: Vec>, verts: Vec>) -> Self { DbgBatch::::default() .primitives(prims) .vertices(verts) diff --git a/core/src/render/text.rs b/core/src/render/text.rs index f451cf74..332d3de1 100644 --- a/core/src/render/text.rs +++ b/core/src/render/text.rs @@ -37,8 +37,14 @@ pub enum Align { BottomRight, } -pub type Batch = - super::Batch, Vertex3, (), Shd, (), Context>; +pub type Batch<'a, Shd> = super::Batch< + &'a [Tri], + &'a [Vertex3], + (), + Shd, + (), + Context, +>; // // Inherent impls @@ -122,10 +128,9 @@ impl Text { /// Useful for customized text rendering. pub fn batch( &self, - ) -> Batch, TexCoord, &ProjMat3>> { - super::Batch::new() - .mesh(&self.geom) - .shader(self.shader()) + ) -> Batch<'_, impl Shader, TexCoord, &ProjMat3>> + { + super::Batch::from(&self.geom).shader(self.shader()) } /// Samples the font at a texture coordinate. diff --git a/core/triangle.ppm b/core/triangle.ppm index a0887752520189fb6c9f0d818478988b75ec54f6..702e7110018548a717815019c595b65b0a1f49cc 100644 GIT binary patch literal 921615 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 literal 921615 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 zfB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U< z00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa z0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV= z5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHaf zKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_ z009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz z00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_< z0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb z2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$## zAOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;| KfWTKO@c#h Date: Fri, 3 Jul 2026 02:47:13 +0300 Subject: [PATCH 40/76] Clean up clipping code a bit, add comments --- core/src/render/clip.rs | 82 +++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/core/src/render/clip.rs b/core/src/render/clip.rs index 46d9e29b..46706080 100644 --- a/core/src/render/clip.rs +++ b/core/src/render/clip.rs @@ -170,26 +170,27 @@ impl ClipPlane { verts_in: &[ClipVert], verts_out: &mut Vec>, ) { - let mut verts = verts_in.iter().chain(&verts_in[..1]); - - let Some(mut v0) = verts.next() else { + let [fst, .., lst] = verts_in else { return; }; + let edges = verts_in + .array_windows() + .map(|[v0, v1]| Edge(v0, v1)) + .chain([Edge(lst, fst)]); - for v1 in verts { + for Edge(v0, v1) in edges { if self.is_inside(v0) { - // v0 is inside; emit it as-is. If v1 is also inside, we don't - // have to do anything; it is emitted on the next iteration. - verts_out.push((*v0).clone()); + // v0 is inside; emit it as-is. If v1 is also inside, no need + // to do anything; it will be emitted on the next iteration. + verts_out.push(v0.clone()); } else { - // v0 is outside, discard it. If v1 is also outside, we don't - // have to do anything; it is discarded on the next iteration. + // v0 is outside, discard it. If v1 is also outside, no need + // to do anything; it will be discarded on the next iteration. } if let Some(v) = self.intersect([v0, v1]) { verts_out.push(v); } - v0 = v1; } } } @@ -288,8 +289,8 @@ pub fn clip_simple_polygon<'a, A: Lerp>( ) { debug_assert!(verts_out.is_empty()); - for (p, i) in zip(planes, 0..) { - p.clip_simple_polygon(verts_in, verts_out); + for (plane, i) in zip(planes, 0..) { + plane.clip_simple_polygon(verts_in, verts_out); verts_in.clear(); if verts_out.is_empty() { // Nothing left to clip; the polygon was fully outside @@ -316,39 +317,48 @@ impl Clip for [Edge>] { // TODO capacity is just a heuristic, should retain vector between calls somehow out.reserve(self.len() / 2); - 'lines: for edge @ Edge(a, b) in self { - let both_outside = a.outcode & b.outcode != 0; - let neither_outside = a.outcode | b.outcode == 0; - - //let mut a = a.clone(); - //let mut b = b.clone(); + 'edges: for edge in self { + // Bitset of planes that both ends are outside + let both_outside = edge.0.outcode & edge.1.outcode; + // Bitset of planes that at least one end is outside + let either_outside = edge.0.outcode | edge.1.outcode; - let mut e = edge.clone(); - - if both_outside { + if both_outside != 0 { + // There is at least one plane outside which both ends are. + // The edge must be fully outside and can be discarded. continue; } - if neither_outside { - out.push(e); + if either_outside == 0 { + // Neither end is outside any plane. The edge is fully visible, + // no clipping needed. + out.push(edge.clone()); continue; } - // Otherwise, clipping is needed - for p in planes { - let a_in = p.is_inside(&e.0); - let b_in = p.is_inside(&e.1); - // TODO Why not handled by both_outside check? - if !a_in && !b_in { - continue 'lines; + + // Otherwise, either only one endpoint is inside all planes, *or* + // the two endpoints are outside *different* planes. Clipping is + // needed to compute the fully-inside segment of the edge *if any*. + let Edge(mut v0, mut v1) = edge.clone(); + for plane in planes { + let v0_inside = plane.is_inside(&v0); + let v1_inside = plane.is_inside(&v1); + + if !v0_inside && !v1_inside { + // We know the original edge was not outside any *single* + // plane, but if it *was* outside the bounding volume as + // a whole, clipping will eventually result in a remainder + // that is fully outside a plane and can be discarded. + continue 'edges; } - if let Some(v) = p.intersect([&e.0, &e.1]) { - if a_in { - e.1 = v; - } else if b_in { - e.0 = v; + if let Some(v) = plane.intersect([&v0, &v1]) { + if v0_inside { + v1 = v; + } else if v1_inside { + v0 = v; } } } - out.push(e); + out.push(Edge(v0, v1)); } } } From 1ced0c391ec22e45f3dfc8235ffe0ccc28a63e8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Tue, 7 Jul 2026 01:33:29 +0300 Subject: [PATCH 41/76] Add From impls equivalent to Vec2::to_vec3() and Point2::to_pt3() --- core/src/math/point.rs | 9 +++++++++ core/src/math/vec.rs | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/core/src/math/point.rs b/core/src/math/point.rs index 13b977ad..559607d5 100644 --- a/core/src/math/point.rs +++ b/core/src/math/point.rs @@ -341,6 +341,15 @@ impl PartialEq for Point { } } +impl From>> for Point<[Sc; 3], Real<3, B>> +where + Sc: Linear + Copy, +{ + fn from(pt: Point<[Sc; 2], Real<2, B>>) -> Self { + pt.to_pt3() + } +} + // Point <-> repr conversions impl From for Point { #[inline] diff --git a/core/src/math/vec.rs b/core/src/math/vec.rs index d4d21f9f..dbfd53ea 100644 --- a/core/src/math/vec.rs +++ b/core/src/math/vec.rs @@ -805,6 +805,15 @@ impl Debug for Vector { } } +impl From>> for Vector<[Sc; 3], Real<3, B>> +where + Sc: Linear + Copy, +{ + fn from(v: Vector<[Sc; 2], Real<2, B>>) -> Self { + v.to_vec3() + } +} + // Vector <-> repr conversions impl From for Vector { #[inline] From 70f785e99fea21e7790b7db877079a82fabb708e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Tue, 7 Jul 2026 00:37:17 +0300 Subject: [PATCH 42/76] Add Pos trait to abstract over vertices and points --- core/src/geom/prim.rs | 201 +++++++++++++++++++++------------------ core/src/render/debug.rs | 6 +- core/src/render/prim.rs | 2 +- geom/src/io.rs | 4 +- geom/src/isect.rs | 28 +++--- geom/src/solids/lathe.rs | 3 + 6 files changed, 130 insertions(+), 114 deletions(-) diff --git a/core/src/geom/prim.rs b/core/src/geom/prim.rs index 3df451a4..1da7106c 100644 --- a/core/src/geom/prim.rs +++ b/core/src/geom/prim.rs @@ -14,6 +14,16 @@ use crate::math::{ }; use crate::render::Model; +/// A trait for types that have a position. Primarily useful for APIs such as +/// `Tri` that can handle either points or vertices. +pub trait Pos { + /// The position type. + type Type; + + /// Returns the position of `self`. + fn pos(&self) -> &Self::Type; +} + /// Vertex with a position and arbitrary other attributes. #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub struct Vertex { @@ -108,7 +118,7 @@ pub const fn vertex(pos: P, attrib: A) -> Vertex { /// Creates a [`Tri`] with the given vertices. #[inline] -pub const fn tri(a: V, b: V, c: V) -> Tri { +pub const fn tri

(a: P, b: P, c: P) -> Tri

{ Tri([a, b, c]) } @@ -147,38 +157,63 @@ impl Tri { } } -impl Tri> { +impl> Tri

{ /// Given a triangle ABC, returns the vectors [AB, AC]. #[inline] - pub fn tangents(&self) -> [P::Diff; 2] { + pub fn tangents(&self) -> [T::Diff; 2] { let [a, b, c] = &self.0; - [b.pos.sub(&a.pos), c.pos.sub(&a.pos)] + [b.pos().sub(&a.pos()), c.pos().sub(&a.pos())] } /// Returns the geometric center, or "balance point", of `self`. /// /// The centroid is simply the average of the three vertex positions. - pub fn centroid(&self) -> P + pub fn centroid(&self) -> T where - P::Diff: Linear, + T::Diff: Linear, { let [ab, ac] = self.tangents(); - self.0[0].pos.add(&ab.add(&ac).mul(1.0 / 3.0)) + self.0[0].pos().add(&ab.add(&ac).mul(1.0 / 3.0)) + } +} + +impl

Tri

{ + /// Returns the area of `self`. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::{tri, vertex}; + /// use retrofire_core::math::{Point3, pt3}; + /// + /// let tri = tri::( + /// pt3(0.0, 0.0, 0.0), + /// pt3(4.0, 0.0, 0.0), + /// pt3(0.0, 3.0, 0.0), + /// ); + /// assert_eq!(tri.area(), 6.0); + /// ``` + pub fn area(&self) -> f32 + where + P: Pos> + Clone>, + { + let [a, b, c] = self.0.each_ref().map(|p| p.pos().clone().into()); + let [t, u] = tri(a, b, c).tangents(); + t.cross(&u).len() / 2.0 } } -impl Tri> { +impl>> Tri

{ /// Returns the winding order of `self`. /// /// # Examples /// ``` /// use retrofire_core::geom::{Tri, vertex, Winding}; - /// use retrofire_core::math::pt2; + /// use retrofire_core::math::{pt2, Point2}; /// - /// let mut tri = Tri([ - /// vertex(pt2::<_, ()>(0.0, 0.0), ()), - /// vertex(pt2(0.0, 3.0), ()), - /// vertex(pt2(4.0, 0.0), ()), + /// let mut tri = Tri::([ + /// pt2(0.0, 0.0), + /// pt2(0.0, 3.0), + /// pt2(4.0, 0.0), /// ]); /// assert_eq!(tri.winding(), Winding::Cw); /// @@ -201,12 +236,12 @@ impl Tri> { /// # Examples /// ``` /// use retrofire_core::geom::{Tri, vertex}; - /// use retrofire_core::math::pt2; + /// use retrofire_core::math::{pt2, Point2}; /// - /// let tri = Tri([ - /// vertex(pt2::<_, ()>(0.0, 0.0), ()), - /// vertex(pt2(0.0, 3.0), ()), - /// vertex(pt2(4.0, 0.0), ()), + /// let tri = Tri::([ + /// pt2(0.0, 0.0), + /// pt2(0.0, 3.0), + /// pt2(4.0, 0.0), /// ]); /// assert_eq!(tri.signed_area(), -6.0); /// ``` @@ -214,27 +249,9 @@ impl Tri> { let [t, u] = self.tangents(); t.perp_dot(u) / 2.0 } - - /// Returns the (positive) area of `self`. - /// - /// # Examples - /// ``` - /// use retrofire_core::geom::{vertex, Tri}; - /// use retrofire_core::math::pt2; - /// - /// let tri = Tri([ - /// vertex(pt2::<_, ()>(0.0, 0.0), ()), - /// vertex(pt2(0.0, 3.0), ()), - /// vertex(pt2(4.0, 0.0), ()), - /// ]); - /// assert_eq!(tri.area(), 6.0); - /// ``` - pub fn area(&self) -> f32 { - self.signed_area().abs() - } } -impl Tri> { +impl>> Tri

{ /// Returns the normal vector of `self`. /// /// The result is normalized to unit length. If self is degenerate and @@ -246,13 +263,13 @@ impl Tri> { /// /// use retrofire_core::assert_approx_eq; /// use retrofire_core::geom::{Tri, vertex}; - /// use retrofire_core::math::{pt3, vec3}; + /// use retrofire_core::math::{pt3, vec3, Point3}; /// /// // Triangle lying in a 45° angle - /// let tri = Tri([ - /// vertex(pt3::<_, ()>(0.0, 0.0, 0.0), ()), - /// vertex(pt3(0.0, 3.0, 3.0), ()), - /// vertex(pt3(4.0, 0.0,0.0), ()), + /// let tri = Tri::([ + /// pt3(0.0, 0.0, 0.0), + /// pt3(0.0, 3.0, 3.0), + /// pt3(4.0, 0.0,0.0), /// ]); /// assert_approx_eq!(tri.normal(), vec3(0.0, FRAC_1_SQRT_2, -FRAC_1_SQRT_2)); /// ``` @@ -267,50 +284,28 @@ impl Tri> { /// # Examples /// ``` /// use retrofire_core::geom::{Tri, Plane3, vertex}; - /// use retrofire_core::math::{pt3, Vec3}; + /// use retrofire_core::math::{Point3, Vec3, pt3}; /// - /// let tri = Tri([ - /// vertex(pt3::(0.0, 0.0, 2.0), ()), - /// vertex(pt3(1.0, 0.0, 2.0), ()), - /// vertex(pt3(0.0, 1.0, 2.0), ()) + /// let tri = Tri::([ + /// pt3(0.0, 0.0, 2.0), + /// pt3(1.0, 0.0, 2.0), + /// pt3(0.0, 1.0, 2.0) /// ]); /// assert_eq!(tri.plane().normal(), Vec3::Z); /// assert_eq!(tri.plane().offset(), 2.0); /// ``` pub fn plane(&self) -> Plane3 { let [a, b, c] = &self.0; - let [p, q, r] = [a.pos, b.pos, c.pos]; - Plane::from_points(p, q, r) + Plane::from_points(*a.pos(), *b.pos(), *c.pos()) } /// Returns the winding order of `self`, as projected to the XY plane. // TODO is this 3D version meaningful/useful enough? - pub fn winding(&self) -> Winding { - // TODO better way to xyz->xy... + pub fn winding_xy(&self) -> Winding { let [u, v] = self.tangents(); - let ([ux, uy, _], [vx, vy, _]) = (u.0, v.0); - let z = vec2::<_, ()>(ux, uy).perp_dot(vec2(vx, vy)); + let z = u.xy().perp_dot(v.xy()); if z < 0.0 { Winding::Cw } else { Winding::Ccw } } - - /// Returns the area of `self`. - /// - /// # Examples - /// ``` - /// use retrofire_core::geom::{tri, vertex}; - /// use retrofire_core::math::pt3; - /// - /// let tri = tri( - /// vertex(pt3::<_, ()>(0.0, 0.0, 0.0), ()), - /// vertex(pt3(4.0, 0.0, 0.0), ()), - /// vertex(pt3(0.0, 3.0, 0.0), ()), - /// ); - /// assert_eq!(tri.area(), 6.0); - /// ``` - pub fn area(&self) -> f32 { - let [t, u] = self.tangents(); - t.cross(&u).len() / 2.0 - } } impl Plane3 { @@ -582,7 +577,7 @@ impl Polyline { /// assert_eq!(edges.next(), None); /// ``` pub fn edges(&self) -> impl Iterator> + '_ { - self.0.array_windows().map(|[a, b]| Edge(a, b)) + self.0.array_windows().map(Edge::from) } /// Returns the sum of the lengths of the edges using a custom metric. @@ -732,6 +727,23 @@ impl Line2 { // Local trait impls // +impl Pos for Vertex { + type Type = P; + + fn pos(&self) -> &Self::Type { + &self.pos + } +} + +impl Pos for Point { + type Type = Self; + + /// Returns `self` itself. + fn pos(&self) -> &Self { + self + } +} + impl Parametric for Ray where T: Affine>, @@ -896,6 +908,11 @@ impl From<[T; 2]> for Edge { Edge(a, b) } } +impl<'a, T> From<&'a [T; 2]> for Edge<&'a T> { + fn from([a, b]: &'a [T; 2]) -> Self { + Edge(a, b) + } +} #[cfg(test)] mod tests { @@ -907,48 +924,44 @@ mod tests { use super::*; - type Pt = Point<[f32; N], Real>; - - fn tri( - a: Pt, - b: Pt, - c: Pt, - ) -> Tri, ()>> { - Tri([a, b, c]).map(|p| vertex(p, ())) - } - #[test] fn triangle_winding_2_cw() { - let tri = tri(pt2(-1.0, 0.0), pt2(0.0, 1.0), pt2(1.0, -1.0)); + let tri = tri::(pt2(-1.0, 0.0), pt2(0.0, 1.0), pt2(1.0, -1.0)); assert_eq!(tri.winding(), Winding::Cw); } #[test] fn triangle_winding_2_ccw() { - let tri = tri(pt2(-2.0, 0.0), pt2(1.0, 0.0), pt2(0.0, 1.0)); + let tri = tri::(pt2(-2.0, 0.0), pt2(1.0, 0.0), pt2(0.0, 1.0)); assert_eq!(tri.winding(), Winding::Ccw); } #[test] fn triangle_winding_3_cw() { - let tri = - tri(pt3(-1.0, 0.0, 0.0), pt3(0.0, 1.0, 1.0), pt3(1.0, -1.0, 0.0)); - assert_eq!(tri.winding(), Winding::Cw); + let tri = tri::( + pt3(-1.0, 0.0, 0.0), + pt3(0.0, 1.0, 1.0), + pt3(1.0, -1.0, 0.0), + ); + assert_eq!(tri.winding_xy(), Winding::Cw); } #[test] fn triangle_winding_3_ccw() { - let tri = - tri(pt3(-1.0, 0.0, 0.0), pt3(1.0, 0.0, 0.0), pt3(0.0, 1.0, -1.0)); - assert_eq!(tri.winding(), Winding::Ccw); + let tri = tri::( + pt3(-1.0, 0.0, 0.0), + pt3(1.0, 0.0, 0.0), + pt3(0.0, 1.0, -1.0), + ); + assert_eq!(tri.winding_xy(), Winding::Ccw); } #[test] fn triangle_area_2() { - let tri = tri(pt2(-1.0, 0.0), pt2(2.0, 0.0), pt2(2.0, 1.0)); + let tri = tri::(pt2(-1.0, 0.0), pt2(2.0, 0.0), pt2(2.0, 1.0)); assert_eq!(tri.area(), 1.5); } #[test] fn triangle_area_3() { // base = 3, height = 2 - let tri = tri( + let tri = tri::( pt3(-1.0, 0.0, -1.0), pt3(2.0, 0.0, -1.0), pt3(0.0, 0.0, 1.0), @@ -958,7 +971,7 @@ mod tests { #[test] fn triangle_plane() { - let tri = tri( + let tri = tri::( pt3(-1.0, -2.0, -1.0), pt3(2.0, -2.0, -1.0), pt3(0.0, -2.0, 1.0), diff --git a/core/src/render/debug.rs b/core/src/render/debug.rs index 83b9930f..62387aef 100644 --- a/core/src/render/debug.rs +++ b/core/src/render/debug.rs @@ -3,7 +3,7 @@ use alloc::vec::Vec; -use crate::geom::{Edge, Tri, Vertex, Vertex3, vertex}; +use crate::geom::{Edge, Pos, Tri, Vertex, Vertex3, vertex}; use crate::math::{ Color, Color4, Color4f, Mat4, Point3, Vec3, color::gray, mat::ProjMat3, pt3, vec::ProjVec3, @@ -138,8 +138,8 @@ pub fn cuboid(v0: Point3, v1: Point3) -> DbgBatch { } /// Draws the smallest axis-aligned box that contains a set of vertices. -pub fn bbox(vs: &[Vertex3]) -> DbgBatch { - let BBox(min, max) = vs.iter().map(|v| &v.pos).collect(); +pub fn bbox(pts: &[impl Pos>]) -> DbgBatch { + let BBox(min, max) = pts.iter().map(Pos::pos).collect(); cuboid(min, max) } diff --git a/core/src/render/prim.rs b/core/src/render/prim.rs index 5e546ab2..f414c986 100644 --- a/core/src/render/prim.rs +++ b/core/src/render/prim.rs @@ -26,7 +26,7 @@ impl Render for Tri { #[inline] fn is_backface(tri: &Self::Screen) -> bool { - tri.winding() == Winding::Cw + tri.winding_xy() == Winding::Cw } #[inline] diff --git a/geom/src/io.rs b/geom/src/io.rs index 885f9e6d..9f58a60a 100644 --- a/geom/src/io.rs +++ b/geom/src/io.rs @@ -513,7 +513,7 @@ v 0.0 -2.0 0.0 assert_eq!( m.faces() - .map(|tri| tri.0.map(|&Vertex { pos, attrib: n }| (pos, n))) + .map(|tri| tri.0.map(|v| (v.pos, v.attrib))) .collect::>(), [ [ @@ -551,7 +551,7 @@ v 0.0 -2.0 0.0 assert_eq!( m.faces() - .map(|tri| tri.0.map(|&Vertex { pos, attrib: uv }| (pos, uv))) + .map(|tri| tri.0.map(|v| (v.pos, v.attrib))) .collect::>(), [ [ diff --git a/geom/src/isect.rs b/geom/src/isect.rs index 5342f15e..1fa93f8f 100644 --- a/geom/src/isect.rs +++ b/geom/src/isect.rs @@ -48,19 +48,6 @@ impl LineIntersect { } } -// -// Trait impls -// - -impl Debug for LineIntersect { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - match self { - Self::Point(p) => write!(f, "Point({p:?})"), - Self::Coincident => f.write_str("Coincident"), - } - } -} - // // 3D Intersect impls // @@ -131,7 +118,7 @@ impl Intersect> for Ray3 { } } -impl Intersect> for Ray3 { +impl Intersect> for Ray3 { type Result = RayIntersect3; // Only closest for now /// Returns the nearest intersection point of `self` and a box, @@ -497,6 +484,19 @@ impl Intersect for Edge> { } } +// +// Foreign trait impls +// + +impl Debug for LineIntersect { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + Self::Point(p) => write!(f, "Point({p:.3?})"), + Self::Coincident => f.write_str("Coincident"), + } + } +} + #[cfg(test)] mod tests { use retrofire_core::math::{Linear, Vec3, pt3}; diff --git a/geom/src/solids/lathe.rs b/geom/src/solids/lathe.rs index a681ae9b..cdc4ec77 100644 --- a/geom/src/solids/lathe.rs +++ b/geom/src/solids/lathe.rs @@ -224,11 +224,14 @@ fn make_cap( // Local trait impls // +// TODO impl Build<()> + impl>> Build for Lathe

{ fn build(self) -> Mesh { self.build_with(&mut |p, n, _| vertex(p.to(), n)) } } +// TODO Shouldn't need parametric with normal if normal not used impl>> Build for Lathe

{ fn build(self) -> Mesh { self.build_with(&mut |p, _, tc| vertex(p.to(), tc)) From 8b96136690abe71a5050011b4df58ffdca15877c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Tue, 7 Jul 2026 20:51:27 +0300 Subject: [PATCH 43/76] Fix Dims constant --- core/src/util/dims.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/util/dims.rs b/core/src/util/dims.rs index b8fa3fa8..6c6ea421 100644 --- a/core/src/util/dims.rs +++ b/core/src/util/dims.rs @@ -32,7 +32,7 @@ pub const QXGA_2048_1536: Dims = Dims(2048, 1536); pub const CGA_320_200: Dims = Dims(320, 200); pub const MODE_13H: Dims = CGA_320_200; pub const QCGA_640_400: Dims = Dims(640, 400); -pub const qWXGA_640_400: Dims = Dims(640, 640); +pub const qWXGA_640_400: Dims = Dims(640, 400); pub const WXGA_1280_800: Dims = Dims(1280, 800); pub const WXGAP_1440_900: Dims = Dims(1440, 900); pub const WSXGAP_1680_1050: Dims = Dims(1680, 1050); From fbf0e1d55303327f3ce8dcf7248dbc6ce52d3a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Wed, 8 Jul 2026 16:52:39 +0300 Subject: [PATCH 44/76] Fix varying interpolation in line drawing The `line` function did not properly interpolate vertex attributes, including depth. This led to visual errors especially when drawing lines together with other geometry. Resolves #324. --- core/src/render/raster.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/core/src/render/raster.rs b/core/src/render/raster.rs index d0eae461..8373ab4a 100644 --- a/core/src/render/raster.rs +++ b/core/src/render/raster.rs @@ -13,6 +13,7 @@ use core::{ fmt::{Debug, Formatter}, + iter::zip, mem::swap, ops::Range, }; @@ -142,10 +143,12 @@ where // Adjust y0 to match the rounded x0 let y0 = v0.pos.y() + dy_dx * (x0 - v0.pos.x()); + let vs = + (v0.pos, v0.attrib).vary_to((v1.pos, v1.attrib), dx.abs() as u32); + let (xs, mut y) = (x0 as usize..x1 as usize, y0); - for x in xs { - let vs = (v0.pos, v0.attrib.clone()); - let vs = vs.clone().vary_to(vs, 1); // TODO a bit silly + for (x, v) in zip(xs, vs) { + let vs = v.clone().vary_to(v, 1); // TODO a bit silly scan_fn(Scanline { y: y as usize, xs: x..x + 1, @@ -162,10 +165,12 @@ where // Adjust x0 to match the rounded y0 let x0 = v0.pos.x() + dx_dy * (y0 - v0.pos.y()); + let vs = (v0.pos, v0.attrib).vary_to((v1.pos, v1.attrib), dy as u32); + let mut x = x0; - for y in y0 as usize..y1 as usize { - let vs = (v0.pos, v0.attrib.clone()); - let vs = vs.clone().vary_to(vs.clone(), 1); + let ys = y0 as usize..y1 as usize; + for (y, v) in zip(ys, vs) { + let vs = v.clone().vary_to(v, 1); // silly... scan_fn(Scanline { y, xs: x as usize..x as usize + 1, From d21fec8e11394f4a3846309daf9082b63b2c8063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sun, 28 Jun 2026 23:55:40 +0300 Subject: [PATCH 45/76] Add a mesh wireframe function --- core/src/render/debug.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/core/src/render/debug.rs b/core/src/render/debug.rs index 62387aef..acaaa4cb 100644 --- a/core/src/render/debug.rs +++ b/core/src/render/debug.rs @@ -3,7 +3,7 @@ use alloc::vec::Vec; -use crate::geom::{Edge, Pos, Tri, Vertex, Vertex3, vertex}; +use crate::geom::{Edge, Mesh, Pos, Tri, Vertex, Vertex3, vertex}; use crate::math::{ Color, Color4, Color4f, Mat4, Point3, Vec3, color::gray, mat::ProjMat3, pt3, vec::ProjVec3, @@ -143,6 +143,24 @@ pub fn bbox(pts: &[impl Pos>]) -> DbgBatch { cuboid(min, max) } +/// Draws a wireframe representation of a mesh. +pub fn wireframe(mesh: &Mesh) -> DbgBatch { + let edges: Vec<_> = mesh + .faces + .iter() + .copied() + .flat_map(|Tri([i, j, k])| [Edge(i, j), Edge(j, k), Edge(k, i)]) + .collect(); + + DbgBatch::new( + edges, + mesh.verts + .iter() + .map(|v| vertex(v.pos, dir_to_rgb(v.pos.to_vec()))) + .collect::>(), + ) +} + /// Draws a circle on the XY plane with the given center and radius. #[cfg(feature = "fp")] pub fn circle(o: Point3, r: f32) -> DbgBatch { From 4c987e62034721dbd05cfc1215f88aaa2af1f9a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 9 Jul 2026 01:18:37 +0300 Subject: [PATCH 46/76] Add a type for mesh debug visualizations --- core/src/render/debug.rs | 136 ++++++++++++++++++++++++++++++++------- 1 file changed, 111 insertions(+), 25 deletions(-) diff --git a/core/src/render/debug.rs b/core/src/render/debug.rs index acaaa4cb..445b20c6 100644 --- a/core/src/render/debug.rs +++ b/core/src/render/debug.rs @@ -3,7 +3,7 @@ use alloc::vec::Vec; -use crate::geom::{Edge, Mesh, Pos, Tri, Vertex, Vertex3, vertex}; +use crate::geom::{Edge, Mesh, Normal3, Pos, Tri, Vertex, Vertex3, vertex}; use crate::math::{ Color, Color4, Color4f, Mat4, Point3, Vec3, color::gray, mat::ProjMat3, pt3, vec::ProjVec3, @@ -16,27 +16,15 @@ use super::{Context, Frag, FragmentShader, VertexShader, scene::BBox}; #[derive(Default)] pub struct Shader; -impl<'a, B> VertexShader, &'a ProjMat3> for Shader { - type Output = Vertex; - - fn shade_vertex( - &self, - v: Vertex3, - m: &'a ProjMat3, - ) -> Self::Output { - vertex(m.apply(&v.pos), v.attrib) - } -} - -impl<'a, B> FragmentShader> for Shader { - fn shade_fragment( - &self, - f: Frag, - _: &'a ProjMat3, - ) -> Option { - Some(f.var.to_color4()) - } -} +/// A type used to draw debug visualizations of meshes. +/// +/// Various mesh properties can be visualized: +/// * Edges ("wireframe" rendering) +/// * Face normals +/// * Vertex normals (if any) +/// * Bounding box +/// * Model-space origin and coordinate axes. +pub struct DbgMesh<'a, A, B>(&'a Mesh, DbgBatch); pub type DbgBatch = super::Batch< Vec>, @@ -76,7 +64,7 @@ pub fn ray(o: Point3, dir: Vec3) -> DbgBatch { let b = b.normalize_or_zero(); let c = dir.cross(&b).normalize_or_zero(); - let (head_w, head_h) = (0.04, 0.1); + let (head_w, head_h) = (0.02, 0.04); let a = o + dir - head_h * dir.normalize_or_zero(); let b = head_w * b; let c = head_w * c; @@ -92,11 +80,16 @@ pub fn ray(o: Point3, dir: Vec3) -> DbgBatch { DbgBatch::new(edges.to_vec(), verts.to_vec()) } +/// Draws a unit-length ray denoting the normal vector of a vertex. +pub fn vertex_normal(v: &Vertex3, scale: f32) -> DbgBatch { + ray(v.pos, scale * v.attrib.to()) +} + /// Draws a unit-length ray denoting the normal vector of a triangle. /// /// The ray originates from the triangle's centroid. -pub fn face_normal(tri: &Tri>) -> DbgBatch { - ray(tri.centroid(), tri.normal().to()) +pub fn face_normal(tri: &Tri>, scale: f32) -> DbgBatch { + ray(tri.centroid(), scale * tri.normal().to()) } /// Draws a visualization of an affine basis. @@ -143,7 +136,74 @@ pub fn bbox(pts: &[impl Pos>]) -> DbgBatch { cuboid(min, max) } +/// Creates a `DbgMesh` object from a mesh. +pub fn mesh(mesh: &Mesh) -> DbgMesh { + DbgMesh(mesh, DbgBatch::default()) +} + +// +// Inherent impls +// + +impl<'a, A: Clone, B> DbgMesh<'a, A, B> { + /// Enables drawing the edges as a wireframe representation. + #[must_use] + pub fn edges(mut self) -> Self { + self.1.append(wireframe(self.0)); + self + } + + /// Enables drawing the normal vectors of the faces. + #[must_use] + pub fn face_normals(mut self, scale: f32) -> Self { + for tri in self.0.faces() { + // TODO inefficient + self.1 + .append(face_normal(&tri.map(|v| v.clone()), scale)); + } + self + } + + /// Enables drawing the local-space bounding box of the mesh. + #[must_use] + pub fn bbox(mut self) -> Self { + self.1.append(bbox(&self.0.verts)); + self + } + + /// Enables drawing the coordinate axes of the local space. + #[must_use] + pub fn basis(mut self) -> Self { + self.1.append(basis(Mat4::::identity())); + self + } + + /// Returns a batch for rendering the enabled visualizations. + #[must_use] + pub fn batch(self) -> DbgBatch { + self.1 + } +} + +impl<'a, B> DbgMesh<'a, Normal3, B> { + #[must_use] + pub fn normals(mut self, scale: f32) -> Self { + for v in &self.0.verts { + // TODO inefficient + self.1.append(vertex_normal(v, scale)); + } + self + } +} + /// Draws a wireframe representation of a mesh. +/// +/// This is a convenience shortcut for +/// ```text +/// # use retrofire_core::render::debug; +/// debug::mesh(mesh).edges().patch() +/// ``` +#[must_use] pub fn wireframe(mesh: &Mesh) -> DbgBatch { let edges: Vec<_> = mesh .faces @@ -217,3 +277,29 @@ impl DbgBatch { .shader(Shader) } } + +// +// Trait impls +// + +impl<'a, B> VertexShader, &'a ProjMat3> for Shader { + type Output = Vertex; + + fn shade_vertex( + &self, + v: Vertex3, + m: &'a ProjMat3, + ) -> Self::Output { + vertex(m.apply(&v.pos), v.attrib) + } +} + +impl<'a, B> FragmentShader> for Shader { + fn shade_fragment( + &self, + f: Frag, + _: &'a ProjMat3, + ) -> Option { + Some(f.var.to_color4()) + } +} From 4c5671938cad2f44488c17d7fab6d10f548895f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 9 Jul 2026 02:31:09 +0300 Subject: [PATCH 47/76] Avoid allocating each debug normal separately --- core/src/render/debug.rs | 127 ++++++++++++++++++++++++--------------- 1 file changed, 77 insertions(+), 50 deletions(-) diff --git a/core/src/render/debug.rs b/core/src/render/debug.rs index 445b20c6..7cd7ac45 100644 --- a/core/src/render/debug.rs +++ b/core/src/render/debug.rs @@ -56,28 +56,10 @@ pub fn dir_to_rgb(v: Vec3) -> Color4f { } /// Draws an illustration of a ray. -pub fn ray(o: Point3, dir: Vec3) -> DbgBatch { - let mut b = dir.cross(&Vec3::Y); - if b.len_sqr() < 1e-6 { - b = dir.cross(&Vec3::X); - } - let b = b.normalize_or_zero(); - let c = dir.cross(&b).normalize_or_zero(); - - let (head_w, head_h) = (0.02, 0.04); - let a = o + dir - head_h * dir.normalize_or_zero(); - let b = head_w * b; - let c = head_w * c; - - let verts = [o, o + dir, a + b, a - b, a + c, a - c] - .map(|p| vertex(p, dir_to_rgb(dir))); - #[rustfmt::skip] - let edges = [ - [0, 1], [1, 2], [1, 3], [1, 4], [1, 5], - [2, 4], [2, 5], [3, 4], [3, 5], - ].map(Edge::from); - - DbgBatch::new(edges.to_vec(), verts.to_vec()) +pub fn ray(orig: Point3, dir: Vec3) -> DbgBatch { + let mut batch = DbgBatch::new(); + batch.ray(orig, dir); + batch } /// Draws a unit-length ray denoting the normal vector of a vertex. @@ -127,7 +109,7 @@ pub fn cuboid(v0: Point3, v1: Point3) -> DbgBatch { [0, 4], [1, 5], [2, 6], [3, 7], ].map(Edge::from); - DbgBatch::new(edges.to_vec(), verts.to_vec()) + DbgBatch::with(edges.to_vec(), verts.to_vec()) } /// Draws the smallest axis-aligned box that contains a set of vertices. @@ -137,7 +119,7 @@ pub fn bbox(pts: &[impl Pos>]) -> DbgBatch { } /// Creates a `DbgMesh` object from a mesh. -pub fn mesh(mesh: &Mesh) -> DbgMesh { +pub fn mesh(mesh: &Mesh) -> DbgMesh<'_, A, B> { DbgMesh(mesh, DbgBatch::default()) } @@ -149,7 +131,20 @@ impl<'a, A: Clone, B> DbgMesh<'a, A, B> { /// Enables drawing the edges as a wireframe representation. #[must_use] pub fn edges(mut self) -> Self { - self.1.append(wireframe(self.0)); + let Mesh { faces, verts } = self.0; + + let n_verts = self.1.verts.len(); + let edges = faces + .iter() + .flat_map(Tri::edges) + .map(|Edge(a, b)| Edge(a + n_verts, b + n_verts)); + let verts = verts + .iter() + .map(|v| vertex(v.pos, dir_to_rgb(v.pos.to_vec()))); + + self.1.prims.extend(edges); + self.1.verts.extend(verts); + self } @@ -157,9 +152,7 @@ impl<'a, A: Clone, B> DbgMesh<'a, A, B> { #[must_use] pub fn face_normals(mut self, scale: f32) -> Self { for tri in self.0.faces() { - // TODO inefficient - self.1 - .append(face_normal(&tri.map(|v| v.clone()), scale)); + self.1.face_normal(&tri.map(|v| v.clone()), scale); } self } @@ -187,10 +180,9 @@ impl<'a, A: Clone, B> DbgMesh<'a, A, B> { impl<'a, B> DbgMesh<'a, Normal3, B> { #[must_use] - pub fn normals(mut self, scale: f32) -> Self { + pub fn vertex_normals(mut self, scale: f32) -> Self { for v in &self.0.verts { - // TODO inefficient - self.1.append(vertex_normal(v, scale)); + self.1.vertex_normal(v, scale); } self } @@ -204,21 +196,8 @@ impl<'a, B> DbgMesh<'a, Normal3, B> { /// debug::mesh(mesh).edges().patch() /// ``` #[must_use] -pub fn wireframe(mesh: &Mesh) -> DbgBatch { - let edges: Vec<_> = mesh - .faces - .iter() - .copied() - .flat_map(|Tri([i, j, k])| [Edge(i, j), Edge(j, k), Edge(k, i)]) - .collect(); - - DbgBatch::new( - edges, - mesh.verts - .iter() - .map(|v| vertex(v.pos, dir_to_rgb(v.pos.to_vec()))) - .collect::>(), - ) +pub fn wireframe(msh: &Mesh) -> DbgBatch { + mesh(msh).edges().batch() } /// Draws a circle on the XY plane with the given center and radius. @@ -236,7 +215,7 @@ pub fn circle(o: Point3, r: f32) -> DbgBatch { let edges: Vec<_> = (0..RES).map(|i| Edge(i, i + 1)).collect(); - DbgBatch::new(edges, verts) + DbgBatch::with(edges, verts) } /// Draws a wireframe sphere with the given center and radius. @@ -266,15 +245,63 @@ pub fn sphere(o: Point3, r: f32) -> DbgBatch { }) .collect(); - DbgBatch::new(edges, verts) + DbgBatch::with(edges, verts) } impl DbgBatch { - fn new(prims: Vec>, verts: Vec>) -> Self { + fn new() -> Self { + DbgBatch::::default() + } + + fn with(prims: Vec>, verts: Vec>) -> Self { DbgBatch::::default() .primitives(prims) .vertices(verts) - .shader(Shader) + } + + fn ray(&mut self, orig: Point3, dir: Vec3) -> &mut Self { + let mut b = dir.cross(&Vec3::Y); + if b.len_sqr() < 1e-6 { + b = dir.cross(&Vec3::X); + } + let b = b.normalize_or_zero(); + let c = dir.cross(&b).normalize_or_zero(); + + let (head_w, head_h) = (0.02, 0.04); + let a = orig + dir - head_h * dir.normalize_or_zero(); + let b = head_w * b; + let c = head_w * c; + + let verts = [orig, orig + dir, a + b, a - b, a + c, a - c] + .map(|p| vertex(p, dir_to_rgb(dir))); + #[rustfmt::skip] + let edges = [ + [0, 1], [1, 2], [1, 3], [1, 4], [1, 5], + [2, 4], [2, 5], [3, 4], [3, 5], + ].map(Edge::from); + + let n = self.verts.len(); + self.verts.extend(verts); + self.prims + .extend(edges.into_iter().map(|e| Edge(e.0 + n, e.1 + n))); + + self + } + + fn vertex_normal( + &mut self, + v: &Vertex3, + scale: f32, + ) -> &mut Self { + self.ray(v.pos, scale * v.attrib.to()) + } + + fn face_normal( + &mut self, + tri: &Tri>, + scale: f32, + ) -> &mut Self { + self.ray(tri.centroid(), scale * tri.normal().to()) } } From 679bda27b6224a9145e7f590df2a61f9dad429d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 9 Jul 2026 02:37:15 +0300 Subject: [PATCH 48/76] Add mesh debug visualizations to solids demo Keys 1 to 5 toggle various visualizations. 0 toggles rendering of the mesh itself. --- demos/src/bin/solids.rs | 62 +++++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/demos/src/bin/solids.rs b/demos/src/bin/solids.rs index d0479786..7fd1707d 100644 --- a/demos/src/bin/solids.rs +++ b/demos/src/bin/solids.rs @@ -8,7 +8,7 @@ use re::prelude::*; use re::core::{ geom::{Polyline, Ray}, math::{ProjVec3, color::gray, spline::HermiteSpline}, - render::{Model, ModelToWorld, cam::Fov, shader}, + render::{Model, cam::Fov, debug, shader}, }; use re::front::{Frame, minifb::Window}; use re::geom::{io::read_obj, solids::*}; @@ -93,19 +93,27 @@ fn main() { let translate = translate(-3.0 * Vec3::Z); let mut carousel = Carousel::default(); + let mut debug = [true, false, false, false, false, false]; + win.run(|frame| { let Frame { t, dt, win, .. } = frame; for key in win.imp.get_keys_pressed(KeyRepeat::No) { + use Key::*; match key { - Key::Space => carousel.start(), + Space => carousel.start(), - Key::Comma | Key::Period => { + Comma | Period => { let (num, denom) = - if key == Key::Comma { (3, 4) } else { (4, 3) }; + if key == Comma { (3, 4) } else { (4, 3) }; lod = (lod * num / denom).clamp(3, 50); objects = objects_n(lod); } + + digit @ (Key0 | Key1 | Key2 | Key3 | Key4 | Key5) => { + debug[digit as usize] ^= true; + } + _ => (), } } @@ -118,21 +126,47 @@ fn main() { let model_view_project: ProjMat3 = spin .then(&translate) .then(&carouse) - .to::() + .to() .then(&cam.world_to_project()); let object = &objects[carousel.idx % objects.len()]; - Batch { - prims: &object.faces, - verts: &object.verts, - uniform: (&model_view_project, &spin), - shader: shader, - viewport: cam.viewport, - target: frame.buf, - ctx: &*frame.ctx, + // TODO only needs creating on change + let mut dbg = debug::mesh(object); + if debug[1] { + dbg = dbg.edges(); + } + if debug[2] { + dbg = dbg.face_normals(0.2); + } + if debug[3] { + dbg = dbg.vertex_normals(0.2); + } + if debug[4] { + dbg = dbg.bbox(); + } + if debug[5] { + dbg = dbg.basis(); + } + + dbg.batch() + .uniform(&model_view_project) + .viewport(cam.viewport) + .target(frame.buf) + .render(); + + if debug[0] { + Batch { + prims: &object.faces, + verts: &object.verts, + uniform: (&model_view_project, &spin), + shader: shader, + viewport: cam.viewport, + target: frame.buf, + ctx: &*frame.ctx, + } + .render(); } - .render(); Continue(()) }); From 34aa1aac1694563cc51030e4923466f5096ae9e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sun, 12 Jul 2026 03:53:19 +0300 Subject: [PATCH 49/76] Avoid rebuilding debug visualizations on every frame Refactor the animation+debug state in solids to a new type. --- core/src/render/debug.rs | 21 ++--- demos/src/bin/solids.rs | 163 +++++++++++++++++++++++---------------- 2 files changed, 107 insertions(+), 77 deletions(-) diff --git a/core/src/render/debug.rs b/core/src/render/debug.rs index 7cd7ac45..c531cf1b 100644 --- a/core/src/render/debug.rs +++ b/core/src/render/debug.rs @@ -13,9 +13,10 @@ use crate::math::{Vary, polar, turns, vec3}; use super::{Context, Frag, FragmentShader, VertexShader, scene::BBox}; -#[derive(Default)] +#[derive(Copy, Clone, Default)] pub struct Shader; +#[derive(Clone, Default)] /// A type used to draw debug visualizations of meshes. /// /// Various mesh properties can be visualized: @@ -24,7 +25,7 @@ pub struct Shader; /// * Vertex normals (if any) /// * Bounding box /// * Model-space origin and coordinate axes. -pub struct DbgMesh<'a, A, B>(&'a Mesh, DbgBatch); +pub struct DbgMesh(Mesh, DbgBatch); pub type DbgBatch = super::Batch< Vec>, @@ -119,7 +120,7 @@ pub fn bbox(pts: &[impl Pos>]) -> DbgBatch { } /// Creates a `DbgMesh` object from a mesh. -pub fn mesh(mesh: &Mesh) -> DbgMesh<'_, A, B> { +pub fn mesh(mesh: Mesh) -> DbgMesh { DbgMesh(mesh, DbgBatch::default()) } @@ -127,11 +128,11 @@ pub fn mesh(mesh: &Mesh) -> DbgMesh<'_, A, B> { // Inherent impls // -impl<'a, A: Clone, B> DbgMesh<'a, A, B> { +impl DbgMesh { /// Enables drawing the edges as a wireframe representation. #[must_use] pub fn edges(mut self) -> Self { - let Mesh { faces, verts } = self.0; + let Mesh { faces, verts } = &self.0; let n_verts = self.1.verts.len(); let edges = faces @@ -173,12 +174,12 @@ impl<'a, A: Clone, B> DbgMesh<'a, A, B> { /// Returns a batch for rendering the enabled visualizations. #[must_use] - pub fn batch(self) -> DbgBatch { - self.1 + pub fn batch(&self) -> &DbgBatch { + &self.1 } } -impl<'a, B> DbgMesh<'a, Normal3, B> { +impl DbgMesh { #[must_use] pub fn vertex_normals(mut self, scale: f32) -> Self { for v in &self.0.verts { @@ -196,8 +197,8 @@ impl<'a, B> DbgMesh<'a, Normal3, B> { /// debug::mesh(mesh).edges().patch() /// ``` #[must_use] -pub fn wireframe(msh: &Mesh) -> DbgBatch { - mesh(msh).edges().batch() +pub fn wireframe(m: Mesh) -> DbgBatch { + mesh(m).edges().batch().clone() } /// Draws a circle on the XY plane with the given center and radius. diff --git a/demos/src/bin/solids.rs b/demos/src/bin/solids.rs index 7fd1707d..01f0077f 100644 --- a/demos/src/bin/solids.rs +++ b/demos/src/bin/solids.rs @@ -8,49 +8,29 @@ use re::prelude::*; use re::core::{ geom::{Polyline, Ray}, math::{ProjVec3, color::gray, spline::HermiteSpline}, - render::{Model, cam::Fov, debug, shader}, + render::{Model, cam::Fov, debug, debug::DbgMesh, shader}, }; use re::front::{Frame, minifb::Window}; use re::geom::{io::read_obj, solids::*}; -// Carousel animation for switching between objects. #[derive(Default)] -struct Carousel { +struct State { + lod: u32, + objects: [Mesh; 14], + debug_mesh: DbgMesh, + debug_flags: [bool; 6], + idx: usize, new_idx: usize, - t: Option, -} - -impl Carousel { - fn start(&mut self) { - if self.t.is_none() { - self.t = Some(0.0); - self.new_idx = self.idx + 1; - } else { - // If already started, skip to next - self.new_idx += 1; - } - } - fn update(&mut self, dt: f32) -> Mat4 { - let Some(t) = self.t.as_mut() else { - return Mat4::identity(); - }; - *t += dt; - let t = *t; - if t >= 0.5 { - self.idx = self.new_idx; - } - if t >= 1.0 { - self.t = None - } - rotate_y(turns(smootherstep(t))) - } + anim_t: Option, } fn main() { eprintln!( - "Press to cycle between objects, \ - <.> and <,> to adjust level of detail..." + "[Space] : cycle between objects\n\ + [.] and [,] : adjust level of detail\n\ + [0] : toggle mesh rendering\n\ + [1] to [5] : toggle visualizations" ); let mut win = Window::builder() @@ -80,38 +60,30 @@ fn main() { let col = diffuse * rgb(r, g, b); vertex(mvp.apply(&v.pos), col) } - fn frag_shader(f: Frag, _: Uniform) -> Color4 { f.var.to_color4() } - let shader = shader::new(vtx_shader, frag_shader); - let mut lod = 10; - let mut objects = objects_n(lod); - let translate = translate(-3.0 * Vec3::Z); - let mut carousel = Carousel::default(); - - let mut debug = [true, false, false, false, false, false]; + let mut state = State::new(); win.run(|frame| { let Frame { t, dt, win, .. } = frame; for key in win.imp.get_keys_pressed(KeyRepeat::No) { use Key::*; match key { - Space => carousel.start(), + Space => state.start_carousel(), Comma | Period => { let (num, denom) = if key == Comma { (3, 4) } else { (4, 3) }; - lod = (lod * num / denom).clamp(3, 50); - objects = objects_n(lod); + state.set_lod((state.lod * num / denom).clamp(3, 50)); } digit @ (Key0 | Key1 | Key2 | Key3 | Key4 | Key5) => { - debug[digit as usize] ^= true; + state.toggle_flag(digit as usize); } _ => (), @@ -120,7 +92,7 @@ fn main() { let theta = rads(t.as_secs_f32()); let spin = rotate_x(theta * 0.37).then(&rotate_y(theta * 0.51)); - let carouse = carousel.update(dt.as_secs_f32()); + let carouse = state.update(dt.as_secs_f32()); // Compose transform stack let model_view_project: ProjMat3 = spin @@ -129,33 +101,17 @@ fn main() { .to() .then(&cam.world_to_project()); - let object = &objects[carousel.idx % objects.len()]; - - // TODO only needs creating on change - let mut dbg = debug::mesh(object); - if debug[1] { - dbg = dbg.edges(); - } - if debug[2] { - dbg = dbg.face_normals(0.2); - } - if debug[3] { - dbg = dbg.vertex_normals(0.2); - } - if debug[4] { - dbg = dbg.bbox(); - } - if debug[5] { - dbg = dbg.basis(); - } - - dbg.batch() + state + .debug_mesh + .batch() + .clone() .uniform(&model_view_project) .viewport(cam.viewport) .target(frame.buf) .render(); - if debug[0] { + if state.debug_flags[0] { + let object = state.object(); Batch { prims: &object.faces, verts: &object.verts, @@ -264,3 +220,76 @@ fn dragon() -> &'static Mesh { }); &DRAGON } + +impl State { + fn new() -> Self { + let mut state = State::default(); + state.lod = 10; + state.objects = objects_n(state.lod); + state.debug_flags[0] = true; + state + } + + fn object(&self) -> &Mesh { + &self.objects[self.idx] + } + + fn set_lod(&mut self, lod: u32) { + self.lod = lod; + self.objects = objects_n(lod); + self.build_debug_mesh(); + } + + fn toggle_flag(&mut self, flag: usize) { + self.debug_flags[flag] ^= true; + self.build_debug_mesh(); + } + + fn start_carousel(&mut self) { + if self.anim_t.is_none() { + self.anim_t = Some(0.0); + self.new_idx = self.idx + 1; + } else { + // If already started, skip to next + self.new_idx += 1; + } + self.new_idx %= self.objects.len(); + } + + fn update(&mut self, dt: f32) -> Mat4 { + let Some(t) = self.anim_t.as_mut() else { + return Mat4::identity(); + }; + *t += dt; + let t = *t; + if t >= 0.5 && self.idx != self.new_idx { + self.idx = self.new_idx; + self.build_debug_mesh(); + } + if t >= 1.0 { + self.anim_t = None + } + rotate_y(turns(smootherstep(t))) + } + + fn build_debug_mesh(&mut self) { + let [_, ed, fns, vns, bb, bas] = self.debug_flags; + let mut dm = debug::mesh(self.objects[self.idx].clone()); + if ed { + dm = dm.edges(); + } + if fns { + dm = dm.face_normals(0.2); + } + if vns { + dm = dm.vertex_normals(0.2); + } + if bb { + dm = dm.bbox(); + } + if bas { + dm = dm.basis(); + } + self.debug_mesh = dm; + } +} From bd701be73fa19b01ebbf5ec426ceae4f0d72f267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sun, 12 Jul 2026 21:56:51 +0300 Subject: [PATCH 50/76] Make Matrix::row_vec() and col_vec() const col_vec has a pretty unfortunate implementation due to const limitations... --- core/src/math/mat.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/core/src/math/mat.rs b/core/src/math/mat.rs index 3a61a5a8..a8b84dea 100644 --- a/core/src/math/mat.rs +++ b/core/src/math/mat.rs @@ -156,8 +156,10 @@ where /// /// let m: Mat2 = mat![1.0, 2.0; 3.0, 4.0]; /// assert_eq!(m.row_vec(0), vec2(1.0, 2.0)); + /// assert_eq!(m.row_vec(1), vec2(3.0, 4.0)); + /// ``` #[inline] - pub fn row_vec(&self, i: usize) -> Vector<[Sc; N], Map::Source> { + pub const fn row_vec(&self, i: usize) -> Vector<[Sc; N], Map::Source> { Vector::new(self.0[i]) } @@ -173,10 +175,19 @@ where /// use retrofire_core::{mat, math::{vec2, Mat2}}; /// /// let m: Mat2 = mat![1.0, 2.0; 3.0, 4.0]; + /// assert_eq!(m.col_vec(0), vec2(1.0, 3.0)); /// assert_eq!(m.col_vec(1), vec2(2.0, 4.0)); + /// ``` #[inline] - pub fn col_vec(&self, i: usize) -> Vector<[Sc; M], Map::Dest> { - Vector::new(self.0.map(|row| row[i])) + pub const fn col_vec(&self, i: usize) -> Vector<[Sc; M], Map::Dest> { + // Manual loop for constness... + let mut res = [self.0[0][i]; M]; // No traits in const + let mut j = 1; + while j < M { + res[j] = self.0[j][i]; + j += 1; + } + Vector::new(res) } } impl From 5d7da155c303e41b40c6cd996af11963dc6f224b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sun, 12 Jul 2026 22:44:03 +0300 Subject: [PATCH 51/76] Add to_affine() method to Mat2 and Mat3 --- core/src/math/mat.rs | 56 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/core/src/math/mat.rs b/core/src/math/mat.rs index a8b84dea..2dba133e 100644 --- a/core/src/math/mat.rs +++ b/core/src/math/mat.rs @@ -7,6 +7,7 @@ use core::{ array, fmt::{self, Debug, Formatter}, + hint::cold_path, marker::PhantomData as Pd, ops::Range, }; @@ -397,6 +398,24 @@ impl Mat2 { self.checked_inverse() .expect("matrix cannot be singular or near-singular") } + + /// Returns the affine 3x3 matrix corresponding to `self`. + /// + /// # Examples + /// ``` + /// use retrofire_core::{mat, math::Mat2}; + /// + /// let m: Mat2 = mat![0.0, 2.0; 3.0, 0.0]; + /// + /// assert_eq!(m.to_affine(), mat![ + /// 0.0, 2.0, 0.0; + /// 3.0, 0.0, 0.0; + /// 0.0, 0.0, 1.0 + /// ]); + /// ``` + pub const fn to_affine(&self) -> Mat3 { + Mat3::from_affine(self.col_vec(0), self.col_vec(1), pt2(0.0, 0.0)) + } } impl Mat3 { @@ -504,6 +523,29 @@ impl Mat3 { pub const fn origin(&self) -> Point2 { self.translation().to_pt() } +} + +impl Mat3 { + /// Returns the 4x4 affine equivalent of `self`. + pub const fn to_affine(&self) -> Mat4 { + let [[a, b, c], [d, e, f], [g, h, i]] = self.0; + mat![ + a, b, c, 0.0; + d, e, f, 0.0; + g, h, i, 0.0; + 0.0, 0.0, 0.0, 1.0; + ] + } +} + +impl Mat3 { + #[inline] + const fn is_affine(&self) -> bool { + let [g, h, i] = self.0[2]; + let affine = g == 0.0 && h == 0.0 && i == 1.0; + + if DIM == 2 { likely(affine) } else { affine } + } /// Returns the determinant of `self`. pub const fn determinant(&self) -> f32 { @@ -805,6 +847,20 @@ impl Mat4 { debug_assert!(inv.is_finite()); inv.to() } + + #[inline] + const fn is_affine(&self) -> bool { + let [a, b, c, d] = self.0[3]; + likely(a == 0.0 && b == 0.0 && c == 0.0 && d == 1.0) + } +} + +#[inline] +const fn likely(cond: bool) -> bool { + if !cond { + cold_path() + } + cond } // From 6dbeb5d47c994cf114dc672a1afa765d70f0c507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sun, 12 Jul 2026 22:48:06 +0300 Subject: [PATCH 52/76] Improve matrix doc comments and doctests --- core/src/math/mat.rs | 76 +++++++++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/core/src/math/mat.rs b/core/src/math/mat.rs index 2dba133e..54080ed9 100644 --- a/core/src/math/mat.rs +++ b/core/src/math/mat.rs @@ -191,6 +191,7 @@ where Vector::new(res) } } + impl Matrix<[[Sc; N]; N], RealToReal> { @@ -318,12 +319,15 @@ impl Mat2 { /// /// # Examples /// ``` - /// use retrofire_core::math::Mat2; + /// use retrofire_core::{mat, math::Mat2}; /// - /// let double: Mat2 = [[2.0, 0.0], [0.0, 2.0]].into(); + /// let double: Mat2 = mat![2.0, 0.0; 0.0, 2.0]; /// assert_eq!(double.determinant(), 4.0); /// - /// let singular: Mat2 = [[1.0, 0.0], [2.0, 0.0]].into(); + /// let flip_y: Mat2 = mat![1.0, 0.0; 0.0, -1.0]; + /// assert_eq!(flip_y.determinant(), -1.0); + /// + /// let singular: Mat2 = mat![1.0, 0.0; 2.0, 0.0]; /// assert_eq!(singular.determinant(), 0.0); /// ``` #[inline] @@ -475,13 +479,13 @@ impl Mat3 { /// use retrofire_core::{mat, math::*}; /// /// let m: Mat3 = mat![ - /// 2.0, 0.0, 4.0; - /// 0.0, 3.0, 5.0; + /// 1.0, 0.0, 3.0; + /// 0.0, 2.0, 4.0; /// 0.0, 0.0, 1.0; /// ]; /// assert_eq!(m.linear(), mat![ - /// 2.0, 0.0; - /// 0.0, 3.0; + /// 1.0, 0.0; + /// 0.0, 2.0 /// ]); /// ``` pub const fn linear(&self) -> Mat2 { @@ -491,16 +495,18 @@ impl Mat3 { /// Returns the translation column vector of `self`. /// - /// # Example + /// Use [`origin`][Self::origin] to get the translation as a point. + /// + /// # Examples /// ``` /// use retrofire_core::{mat, math::*}; /// /// let m: Mat3 = mat![ - /// 2.0, 0.0, 4.0; - /// 0.0, 3.0, 5.0; + /// 1.0, 0.0, 3.0; + /// 0.0, 2.0, 4.0; /// 0.0, 0.0, 1.0; /// ]; - /// assert_eq!(m.translation(), vec2(4.0, 5.0)); + /// assert_eq!(m.translation(), vec2(3.0, 4.0)); /// ``` pub const fn translation(&self) -> Vec2 { let [r, s, _] = self.0; @@ -509,17 +515,20 @@ impl Mat3 { /// Returns the translation column vector of `self` as a point. /// + /// Use [`translation`][1] to get the translation as a vector. + /// /// # Example /// ``` /// use retrofire_core::{mat, math::*}; /// /// let m: Mat3 = mat![ - /// 2.0, 0.0, 4.0; - /// 0.0, 3.0, 5.0; + /// 1.0, 0.0, 3.0; + /// 0.0, 2.0, 4.0; /// 0.0, 0.0, 1.0; /// ]; - /// assert_eq!(m.origin(), pt2(4.0, 5.0)); + /// assert_eq!(m.origin(), pt2(3.0, 4.0)); /// ``` + /// [1]: Self::translation pub const fn origin(&self) -> Point2 { self.translation().to_pt() } @@ -567,7 +576,7 @@ impl Mat3 { /// /// 1. Remove the given row and column from `self` to get a 2x2 submatrix; /// 2. Compute its determinant; - /// 3. If exactly one of `row` and `col` is even, multiply by -1. + /// 3. If `row` is even XOR `col` is even, multiply by -1. #[inline] const fn cofactor(&self, row: usize, col: usize) -> f32 { // This automatically takes care of the negation @@ -575,7 +584,8 @@ impl Mat3 { let r2 = (row + 2) % 3; let c1 = (col + 1) % 3; let c2 = (col + 2) % 3; - self.0[r1][c1] * self.0[r2][c2] - self.0[r1][c2] * self.0[r2][c1] + let m = self.0; + m[r1][c1] * m[r2][c2] - m[r1][c2] * m[r2][c1] } /// Returns the inverse of `self`, or `None` if `self` is singular. @@ -625,12 +635,12 @@ impl Mat3 { Some(Mat3::new(res)) } - /// TODO + /// Returns the inverse of self. /// /// # Panics - /// If the matrix is singular or near-singular. + /// If the inverse does not exist (the matrix is singular or near-singular). #[must_use] - pub fn inverse(&self) -> Mat3 { + pub fn inverse(&self) -> Mat3 { self.checked_inverse() .expect("matrix cannot be singular or near-singular") } @@ -675,8 +685,9 @@ impl Mat4 { /// let m = scale(5.0).then(&translate((1.0, 2.0, 3.0))); /// let pt = pt3(1.0, -1.0, 0.5); /// - /// // Only the scale is applied because the translate is not linear + /// // Only scaling is applied because translation is not linear /// assert_approx_eq!(m.linear().apply(&pt), pt3(5.0, -5.0, 2.5)); + /// ``` pub const fn linear(&self) -> Mat3 { let [r, s, t, _] = self.0; mat![ @@ -688,6 +699,8 @@ impl Mat4 { /// Returns the translation column vector of `self`. /// + /// Use [`origin`][Self::origin] to get the translation as a point. + /// /// # Example /// ``` /// use retrofire_core::math::*; @@ -695,12 +708,15 @@ impl Mat4 { /// let trans = vec3(1.0, 2.0, 3.0); /// let m = scale(5.0).then(&translate(trans)); /// assert_eq!(m.translation(), trans); + /// ``` pub const fn translation(&self) -> Vec3 { vec3(self.0[0][3], self.0[1][3], self.0[2][3]) } /// Returns the translation column vector of `self` as a point. /// + /// Use [`translation`][1] to get the translation as a vector. + /// /// # Example /// ``` /// use retrofire_core::math::*; @@ -708,6 +724,8 @@ impl Mat4 { /// let trans = vec3(1.0, 2.0, 3.0); /// let m = scale(5.0).then(&translate(trans)); /// assert_eq!(m.origin(), pt3(1.0, 2.0, 3.0)); + /// ``` + /// [1]: Self::translation pub const fn origin(&self) -> Point3 { self.translation().to_pt() } @@ -721,13 +739,16 @@ impl Mat4 { /// ⎜ i j k l ⎟ /// ⎝ m n o p ⎠ /// ``` - /// its determinant can be computed by multiplying each element *e* on row 0 - /// with its *minors*: the determinant of the submatrix obtained by removing - /// the row and column of *e*: + /// its determinant can be computed by multiplying each element *x* on some + /// row *n* with the determinant of its *minor*, the submatrix obtained by + /// removing the row and column of *x*. + /// + /// When M is affine, its determinant is exactly the determinant of its + /// top-right 3x3 submatrix. This is easy to show by choosing *n* = 3: /// ```text - /// ⎜ f g h ⎜ ⎜ e g h ⎜ - /// det(M) = a · ⎜ j k l ⎜ - b · ⎜ i k l ⎜ + c * ··· - d * ··· - /// ⎜ n o p ⎜ ⎜ m o p ⎜ + /// ⎜ b c d ⎜ ⎜ a c d ⎜ ⎜ a c d ⎜ + /// det(M) = 0 · ⎜ f g h ⎜ + 0 · ··· - 0 * ··· + 1 · ⎜ e g h ⎜ = ⎜ e g h ⎜ + /// ⎜ j k l ⎜ ⎜ i k l ⎜ ⎜ i k l ⎜ /// ``` pub fn determinant(&self) -> f32 { let [[a, b, c, d], r, s, t] = self.0; @@ -897,10 +918,12 @@ impl ApproxEq for Matrix where Repr: ApproxEq, { + #[inline] fn approx_eq_eps(&self, other: &Self, rel_eps: &E) -> bool { self.0.approx_eq_eps(&other.0, rel_eps) } + #[inline] fn relative_epsilon() -> E { Repr::relative_epsilon() } @@ -1384,6 +1407,7 @@ pub const fn perspective( /// # Parameters /// * `lbn`: The left-bottom-near corner of the projection box. /// * `rtf`: The right-bottom-far corner of the projection box. +// TODO Take a Range like `viewport` does? Or have `viewport` take separate? pub const fn orthographic(lbn: Point3, rtf: Point3) -> ProjMat3 { // Done manually due until const traits are stable let [x0, y0, z0] = lbn.0; From 3ce2513cfa43d5255c633189cc38dd4086a77e35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sun, 12 Jul 2026 22:57:15 +0300 Subject: [PATCH 53/76] Optimize matrix determinant and inversion If the last row is 0, ..., 1 (which should always be for affine matrices) inverse and determinant calculations can be simplified quite a bit. --- core/src/math/mat.rs | 158 ++++++++++++++++++++++++++++++------------- 1 file changed, 110 insertions(+), 48 deletions(-) diff --git a/core/src/math/mat.rs b/core/src/math/mat.rs index 54080ed9..d7fbd87e 100644 --- a/core/src/math/mat.rs +++ b/core/src/math/mat.rs @@ -397,6 +397,7 @@ impl Mat2 { /// // This will panic /// let _ = singular.inverse(); /// ``` + #[inline] #[must_use] pub const fn inverse(&self) -> Mat2 { self.checked_inverse() @@ -417,6 +418,7 @@ impl Mat2 { /// 0.0, 0.0, 1.0 /// ]); /// ``` + #[inline] pub const fn to_affine(&self) -> Mat3 { Mat3::from_affine(self.col_vec(0), self.col_vec(1), pt2(0.0, 0.0)) } @@ -508,6 +510,7 @@ impl Mat3 { /// ]; /// assert_eq!(m.translation(), vec2(3.0, 4.0)); /// ``` + #[inline] pub const fn translation(&self) -> Vec2 { let [r, s, _] = self.0; vec2(r[2], s[2]) @@ -529,6 +532,7 @@ impl Mat3 { /// assert_eq!(m.origin(), pt2(3.0, 4.0)); /// ``` /// [1]: Self::translation + #[inline] pub const fn origin(&self) -> Point2 { self.translation().to_pt() } @@ -550,23 +554,22 @@ impl Mat3 { impl Mat3 { #[inline] const fn is_affine(&self) -> bool { - let [g, h, i] = self.0[2]; - let affine = g == 0.0 && h == 0.0 && i == 1.0; - + // No array == in const... + let affine = matches!(self.0[2], [0.0, 0.0, 1.0]); if DIM == 2 { likely(affine) } else { affine } } /// Returns the determinant of `self`. - pub const fn determinant(&self) -> f32 { - let [a, b, c] = self.0[0]; - - // assert!(g == 0.0 && h == 0.0 && i == 1.0); - // TODO If affine (as should be), reduces to: - // a * e - b * d - - a * self.cofactor(0, 0) - + b * self.cofactor(0, 1) - + c * self.cofactor(0, 2) + #[inline] + pub fn determinant(&self) -> f32 { + let [[a, b, c], [d, e, _], _] = self.0; + if self.is_affine() { + a * e - b * d + } else { + a * self.cofactor(0, 0) + + b * self.cofactor(0, 1) + + c * self.cofactor(0, 2) + } } /// Returns the cofactor of the element at the given row and column. @@ -588,6 +591,8 @@ impl Mat3 { m[r1][c1] * m[r2][c2] - m[r1][c2] * m[r2][c1] } + // TODO separate impls for DIM 2 and 3 + /// Returns the inverse of `self`, or `None` if `self` is singular. /// /// # Examples @@ -606,39 +611,55 @@ impl Mat3 { /// ])); /// ``` #[must_use] - pub const fn checked_inverse(&self) -> Option> { + pub fn checked_inverse(&self) -> Option> { let det = self.determinant(); if det.abs() < 1e-6 { return None; } - - // Inverse is transpose of cofactor matrix divided by determinant - let mut res = [[0.0; 3]; 3]; let r_det = 1.0 / det; - let mut i = 0; - while i < 3 { - res[i][0] = r_det * self.cofactor(0, i); - res[i][1] = r_det * self.cofactor(1, i); - res[i][2] = r_det * self.cofactor(2, i); - i += 1; - } - /*let c_a = self.cofactor(0, 0); // = e - let c_b = self.cofactor(0, 1); // = d - let c_c = self.cofactor(0, 2); // = 0 - let c_d = self.cofactor(1, 0); // = b - let c_e = self.cofactor(1, 1); // = a - let c_f = self.cofactor(1, 2); // = 0 - let c_g = self.cofactor(2, 0); // = b * f - c * e - let c_h = self.cofactor(2, 1); // = a * f - c * d - let c_i = self.cofactor(2, 2); // = a * e - b * d*/ - Some(Mat3::new(res)) + // Inverse is transpose of cofactor matrix divided by determinant: + // + // 1 ( co(a) co(d) co(g) ) + // --- ( co(b) co(e) co(h) ) + // det ( co(c) co(f) co(i) ) + + if self.is_affine() { + // When (g, h, i) = (0, 0, 1), simplifies to: + // 1 ( e -b bf-ce ) + // --- ( -d a cd-af ) + // det ( 0 0 ae-bd ) + // ^^^^^--- = 1 after div by det + + let [[a, b, c], [d, e, f], _] = self.0; + let a_ = a * r_det; + let b_ = b * r_det; + let d_ = d * r_det; + let e_ = e * r_det; + Some(mat![ + e_, -b_, b_ * f - c * e_; + -d_, a_, c * d_ - a_ * f; + 0.0, 0.0, 1.0; + ]) + } else { + // No for or from_fn in const :( + let mut res = [[0.0; 3]; 3]; + let mut i = 0; + while i < 3 { + res[i][0] = r_det * self.cofactor(0, i); + res[i][1] = r_det * self.cofactor(1, i); + res[i][2] = r_det * self.cofactor(2, i); + i += 1; + } + Some(Mat3::new(res)) + } } /// Returns the inverse of self. /// /// # Panics /// If the inverse does not exist (the matrix is singular or near-singular). + #[inline] #[must_use] pub fn inverse(&self) -> Mat3 { self.checked_inverse() @@ -688,6 +709,7 @@ impl Mat4 { /// // Only scaling is applied because translation is not linear /// assert_approx_eq!(m.linear().apply(&pt), pt3(5.0, -5.0, 2.5)); /// ``` + #[inline] pub const fn linear(&self) -> Mat3 { let [r, s, t, _] = self.0; mat![ @@ -709,6 +731,7 @@ impl Mat4 { /// let m = scale(5.0).then(&translate(trans)); /// assert_eq!(m.translation(), trans); /// ``` + #[inline] pub const fn translation(&self) -> Vec3 { vec3(self.0[0][3], self.0[1][3], self.0[2][3]) } @@ -726,6 +749,7 @@ impl Mat4 { /// assert_eq!(m.origin(), pt3(1.0, 2.0, 3.0)); /// ``` /// [1]: Self::translation + #[inline] pub const fn origin(&self) -> Point3 { self.translation().to_pt() } @@ -751,14 +775,17 @@ impl Mat4 { /// ⎜ j k l ⎜ ⎜ i k l ⎜ ⎜ i k l ⎜ /// ``` pub fn determinant(&self) -> f32 { - let [[a, b, c, d], r, s, t] = self.0; - - let det2 = |m, n| s[m] * t[n] - s[n] * t[m]; - let det3 = - |j, k, l| r[j] * det2(k, l) - r[k] * det2(j, l) + r[l] * det2(j, k); - - a * det3(1, 2, 3) - b * det3(0, 2, 3) + c * det3(0, 1, 3) - - d * det3(0, 1, 2) + if self.is_affine() { + self.linear().determinant() + } else { + let [[a, b, c, d], r, s, t] = self.0; + let det2 = |m, n| s[m] * t[n] - s[n] * t[m]; + let det3 = |j, k, l| { + r[j] * det2(k, l) - r[k] * det2(j, l) + r[l] * det2(j, k) + }; + a * det3(1, 2, 3) - b * det3(0, 2, 3) + c * det3(0, 1, 3) + - d * det3(0, 1, 2) + } } #[must_use] @@ -786,8 +813,8 @@ impl Mat4 { /// /// # Panics /// If debug assertions are enabled, panics if `self` is singular or - /// near-singular. If not enabled, the return value is unspecified and - /// may contain non-finite values (infinities and NaNs). + /// near-singular. Otherwise, the return value is unspecified and may + /// contain non-finite values (infinities and NaNs). // TODO example #[must_use] pub fn inverse(&self) -> Mat4 { @@ -818,6 +845,14 @@ impl Mat4 { ); } + if self.is_affine() { + let lin: Mat3<(), (), 3> = self.linear().to(); + let trans: Vec3 = self.translation().to(); + return translate(-trans) + .then(&lin.inverse().to_affine()) + .to(); + } + // This algorithm attempts to reduce `this` to the identity matrix // by simultaneously applying elementary row operations to it and // another matrix `inv` which starts as the identity matrix. Once @@ -851,9 +886,9 @@ impl Mat4 { } // now in upper echelon form, back-substitute variables for &idx in &[3, 2, 1] { - let diag = this.0[idx][idx]; + let r_diag = this.0[idx][idx].recip(); for r in 0..idx { - let x = this.0[r][idx] / diag; + let x = this.0[r][idx] * r_diag; sub_row(this, idx, r, x); sub_row(inv, idx, r, x); @@ -1692,6 +1727,27 @@ mod tests { ); } + #[test] + fn inversion() { + let sc = scale((1.0, -2.0, 5.0)); + assert_eq!(sc.inverse(), scale((1.0, -0.5, 0.2))); + + let rot = rotate_x(degs(123.0)); + assert_approx_eq!(rot.inverse(), rotate_x(degs(-123.0))); + + let tr = translate((1.0, 2.0, -3.0)); + assert_eq!(tr.inverse(), translate((-1.0, -2.0, 3.0))); + + let sc_rot_trans = sc.then(&rot).then(&tr); + + assert_approx_eq!( + sc_rot_trans.inverse(), + translate((-1.0, -2.0, 3.0)) + .then(&rotate_x(degs(-123.0))) + .then(&scale((1.0, -0.5, 0.2))) + ); + } + #[test] fn scaling() { let m = scale((1.0, -2.0, 3.0)); @@ -1946,6 +2002,12 @@ mod tests { assert_approx_eq!(rot.determinant(), 1.0); } + #[test] + fn determinant_of_translation_is_one() { + let trans = translate((2.0, 3.0, 4.0)); + assert_eq!(trans.determinant(), 1.0); + } + #[test] fn matrix_composed_with_inverse_is_identity() { let m: Mat4 = translate((1.0e3, -2.0e2, 0.0)) @@ -1954,8 +2016,8 @@ mod tests { let m_inv: Mat4 = m.inverse(); - assert_eq!(m.compose(&m_inv), Mat4::identity()); - assert_eq!(m_inv.compose(&m), Mat4::identity()); + assert_eq!(m.then(&m_inv), >::identity()); + assert_eq!(m.compose(&m_inv), >::identity()); } #[test] From af9b57ea49514cae80097e1579712b196a072ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Mon, 11 May 2026 23:50:51 +0300 Subject: [PATCH 54/76] Considerably improve matrix doc comments and doctests Add a comprehensive API overview to the module docs. --- core/src/math/mat.rs | 341 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 299 insertions(+), 42 deletions(-) diff --git a/core/src/math/mat.rs b/core/src/math/mat.rs index d7fbd87e..3e631989 100644 --- a/core/src/math/mat.rs +++ b/core/src/math/mat.rs @@ -1,6 +1,179 @@ -//! Matrices and linear and affine transforms. +//! Matrices and linear, affine, and projective transforms. //! -//! TODO Docs +//! Retrofire matrices encode their source and target spaces (coordinate frames) +//! in their types. For example, a `Mat4` may only be used to map +//! points in model space (`Point3`) to points in view space +//! (`Point3`). Similarly, matrices can only be composed (multiplied) +//! if they have compatible source and target spaces. +//! +//! The generic "base" matrix type [`Matrix`](Matrix) rarely needs +//! to be named directly; in normal use only the type aliases [`Mat2`], [`Mat3`], +//! [`Mat4`], and [`ProjMat3`] are needed. These represent 2x2, 3x3, and 4x4 real +//! matrices and 4x4 projective matrices respectively. +//! +//! # Creating matrices +//! +//! ``` +//! use retrofire_core::mat; +//! use retrofire_core::math::{ +//! Mat2, Mat3, Mat4, Vec3, pt3, vec3, +//! }; +//! use retrofire_core::render::{Model, World, View}; +//! +//! // Identity matrix: +//! let id: Mat4 = Mat4::identity(); +//! +//! // () denotes a generic "don't care" frame: +//! let id = Mat4::<()>::identity(); +//! +//! // The source parameter defaults to (); the target parameter defaults to +//! // the source parameter: +//! let id: Mat4 = Mat4::identity(); +//! let id = ::identity(); +//! +//! // Note that this way is ambiguous due to the way Rust resolves methods: +//! // let id = Mat4::identity(); +//! +//! // The `Default` impl gives the identity matrix: +//! let also_id = ::default(); +//! +//! assert_eq!(id, also_id); +//! +//! // From an array: +//! let from_array = ::new([[0.0, 2.0], [3.0, 0.0]]); +//! +//! // With a macro: +//! let from_macro: Mat2 = mat![ +//! 0.0, 2.0; +//! 3.0, 0.0; +//! ]; +//! +//! assert_eq!(from_array, from_macro); +//! +//! // From linear basis vectors: +//! let reflect_x = ::from_linear(-Vec3::X, Vec3::Y, Vec3::Z); +//! +//! // From an affine basis (basis vectors plus origin point): +//! let glide_reflect = ::from_affine( +//! -Vec3::X, Vec3::Y, Vec3::Z, pt3(0.0, 2.0, 0.0) +//! ); +//! ``` +//! +//! # Elementary affine transforms +//! ``` +//! # use retrofire_core::math::*; +//! # use retrofire_core::render::*; +//! use retrofire_core::math::{scale, rotate_x, rotate, translate}; +//! +//! // `scale` takes anything that's `Into`: +//! let sc = scale((1.0, 2.0, 3.0)); +//! let sc = scale(vec3(1.0, 2.0, 3.0)); +//! let sc_uniform = scale(3.0); +//! +//! // Rotation about one of the cardinal axes: +//! let rot_x = rotate_x(degs(90.0)); +//! +//! // Rotation about an arbitrary axis: +//! let rot_arb = rotate(vec3(1.0, 1.0, 0.0), degs(30.0)); +//! +//! // Translation: +//! let tr = translate((1.0, 2.0, 3.0)); +//! let tr_along_z = translate(4.0 * Vec3::Z); +//! +//! // The transform constructors return "()" matrices to avoid type inference +//! // ambiguities. Coerce to the desired mapping with the .to() method: +//! let model_to_view: Mat4 = translate((0.0, 0.0, -4.0)).to(); +//! ``` +//! +//! ## Projection and viewport transforms +//! +//! The `perspective` and `orthographic` functions return view-to-clip space +//! projective matrices. The `viewport` function returns NDC-to-screen space +//! matrices. +//! ``` +//! # use retrofire_core::math::*; +//! +//! // Focal ratio, aspect ratio, and the near-far plane distances. +//! let persp /*: ProjMat3 */ = perspective(1.0, 1.0, 0.1..1000.0); +//! +//! // Left-bottom-near and right-top-far corners of the orthographic clip box. +//! let ortho /*: ProjMat3 */ = orthographic( +//! pt3(-2.0, -1.0, -1.0), +//! pt3(2.0, 1.0, 1.0) +//! ); +//! +//! let viewp /*: Mat4 */ = viewport(pt2(10, 10)..pt2(630, 470)); +//! ``` +//! +//! ## Applying transforms to vectors and points +//! ``` +//! # use retrofire_core::math::*; +//! # let sc = scale((1.0, 2.0, 3.0)); +//! # let tr = translate((1.0, 2.0, 3.0)); +//! +//! let v = vec3(0.0, 1.0, -1.0); +//! +//! assert_eq!(sc.apply(&v), vec3(0.0, 2.0, -3.0)); +//! +//! // In most cases it is unnecessary to manually handle 4D homogeneous +//! // vectors, it is managed by the types. Translations do not affect vectors: +//! assert_eq!(tr.apply(&v), v); +//! // But they affect points: +//! let p = pt3(0.0, 1.0, -1.0); +//! assert_eq!(tr.apply(&p), pt3(1.0, 3.0, 2.0)); +//! ``` +//! +//! ## Composing transforms +//! +//! Transforms can be composed with the methods [`then`][Mat4::then] and +//! [`compose`][Mat4::compose]: +//! ``` +//! # use retrofire_core::math::*; +//! # let sc = scale((1.0, 2.0, 3.0)); +//! # let tr = translate((1.0, 2.0, 3.0)); +//! +//! let scale_then_translate = sc.then(&tr); +//! let translate_then_scale = sc.compose(&tr); +//! +//! let p = pt3(0.0, 1.0, -1.0); +//! assert_eq!(scale_then_translate.apply(&p), pt3(1.0, 4.0, 0.0)); +//! assert_eq!(translate_then_scale.apply(&p), pt3(1.0, 6.0, 6.0)); +//! ``` +//! +//! ## Matrix properties +//! ``` +//! # use retrofire_core::{*, math::*}; +//! # let sc = scale((1.0, 2.0, 3.0)); +//! # let tr = translate((1.0, 2.0, 3.0)); +//! +//! // Row and col vectors +//! assert_eq!(sc.col_vec(1), [0.0, 2.0, 0.0, 0.0].into()); +//! assert_approx_eq!(tr.row_vec(2), [0.0, 0.0, 1.0, 3.0].into()); +//! +//! // Determinant +//! assert_eq!(sc.determinant(), 1.0 * 2.0 * 3.0); +//! assert_approx_eq!(tr.determinant(), 1.0); +//! +//! // Inversion +//! assert_eq!(sc.inverse(), scale((1.0, 1.0/2.0, 1.0/3.0))); +//! assert_eq!(tr.inverse(), translate((-1.0, -2.0, -3.0))); +//! +//! assert_eq!(sc.then(&sc.inverse()), Mat4::identity()); +//! +//! // Checked inversion, returning None if singular: +//! let singular: Mat2 = mat![0.0, 1.0; 0.0, 2.0]; +//! assert_eq!(singular.checked_inverse(), None::); +//! +//! // Decomposition into parts: +//! assert_eq!(sc.linear(), mat![ +//! 1.0, 0.0, 0.0; +//! 0.0, 2.0, 0.0; +//! 0.0, 0.0, 3.0; +//! ]); +//! assert_eq!(tr.translation(), vec3(1.0, 2.0, 3.0)); +//! // origin() is the same as translation(), but returns a point +//! assert_eq!(tr.origin(), pt3(1.0, 2.0, 3.0)); +//! ``` #![allow(clippy::needless_range_loop)] @@ -49,11 +222,14 @@ pub trait Apply { type Output; /// Applies this transform to a value. + /// + /// # Examples + /// For examples, see the [module documentation][self]. #[must_use] fn apply(&self, t: &T) -> Self::Output; } -/// A change of basis in real vector space of dimension `DIM`. +/// Mapping between frames in real vector space of dimension `DIM`. #[derive(Copy, Clone, Default, Eq, PartialEq)] pub struct RealToReal( Pd<(SrcBasis, DstBasis)>, @@ -68,18 +244,19 @@ pub struct RealToProj(Pd); #[derive(Copy, Eq, PartialEq)] pub struct Matrix(pub Repr, Pd); -/// Type alias for a 2x2 float matrix. +/// Type alias for a 2x2 linear matrix. pub type Mat2 = Matrix<[[f32; 2]; 2], RealToReal>; -/// Type alias for a 3x3 float matrix. +/// Type alias for a 3x3 affine matrix. pub type Mat3 = Matrix<[[f32; 3]; 3], RealToReal>; -/// Type alias for a 4x4 float matrix. +/// Type alias for a 4x4 affine matrix. pub type Mat4 = Matrix<[[f32; 4]; 4], RealToReal>; +/// Type alias for a 4x4 projective matrix. pub type ProjMat3 = Matrix<[[f32; 4]; 4], RealToProj>; // @@ -114,6 +291,14 @@ macro_rules! mat { impl Matrix { /// Returns a matrix with the given elements. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::Mat2; + /// + /// let m = ::new([[2.0, 0.0], [0.0, 3.0]]); + /// assert_eq!(m.0, [[2.0, 0.0], [0.0, 3.0]]); + /// ``` #[inline] pub const fn new(els: Repr) -> Self { Self(els, Pd) @@ -130,12 +315,17 @@ impl Matrix { { Matrix::new(self.0) } + + /// Applies this matrix to an object. + /// + /// This is an inherent helper method delegating to the appropriate + /// [`Apply`] implementation. #[inline] - pub fn apply(&self, t: &T) -> >::Output + pub fn apply(&self, obj: &T) -> >::Output where Self: Apply, { - Apply::apply(self, t) + Apply::apply(self, obj) } } @@ -144,9 +334,9 @@ where Sc: Linear + Copy, Map: LinearMap, { - /// Returns the row vector of `self` with index `i`. + /// Returns the row vector of `self` with the given index. /// - /// The returned vector is in space `Map::Source`. + /// The returned vector is in the *source* space of `Self`. /// /// # Panics /// If `i >= M`. @@ -164,9 +354,9 @@ where Vector::new(self.0[i]) } - /// Returns the column vector of `self` with index `i`. + /// Returns the column vector of `self` with the given index. /// - /// The returned vector is in space `Map::Dest`. + /// The returned vector is in the *destination* space of `Self`. /// /// # Panics /// If `i >= N`. @@ -197,6 +387,9 @@ impl { /// Returns `self` with its rows and columns swapped. /// + /// Note that this also swaps the source and destination spaces and thus + /// returns a matrix of a different type. + /// /// # Examples /// ``` /// use retrofire_core::{mat, math::{vec2, Mat2}}; @@ -222,9 +415,7 @@ const fn transpose(a: &mut [[Sc; N]; N]) { while i < N { let mut j = i + 1; while j < N { - let tmp = a[i][j]; - a[i][j] = a[j][i]; - a[j][i] = tmp; + (a[i][j], a[j][i]) = (a[j][i], a[i][j]); j += 1; } i += 1; @@ -245,6 +436,20 @@ impl Matrix<[[f32; N]; N], Map> { /// It is the neutral element of matrix multiplication: /// **A · I** = **I · A** = **A**, as well as matrix-vector /// multiplication: **I·v** = **v**. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{Mat4, Vec3, vec3, scale}; + /// + /// let id = ::identity(); + /// + /// let v: Vec3 = vec3(0.0, -2.0, 1.0); + /// assert_eq!(id.apply(&v), v); + /// + /// let scale = scale((1.0, 2.0, 3.0)); + /// assert_eq!(scale.then(&id), scale); + /// assert_eq!(id.then(&scale), scale); + /// ``` pub const fn identity() -> Self { // Needs const traits to be more generic; // const array::map/from_fn for a nicer impl @@ -278,6 +483,18 @@ where /// (𝗠 ∘ 𝗡) 𝘃 = 𝗠(𝗡 𝘃) /// ``` /// for some matrices 𝗠 and 𝗡 and a vector 𝘃. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{scale, translate, pt3, Point3}; + /// + /// let sc = scale(2.0); + /// let tr = translate((1.0, 2.0, 3.0)); + /// + /// let pt: Point3 = pt3(0.0, -1.0, 1.0); + /// + /// assert_eq!(tr.compose(&sc).apply(&pt), tr.apply(&sc.apply(&pt))); + /// ``` #[inline] #[must_use] pub fn compose( @@ -304,6 +521,18 @@ where /// the resulting matrix is equivalent to first applying `self` and then /// `other`. The call `self.then(other)` is thus equivalent to /// `other.compose(self)`. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{scale, translate, pt3, Point3}; + /// + /// let sc = scale(2.0); + /// let tr = translate((1.0, 2.0, 3.0)); + /// + /// let pt: Point3 = pt3(0.0, -1.0, 1.0); + /// + /// assert_eq!(sc.then(&tr).apply(&pt), tr.apply(&sc.apply(&pt))); + /// ``` #[must_use] #[inline] pub fn then>( @@ -333,7 +562,7 @@ impl Mat2 { #[inline] pub const fn determinant(&self) -> f32 { let [[a, b], [c, d]] = self.0; - a * d - b * c + det2(a, b, c, d) } /// Returns the [inverse][Self::inverse] of `self`, or `None` if `self` @@ -438,13 +667,13 @@ impl Mat3 { /// 0.0, 3.0, 0.0; /// 2.0, 0.0, 0.0; /// 0.0, 0.0, 1.0; - /// ]) + /// ]); /// ``` pub const fn from_linear(i: Vec2, j: Vec2) -> Self { Self::from_affine(i, j, Point2::origin()) } - /// Constructs a matrix from an affine basis, or frame. + /// Constructs a matrix from an affine basis, also called a frame. /// /// The basis does not have to be orthonormal. /// @@ -459,7 +688,7 @@ impl Mat3 { /// 0.0, 3.0, 4.0; /// 2.0, 0.0, 5.0; /// 0.0, 0.0, 1.0; - /// ]) + /// ]); /// ``` pub const fn from_affine( i: Vec2, @@ -564,7 +793,7 @@ impl Mat3 { pub fn determinant(&self) -> f32 { let [[a, b, c], [d, e, _], _] = self.0; if self.is_affine() { - a * e - b * d + det2(a, b, d, e) } else { a * self.cofactor(0, 0) + b * self.cofactor(0, 1) @@ -588,7 +817,7 @@ impl Mat3 { let c1 = (col + 1) % 3; let c2 = (col + 2) % 3; let m = self.0; - m[r1][c1] * m[r2][c2] - m[r1][c2] * m[r2][c1] + det2(m[r1][c1], m[r1][c2], m[r2][c1], m[r2][c2]) } // TODO separate impls for DIM 2 and 3 @@ -620,16 +849,16 @@ impl Mat3 { // Inverse is transpose of cofactor matrix divided by determinant: // - // 1 ( co(a) co(d) co(g) ) - // --- ( co(b) co(e) co(h) ) - // det ( co(c) co(f) co(i) ) + // 1 ⎛ co(a) co(d) co(g) ⎞ + // --- ⎜ co(b) co(e) co(h) ⎟ + // det ⎝ co(c) co(f) co(i) ⎠ if self.is_affine() { // When (g, h, i) = (0, 0, 1), simplifies to: - // 1 ( e -b bf-ce ) - // --- ( -d a cd-af ) - // det ( 0 0 ae-bd ) - // ^^^^^--- = 1 after div by det + // 1 ⎛ e -b bf-ce ⎞ + // ----- ⎜ -d a cd-af ⎟ + // ae-bd ⎝ 0 0 ae-bd ⎠ + // ^^^^^--- = 1 after div by det let [[a, b, c], [d, e, f], _] = self.0; let a_ = a * r_det; @@ -637,9 +866,9 @@ impl Mat3 { let d_ = d * r_det; let e_ = e * r_det; Some(mat![ - e_, -b_, b_ * f - c * e_; - -d_, a_, c * d_ - a_ * f; - 0.0, 0.0, 1.0; + e_, -b_, det2(b_, c, e_, f); + -d_, a_, det2(c, a_, f, d_); + 0.0, 0.0, 1.0; ]) } else { // No for or from_fn in const :( @@ -808,7 +1037,8 @@ impl Mat4 { /// Only matrices with a nonzero determinant have a defined inverse. /// A matrix without an inverse is said to be singular. /// - /// Note: This method uses naive Gauss–Jordan elimination and may + /// This method has a fast path if `self` is affine (which it should + /// always be); otherwise it uses Gauss–Jordan elimination which may /// suffer from imprecision or numerical instability in certain cases. /// /// # Panics @@ -846,6 +1076,7 @@ impl Mat4 { } if self.is_affine() { + // M = LT <=> M^-1 = T^-1 L^-1 let lin: Mat3<(), (), 3> = self.linear().to(); let trans: Vec3 = self.translation().to(); return translate(-trans) @@ -906,8 +1137,8 @@ impl Mat4 { #[inline] const fn is_affine(&self) -> bool { - let [a, b, c, d] = self.0[3]; - likely(a == 0.0 && b == 0.0 && c == 0.0 && d == 1.0) + // no array == in const + likely(matches!(self.0[3], [0.0, 0.0, 0.0, 1.0])) } } @@ -919,6 +1150,12 @@ const fn likely(cond: bool) -> bool { cond } +/// Computes the determinant of the matrix [[a, b], [c, d]]. +#[inline] +const fn det2(a: f32, b: f32, c: f32, d: f32) -> f32 { + a * d - b * c +} + // // Local trait impls // @@ -1208,7 +1445,10 @@ impl From for Matrix { /// Returns a matrix applying a scaling by the given factors. /// /// # Examples -/// See the [`scale`] method for an example. +/// ``` +/// +/// +/// `` pub fn scale(factor: impl Into) -> Mat4 { let [x, y, z] = factor.into().0; mat![ @@ -1299,10 +1539,10 @@ fn orient(new_y: Vec3, new_z: Vec3) -> Mat4 { /// # Example /// ``` /// use retrofire_core::assert_approx_eq; -/// use retrofire_core::math::{Apply, degs, rotate_x, vec3}; +/// use retrofire_core::math::{Apply, degs, rotate_x, Vec3}; /// /// let m = rotate_x(degs(90.0)); -/// assert_approx_eq!(m.apply(&vec3(0.0, 1.0, 0.0)), vec3(0.0, 0.0, 1.0)); +/// assert_approx_eq!(m.apply(&Vec3::Y), Vec3::Z); /// ``` #[cfg(feature = "fp")] pub fn rotate_x(a: Angle) -> Mat4 { @@ -1314,15 +1554,15 @@ pub fn rotate_x(a: Angle) -> Mat4 { 0.0, 0.0, 0.0, 1.0; ] } -/// Returns a matrix applying a 3D rotation about the y-axis (on the xz plane). +/// Returns a matrix applying a 3D rotation about the y-axis (on the zx plane). /// /// # Example /// ``` /// use retrofire_core::assert_approx_eq; -/// use retrofire_core::math::{Apply, degs, rotate_y, vec3}; +/// use retrofire_core::math::{Apply, degs, rotate_y, Vec3}; /// /// let m = rotate_y(degs(90.0)); -/// assert_approx_eq!(m.apply(&vec3(1.0, 0.0, 0.0)), vec3(0.0, 0.0, -1.0)); +/// assert_approx_eq!(m.apply(&Vec3::X), -Vec3::Z); ///``` #[cfg(feature = "fp")] pub fn rotate_y(a: Angle) -> Mat4 { @@ -1338,10 +1578,11 @@ pub fn rotate_y(a: Angle) -> Mat4 { /// # Example /// ``` /// use retrofire_core::assert_approx_eq; -/// use retrofire_core::math::{Apply, degs, rotate_z, vec3}; +/// use retrofire_core::math::{Apply, degs, rotate_z, Vec3}; /// /// let m = rotate_z(degs(90.0)); -/// assert_approx_eq!(m.apply(&vec3(1.0, 0.0, 0.0)), vec3(0.0, 1.0, 0.0)); +/// assert_approx_eq!(m.apply(&Vec3::X), Vec3::Y); +/// ``` #[cfg(feature = "fp")] pub fn rotate_z(a: Angle) -> Mat4 { let (sin, cos) = a.sin_cos(); @@ -1371,6 +1612,9 @@ pub fn rotate_pyr(pitch: Angle, yaw: Angle, roll: Angle) -> Mat4 { } /// Returns a matrix applying a 2D rotation by an angle. +/// +/// # Examples +/// TODO #[cfg(feature = "fp")] pub fn rotate2(a: Angle) -> Mat3 { let (sin, cos) = a.sin_cos(); @@ -1382,6 +1626,9 @@ pub fn rotate2(a: Angle) -> Mat3 { } /// Returns a matrix applying a 3D rotation about an arbitrary axis. +/// +/// # Examples +/// TODO #[cfg(feature = "fp")] pub fn rotate(axis: Vec3, a: Angle) -> Mat4 { // 1. Change of basis such that `axis` is mapped to the z-axis, @@ -1413,6 +1660,9 @@ pub fn rotate(axis: Vec3, a: Angle) -> Mat4 { /// # Panics /// * If any parameter value is nonpositive. /// * If `near_far` is an empty range. +/// +/// # Examples +/// TODO pub const fn perspective( focal_ratio: f32, aspect_ratio: f32, @@ -1442,6 +1692,9 @@ pub const fn perspective( /// # Parameters /// * `lbn`: The left-bottom-near corner of the projection box. /// * `rtf`: The right-bottom-far corner of the projection box. +/// +/// # Examples +/// TODO // TODO Take a Range like `viewport` does? Or have `viewport` take separate? pub const fn orthographic(lbn: Point3, rtf: Point3) -> ProjMat3 { // Done manually due until const traits are stable @@ -1463,6 +1716,9 @@ pub const fn orthographic(lbn: Point3, rtf: Point3) -> ProjMat3 { /// A viewport matrix is used to transform points from the NDC space to /// screen space for rasterization. NDC coordinates (-1, -1, _) are mapped /// to `bounds.start` and NDC coordinates (1, 1, _) to `bounds.end`. +/// +/// # Examples +/// TODO pub const fn viewport(bounds: Range) -> Mat4 { let Range { start, end } = bounds; let [x0, y0] = [start.x() as f32, start.y() as f32]; @@ -1728,6 +1984,7 @@ mod tests { } #[test] + #[cfg(feature = "fp")] fn inversion() { let sc = scale((1.0, -2.0, 5.0)); assert_eq!(sc.inverse(), scale((1.0, -0.5, 0.2))); From 5b2bf3d6b99d3e507dc35d0639e4df7f9e750999 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Mon, 11 May 2026 23:51:05 +0300 Subject: [PATCH 55/76] Add matrix inversion benchmark --- Cargo.toml | 4 ++ benches/mat.rs | 174 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 benches/mat.rs diff --git a/Cargo.toml b/Cargo.toml index facfe89a..295e4361 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,3 +109,7 @@ harness = false [[bench]] name = "noise" harness = false + +[[bench]] +name = "mat" +harness = false diff --git a/benches/mat.rs b/benches/mat.rs new file mode 100644 index 00000000..52640008 --- /dev/null +++ b/benches/mat.rs @@ -0,0 +1,174 @@ +//! Matrix manipulation benchmarks. + +use divan::Bencher; + +use retrofire_core::{ + math::rand::{DefaultRng, Distrib}, + math::{Mat2, Mat3, Mat4, Vec2, Vec3, splat}, +}; + +#[divan::bench] +fn invert2(b: Bencher) { + let rng = &mut DefaultRng::default(); + let vecs = splat(-1e3)..splat(1e3); + + b.with_inputs(|| { + let x: Vec2 = vecs.sample(rng); + let y = x.perp(); + Mat2::new([x.0, y.0]) + }) + .counter(1u32) + .bench_local_values(|m: Mat2| m.inverse()); +} + +#[divan::bench] +fn invert3_lin(b: Bencher) { + let rng = &mut DefaultRng::default(); + let vec3s = splat(-1e3)..splat(1e3); + + b.with_inputs(|| { + let x: Vec3 = vec3s.sample(rng); + let y = Vec3::Z.cross(&x); + let z = x.cross(&y); + Mat3::new([x.0, y.0, z.0]) + }) + .counter(1u32) + .bench_local_values(|m: Mat3<(), (), 3>| m.inverse()); +} + +#[divan::bench] +fn invert3_aff(b: Bencher) { + let rng = &mut DefaultRng::default(); + let vecs = splat(-1e3)..splat(1e3); + + b.with_inputs(|| { + let x: Vec2 = vecs.sample(rng); + let y = x.perp(); + let o: Vec2 = vecs.sample(rng); + + Mat3::from_affine(x, y, o.to_pt()) + }) + .counter(1u32) + .bench_local_values(|m: Mat3| m.inverse()); +} + +#[divan::bench] +fn invert4_aff(b: Bencher) { + let rng = &mut DefaultRng::default(); + let vecs = splat(-1e3)..splat(1e3); + + b.with_inputs(|| { + let x: Vec3 = vecs.sample(rng); + let y = Vec3::Z.cross(&x); + let z = x.cross(&y); + let o: Vec3 = vecs.sample(rng); + + Mat4::from_affine(x, y, z, o.to_pt()) + }) + .counter(1u32) + .bench_local_values(|m: Mat4| m.inverse()); +} + +#[divan::bench] +fn invert4_lin(b: Bencher) { + let rng = &mut DefaultRng::default(); + let vecs = splat(-1e3)..splat(1e3); + + b.with_inputs(|| { + let x: Vec3 = vecs.sample(rng); + let y = Vec3::Z.cross(&x); + let z = x.cross(&y); + let o: Vec3 = vecs.sample(rng); + + let mut mat = Mat4::from_affine(x, y, z, o.to_pt()); + mat.0[3][1] = 0.5; + mat + }) + .counter(1u32) + .bench_local_values(|m: Mat4| m.inverse()); +} + +#[divan::bench] +fn det2(b: Bencher) { + let rng = &mut DefaultRng::default(); + let vecs = splat(-1e3)..splat(1e3); + + b.with_inputs(|| { + let x: Vec2 = vecs.sample(rng); + let y = x.perp(); + Mat2::new([x.0, y.0]) + }) + .counter(1u32) + .bench_local_values(|m: Mat2| m.determinant()); +} + +#[divan::bench] +fn det3_lin(b: Bencher) { + let rng = &mut DefaultRng::default(); + let vec3s = splat(-1e3)..splat(1e3); + + b.with_inputs(|| { + let x: Vec3 = vec3s.sample(rng); + let y = Vec3::Z.cross(&x); + let z = x.cross(&y); + Mat3::new([x.0, y.0, z.0]) + }) + .counter(1u32) + .bench_local_values(|m: Mat3<(), (), 3>| m.determinant()); +} + +#[divan::bench] +fn det3_aff(b: Bencher) { + let rng = &mut DefaultRng::default(); + let vecs = splat(-1e3)..splat(1e3); + + b.with_inputs(|| { + let x: Vec2 = vecs.sample(rng); + let y = x.perp(); + let o: Vec2 = vecs.sample(rng); + + Mat3::from_affine(x, y, o.to_pt()) + }) + .counter(1u32) + .bench_local_values(|m: Mat3| m.determinant()); +} + +#[divan::bench] +fn det4_aff(b: Bencher) { + let rng = &mut DefaultRng::default(); + let vecs = splat(-1e3)..splat(1e3); + + b.with_inputs(|| { + let x: Vec3 = vecs.sample(rng); + let y = Vec3::Z.cross(&x); + let z = x.cross(&y); + let o: Vec3 = vecs.sample(rng); + + Mat4::from_affine(x, y, z, o.to_pt()) + }) + .counter(1u32) + .bench_local_values(|m: Mat4| m.determinant()); +} + +#[divan::bench] +fn det4_lin(b: Bencher) { + let rng = &mut DefaultRng::default(); + let vecs = splat(-1e3)..splat(1e3); + + b.with_inputs(|| { + let x: Vec3 = vecs.sample(rng); + let y = Vec3::Z.cross(&x); + let z = x.cross(&y); + let o: Vec3 = vecs.sample(rng); + + let mut mat = Mat4::from_affine(x, y, z, o.to_pt()); + mat.0[3][1] = 0.5; + mat + }) + .counter(1u32) + .bench_local_values(|m: Mat4| m.determinant()); +} + +fn main() { + divan::main() +} From 78d6c73900cacde1e927d3f79fcd5b15b048908f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sat, 27 Jun 2026 23:50:05 +0300 Subject: [PATCH 56/76] Add module-level documentation to math::vec --- core/src/math/vec.rs | 76 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/core/src/math/vec.rs b/core/src/math/vec.rs index dbfd53ea..70832eed 100644 --- a/core/src/math/vec.rs +++ b/core/src/math/vec.rs @@ -1,8 +1,81 @@ //! Real and projective vectors. //! -//! TODO +//! Vectors in Retrofire are parameterized by the "space", or coordinate frame, +//! that they are defined in. This helps rule out many bugs caused by +//! accidentally mixing vectors defined in different frames. //! +//! The most commonly used vector types are [`Vec2`] and [`Vec3`] as well as +//! their integer counterparts [`Vec2i`] and [`Vec3i`]. Unlike most similar +//! libraries, there's no `Vec4`, and there is in most cases no need to +//! directly manage homogeneous vectors. //! +//! # Creating vectors +//! ``` +//! use retrofire_core::math::{Vec3, vec3, splat}; +//! +//! // The following lines are equivalent: +//! let x: Vec3 = vec3(1.0, 0.0, 0.0); +//! let x = ::new([1.0, 0.0, 0.0]); +//! let x: Vec3 = Vec3::X; +//! let x: Vec3 = [1.0, 0.0, 0.0].into(); +//! +//! // Use `splat` to broadcast a scalar: +//! let one: Vec3 = splat(1.0); +//! assert_eq!(one, vec3(1.0, 1.0, 1.0)); +//! ``` +//! +//! # Components and properties +//! ``` +//! # use retrofire_core::math::{Vec3, vec3}; +//! let mut v: Vec3 = vec3(1.0, 2.0, 3.0); +//! +//! // Accessing all components (note that .into() does not work with +//! // assert_eq!() due to type inference ambiguity) +//! assert_eq!(v.0, [1.0, 2.0, 3.0]); +//! assert_eq!(<[_;_]>::from(v), [1.0, 2.0, 3.0]); +//! assert_eq!(<(_,_,_)>::from(v), (1.0, 2.0, 3.0)); +//! +//! // Accessing an ndividual component +//! assert_eq!(v.y(), 2.0); +//! assert_eq!(v[2], 3.0); +//! v[0] = 4.0; +//! assert_eq!(v.x(), 4.0); +//! ``` +//! +//! # Vector operations +//! ``` +//! # use retrofire_core::math::*; +//! let v: Vec3 = vec3(1.0, 2.0, 3.0); +//! +//! // Standard overloaded operators are provided: +//! assert_eq!(v + v, vec3(2.0, 4.0, 6.0)); +//! assert_eq!(-v, vec3(-1.0, -2.0, -3.0)); +//! assert_eq!(1.5 * v, vec3(1.5, 3.0, 4.5)); +//! +//! // Length and normalization: +//! assert_eq!(v.len(), f32::sqrt(1.0 + 4.0 + 9.0)); +//! // Use ´len_sqr` to avoid the square root when unneeded: +//! assert_eq!(v.len_sqr(), 1.0 + 4.0 + 9.0); +//! +//! assert_eq!(v.normalize().len(), 0.99999994); +//! assert_eq!(v.normalize_approx().len(), 0.9998242); +//! +//! // Dot and cross products: +//! assert_eq!(v.dot(&Vec3::Y), 2.0); +//! assert_eq!(v.cross(&Vec3::Y), vec3(-3.0, 0.0, 1.0)); +//! +//! // 2D vectors implement the "perp" and "perp dot" operations: +//! let u: Vec2 = vec2(2.0, 3.0); +//! assert_eq!(u.perp(), vec2(-3.0, 2.0)); +//! assert_eq!(u.perp_dot(Vec2::Y), 2.0); +//! +//! // Projections: +//! assert_eq!(v.scalar_project(&Vec3::Y), 2.0); +//! assert_eq!(v.vector_project(&Vec3::Z), 3.0 * Vec3::Z); +//! +//! // Mapping components: +//! assert_eq!(v.map(|c| c * c), vec3(1.0, 4.0, 9.0)); +//! ``` use core::{ array, @@ -30,6 +103,7 @@ use super::{Angle, acos}; /// A generic vector type. Represents an element of a vector space. /// +/// For more information, see the [module documentation](self). // or a module, // a generalization of a vector space where the scalars can be integers // (technically, the scalar type can be any *ring*-like type). From 956443735a839480ca1aa3734f926a5de51f2fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Tue, 14 Jul 2026 19:41:07 +0300 Subject: [PATCH 57/76] Fix some pedantic clippy lints --- core/src/geom/prim.rs | 16 +++++++--------- core/src/math.rs | 6 ++++-- core/src/math/mat.rs | 6 +++--- core/src/math/space.rs | 4 ++++ core/src/math/vary.rs | 6 +++--- core/src/math/vec.rs | 3 +++ core/src/render.rs | 7 +++---- core/src/render/batch.rs | 8 ++++++++ core/src/render/debug.rs | 2 +- core/src/render/light.rs | 18 ++++++++---------- core/src/render/scene.rs | 6 +++--- core/src/render/stats.rs | 23 +++++++++++++++++------ core/src/render/target.rs | 12 ++++++------ core/src/render/text.rs | 1 + core/src/util/buf.rs | 2 +- demos/wasm/src/triangle.rs | 4 ++-- geom/src/io.rs | 6 +++--- geom/src/solids/platonic.rs | 8 ++++++-- 18 files changed, 83 insertions(+), 55 deletions(-) diff --git a/core/src/geom/prim.rs b/core/src/geom/prim.rs index 1da7106c..e0cd8d98 100644 --- a/core/src/geom/prim.rs +++ b/core/src/geom/prim.rs @@ -162,7 +162,7 @@ impl> Tri

{ #[inline] pub fn tangents(&self) -> [T::Diff; 2] { let [a, b, c] = &self.0; - [b.pos().sub(&a.pos()), c.pos().sub(&a.pos())] + [b.pos().sub(a.pos()), c.pos().sub(a.pos())] } /// Returns the geometric center, or "balance point", of `self`. @@ -697,15 +697,13 @@ impl Line2 { pub const fn slope_intercept(&self) -> Option<(f32, f32)> { // ax + by + c = 0 let [a, b, c] = self.coeffs(); - - if b != 0.0 { - // by = -ax - c <=> y = -a/b x - c/b - let m = -a / b; // slope - let y0 = -c / b; // y intercept - Some((m, y0)) - } else { - None + if b == 0.0 { + return None; } + // by = -ax - c <=> y = -a/b x - c/b + let m = -a / b; // slope + let y0 = -c / b; // y intercept + Some((m, y0)) } /// Returns diff --git a/core/src/math.rs b/core/src/math.rs index dfd0153a..4ed2457a 100644 --- a/core/src/math.rs +++ b/core/src/math.rs @@ -106,6 +106,7 @@ pub trait Lerp: Clone + Debug + Sized { /// /// assert_eq!(f32::lerp(&1.0, &5.0, 0.25), 2.0); /// ``` + #[must_use] fn lerp(&self, other: &Self, t: f32) -> Self; /// Returns the (unweighted) average of `self` and `other`. @@ -118,6 +119,7 @@ pub trait Lerp: Clone + Debug + Sized { /// let b = pt2(3.0, -2.0); /// assert_eq!(a.midpoint(&b), pt2(1.0, 0.0)); /// ``` + #[must_use] fn midpoint(&self, other: &Self) -> Self { self.lerp(other, 0.5) } @@ -156,7 +158,7 @@ pub fn inv_lerp(t: f32, min: f32, max: f32) -> f32 { } /// The square root of three. -pub const SQRT_3: f32 = 1.7320508; +pub const SQRT_3: f32 = 1.732_050_8; impl Lerp for T where @@ -207,7 +209,7 @@ impl Lerp for [T; N] { } impl Lerp for () { - fn lerp(&self, _: &Self, _: f32) {} + fn lerp(&self, (): &(), _: f32) {} } impl Lerp for (U, V) { diff --git a/core/src/math/mat.rs b/core/src/math/mat.rs index 3e631989..ab28146d 100644 --- a/core/src/math/mat.rs +++ b/core/src/math/mat.rs @@ -1145,7 +1145,7 @@ impl Mat4 { #[inline] const fn likely(cond: bool) -> bool { if !cond { - cold_path() + cold_path(); } cond } @@ -1234,7 +1234,7 @@ impl Apply> for Mat2 { /// Mp = ⎛ M00 M01 ⎞ ⎛ v0 ⎞ = ⎛ v0' ⎞ /// ⎝ M10 M11 ⎠ ⎝ v1 ⎠ ⎝ v1' ⎠ /// ``` - #[inline(always)] + #[inline] fn apply(&self, pt: &Point2) -> Point2 { self.apply(&pt.to_vec()).to_pt() } @@ -1318,7 +1318,7 @@ impl Apply> for Mat3 { /// M·P = ⎜ x1 y1 z1 ⎟ ⎜ p1 ⎟ = ⎜ p1' ⎟ /// ⎝ x2 y2 z2 ⎠ ⎝ p2 ⎠ ⎝ p2' ⎠ /// ``` - #[inline(always)] + #[inline] fn apply(&self, p: &Point3) -> Point3 { self.apply(&p.to_vec()).to_pt() } diff --git a/core/src/math/space.rs b/core/src/math/space.rs index 946adefa..51fa49cb 100644 --- a/core/src/math/space.rs +++ b/core/src/math/space.rs @@ -26,11 +26,13 @@ pub trait Affine: Sized { /// Adds `diff` to `self` component-wise. /// /// `add` is commutative and associative. + #[must_use] fn add(&self, diff: &Self::Diff) -> Self; /// Subtracts `other` from `self`, returning the (signed) difference. /// /// `sub` is anti-commutative: `v.sub(w) == w.sub(v).neg()`. + #[must_use] fn sub(&self, other: &Self) -> Self::Diff; /// Returns an affine combination of points. @@ -75,6 +77,7 @@ pub trait Linear: Affine { /// Returns the additive inverse of `self`. #[inline] + #[must_use] fn neg(&self) -> Self { Self::zero().sub(self) } @@ -91,6 +94,7 @@ pub trait Linear: Affine { /// v.mul(a).add(&w.mul(a)) == v.add(&w).mul(a); /// v.mul(a).sub(&w.mul(a)) == v.add(&w).sub(&a); /// ``` + #[must_use] fn mul(&self, scalar: Self::Scalar) -> Self; } diff --git a/core/src/math/vary.rs b/core/src/math/vary.rs index ec9fe73a..71609a85 100644 --- a/core/src/math/vary.rs +++ b/core/src/math/vary.rs @@ -95,11 +95,11 @@ impl Vary for () { type Iter = Iter<()>; type Diff = (); - fn vary(self, _: Self::Diff, n: Option) -> Self::Iter { + fn vary(self, (): (), n: Option) -> Iter<()> { Iter { val: (), step: (), n } } - fn dv_dt(&self, _: &Self, _: f32) {} - fn step(&self, _: &Self::Diff) {} + fn dv_dt(&self, (): &(), _: f32) {} + fn step(&self, (): &()) {} } impl ZDiv for () {} diff --git a/core/src/math/vec.rs b/core/src/math/vec.rs index 70832eed..5ee51b8c 100644 --- a/core/src/math/vec.rs +++ b/core/src/math/vec.rs @@ -257,6 +257,7 @@ impl Vector<[f32; N], Sp> { /// assert_eq!(normalized.len(), 0.99844766); /// ``` #[inline] + #[must_use] pub fn normalize_approx(&self) -> Self { *self * fast_recip_sqrt(self.len_sqr()) } @@ -579,6 +580,7 @@ impl Vec2 { /// assert_eq!(::Y.perp(), -Vec2::X); /// ``` #[inline] + #[must_use] pub const fn perp(self) -> Self { vec2(-self.y(), self.x()) } @@ -698,6 +700,7 @@ where /// t | / / /// +--------------- > self /// ``` + #[must_use] pub fn cross(&self, other: &Self) -> Self where Sc: Linear, diff --git a/core/src/render.rs b/core/src/render.rs index 395fe598..bf477389 100644 --- a/core/src/render.rs +++ b/core/src/render.rs @@ -6,7 +6,7 @@ //! geometric shapes such as triangles. use alloc::vec::Vec; -use core::{fmt::Debug, ops::DerefMut}; +use core::fmt::Debug; use crate::geom::Vertex; use crate::math::{ @@ -192,7 +192,7 @@ fn rasterize( shader: &Shd, uniform: Uni, to_screen: Mat4, - mut target: &mut impl Target, + target: &mut impl Target, ctx: &Context, ) -> (usize, usize) where @@ -217,7 +217,6 @@ where out.1 += 3; // TODO Get number of verts in prim somehow // 4. Fragment shader and rasterization - let target = target.deref_mut(); Prim::rasterize(prim, |scanline| { // Convert to fragments, shade, and draw to target target.rasterize(scanline, shader, uniform, ctx); @@ -234,7 +233,7 @@ fn primitive_assembly + Clone, Var: Vary>( prims .iter() .cloned() - .map(|prim| Prim::inline(prim, &verts)) + .map(|prim| Prim::inline(prim, verts)) // Collect needed because clip takes a slice... .collect() } diff --git a/core/src/render/batch.rs b/core/src/render/batch.rs index 037895d9..7dc78b76 100644 --- a/core/src/render/batch.rs +++ b/core/src/render/batch.rs @@ -53,6 +53,7 @@ impl Batch<(), (), (), (), (), Context> { impl Batch { /// Sets the primitives to be rendered. + #[must_use] pub fn primitives( self, prims: Ps, @@ -61,6 +62,7 @@ impl Batch { } /// Sets the vertices to be rendered. + #[must_use] pub fn vertices( self, verts: Vs, @@ -72,6 +74,7 @@ impl Batch { /// /// You can also create a new batch from a moved or borrowed mesh /// directly using the `From` or `From<&Mesh>` impls. + #[must_use] pub fn mesh( self, mesh: &Mesh, @@ -82,6 +85,7 @@ impl Batch { } /// Sets the uniform data to be passed to the vertex shaders. + #[must_use] pub fn uniform( self, uniform: U, @@ -90,6 +94,7 @@ impl Batch { } /// Sets the combined vertex and fragment shader. + #[must_use] pub fn shader( self, shader: S, @@ -103,17 +108,20 @@ impl Batch { } /// Sets the viewport matrix. + #[must_use] pub fn viewport(self, viewport: Mat4) -> Self { update!(viewport; self verts prims uniform shader target ctx) } /// Sets the render target. // TODO what bound for T? + #[must_use] pub fn target(self, target: T) -> Batch { update!(target; self verts prims uniform shader viewport ctx) } /// Sets the rendering context. + #[must_use] pub fn context( self, ctx: &Context, diff --git a/core/src/render/debug.rs b/core/src/render/debug.rs index c531cf1b..c57e24cc 100644 --- a/core/src/render/debug.rs +++ b/core/src/render/debug.rs @@ -153,7 +153,7 @@ impl DbgMesh { #[must_use] pub fn face_normals(mut self, scale: f32) -> Self { for tri in self.0.faces() { - self.1.face_normal(&tri.map(|v| v.clone()), scale); + self.1.face_normal(&tri.map(Clone::clone), scale); } self } diff --git a/core/src/render/light.rs b/core/src/render/light.rs index 1f61cda7..a1d14e82 100644 --- a/core/src/render/light.rs +++ b/core/src/render/light.rs @@ -31,11 +31,9 @@ pub enum Kind { impl Light { /// Creates a new light source of the given color and kind. pub fn new(color: Color3f, mut kind: Kind) -> Self { - match &mut kind { - Kind::Directional(dir) => *dir = dir.normalize(), - Kind::Spot { dir, .. } => *dir = dir.normalize(), - _ => {} - }; + if let Kind::Directional(dir) | Kind::Spot { dir, .. } = &mut kind { + *dir = dir.normalize(); + } Self { color, kind, ..Self::default() } } @@ -43,9 +41,10 @@ impl Light { #[inline] pub fn direction(&self, pt: Point3) -> Vec3 { match self.kind { - Kind::Point(pos) => (pos - pt).normalize_approx(), + Kind::Point(pos) | Kind::Spot { pos, .. } => { + (pos - pt).normalize_approx() + } Kind::Directional(dir) => dir, - Kind::Spot { pos, .. } => (pos - pt).normalize_approx(), } } @@ -53,8 +52,7 @@ impl Light { pub fn eval(&self, pt: Point3) -> (Color3f, Vec3) { let pt_dir = self.direction(pt); let color = match self.kind { - Kind::Point(_) => self.color, - Kind::Directional(_) => self.color, + Kind::Point(_) | Kind::Directional(_) => self.color, Kind::Spot { dir, radii, .. } => { let dot = pt_dir.dot(&dir); let (r0, r1) = (1.0 - radii.0, 1.0 - radii.1); @@ -82,7 +80,7 @@ impl Light { radii, }, }; - Light { kind, color, falloff } + Light { color, kind, falloff } } } diff --git a/core/src/render/scene.rs b/core/src/render/scene.rs index 261e2251..d78326f7 100644 --- a/core/src/render/scene.rs +++ b/core/src/render/scene.rs @@ -102,9 +102,9 @@ impl Default for Obj { /// Returns an empty `Obj`. fn default() -> Self { Self { - geom: Default::default(), - bbox: Default::default(), - tf: Default::default(), + geom: Mesh::default(), + bbox: BBox::default(), + tf: Mat4::default(), } } } diff --git a/core/src/render/stats.rs b/core/src/render/stats.rs index c3882596..00ae4382 100644 --- a/core/src/render/stats.rs +++ b/core/src/render/stats.rs @@ -54,14 +54,14 @@ impl Stats { Self { #[cfg(feature = "std")] start: Instant::now(), - wall_time: Default::default(), - render_time: Default::default(), + wall_time: Duration::default(), + render_time: Duration::default(), calls: 0.0, frames: 0.0, - objs: Default::default(), - prims: Default::default(), - verts: Default::default(), - frags: Default::default(), + objs: Throughput::default(), + prims: Throughput::default(), + verts: Throughput::default(), + frags: Throughput::default(), } } @@ -74,6 +74,7 @@ impl Stats { } } + #[must_use] pub fn finish(self) -> Self { Self { #[cfg(feature = "std")] @@ -98,6 +99,7 @@ impl Stats { } /// Returns the average throughput in items per second. + #[must_use] pub fn per_render_sec(&self) -> Self { let secs = if self.render_time.is_zero() { 1.0 @@ -120,6 +122,7 @@ impl Stats { } } + #[must_use] pub fn per_wall_sec(&self) -> Self { let secs = if self.wall_time.is_zero() { 1.0 @@ -141,7 +144,9 @@ impl Stats { frags, } } + /// Returns the average throughput in items per frame. + #[must_use] pub fn per_frame(&self) -> Self { let frames = self.frames.max(1.0) as u32; let [objs, prims, verts, frags] = self @@ -186,6 +191,12 @@ impl Throughput { } } +impl Default for Stats { + fn default() -> Self { + Self::new() + } +} + impl Display for Stats { #[rustfmt::skip] #[inline(never)] diff --git a/core/src/render/target.rs b/core/src/render/target.rs index 652f86ee..f18d4df4 100644 --- a/core/src/render/target.rs +++ b/core/src/render/target.rs @@ -61,7 +61,7 @@ impl Target for &mut T { uni: U, ctx: &Context, ) { - (*self).rasterize(sl, fs, uni, ctx) + (*self).rasterize(sl, fs, uni, ctx); } } impl Target for &RefCell { @@ -73,7 +73,7 @@ impl Target for &RefCell { uni: U, ctx: &Context, ) { - RefCell::borrow_mut(self).rasterize(sl, fs, uni, ctx) + RefCell::borrow_mut(self).rasterize(sl, fs, uni, ctx); } } @@ -96,7 +96,7 @@ where let Self { color_buf, depth_buf } = self; let fmt = color_buf.fmt; // borrowck... let conv = |c: Color4| c.into_pixel(fmt); - rasterize_fb(color_buf, depth_buf, sl, fs, uni, conv, ctx) + rasterize_fb(color_buf, depth_buf, sl, fs, uni, conv, ctx); } } @@ -117,7 +117,7 @@ where ctx: &Context, ) { let conv = |c: Color4| c.into_pixel(self.fmt); - rasterize(&mut self.buf, sl, fs, uni, conv, ctx) + rasterize(&mut self.buf, sl, fs, uni, conv, ctx); } } @@ -130,7 +130,7 @@ impl Target for Buf2 { uni: U, ctx: &Context, ) { - rasterize(self, sl, fs, uni, |c| c, ctx) + rasterize(self, sl, fs, uni, |c| c, ctx); } } @@ -143,7 +143,7 @@ impl Target for Buf2 { uni: U, ctx: &Context, ) { - rasterize(self, sl, fs, uni, |c| c.to_rgb(), ctx) + rasterize(self, sl, fs, uni, |c| c.to_rgb(), ctx); } } diff --git a/core/src/render/text.rs b/core/src/render/text.rs index 332d3de1..0b6e9940 100644 --- a/core/src/render/text.rs +++ b/core/src/render/text.rs @@ -64,6 +64,7 @@ impl Text { } /// Sets the anchor point of the text. + #[must_use] pub fn anchor(mut self, pt: impl Into) -> Self { self.anchor = pt.into(); self diff --git a/core/src/util/buf.rs b/core/src/util/buf.rs index 5ab24608..62d74a96 100644 --- a/core/src/util/buf.rs +++ b/core/src/util/buf.rs @@ -576,7 +576,7 @@ pub mod inner { #[track_caller] #[inline(never)] fn out_of_bounds(Dims(w, h): Dims, x: u32, y: u32) -> ! { - panic!("position (x={x}, y={y}) out of bounds (0..{w}, 0..{h})",) + panic!("position (x={x}, y={y}) out of bounds (0..{w}, 0..{h})") } impl> Inner { diff --git a/demos/wasm/src/triangle.rs b/demos/wasm/src/triangle.rs index 00e359f1..7a7a50b0 100644 --- a/demos/wasm/src/triangle.rs +++ b/demos/wasm/src/triangle.rs @@ -36,8 +36,8 @@ pub fn start() { let mvp = mv.then(&proj); let sh = Shader::new( - |v: Vertex3, _| vertex(mvp.apply(&v.pos), v.attrib), - |f: Frag, _| f.var.to_color4(), + |v: Vertex3, ()| vertex(mvp.apply(&v.pos), v.attrib), + |f: Frag, ()| f.var.to_color4(), ); render([tri(0, 1, 2)], vs, &sh, (), vp, &mut frame.buf, frame.ctx); diff --git a/geom/src/io.rs b/geom/src/io.rs index 9f58a60a..6dda2446 100644 --- a/geom/src/io.rs +++ b/geom/src/io.rs @@ -355,11 +355,11 @@ fn parse_indices(param: &str) -> Result { let pos = next(indices).and_then(parse_index)?; // Texcoord and normal are optional let uv = if let Some(uv) = indices.next() { - if !uv.is_empty() { - Some(parse_index(uv)?) - } else { + if uv.is_empty() { // `1//2`: only position and normal None + } else { + Some(parse_index(uv)?) } } else { None diff --git a/geom/src/solids/platonic.rs b/geom/src/solids/platonic.rs index dda3279e..40e9aab2 100644 --- a/geom/src/solids/platonic.rs +++ b/geom/src/solids/platonic.rs @@ -1,7 +1,11 @@ //! The five Platonic solids: tetrahedron, cube, octahedron, dodecahedron, //! and icosahedron. -use core::{array::from_fn, f32::consts::SQRT_2, iter::zip}; +use core::{ + array::from_fn, + f32::consts::{GOLDEN_RATIO, SQRT_2}, + iter::zip, +}; use retrofire_core::{ geom::{Mesh, Normal3, Vertex3, vertex}, @@ -301,7 +305,7 @@ impl Build for Octahedron { } /// The golden ratio constant φ. -const PHI: f32 = 1.618034_f32; +const PHI: f32 = GOLDEN_RATIO; /// Reciprocal of φ. const R_PHI: f32 = 1.0 / PHI; From c90e47310ca553405b303410b3f0ac9bff5c36cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sun, 12 Jul 2026 20:57:07 +0300 Subject: [PATCH 58/76] Implement Mat2 and Mat3::solve() These use Cramer's rule to solve Mx = y without computing the full inverse. --- core/src/math/mat.rs | 149 +++++++++++++++++++++++++++++++++++++++++-- geom/src/isect.rs | 124 ++++++++++++++++++++++++++++------- 2 files changed, 243 insertions(+), 30 deletions(-) diff --git a/core/src/math/mat.rs b/core/src/math/mat.rs index ab28146d..c3d14c6a 100644 --- a/core/src/math/mat.rs +++ b/core/src/math/mat.rs @@ -565,6 +565,38 @@ impl Mat2 { det2(a, b, c, d) } + /// Solves the system of linear equations + /// ```text + /// ⎛ a b ⎞ ⎛ x ⎞ = ⎛ u ⎞ + /// ⎝ c d ⎠ ⎝ y ⎠ = ⎝ v ⎠, + /// ``` + /// that is, + /// ```text + /// ax + by = u + /// cx + dy = v, + /// ``` + /// for x and y. Returns None if no unique solution exists. + /// + /// Uses Cramer's rule: + /// ```text + /// x = det ⎛ u b ⎞ / det A = (ud - bv) / (ad - bc) + /// ⎝ v d ⎠ + /// y = det ⎛ a u ⎞ / det A = (av - uc) / (ad - bc) + /// ⎝ c v ⎠ + /// ``` + pub fn solve(&self, uv: Vec2) -> Option> { + let det = self.determinant(); + if det.approx_eq(&0.0) { + return None; + } + let [u, v] = uv.0; + let [[a, b], [c, d]] = self.0; + let x = (u * d - b * v) / det; + let y = (a * v - u * c) / det; + + Some(vec2(x, y)) + } + /// Returns the [inverse][Self::inverse] of `self`, or `None` if `self` /// is not invertible. /// @@ -768,6 +800,13 @@ impl Mat3 { } impl Mat3 { + pub const fn from_linear(i: Vec3, j: Vec3, k: Vec3) -> Self { + mat![ + i.x(), j.x(), k.x(); + i.y(), j.y(), k.y(); + i.z(), j.z(), k.z(); + ] + } /// Returns the 4x4 affine equivalent of `self`. pub const fn to_affine(&self) -> Mat4 { let [[a, b, c], [d, e, f], [g, h, i]] = self.0; @@ -778,6 +817,42 @@ impl Mat3 { 0.0, 0.0, 0.0, 1.0; ] } + + /// Solves the system of linear equations + /// ```text + /// ⎛ a b c ⎞ ⎛ x ⎞ = ⎛ t ⎞ + /// ⎜ d e f ⎟ ⎜ y ⎟ = ⎜ u ⎟ + /// ⎝ g h i ⎠ ⎝ z ⎠ = ⎝ v ⎠, + /// ``` + /// that is, + /// ```text + /// ax + by + cz = t + /// dx + ey + fz = u + /// gx + hy + iz = v, + /// ``` + /// for x, y, and z. Returns None if no unique solution exists. + /// + /// Uses Cramer's rule: + /// + /// ```text + /// ⎛ t b c ⎞ ⎛ a t c ⎞ ⎛ a b t ⎞ + /// (x, y, z) = ( det ⎜ u e f ⎟, det ⎜ d u f ⎟, det ⎜ d e u ⎟ ) / det A + /// ⎝ v h i ⎠ ⎝ g v i ⎠ ⎝ g h v ⎠ + /// ``` + pub fn solve(&self, tuv: Vec3) -> Option> { + let det = self.determinant(); + if det.approx_eq(&0.0) { + return None; + } + let [t, u, v] = tuv.0; + let [[a, b, c], [d, e, f], [g, h, i]] = self.0; + + let ax: Self = mat![t, b, c; u, e, f; v, h, i]; + let ay: Self = mat![a, t, c; d, u, f; g, v, i]; + let az: Self = mat![a, b, t; d, e, u; g, h, v]; + + Some(vec3(ax.determinant(), ay.determinant(), az.determinant()) / det) + } } impl Mat3 { @@ -1766,7 +1841,7 @@ mod tests { } #[test] fn determinant_of_reflection_is_negative_one() { - let refl: Mat2 = [[0.0, 1.0], [1.0, 0.0]].into(); + let refl: Mat2 = mat![0.0, 1.0; 1.0, 0.0]; assert_eq!(refl.determinant(), -1.0); } @@ -1777,25 +1852,58 @@ mod tests { } #[test] fn inverse_of_inverse_is_original() { - let m: Mat2 = [[0.5, 1.5], [1.0, -0.5]].into(); + let m: Mat2 = mat![0.5, 1.5; 1.0, -0.5]; let m_inv: Mat2 = m.inverse(); assert_approx_eq!(m_inv.inverse(), m); } #[test] fn inverse_of_singular_does_not_exist() { - let singular: Mat2 = mat![ - 1.0, 0.0; - 2.0, 0.0; - ]; + let singular: Mat2 = mat![1.0, 0.0; 2.0, 0.0]; assert_eq!(singular.checked_inverse(), None); } #[test] fn composition_of_inverse_is_identity() { - let m: Mat2 = [[0.5, 1.5], [1.0, -0.5]].into(); + let m: Mat2 = mat![0.5, 1.5; 1.0, -0.5]; let m_inv: Mat2 = m.inverse(); assert_approx_eq!(m.compose(&m_inv), Mat2::identity()); assert_approx_eq!(m.then(&m_inv), Mat2::identity()); } + + #[test] + fn solve_identity() { + // 1*x + 0*y = 4 + // 0*x + 1*y = -5 + let m: Mat2 = Mat2::identity(); + assert_eq!(m.solve(vec2(4.0, -5.0)), Some(vec2(4.0, -5.0))); + } + #[test] + fn solve_scale() { + // 2*x + 0*y = 4 + // 0*x - 3*y = 6 + let m: Mat2 = mat![2.0, 0.0; 0.0, -3.0]; + assert_eq!(m.solve(vec2(4.0, 6.0)), Some(vec2(2.0, -2.0))); + } + #[test] + fn solve_flip() { + // 0*x + 1*y = 2 + // 1*x + 0*y = 3 + let m: Mat2 = mat![0.0, 1.0; 1.0, 0.0]; + assert_eq!(m.solve(vec2(2.0, 3.0)), Some(vec2(3.0, 2.0))); + } + #[test] + fn solve_mixed() { + // 2*x - 3*y = 4 + // 5*x + 6*y = 7 + // <=> + // -4*x + 6*y = -8 + // 5*x + 6*y = 7 + // ---------------- + // -9*x = -15 + // x = 15/9 + // 3y = 2x - 4 <=> y = (2*15/9 - 4)/3 = (10 - 12)/9 = -2/9 + let m: Mat2 = mat![2.0, -3.0; 5.0, 6.0]; + assert_eq!(m.solve(vec2(4.0, 7.0)), Some(vec2(15.0, -2.0) / 9.0)); + } } mod mat3 { @@ -1915,6 +2023,33 @@ mod tests { assert_approx_eq!(singular.checked_inverse(), None); } + #[test] + fn solve_identity() { + // 1*x + 0*y + 0*z = 3 + // 0*x + 1*y + 0*z = -4 + // 0*x + 0*y + 1*z = 5 + let m = Mat3::<(), (), 3>::identity(); + assert_eq!( + m.solve(vec3(3.0, -4.0, 5.0)), + Some(vec3(3.0, -4.0, 5.0)) + ); + } + #[test] + fn solve_scale() { + // 2*x + 0*y + 0*z = 1 + // 0*x - 3*y + 0*z = 3 + // 0*x + 0*y + 4*z = 2 + let m: Mat3<(), (), 3> = mat![ + 2.0, 0.0, 0.0; + 0.0, -3.0, 0.0; + 0.0, 0.0, 4.0; + ]; + assert_eq!( + m.solve(vec3(1.0, 3.0, 2.0)), + Some(vec3(0.5, -1.0, 0.5)) + ); + } + #[test] fn matrix_debug() { assert_eq!( diff --git a/geom/src/isect.rs b/geom/src/isect.rs index 1fa93f8f..38c30e44 100644 --- a/geom/src/isect.rs +++ b/geom/src/isect.rs @@ -1,9 +1,9 @@ use core::fmt::{Debug, Formatter}; use retrofire_core::{ - geom::{Edge, Line2, Plane3, Ray, Ray2, Ray3}, + geom::{Edge, Line2, Plane3, Pos, Ray, Ray2, Ray3, Tri}, mat, - math::{ApproxEq, Mat2, Point2, Point3, pt2, vec3}, + math::{ApproxEq, Mat2, Mat3, Point2, Point3, vec2, vec3}, render::scene::BBox, }; @@ -118,6 +118,96 @@ impl Intersect> for Ray3 { } } +impl>> Intersect> for Ray3 { + type Result = RayIntersect3; + + /// Returns the intersection of `self` and a triangle, or `None` if they + /// do not intersect. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::{tri, Ray, Ray3}; + /// use retrofire_core::math::{pt3, vec3}; + /// use retrofire_geom::Intersect; + /// + /// let t = tri(pt3(0.0, 0.0, 0.0), pt3(2.0, 0.0, 0.0), pt3(0.0, 2.0, 0.0)); + /// + /// let ray: Ray3 = Ray(pt3(1.0, 0.5, 4.0), vec3(0.0, 0.0, -1.0)); + /// assert_eq!(ray.intersect(&t), Some((4.0, pt3(1.0, 0.5, 0.0)))); + /// + /// let ray: Ray3 = Ray(pt3(0.0, 0.0, 4.0), vec3(0.0, -1.0, -1.0)); + /// assert_eq!(ray.intersect(&t), None); + /// ``` + fn intersect(&self, tri: &Tri

) -> Self::Result { + /* + tri ABC + tangents ab = B-A, ac = C-A + ray P = O + t * d + + triangle plane parametric: + P = a + u * ab + v * ac + + solve linear equation: + O + t * d = a + u * ab + v * ac + + O - a = t * -d + u * ab + v * ac + + ( t ) + O - a = ( -d ab ac ) ( u ) + ( v ) + + ( -d_x ab_x ac_x ) ( t ) + O - a = ( -d_y ab_y ac_y ) ( u ) + ( -d_z ab_z ac_z ) ( v ) + + inside triangle iff 0 <= t && 0 <= u && 0 <= v && u + v <= 1 + */ + + struct Plane; + + let &Ray(orig, dir) = self; + let &a = tri.0[0].pos(); + let [ab, ac] = tri.tangents(); + + let m = Mat3::::from_linear(-dir, ab, ac); + let [t, u, v] = m.solve(orig - a)?.0; + + // If inside the triangle + (t >= 0.0 && u >= 0.0 && v >= 0.0 && u + v <= 1.0) + .then(|| (t, orig + t * dir)) + } +} + +impl>> Intersect> for Ray2 { + type Result = RayIntersect2; + + /// Returns the intersection of `self` and a triangle, or `None` if they + /// do not intersect. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::{tri, Ray, Ray2}; + /// use retrofire_core::math::{pt2, vec2}; + /// use retrofire_geom::Intersect; + /// + /// let t = tri(pt2(0.0, 0.0), pt2(2.0, 0.0), pt2(0.0, 2.0)); + /// + /// let ray: Ray2 = Ray(pt2(-2.0, 1.0), vec2(1.0, 0.0)); + /// assert_eq!(ray.intersect(&t), Some((2.0, pt2(0.0, 1.0)))); + /// + /// let ray: Ray2 = Ray(pt2(-2.0, 1.0), vec2(1.0, 11.0)); + /// assert_eq!(ray.intersect(&t), None); + /// ``` + fn intersect(&self, tri: &Tri

) -> Self::Result { + let tri = Tri(tri.0.each_ref().map(|p| p.pos().clone())); + + tri.edges() + .iter() + .filter_map(|e| self.intersect(&Edge(*e.0, *e.1))) + .min_by(|(t, _), (u, _)| t.total_cmp(u)) + } +} + impl Intersect> for Ray3 { type Result = RayIntersect3; // Only closest for now @@ -289,16 +379,18 @@ impl Intersect for Line2 { /// /// # Examples /// ``` + /// use core::assert_matches; + /// /// use retrofire_core::{ - /// assert_approx_eq, geom::{Ray, Line2}, math::{pt2, vec2}, + /// geom::{Ray, Line2}, math::{pt2, vec2, ApproxEq}, /// }; /// use retrofire_geom::{Intersect, isect::LineIntersect::*}; /// /// let horiz: Line2 = Ray(pt2(0.0, 2.0), vec2(1.0, 0.0)).into(); /// let vert: Line2 = Ray(pt2(3.0, 0.0), vec2(0.0, 1.0)).into(); /// - /// let isect = horiz.intersect(&vert).and_then(|i| i.point()); - /// assert_approx_eq!(isect, Some(pt2(3.0, 2.0))); + /// let isect = horiz.intersect(&vert); + /// assert_matches!(isect, Some(Point(p)) if p.approx_eq(&pt2(3.0, 2.0))); /// /// let horiz2 = ::from(Ray(pt2(0.0, 3.0), vec2(1.0, 0.0))); /// assert_eq!(horiz.intersect(&horiz2), None); @@ -314,23 +406,9 @@ impl Intersect for Line2 { // Solve the system of equations for x and y: // ax + by + c = 0 // self // dx + ey + f = 0 // other - // - // Write in matrix form and solve: - // (a b) (x) = (-c) - // (d e) (y) (-f) - // - // -1 - // (x) = (a b) (-c) - // (y) (d e) (-f) - let abde: Mat2 = mat![ - a, b; - d, e; - ]; - match abde.checked_inverse() { - Some(inv) => { - let res = inv.apply(&pt2(-c, -f)); - Some(LineIntersect::Point(res)) - } + let abde: Mat2 = mat![a, b; d, e]; + match abde.solve(vec2(-c, -f)) { + Some(res) => Some(LineIntersect::Point(res.to_pt())), None if [a, b, c].approx_eq(&[d, e, f]) => { Some(LineIntersect::Coincident) } @@ -491,7 +569,7 @@ impl Intersect for Edge> { impl Debug for LineIntersect { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { match self { - Self::Point(p) => write!(f, "Point({p:.3?})"), + Self::Point(p) => Debug::fmt(p, f), Self::Coincident => f.write_str("Coincident"), } } From 6bfa691a9e36115501fb8e7b855d2c18725d5dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Tue, 14 Jul 2026 15:50:00 +0300 Subject: [PATCH 59/76] Refactor and add more matrix benchmarks --- benches/mat.rs | 412 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 268 insertions(+), 144 deletions(-) diff --git a/benches/mat.rs b/benches/mat.rs index 52640008..bc990733 100644 --- a/benches/mat.rs +++ b/benches/mat.rs @@ -1,172 +1,296 @@ //! Matrix manipulation benchmarks. +use core::ops::Range; + use divan::Bencher; -use retrofire_core::{ - math::rand::{DefaultRng, Distrib}, - math::{Mat2, Mat3, Mat4, Vec2, Vec3, splat}, +use retrofire_core::math::{ + Mat2, Mat3, Mat4, Vec2, Vec3, + rand::{DefaultRng, Distrib}, + splat, }; -#[divan::bench] -fn invert2(b: Bencher) { - let rng = &mut DefaultRng::default(); - let vecs = splat(-1e3)..splat(1e3); - - b.with_inputs(|| { - let x: Vec2 = vecs.sample(rng); - let y = x.perp(); - Mat2::new([x.0, y.0]) - }) - .counter(1u32) - .bench_local_values(|m: Mat2| m.inverse()); -} +mod application { + use super::*; + #[divan::bench] + fn apply2(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC2S.sample(rng); -#[divan::bench] -fn invert3_lin(b: Bencher) { - let rng = &mut DefaultRng::default(); - let vec3s = splat(-1e3)..splat(1e3); - - b.with_inputs(|| { - let x: Vec3 = vec3s.sample(rng); - let y = Vec3::Z.cross(&x); - let z = x.cross(&y); - Mat3::new([x.0, y.0, z.0]) - }) - .counter(1u32) - .bench_local_values(|m: Mat3<(), (), 3>| m.inverse()); -} + b.with_inputs(|| random_mat2(rng)) + .counter(1u32) + .bench_local_values(|m: Mat2| m.apply(&v)); + } -#[divan::bench] -fn invert3_aff(b: Bencher) { - let rng = &mut DefaultRng::default(); - let vecs = splat(-1e3)..splat(1e3); + #[divan::bench] + fn apply3_lin(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC3S.sample(rng); - b.with_inputs(|| { - let x: Vec2 = vecs.sample(rng); - let y = x.perp(); - let o: Vec2 = vecs.sample(rng); + b.with_inputs(|| random_mat3_lin(rng)) + .counter(1u32) + .bench_local_values(|m: Mat3<(), (), 3>| m.apply(&v)); + } - Mat3::from_affine(x, y, o.to_pt()) - }) - .counter(1u32) - .bench_local_values(|m: Mat3| m.inverse()); -} + #[divan::bench] + fn apply3_aff(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC2S.sample(rng); + + b.with_inputs(|| random_mat3_aff(rng)) + .counter(1u32) + .bench_local_values(|m: Mat3| m.apply(&v)); + } + + #[divan::bench] + fn apply4(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC3S.sample(rng); -#[divan::bench] -fn invert4_aff(b: Bencher) { - let rng = &mut DefaultRng::default(); - let vecs = splat(-1e3)..splat(1e3); - - b.with_inputs(|| { - let x: Vec3 = vecs.sample(rng); - let y = Vec3::Z.cross(&x); - let z = x.cross(&y); - let o: Vec3 = vecs.sample(rng); - - Mat4::from_affine(x, y, z, o.to_pt()) - }) - .counter(1u32) - .bench_local_values(|m: Mat4| m.inverse()); + b.with_inputs(|| random_mat4(rng)) + .counter(1u32) + .bench_local_values(|m: Mat4| m.apply(&v)); + } } -#[divan::bench] -fn invert4_lin(b: Bencher) { - let rng = &mut DefaultRng::default(); - let vecs = splat(-1e3)..splat(1e3); - - b.with_inputs(|| { - let x: Vec3 = vecs.sample(rng); - let y = Vec3::Z.cross(&x); - let z = x.cross(&y); - let o: Vec3 = vecs.sample(rng); - - let mut mat = Mat4::from_affine(x, y, z, o.to_pt()); - mat.0[3][1] = 0.5; - mat - }) - .counter(1u32) - .bench_local_values(|m: Mat4| m.inverse()); +mod inverse_application { + use super::*; + + #[divan::bench] + fn apply_inv2(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC2S.sample(rng); + + b.with_inputs(|| random_mat2(rng)) + .counter(1u32) + .bench_local_values(|m: Mat2| m.inverse().apply(&v)); + } + + #[divan::bench] + fn apply_inv3_lin(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC3S.sample(rng); + + b.with_inputs(|| random_mat3_lin(rng)) + .counter(1u32) + .bench_local_values(|m: Mat3<(), (), 3>| m.inverse().apply(&v)); + } + + #[divan::bench] + fn apply_inv3_aff(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC2S.sample(rng); + + b.with_inputs(|| random_mat3_aff(rng)) + .counter(1u32) + .bench_local_values(|m: Mat3| m.inverse().apply(&v)); + } + + #[divan::bench] + fn apply_inv4(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC3S.sample(rng); + + b.with_inputs(|| random_mat4(rng)) + .counter(1u32) + .bench_local_values(|m: Mat4| m.inverse().apply(&v)); + } } -#[divan::bench] -fn det2(b: Bencher) { - let rng = &mut DefaultRng::default(); - let vecs = splat(-1e3)..splat(1e3); - - b.with_inputs(|| { - let x: Vec2 = vecs.sample(rng); - let y = x.perp(); - Mat2::new([x.0, y.0]) - }) - .counter(1u32) - .bench_local_values(|m: Mat2| m.determinant()); +mod composition { + use super::*; + #[divan::bench] + fn compose2(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| (random_mat2(rng), random_mat2(rng))) + .counter(1u32) + .bench_local_values(|(m, n)| m.compose(&n)); + } + + #[divan::bench] + fn compose3_lin(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| (random_mat3_lin(rng), random_mat3_lin(rng))) + .counter(1u32) + .bench_local_values(|(m, n)| m.compose(&n)); + } + + #[divan::bench] + fn compose3_aff(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| (random_mat3_aff(rng), random_mat3_aff(rng))) + .counter(1u32) + .bench_local_values(|(m, n)| m.compose(&n)); + } + + #[divan::bench] + fn compose4(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| (random_mat4(rng), random_mat4(rng))) + .counter(1u32) + .bench_local_values(|(m, n)| m.compose(&n)); + } } -#[divan::bench] -fn det3_lin(b: Bencher) { - let rng = &mut DefaultRng::default(); - let vec3s = splat(-1e3)..splat(1e3); - - b.with_inputs(|| { - let x: Vec3 = vec3s.sample(rng); - let y = Vec3::Z.cross(&x); - let z = x.cross(&y); - Mat3::new([x.0, y.0, z.0]) - }) - .counter(1u32) - .bench_local_values(|m: Mat3<(), (), 3>| m.determinant()); +mod solving { + use super::*; + #[divan::bench] + fn solve2(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC2S.sample(rng); + + b.with_inputs(|| random_mat2(rng)) + .counter(1u32) + .bench_local_values(|m: Mat2| m.solve(v)); + } + + #[divan::bench] + fn solve3(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC3S.sample(rng); + + b.with_inputs(|| random_mat3_lin(rng)) + .counter(1u32) + .bench_local_values(|m: Mat3<(), (), 3>| m.solve(v)); + } } -#[divan::bench] -fn det3_aff(b: Bencher) { - let rng = &mut DefaultRng::default(); - let vecs = splat(-1e3)..splat(1e3); +mod inversion { + use super::*; + + #[divan::bench] + fn invert2(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| random_mat2(rng)) + .counter(1u32) + .bench_local_values(|m: Mat2| m.inverse()); + } + + #[divan::bench] + fn invert3_lin(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| random_mat3_lin(rng)) + .counter(1u32) + .bench_local_values(|m: Mat3<(), (), 3>| m.inverse()); + } + + #[divan::bench] + fn invert3_aff(b: Bencher) { + let rng = &mut DefaultRng::default(); - b.with_inputs(|| { - let x: Vec2 = vecs.sample(rng); - let y = x.perp(); - let o: Vec2 = vecs.sample(rng); + b.with_inputs(|| random_mat3_aff(rng)) + .counter(1u32) + .bench_local_values(|m: Mat3| m.inverse()); + } - Mat3::from_affine(x, y, o.to_pt()) - }) - .counter(1u32) - .bench_local_values(|m: Mat3| m.determinant()); + #[divan::bench] + fn invert4_aff(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| random_mat4(rng)) + .counter(1u32) + .bench_local_values(|m: Mat4| m.inverse()); + } + #[divan::bench] + + fn invert4_lin(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| { + let mut mat = random_mat4(rng); + mat.0[3][1] = 0.5; // Make non-affine (simulate projection matrix) + mat + }) + .counter(1u32) + .bench_local_values(|m: Mat4| m.inverse()); + } } -#[divan::bench] -fn det4_aff(b: Bencher) { - let rng = &mut DefaultRng::default(); - let vecs = splat(-1e3)..splat(1e3); - - b.with_inputs(|| { - let x: Vec3 = vecs.sample(rng); - let y = Vec3::Z.cross(&x); - let z = x.cross(&y); - let o: Vec3 = vecs.sample(rng); - - Mat4::from_affine(x, y, z, o.to_pt()) - }) - .counter(1u32) - .bench_local_values(|m: Mat4| m.determinant()); +mod determinant { + use super::*; + + #[divan::bench] + fn det2(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| random_mat2(rng)) + .counter(1u32) + .bench_local_values(|m: Mat2| m.determinant()); + } + + #[divan::bench] + fn det3_lin(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| random_mat3_lin(rng)) + .counter(1u32) + .bench_local_values(|m: Mat3<(), (), 3>| m.determinant()); + } + + #[divan::bench] + fn det3_aff(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| random_mat3_aff(rng)) + .counter(1u32) + .bench_local_values(|m: Mat3| m.determinant()); + } + + #[divan::bench] + fn det4_aff(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| random_mat4(rng)) + .counter(1u32) + .bench_local_values(|m: Mat4| m.determinant()); + } + + #[divan::bench] + fn det4_lin(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| { + let mut mat = random_mat4(rng); + mat.0[3][1] = 0.5; // Make non-affine (simulate projection matrix) + mat + }) + .counter(1u32) + .bench_local_values(|m: Mat4| m.determinant()); + } } -#[divan::bench] -fn det4_lin(b: Bencher) { - let rng = &mut DefaultRng::default(); - let vecs = splat(-1e3)..splat(1e3); - - b.with_inputs(|| { - let x: Vec3 = vecs.sample(rng); - let y = Vec3::Z.cross(&x); - let z = x.cross(&y); - let o: Vec3 = vecs.sample(rng); - - let mut mat = Mat4::from_affine(x, y, z, o.to_pt()); - mat.0[3][1] = 0.5; - mat - }) - .counter(1u32) - .bench_local_values(|m: Mat4| m.determinant()); +const VEC2S: Range = splat(-1e3)..splat(1e3); +const VEC3S: Range = splat(-1e3)..splat(1e3); + +fn random_mat2(rng: &mut DefaultRng) -> Mat2 { + let x = VEC2S.sample(rng); + let y = x.perp(); + Mat2::new([x.0, y.0]) +} +fn random_mat3_lin(rng: &mut DefaultRng) -> Mat3<(), (), 3> { + let x = VEC3S.sample(rng); + let y = Vec3::Z.cross(&x); + let z = x.cross(&y); + Mat3::new([x.0, y.0, z.0]) +} +fn random_mat3_aff(rng: &mut DefaultRng) -> Mat3 { + let x = VEC2S.sample(rng); + let y = x.perp(); + let o = VEC2S.sample(rng).to_pt(); + Mat3::from_affine(x, y, o) +} +fn random_mat4(rng: &mut DefaultRng) -> Mat4 { + let x = VEC3S.sample(rng); + let y = Vec3::Z.cross(&x); + let z = x.cross(&y); + let o = VEC3S.sample(rng).to_pt(); + Mat4::from_affine(x, y, z, o) } fn main() { From 8ddd8ed31b91fd0fc89abee52d6d224e8e0ddd11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Tue, 7 Jul 2026 18:08:47 +0300 Subject: [PATCH 60/76] Add Tri::contains(Point2) method --- core/src/geom/prim.rs | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/core/src/geom/prim.rs b/core/src/geom/prim.rs index e0cd8d98..59b3935a 100644 --- a/core/src/geom/prim.rs +++ b/core/src/geom/prim.rs @@ -6,8 +6,8 @@ use alloc::vec::Vec; use core::fmt::{self, Debug, Formatter}; use crate::math::{ - Affine, ApproxEq, Lerp, Linear, Mat4, Parametric, Point, Point2, Point3, - Vec2, Vec3, Vector, + Affine, ApproxEq, Lerp, Linear, Mat3, Mat4, Parametric, Point, Point2, + Point3, Vec2, Vec3, Vector, pt2, space::{Hom, Real}, vec::dot, vec2, vec3, @@ -200,6 +200,38 @@ impl

Tri

{ let [t, u] = tri(a, b, c).tangents(); t.cross(&u).len() / 2.0 } + + /// Returns whether a point is within the bounds of `self`. + /// + /// # Examples + /// ``` + /// use retrofire_core::{geom::{Tri}, math::{Point2, pt2}}; + /// + /// let tri: Tri = Tri([pt2(-2.0, 0.0), pt2(3.0, 0.0), pt2(0.0, 4.0)]); + /// + /// assert!(tri.contains(pt2(0.0, 2.0))); + /// + /// assert!(!tri.contains(pt2(0.0, -1.0))); + /// assert!(!tri.contains(pt2(2.0, 3.0))); + /// ``` + pub fn contains(&self, pt: P::Type) -> bool + where + P: Pos> + Clone>, + { + let [x, y, _] = pt.into().0; + let pt = pt2(x, y); + + let [a, b, c] = self.0.each_ref().map(|p| p.pos().clone().into()); + + // TODO xy may be a bad choice + let [u, v] = [(b - a).xy(), (c - a).xy()]; + + let to_barycentric: Mat3 = + Mat3::from_affine(u, v, pt2(a.x(), a.y())).inverse(); + let [x, y] = to_barycentric.apply(&pt).0; + + x >= 0.0 && y >= 0.0 && x + y <= 1.0 + } } impl>> Tri

{ @@ -897,7 +929,7 @@ impl From>> for Line2 { impl From>> for Line2 { /// Returns the line coincident with the given edge. fn from(e: Edge>) -> Self { - Ray(e.0, e.1 - e.0).into() + Self::from(Ray(e.0, e.1 - e.0)) } } From b97623e6603c6404ac7612956dc4e82f84786112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Tue, 7 Jul 2026 18:09:23 +0300 Subject: [PATCH 61/76] Implement ray/tri intersection for 2d and 3d --- core/src/geom/prim.rs | 38 +++----------------------------------- geom/src/isect.rs | 37 ++++++++++++++----------------------- 2 files changed, 17 insertions(+), 58 deletions(-) diff --git a/core/src/geom/prim.rs b/core/src/geom/prim.rs index 59b3935a..ad23c82c 100644 --- a/core/src/geom/prim.rs +++ b/core/src/geom/prim.rs @@ -6,8 +6,8 @@ use alloc::vec::Vec; use core::fmt::{self, Debug, Formatter}; use crate::math::{ - Affine, ApproxEq, Lerp, Linear, Mat3, Mat4, Parametric, Point, Point2, - Point3, Vec2, Vec3, Vector, pt2, + Affine, ApproxEq, Lerp, Linear, Mat4, Parametric, Point, Point2, Point3, + Vec2, Vec3, Vector, pt2, space::{Hom, Real}, vec::dot, vec2, vec3, @@ -200,38 +200,6 @@ impl

Tri

{ let [t, u] = tri(a, b, c).tangents(); t.cross(&u).len() / 2.0 } - - /// Returns whether a point is within the bounds of `self`. - /// - /// # Examples - /// ``` - /// use retrofire_core::{geom::{Tri}, math::{Point2, pt2}}; - /// - /// let tri: Tri = Tri([pt2(-2.0, 0.0), pt2(3.0, 0.0), pt2(0.0, 4.0)]); - /// - /// assert!(tri.contains(pt2(0.0, 2.0))); - /// - /// assert!(!tri.contains(pt2(0.0, -1.0))); - /// assert!(!tri.contains(pt2(2.0, 3.0))); - /// ``` - pub fn contains(&self, pt: P::Type) -> bool - where - P: Pos> + Clone>, - { - let [x, y, _] = pt.into().0; - let pt = pt2(x, y); - - let [a, b, c] = self.0.each_ref().map(|p| p.pos().clone().into()); - - // TODO xy may be a bad choice - let [u, v] = [(b - a).xy(), (c - a).xy()]; - - let to_barycentric: Mat3 = - Mat3::from_affine(u, v, pt2(a.x(), a.y())).inverse(); - let [x, y] = to_barycentric.apply(&pt).0; - - x >= 0.0 && y >= 0.0 && x + y <= 1.0 - } } impl>> Tri

{ @@ -871,7 +839,7 @@ impl Default for Sphere { } impl Debug for Line2 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { // ax + by + c = 0 let [a, _, c] = self.coeffs(); diff --git a/geom/src/isect.rs b/geom/src/isect.rs index 38c30e44..8396f096 100644 --- a/geom/src/isect.rs +++ b/geom/src/isect.rs @@ -139,29 +139,20 @@ impl>> Intersect> for Ray3 { /// assert_eq!(ray.intersect(&t), None); /// ``` fn intersect(&self, tri: &Tri

) -> Self::Result { - /* - tri ABC - tangents ab = B-A, ac = C-A - ray P = O + t * d - - triangle plane parametric: - P = a + u * ab + v * ac - - solve linear equation: - O + t * d = a + u * ab + v * ac - - O - a = t * -d + u * ab + v * ac - - ( t ) - O - a = ( -d ab ac ) ( u ) - ( v ) - - ( -d_x ab_x ac_x ) ( t ) - O - a = ( -d_y ab_y ac_y ) ( u ) - ( -d_z ab_z ac_z ) ( v ) - - inside triangle iff 0 <= t && 0 <= u && 0 <= v && u + v <= 1 - */ + // tri ABC, ab = B-A, ac = C-A + // ray P = O + t * d + // + // triangle plane parametric: + // P = A + u * ab + v * ac + // + // solve linear equation for (t, u, v): + // O + t * d = a + u * ab + v * ac + // + // ( t ) + // <=> O - A = ( -d ab ac ) ( u ) + // ( v ) + // + // inside triangle iff 0 <= t && 0 <= u && 0 <= v && u + v <= 1 struct Plane; From 322654ce2cf00b54142d32961e7f348b5306c1d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Wed, 8 Jul 2026 11:32:53 +0300 Subject: [PATCH 62/76] Implement ray/mesh and ray/obj intersection --- core/src/geom/prim.rs | 9 ++- geom/src/isect.rs | 156 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 155 insertions(+), 10 deletions(-) diff --git a/core/src/geom/prim.rs b/core/src/geom/prim.rs index ad23c82c..666b714c 100644 --- a/core/src/geom/prim.rs +++ b/core/src/geom/prim.rs @@ -7,7 +7,7 @@ use core::fmt::{self, Debug, Formatter}; use crate::math::{ Affine, ApproxEq, Lerp, Linear, Mat4, Parametric, Point, Point2, Point3, - Vec2, Vec3, Vector, pt2, + Vec2, Vec3, Vector, space::{Hom, Real}, vec::dot, vec2, vec3, @@ -732,6 +732,13 @@ impl Pos for Vertex { &self.pos } } +impl Pos for &Vertex { + type Type = P; + + fn pos(&self) -> &Self::Type { + &self.pos + } +} impl Pos for Point { type Type = Self; diff --git a/geom/src/isect.rs b/geom/src/isect.rs index 8396f096..d6a2e633 100644 --- a/geom/src/isect.rs +++ b/geom/src/isect.rs @@ -1,10 +1,13 @@ -use core::fmt::{Debug, Formatter}; +use core::{ + fmt::{Debug, Formatter}, + iter::zip, +}; +use retrofire_core::{assert_approx_eq, mat}; use retrofire_core::{ - geom::{Edge, Line2, Plane3, Pos, Ray, Ray2, Ray3, Tri}, - mat, + geom::{Edge, Line2, Mesh, Plane3, Pos, Ray, Ray2, Ray3, Tri}, math::{ApproxEq, Mat2, Mat3, Point2, Point3, vec2, vec3}, - render::scene::BBox, + render::{Model, Obj, World, scene::BBox}, }; #[cfg(feature = "std")] @@ -354,6 +357,46 @@ impl Intersect> for Ray3 { } } +pub type RayMeshIntersect3 = Option<(f32, Point3, usize)>; + +impl Intersect> for Ray3 { + type Result = RayMeshIntersect3; + + /// Returns the closest intersection of `self` and a mesh, or `None` if + /// they do not intersect. + fn intersect(&self, mesh: &Mesh) -> Self::Result { + zip(mesh.faces(), 0..) + .filter_map(|(face, i)| { + let (t, pt) = self.intersect(&face)?; + Some((t, pt, i)) + }) + .min_by(|(t, ..), (u, ..)| t.total_cmp(u)) + } +} + +impl Intersect> for Ray3 { + type Result = RayMeshIntersect3; + + /// Returns the closest intersection of `self` and an `Obj`, or `None` if + /// they do not intersect. + fn intersect(&self, obj: &Obj) -> Self::Result { + let inv_tf = obj.tf.inverse(); + + let ray: Ray3 = + Ray(inv_tf.apply(&self.0), inv_tf.apply(&self.1)); + + ray.intersect(&obj.bbox)?; + let (_, model_pt, face) = ray.intersect(&obj.geom)?; + + let world_pt = obj.tf.apply(&model_pt); + let t = (world_pt - self.0).len() / self.1.len(); + + assert_approx_eq!(self.0 + t * self.1, world_pt, eps = 1e-4); + + Some((t, world_pt, face)) + } +} + // // 2D intersection // @@ -507,11 +550,9 @@ impl Intersect>> for Ray2 { return None; } let t1 = (edge.1 - pt).dot(&self.1); - if t0 <= t1 { - Some((t0 / self.1.len_sqr(), edge.0)) - } else { - Some((t1 / self.1.len_sqr(), edge.1)) - } + + let (t, pt) = if t0 <= t1 { (t0, edge.0) } else { (t1, edge.1) }; + Some((t / self.1.len_sqr(), pt)) } } @@ -776,5 +817,102 @@ mod tests { assert_eq!(ray.intersect(&SPHERE), None); } } + + mod ray_mesh { + use retrofire_core::geom::Normal3; + + use super::*; + use crate::solids::{Build, Cube}; + + #[test] + fn ray_hits_cube_from_outside() { + let cube: Mesh = Cube { side_len: 1.0 }.build(); + + let along_z = Ray(pt3(0.2, -0.1, -3.0), vec3(0.0, 0.0, 1.0)); + assert_eq!( + along_z.intersect(&cube), + Some((2.5, pt3(0.2, -0.1, -0.5), 8)) + ); + + let along_y = Ray(pt3(0.0, 10.0, 0.0), vec3(0.0, -2.0, 0.0)); + assert_eq!( + along_y.intersect(&cube), + Some((4.75, pt3(0.0, 0.5, 0.0), 6)) + ); + + let diagonal = Ray(pt3(4.0, -4.0, 4.0), vec3(-1.0, 1.0, -1.0)); + assert_eq!( + diagonal.intersect(&cube), + Some((3.5, pt3(0.5, -0.5, 0.5), 2)) + ); + } + #[test] + fn ray_hits_cube_from_inside() { + let cube: Mesh = Cube { side_len: 1.0 }.build(); + + let from_origin = Ray(pt3(0.0, 0.0, 0.0), vec3(-0.5, 0.25, -1.0)); + assert_eq!( + from_origin.intersect(&cube), + Some((0.5, pt3(-0.25, 0.125, -0.5), 9)) + ); + + let along_z = Ray(pt3(0.2, 0.3, 0.25), vec3(0.0, 0.0, 2.0)); + assert_eq!( + along_z.intersect(&cube), + Some((0.125, pt3(0.2, 0.3, 0.5), 10)) + ); + } + #[test] + fn ray_misses_cube() { + let cube: Mesh = Cube { side_len: 1.0 }.build(); + + let passes_cube = Ray(pt3(1.0, 0.0, -3.0), vec3(0.0, 0.0, 1.0)); + assert_eq!(passes_cube.intersect(&cube), None); + + let opposite_dir = Ray(pt3(0.0, 0.0, 1.0), vec3(0.0, 0.0, 1.0)); + assert_eq!(opposite_dir.intersect(&cube), None); + } + + #[test] + #[ignore = "TODO"] + #[cfg(feature = "std")] + fn torus() { + let torus: Mesh = crate::solids::Torus { + major_radius: 1.0, + minor_radius: 0.3, + major_sectors: 16, + minor_sectors: 8, + } + .build(); + + let ray = Ray(pt3(0.0, 0.0, -3.0), vec3(0.0, 0.0, 1.0)); + let ip = ray.intersect(&torus); + + assert_eq!(ip, None); + } + } + + mod ray_obj { + use retrofire_core::{geom::Normal3, math::translate}; + + use super::*; + use crate::solids::{Build, Cube}; + + #[test] + #[ignore = "TODO"] + fn obj() { + let cube = Obj::::with_transform( + Cube { side_len: 1.0 }.build(), + translate((1.0, 2.0, 3.0)).to(), + ); + + let ray = Ray(pt3(1.0, 2.0, 1.0), vec3(0.0, 0.0, 2.0)); + assert_eq!(ray.intersect(&cube), None /* TODO */); + + let ray = Ray(pt3(0.0, 0.0, -3.0), vec3(0.0, 0.0, 1.0)); + assert_eq!(ray.intersect(&cube), None); + } + } + // TODO 2D tests from stash } From 87c6277447e6c70c0bb146db4c5a950b90f7c103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Wed, 15 Jul 2026 19:03:33 +0300 Subject: [PATCH 63/76] Add screenshots of the Stanford dragon and curses demo --- README.md | 7 +++++-- docs/curses.png | Bin 0 -> 49618 bytes docs/dragon.jpg | Bin 0 -> 61299 bytes 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 docs/curses.png create mode 100644 docs/dragon.jpg diff --git a/README.md b/README.md index ec161b17..cbb6fac0 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,8 @@ The `retrofire-demos` package depends on `retrofire`. # Screenshots -The classic Stanford bunny. -![The classic Stanford bunny 3D model.](docs/bunny.jpg) +The classic Stanford dragon. +![A detailed 3D model of a dragon sculpture.](docs/dragon.jpg) A first-person mouse-and-keyboard scene with many "Rust crates" strewn on a checkered floor. @@ -125,6 +125,9 @@ checkered floor. Ten thousand spherical particles positioned randomly in a sphere. ![Ten thousand spherical particles in random positions.](docs/sprites.jpg) +A colorful torus rendered in a terminal. +![A torus rendered in a terminal using block characters.](docs/curses.png) + # License Copyright 2020-2025 Johannes Dahlström. diff --git a/docs/curses.png b/docs/curses.png new file mode 100644 index 0000000000000000000000000000000000000000..36197afaa58151d7de42f573c6e4f092f357e6ba GIT binary patch literal 49618 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 literal 0 HcmV?d00001 diff --git a/docs/dragon.jpg b/docs/dragon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c64b3667af0cb86fbdfafc562de03f2648053001 GIT binary patch literal 61299 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 literal 0 HcmV?d00001 From ace88194578b579063e02bdedddaf6f56e8b85c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 16 Jul 2026 20:52:52 +0300 Subject: [PATCH 64/76] Implement Parametric for Polygon, similar to Polyline but wrapping --- core/src/geom/prim.rs | 50 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/core/src/geom/prim.rs b/core/src/geom/prim.rs index 666b714c..0bc92117 100644 --- a/core/src/geom/prim.rs +++ b/core/src/geom/prim.rs @@ -808,6 +808,56 @@ impl Parametric for Polyline { } } +impl Parametric for Polygon { + /// Returns the point on `self` at the given *t* value. + /// + /// If the number of vertices in `self` is *n* > 1, the vertex at index + /// *k* < *n* corresponds to *t* = *k* / *n*. Intermediate values + /// of *t* are linearly interpolated between the two closest vertices. + /// Values *t* < 0 and *t* >= 1 "wrap around" to the range [0, 1); in + /// particular, *t* = 1 maps to the first vertex. A polygon with a single + /// vertex returns the value of that vertex for any value of *t*. + /// + /// # Panics + /// If `self` has no vertices. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::{Polygon, Edge}; + /// use retrofire_core::math::{pt2, Point2, Parametric}; + /// + /// let pl = Polygon::( + /// vec![pt2(0.0, 0.0), pt2(1.0, 2.0), pt2(2.0, 1.0)]); + /// + /// assert_eq!(pl.eval(0.0), pl.0[0]); + /// assert_eq!(pl.eval(1.0/3.0), pl.0[1]); + /// assert_eq!(pl.eval(2.0/3.0), pl.0[2]); + /// assert_eq!(pl.eval(1.0), pl.0[0]); + /// + /// // Values not corresponding to a vertex are interpolated: + /// assert_eq!(pl.eval(0.5), pt2(1.5, 1.5)); + /// + /// // Values of t outside 0.0..=1.0 wrap around: + /// assert_eq!(pl.eval(-1.25), pl.eval(0.75)); + /// assert_eq!(pl.eval(3.0), pl.eval(0.0)); + /// ``` + fn eval(&self, t: f32) -> T { + let pts = &self.0; + assert!(!pts.is_empty(), "cannot eval an empty polyline"); + + let max = pts.len(); + let i = t.rem_euclid(1.0) * max as f32; + let t_rem = i % 1.0; + let i = i as usize; + + if i == max { + pts[0].clone() + } else { + pts[i].lerp(&pts[(i + 1) % pts.len()], t_rem) + } + } +} + impl Lerp for Vertex { fn lerp(&self, other: &Self, t: f32) -> Self { vertex( From add4f6635fe394469d8d074ec843a27dd79ef3ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 16 Jul 2026 21:35:54 +0300 Subject: [PATCH 65/76] Impl Index for Polygon --- core/src/geom/prim.rs | 63 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/core/src/geom/prim.rs b/core/src/geom/prim.rs index 0bc92117..f87b7c01 100644 --- a/core/src/geom/prim.rs +++ b/core/src/geom/prim.rs @@ -3,7 +3,10 @@ //! Includes vertices, polygons, planes, rays, and more. use alloc::vec::Vec; -use core::fmt::{self, Debug, Formatter}; +use core::{ + fmt::{self, Debug, Formatter}, + ops::Index, +}; use crate::math::{ Affine, ApproxEq, Lerp, Linear, Mat4, Parametric, Point, Point2, Point3, @@ -759,7 +762,7 @@ where } impl Parametric for Polyline { - /// Returns the point on `self` at *t*. + /// Returns the point on `self` at the given *t* value. /// /// If the number of vertices in `self` is *n* > 1, the vertex at index /// *k* < *n* corresponds to `t` = *k* / (*n* - 1). Intermediate values @@ -842,19 +845,15 @@ impl Parametric for Polygon { /// assert_eq!(pl.eval(3.0), pl.eval(0.0)); /// ``` fn eval(&self, t: f32) -> T { - let pts = &self.0; - assert!(!pts.is_empty(), "cannot eval an empty polyline"); + use crate::math::float::f32; + assert!(!self.0.is_empty(), "cannot eval an empty polygon"); - let max = pts.len(); - let i = t.rem_euclid(1.0) * max as f32; - let t_rem = i % 1.0; - let i = i as usize; + let t = t * self.0.len() as f32; + let t_int = f32::floor(t); + let t_fract = t - t_int; + let i = t_int as isize; - if i == max { - pts[0].clone() - } else { - pts[i].lerp(&pts[(i + 1) % pts.len()], t_rem) - } + self[i].lerp(&self[i + 1], t_fract) } } @@ -969,6 +968,44 @@ impl<'a, T> From<&'a [T; 2]> for Edge<&'a T> { } } +impl FromIterator

for Polygon

{ + fn from_iter>(it: I) -> Self { + Self::new(it) + } +} + +impl Index for Polygon { + type Output = V; + + /// Returns the vertex at an index modulo the number of vertices. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::Polygon; + /// use retrofire_core::math::{pt2, Point2}; + /// + /// let poly = Polygon::::new([ + /// pt2(0.0, 0.0), + /// pt2(1.0, 0.0), + /// pt2(0.0, 1.0) + /// ]); + /// + /// assert_eq!(poly[1], pt2(1.0, 0.0)); + /// // Out-of-range indices "wrap around": + /// assert_eq!(poly[-1], pt2(0.0, 1.0)); + /// assert_eq!(poly[3], pt2(0.0, 0.0)); + /// ``` + fn index(&self, index: isize) -> &Self::Output { + &self.0[index.rem_euclid(self.0.len() as isize) as usize] + } +} + +impl FromIterator

for Polyline

{ + fn from_iter>(it: I) -> Self { + Self::new(it) + } +} + #[cfg(test)] mod tests { use alloc::{format, vec}; From 8d0976117012e0da04aeec7588bca1e33520de23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 16 Jul 2026 21:47:59 +0300 Subject: [PATCH 66/76] Add method to Polygon that generates 2D vertex normals --- core/src/geom/prim.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/geom/prim.rs b/core/src/geom/prim.rs index f87b7c01..757fb838 100644 --- a/core/src/geom/prim.rs +++ b/core/src/geom/prim.rs @@ -995,6 +995,7 @@ impl Index for Polygon { /// assert_eq!(poly[-1], pt2(0.0, 1.0)); /// assert_eq!(poly[3], pt2(0.0, 0.0)); /// ``` + #[inline] fn index(&self, index: isize) -> &Self::Output { &self.0[index.rem_euclid(self.0.len() as isize) as usize] } From 408d06a7d237b3a618d2e4ca81a3e8cb3374327b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 16 Jul 2026 21:49:12 +0300 Subject: [PATCH 67/76] Add functions to iterate over Parametrics --- core/src/math/param.rs | 26 +++++++++++++++++++++++--- geom/src/solids/lathe.rs | 9 ++++----- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/core/src/math/param.rs b/core/src/math/param.rs index 4fd6d4e8..9fe200aa 100644 --- a/core/src/math/param.rs +++ b/core/src/math/param.rs @@ -1,6 +1,6 @@ -use core::ops::Range; +use core::ops::{Range, RangeInclusive}; -use super::Lerp; +use super::{Lerp, Vary}; /// Represents a single-variable parametric curve. // TODO More documentation @@ -11,10 +11,30 @@ pub trait Parametric { /// The "canonical" domain of this function is `t` ∈ [0.0, 1.0], /// but implementations should return "reasonable" values outside /// the unit interval as well. - #[allow(unused)] fn eval(&self, t: f32) -> T; } +/// Returns an iterator that yields values from a parametric for a sequence +/// of regularly spaced *t* values in the (inclusive) range [0, 1]. +pub fn iter>( + param: &P, + n: usize, +) -> impl Iterator + Clone { + iter_range(param, n, 0.0..=1.0) +} + +/// Returns an iterator that yields values from a parametric for a sequence +/// of regularly spaced *t* values in the given inclusive range. +pub fn iter_range>( + param: &P, + n: usize, + r: RangeInclusive, +) -> impl Iterator + Clone { + r.start() + .vary_to(*r.end(), n as _) + .map(|t| param.eval(t)) +} + impl T, T> Parametric for F { /// Returns `self(t)`. fn eval(&self, t: f32) -> T { diff --git a/geom/src/solids/lathe.rs b/geom/src/solids/lathe.rs index cdc4ec77..55cc2038 100644 --- a/geom/src/solids/lathe.rs +++ b/geom/src/solids/lathe.rs @@ -8,8 +8,8 @@ use retrofire_core::geom::{ vertex, }; use retrofire_core::math::{ - Angle, Lerp, Parametric, Point3, Vary, Vec3, polar, pt2, pt3, rotate2, - turns, vec2, vec3, + Angle, Lerp, Parametric, Point3, Vary, Vec3, param, polar, pt2, pt3, + rotate2, turns, vec2, vec3, }; use retrofire_core::render::{TexCoord, uv}; @@ -176,9 +176,8 @@ fn create_verts( let start = rotate2(start); // Create vertices - for (v, Vertex { pos, attrib: n }) in 0.0 - .vary_to(1.0, verts_per_sec as u32) - .map(|t| (t, pts.eval(t))) + for (v, Vertex { pos, attrib: n }) in + param::iter(&(0.0..1.0), verts_per_sec).map(|t| (t, pts.eval(t))) { let mut pos_xz = start.apply(&pt2(pos.x(), 0.0)); let mut n_xz = start.apply(&vec2(n.x(), 0.0)); From a44516281c1d3efd97c677bdaec1266488500ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sun, 21 Dec 2025 23:04:46 +0200 Subject: [PATCH 68/76] Implement polygon triangulation Uses the ear-clipping algorithm. --- core/src/geom/mesh.rs | 2 +- core/src/geom/prim.rs | 124 +++++++++++++++++++++++-- core/src/math/point.rs | 18 ++++ core/src/math/space.rs | 29 +++++- core/src/math/vec.rs | 13 +++ demos/src/bin/bezier.rs | 58 ++++++++---- demos/src/bin/solids.rs | 3 +- geom/src/lib.rs | 200 ++++++++++++++++++++++++++++++++++++++++ geom/src/solids.rs | 4 + 9 files changed, 419 insertions(+), 32 deletions(-) diff --git a/core/src/geom/mesh.rs b/core/src/geom/mesh.rs index 9cba28ba..ff0b2e2a 100644 --- a/core/src/geom/mesh.rs +++ b/core/src/geom/mesh.rs @@ -230,7 +230,7 @@ impl Builder { } // ...and normalize to unit length. for v in &mut verts { - v.attrib = v.attrib.normalize(); + v.attrib = v.attrib.normalize_or_zero(); } // No need to sanity check again diff --git a/core/src/geom/prim.rs b/core/src/geom/prim.rs index 757fb838..38277617 100644 --- a/core/src/geom/prim.rs +++ b/core/src/geom/prim.rs @@ -160,10 +160,10 @@ impl Tri { } } -impl> Tri

{ +impl> Tri { /// Given a triangle ABC, returns the vectors [AB, AC]. #[inline] - pub fn tangents(&self) -> [T::Diff; 2] { + pub fn tangents(&self) -> [P::Diff; 2] { let [a, b, c] = &self.0; [b.pos().sub(a.pos()), c.pos().sub(a.pos())] } @@ -171,12 +171,13 @@ impl> Tri

{ /// Returns the geometric center, or "balance point", of `self`. /// /// The centroid is simply the average of the three vertex positions. - pub fn centroid(&self) -> T + pub fn centroid(&self) -> P + // TODO Should this return Vertex? where - T::Diff: Linear, + P: Clone, + P::Diff: Linear, { - let [ab, ac] = self.tangents(); - self.0[0].pos().add(&ab.add(&ac).mul(1.0 / 3.0)) + P::centroid(&self.0.map(|v| v.pos().clone())) } } @@ -252,6 +253,49 @@ impl>> Tri

{ let [t, u] = self.tangents(); t.perp_dot(u) / 2.0 } + + /// Returns the (positive) area of `self`. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::{vertex, Tri}; + /// use retrofire_core::math::pt2; + /// + /// let tri = Tri([ + /// vertex(pt2::<_, ()>(0.0, 0.0), ()), + /// vertex(pt2(0.0, 3.0), ()), + /// vertex(pt2(4.0, 0.0), ()), + /// ]); + /// assert_eq!(tri.area(), 6.0); + /// ``` + pub fn area(&self) -> f32 { + self.signed_area().abs() + } + + /// Returns whether the given point is within the bounds of `self`. + /// + /// # Examples TODO broke + /// ``` + /// use retrofire_core::{geom::{Tri}, math::{Point2, pt2}}; + /// + /// let tri: Tri = Tri([pt2(-2.0, 0.0), pt2(3.0, 0.0), pt2(0.0, 4.0)]); + /// + /// assert!(tri.contains(pt2(0.0, 0.0))); + /// + /// assert!(!tri.contains(pt2(0.0, -1.0))); + /// assert!(!tri.contains(pt2(2.0, 3.0))); + /// ``` + pub fn contains(&self, pt: Point2) -> bool { + // For each of the three lines defined by the triangle's edges, + // compute which side the point is. If it's on the same side of + // each line, it is inside the triangle. + + let [sign_ab, sign_bc, sign_ca] = self + .edges() + .map(|e| Edge(e.0.pos(), e.1.pos()).sign(pt)); + + sign_ab == sign_bc && sign_bc == sign_ca + } } impl>> Tri

{ @@ -610,7 +654,7 @@ impl Polyline { } impl Polyline>> { - /// Returns the sum of the lengths of the edges of `self`. + /// Returns the sum of the (Euclidean) lengths of the edges of `self`. /// /// # Examples /// ``` @@ -661,10 +705,59 @@ impl Polygon { }; self.0 .array_windows() - .map(|[a, b]| Edge(a, b)) + .map(Edge::from) .chain(last_first) } } +impl Polygon> { + /// Returns the vertex winding order of `self`. + pub fn winding(&self) -> Option { + if self.0.len() < 3 { + return None; + } + + // Find (any) vertex Q on the convex hull of the polygon; the leftmost + // one works fine. The winding of the polygon is that of triangle PQR + // where P and R are the vertices adjacent to Q. + + let mut min = (&self.0[0], 0); + for (p, i) in self.0.iter().zip(0..) { + if p.x() < min.0.x() { + min = (p, i); + } + } + + let b = min.0; + let a = if min.1 == 0 { + self.0[self.0.len() - 1] + } else { + self.0[min.1 - 1] + }; + let c = self.0[(min.1 + 1) % self.0.len()]; + + Some(tri(a, *b, c).map(|p| vertex(p, ())).winding()) + } +} + +impl Edge> { + #[inline] + pub const fn normal(&self) -> Vec2 { + let Edge(a, b) = self; + vec2(a.y() - b.y(), b.x() - a.x()) + } + + /// ASfg + /// + /// # E + #[inline] + pub const fn sign(&self, pt: Point2) -> f32 { + let Self(e0, e1) = self; + // Manual sub because of const... + let e0_e1: Vec2 = vec2(e1.x() - e0.x(), e1.y() - e0.y()); + let e0_pt = vec2(pt.x() - e0.x(), pt.y() - e0.y()); + (e0_e1).perp_dot(e0_pt).signum() + } +} impl Line2 { /// Two-dimensional line, given by the line equation ax + by = c. @@ -683,7 +776,7 @@ impl Line2 { /// If the points coincide. pub fn from_points(p: Point2, q: Point2) -> Self { // TODO not const due to normalize - Edge(p, q).into() + Self::from(Edge(p, q)) } /// Returns the slope and y-intercept of `self` if `self` is not vertical. @@ -709,7 +802,7 @@ impl Line2 { Some((m, y0)) } - /// Returns + // Returns TODO pub fn normal(&self) -> Vec2 { vec2(self.0[0], self.0[1]).normalize() } @@ -722,6 +815,11 @@ impl Line2 { pub const fn coeffs(&self) -> [f32; 3] { self.0.0 } + + #[inline] + pub fn signed_dist(&self, pt: Point2) -> f32 { + self.0.dot(&pt.to_hom()) + } } // @@ -920,6 +1018,12 @@ impl Debug for Line2 { f.write_str(")") } } +// +// impl

From

for Vertex { +// fn from(pos: P) -> Self { +// Self { pos, attrib: () } +// } +// } impl From>> for Line2 { /// Returns the line coincident with the given ray. diff --git a/core/src/math/point.rs b/core/src/math/point.rs index 559607d5..64132fdd 100644 --- a/core/src/math/point.rs +++ b/core/src/math/point.rs @@ -212,6 +212,11 @@ impl Point<[Sc; 2], Real<2, B>> { self.0[1] } + #[inline] + pub const fn yx(&self) -> Point<[Sc; 2], Real<2, B>> { + pt2(self.y(), self.x()) + } + /// Converts `self` to a `Point3`, with z equal to 0. #[inline] pub fn to_pt3(self) -> Point<[Sc; 3], Real<3, B>> @@ -246,6 +251,19 @@ impl Point<[Sc; 3], Real<3, B>> { pub const fn z(&self) -> Sc { self.0[2] } + + #[inline] + pub const fn xy(&self) -> Point<[Sc; 2], Real<2, B>> { + pt2(self.x(), self.y()) + } + #[inline] + pub const fn xz(&self) -> Point<[Sc; 2], Real<2, B>> { + pt2(self.x(), self.z()) + } + #[inline] + pub const fn yz(&self) -> Point<[Sc; 2], Real<2, B>> { + pt2(self.y(), self.z()) + } } impl Point3 { diff --git a/core/src/math/space.rs b/core/src/math/space.rs index 51fa49cb..f9d2c4a6 100644 --- a/core/src/math/space.rs +++ b/core/src/math/space.rs @@ -42,7 +42,7 @@ pub trait Affine: Sized { /// returns /// /// w1 * P1 + ... + w*n* * P*n* - fn combine( + fn combine_x( weights: &[S; N], points: &[Self; N], ) -> Self @@ -56,6 +56,33 @@ pub trait Affine: Sized { zip(&weights[1..], &points[1..]) .fold(p0.clone(), |res, (w, q)| res.add(&q.sub(p0).mul(*w))) } + + /// TODO + fn combine(&self, others: I) -> Self + where + I: IntoIterator, + Self::Diff: Linear, + { + let v = others + .into_iter() + .fold(Self::Diff::zero(), |res, (q, w)| { + res.add(&q.sub(self).mul(w)) + }); + + self.add(&v) + } + + fn centroid(pts: impl AsRef<[Self]>) -> Self + where + Self: Clone, + Self::Diff: Linear, + { + let [pt, rest @ ..] = pts.as_ref() else { + panic!("Empty set has no centroid") + }; + let weight = 1.0 / pts.as_ref().len() as f32; + pt.combine(rest.iter().cloned().map(|pt| (pt, weight))) + } } /// Trait for types representing elements of a linear space (vector space). diff --git a/core/src/math/vec.rs b/core/src/math/vec.rs index 5ee51b8c..e2d39ee8 100644 --- a/core/src/math/vec.rs +++ b/core/src/math/vec.rs @@ -545,6 +545,11 @@ impl Vector<[Sc; 2], Real<2, B>> { self.0[1] } + #[inline] + pub const fn yx(&self) -> Vector<[Sc; 2], Real<2, B>> { + vec2(self.y(), self.x()) + } + /// Converts `self` to a `Vec3`, with z set to 0. /// /// # Examples @@ -673,6 +678,14 @@ where pub const fn xy(&self) -> Vector<[Sc; 2], Real<2, B>> { vec2(self.x(), self.y()) } + #[inline] + pub const fn xz(&self) -> Vector<[Sc; 2], Real<2, B>> { + vec2(self.x(), self.z()) + } + #[inline] + pub const fn yz(&self) -> Vector<[Sc; 2], Real<2, B>> { + vec2(self.y(), self.z()) + } /// Returns the cross product of `self` with `other`. /// diff --git a/demos/src/bin/bezier.rs b/demos/src/bin/bezier.rs index 10d7a57f..8c5c9a75 100644 --- a/demos/src/bin/bezier.rs +++ b/demos/src/bin/bezier.rs @@ -1,15 +1,16 @@ use core::ops::ControlFlow::Continue; - +use minifb::{MouseButton, MouseMode}; use re::prelude::*; +use re::core::geom::Polygon; use re::core::{ - geom::Ray, + geom::Edge, math::rand::{Distrib, Uniform, VectorsOnUnitDisk, Xorshift64}, - math::spline::approximate, render::raster::line, util::dims, }; use re::front::{Frame, minifb::Window}; +use re::geom::triangulate; fn main() { let dims @ Dims(w, h) = dims::SVGA_800_600; @@ -17,6 +18,7 @@ fn main() { let mut win = Window::builder() .title("retrofire//bezier") .dims(dims) + .target_fps(Some(30)) .build() .expect("should create window"); @@ -31,17 +33,21 @@ fn main() { (pos, vel).samples(rng).take(32).collect(); // Disable some unneeded things - win.ctx.color_clear = None; - win.ctx.depth_clear = None; + //win.ctx.color_clear = None; + //win.ctx.depth_clear = None; - win.run(|Frame { dt, buf, .. }| { + let mut poly = Polygon::default(); + let mut down = false; + win.run(|Frame { dt, buf, win, .. }| { let buf = &mut buf.borrow_mut().color_buf.buf; - // Fade out previous frame a bit - buf.iter_mut() - .for_each(|c| *c = c.saturating_sub(0x08_08_02)); + if !down && win.imp.get_mouse_down(MouseButton::Left) { + down = true; + let (mx, my) = win.imp.get_mouse_pos(MouseMode::Clamp).unwrap(); + poly.0.push(pt2(mx, my)); + } - let rays: Vec> = pos_vels + /* let rays: Vec> = pos_vels .chunks(2) .map(|ch| Ray(ch[0].0, (ch[1].0 - ch[0].0) * 0.4)) .collect(); @@ -55,19 +61,33 @@ fn main() { line(vs, |sl| { buf[sl.y][sl.xs].fill(0xFF_FF_FF); }) + eprintln!("{}", poly.0.len()); + }*/ + + if !win.imp.get_mouse_down(MouseButton::Left) { + down = false; } - let dt = dt.as_secs_f32(); - for (pos, vel) in &mut pos_vels { - *pos = (*pos + 80.0 * *vel * dt).clamp(&min, &max); - let [dx, dy] = &mut vel.0; - if pos.x() == min.x() || pos.x() == max.x() { - *dx = -*dx; - } - if pos.y() == min.y() || pos.y() == max.y() { - *dy = -*dy; + let tris = triangulate(&poly); + + for tri in tris { + for Edge(&p, &q) in tri.edges() { + let a = vertex(p.to_pt3(), ()); + let b = vertex(q.to_pt3(), ()); + line([a, b], |sl| { + buf[sl.y][sl.xs].fill(0xFFFF); + }); } } + + for Edge(&p, &q) in poly.edges() { + let a = vertex(p.to_pt3(), ()); + let b = vertex(q.to_pt3(), ()); + line([a, b], |sl| { + buf[sl.y][sl.xs].fill(!0); + }); + } + Continue(()) }); } diff --git a/demos/src/bin/solids.rs b/demos/src/bin/solids.rs index 01f0077f..26dbd34c 100644 --- a/demos/src/bin/solids.rs +++ b/demos/src/bin/solids.rs @@ -66,8 +66,8 @@ fn main() { let shader = shader::new(vtx_shader, frag_shader); let translate = translate(-3.0 * Vec3::Z); - let mut state = State::new(); + win.run(|frame| { let Frame { t, dt, win, .. } = frame; @@ -139,6 +139,7 @@ fn objects_n(res: u32) -> [Mesh; 14] { let major_sectors = 3 * res; let minor_sectors = 2 * res; + [ // The five Platonic solids Tetrahedron.build(), diff --git a/geom/src/lib.rs b/geom/src/lib.rs index 35e4e144..671061d1 100644 --- a/geom/src/lib.rs +++ b/geom/src/lib.rs @@ -9,4 +9,204 @@ pub mod io; pub mod isect; pub mod solids; +use alloc::vec::Vec; +use core::{fmt::Debug, hint::cold_path}; + +use retrofire_core::{ + geom::{Mesh, Polygon, Tri, Vertex2, Winding, tri, vertex}, + math::{Point2, pt3}, +}; + pub use isect::Intersect; + +/// Converts a polygon into a triangle mesh that partitions the polygon. +/// +/// In other words, returns a set of triangles that exactly covers the input +/// polygon with no overlap. Given a polygon with *n* vertices, the returned +/// mesh will have exactly *n* vertices and *n*-2 faces. +/// +/// This function is based on the so-called ear-clipping algorithm [TODO ref]. +pub fn triangulate(poly: &Polygon) -> Mesh<()> { + let pts = &poly.0; + let len = pts.len(); + if len < 3 { + cold_path(); + return Mesh::default(); + } + let mut res = Mesh::new( + Vec::with_capacity(len - 2), + pts.iter() + // TODO Should be able to return a "Mesh2" with points intact + .map(|pt| vertex(pt3(pt.x(), 0.0, pt.y()), ())), + ); + + // A very fast path for convex polygons (O(n) vs O(n²)) + if poly.is_convex() { + res.faces + .extend((2..len).map(|i| tri(0, i - 1, i))); + return res; + } + + let mut clipper = Clipper { + pts: &pts, + ixs: (0..len).collect(), + wnd: poly.winding(), + }; + for n in 0..len * len { + if let Some(tri) = clipper.clip_if_ear(n % clipper.ixs.len()) { + res.faces.push(tri); + if clipper.ixs.is_empty() { + return res; + } + } + } + panic!("triangulation stuck, polygon is likely self-intersecting"); + + /* eprintln!( + "Triangulated {}-gon -> {} triangles in {count} iters ({:.3} ms)", + poly.0.len(), + faces.len(), + start.elapsed().as_secs_f32() * 1000.0 + ); + + debug_assert_eq!(res.faces.len(), pts.len() - 2); + debug_assert_eq!(res.faces.capacity(), pts.len() - 2); + */ +} + +/// Helper type for implementing the ear clipping triangulation algorithm. +struct Clipper<'a, B> { + pts: &'a [Point2], + ixs: Vec, + wnd: Winding, +} + +impl Clipper<'_, B> { + fn clip_if_ear(&mut self, i: usize) -> Option> { + let j = self.next(i); + if self.is_convex(j) && !self.contains_point(j) { + let k = self.next(j); + // Valid ear -> clip it + let res = tri(self.ixs[i], self.ixs[j], self.ixs[k]); + self.ixs.remove(j); + Some(res) + } else { + None + } + } + /// Returns true iff the vertex at ixs[j] is convex, that is, its winding + /// is equal to the winding of the entire polygon. Convex vertices are + /// candidates for clipping, non-convex (aka reflex) vertices never are. + fn is_convex(&self, j: usize) -> bool { + let i = self.prev(j); + let k = self.next(j); + self.winding(i, j, k) == self.wnd + } + + /// Returns the winding of the triangle given by the indices i, j, and k. + /// Assumes that the indices are valid. + fn winding(&self, i: usize, j: usize, k: usize) -> Winding { + let Self { pts, ixs, .. } = &self; + let ij = pts[ixs[j]] - pts[ixs[i]]; + let jk = pts[ixs[k]] - pts[ixs[j]]; + if ij.perp_dot(jk) < 0.0 { + Winding::Cw + } else { + Winding::Ccw + } + } + + /// Returns true iff the triangle at ixs[j] contains any other + /// vertex. + /// Only vertices that are reflex need to be considered, because if *any* + /// vertices are contained, at least one of them must be reflex. + fn contains_point(&self, j: usize) -> bool { + let pts = &self.pts; + let ixs = &self.ixs; + + let i = self.prev(j); + let k = self.next(j); + + // TODO Tri should have contains() + let tri: Tri> = + Tri([i, j, k].map(|h| vertex(pts[ixs[h]], ()))); + + for h in 0..ixs.len() { + if [i, j, k].contains(&h) { + continue; + } + // TODO this should be precomputed + // TODO once all are convex, fast path out + if self.is_convex(h) { + continue; + } + if tri.contains(pts[ixs[h]]) { + return true; + } + } + false + } + + #[inline] + fn next(&self, i: usize) -> usize { + if i + 1 == self.ixs.len() { 0 } else { i + 1 } + } + + #[inline] + fn prev(&self, i: usize) -> usize { + (if i == 0 { self.ixs.len() } else { i }) - 1 + } +} + +#[allow(unused)] +#[cfg(test)] +mod tests { + use super::*; + use retrofire_core::math::pt2; + + #[test] + fn triangulate_tri() { + let pts: [Point2; _] = [pt2(0.0, 0.0), pt2(1.0, 0.0), pt2(1.0, 1.0)]; + let p = Polygon::new(pts); + let t = triangulate(&p); + // FIXME assert_eq!(t, [Tri(pts)]); + } + #[test] + fn triangulate_quad() { + let [a, b, c, d]: [Point2; _] = + [pt2(0.0, 0.0), pt2(1.0, 0.0), pt2(1.0, 1.0), pt2(0.0, 1.0)]; + let p = Polygon::new([a, b, c, d]); + let t = triangulate(&p); + // FIXME assert_eq!(t, [tri(a, b, c), tri(a, c, d)]); + } + #[test] + fn triangulate_concave_quad() { + let [a, b, c, d]: [Point2; _] = + [pt2(0.0, 0.0), pt2(0.5, 0.2), pt2(1.0, 0.0), pt2(0.5, 1.0)]; + let p = Polygon::new([a, b, c, d]); + let t = triangulate(&p); + // FIXME assert_eq!(t, [tri(b, c, d), tri(a, b, d)]); + } + #[test] + fn triangulate_concave_6gon() { + let [a, b, c, d, e, f]: [Point2; _] = [ + pt2(0.0, 0.0), + pt2(1.0, 0.0), + pt2(0.4, 0.8), + pt2(1.0, 1.0), + pt2(0.0, 1.0), + pt2(0.6, 0.2), + ]; + let p = Polygon::new([a, b, c, d, e, f]); + let t = triangulate(&p); + // FIXME assert_eq!(t, [tri(c, d, e), tri(f, a, b), tri(c, e, f), tri(b, c, f)]); + } + #[test] + fn triangulate_self_intersecting() { + let [a, b, c, d]: [Point2; _] = + [pt2(0.0, 0.0), pt2(1.0, 0.0), pt2(0.0, 1.0), pt2(1.0, 1.0)]; + let p = Polygon::new([a, b, c, d]); + let t = triangulate(&p); + // FIXME assert_eq!(t, []); + } +} diff --git a/geom/src/solids.rs b/geom/src/solids.rs index 52c311bd..020c381c 100644 --- a/geom/src/solids.rs +++ b/geom/src/solids.rs @@ -4,6 +4,8 @@ mod lathe; mod platonic; +mod prism; + use alloc::vec::Vec; use retrofire_core::geom::{Mesh, Normal3, Tri, mesh::Builder, tri, vertex}; @@ -14,6 +16,8 @@ pub use lathe::*; pub use platonic::*; +pub use prism::*; + pub trait Build: Sized { fn build(self) -> Mesh; From 8bf1993b713e649e8ca39502112b2ec1e486c069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Mon, 29 Jun 2026 00:04:45 +0300 Subject: [PATCH 69/76] Add triangulation benchmark --- Cargo.toml | 4 +++ benches/poly.rs | 91 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 benches/poly.rs diff --git a/Cargo.toml b/Cargo.toml index 295e4361..02104260 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,6 +102,10 @@ harness = false name = "isect" harness = false +[[bench]] +name = "poly" +harness = false + [[bench]] name = "vec" harness = false diff --git a/benches/poly.rs b/benches/poly.rs new file mode 100644 index 00000000..85a959e8 --- /dev/null +++ b/benches/poly.rs @@ -0,0 +1,91 @@ +//! Polygon operations benchmarks. + +use core::iter::chain; + +use divan::Bencher; + +use retrofire_core::{ + geom::Polygon, + math::{Point2, Vary, polar, pt2, turns}, +}; +use retrofire_geom::triangulate; + +static SIDES: [u32; 5] = [6, 10, 20, 100, 1000]; + +#[divan::bench(sample_size = 100)] +fn triangulate_triangle(b: Bencher) { + let make_poly = || { + let p = Polygon::new( + turns(0.0) + .vary_to(turns(0.7), 3) + .map(|a| polar(1.0, a).to_cart().to_pt()), + ); + assert_eq!(p.0.len(), 3); + p + }; + + b.with_inputs(make_poly) + .counter(3u32) + .bench_local_values(|poly: Polygon| triangulate(&poly)); +} + +#[divan::bench(args = SIDES, sample_size = 100)] +fn triangulate_convex(b: Bencher, sides: u32) { + b.with_inputs(|| { + let p = Polygon::new( + turns(0.0) + .vary_to(turns(1.0), sides + 1) + .take(sides as usize) + .map(|a| polar(1.0, a).to_cart().to_pt()), + ); + assert_eq!(p.0.len(), sides as usize); + p + }) + .counter(sides) + .bench_local_values(|poly: Polygon| triangulate(&poly)); +} + +#[divan::bench(args = SIDES, sample_size = 100, max_time=2)] +fn triangulate_almost_convex(b: Bencher, sides: u32) { + let make_poly = || { + let p = Polygon::new( + turns(0.1) + .vary_to(turns(0.9), sides - 1) + .map(|a| polar(1.0, a).to_cart().to_pt()) + .chain([pt2(0.0, 0.0)]), + ); + assert!(!p.is_convex()); + assert_eq!(p.0.len(), sides as usize); + p + }; + + b.with_inputs(make_poly) + .counter(sides) + .bench_local_values(|poly: Polygon| triangulate(&poly)); +} + +#[divan::bench(args = SIDES, sample_size = 100, max_time=2)] +fn triangulate_concave(b: Bencher, sides: u32) { + let make_poly = || { + let p = Polygon::new( + turns(0.0) + .vary_to(turns(1.0), sides + 1) + .take(sides as usize) + .map(|a| { + let r = 1.0 - 0.8 * (1.5 * a).sin().abs().powf(0.5); + polar(r, a + turns(0.1 * r)).to_cart().to_pt() + }), + ); + assert!(!p.is_convex()); + assert_eq!(p.0.len(), sides as usize); + p + }; + + b.with_inputs(make_poly) + .counter(sides) + .bench_local_values(|poly: Polygon| triangulate(&poly)); +} + +fn main() { + divan::main() +} From 514702152639fdf0e542bfeea37ffc08234a98e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 16 Jul 2026 13:57:55 +0300 Subject: [PATCH 70/76] Move triangulate to own module, return indices instead of mesh --- benches/poly.rs | 2 +- core/src/geom/mesh.rs | 12 +++ demos/src/bin/bezier.rs | 4 +- geom/src/lib.rs | 201 +--------------------------------------- geom/src/tri.rs | 197 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 214 insertions(+), 202 deletions(-) create mode 100644 geom/src/tri.rs diff --git a/benches/poly.rs b/benches/poly.rs index 85a959e8..0dcd3164 100644 --- a/benches/poly.rs +++ b/benches/poly.rs @@ -8,7 +8,7 @@ use retrofire_core::{ geom::Polygon, math::{Point2, Vary, polar, pt2, turns}, }; -use retrofire_geom::triangulate; +use retrofire_geom::tri::triangulate; static SIDES: [u32; 5] = [6, 10, 20, 100, 1000]; diff --git a/core/src/geom/mesh.rs b/core/src/geom/mesh.rs index ff0b2e2a..d5f8e3e6 100644 --- a/core/src/geom/mesh.rs +++ b/core/src/geom/mesh.rs @@ -100,6 +100,18 @@ impl Mesh { .extend(faces.into_iter().map(|tri| tri.map(|i| i + n))); self } + + #[must_use] + pub fn to(self) -> Mesh { + Mesh { + faces: self.faces, + verts: self + .verts + .into_iter() + .map(|v| vertex(v.pos.to(), v.attrib)) + .collect(), + } + } } #[inline(never)] diff --git a/demos/src/bin/bezier.rs b/demos/src/bin/bezier.rs index 8c5c9a75..188cdbf7 100644 --- a/demos/src/bin/bezier.rs +++ b/demos/src/bin/bezier.rs @@ -1,10 +1,10 @@ use core::ops::ControlFlow::Continue; use minifb::{MouseButton, MouseMode}; + use re::prelude::*; -use re::core::geom::Polygon; use re::core::{ - geom::Edge, + geom::{Edge, Polygon}, math::rand::{Distrib, Uniform, VectorsOnUnitDisk, Xorshift64}, render::raster::line, util::dims, diff --git a/geom/src/lib.rs b/geom/src/lib.rs index 671061d1..c741406a 100644 --- a/geom/src/lib.rs +++ b/geom/src/lib.rs @@ -9,204 +9,7 @@ pub mod io; pub mod isect; pub mod solids; -use alloc::vec::Vec; -use core::{fmt::Debug, hint::cold_path}; - -use retrofire_core::{ - geom::{Mesh, Polygon, Tri, Vertex2, Winding, tri, vertex}, - math::{Point2, pt3}, -}; +mod tri; pub use isect::Intersect; - -/// Converts a polygon into a triangle mesh that partitions the polygon. -/// -/// In other words, returns a set of triangles that exactly covers the input -/// polygon with no overlap. Given a polygon with *n* vertices, the returned -/// mesh will have exactly *n* vertices and *n*-2 faces. -/// -/// This function is based on the so-called ear-clipping algorithm [TODO ref]. -pub fn triangulate(poly: &Polygon) -> Mesh<()> { - let pts = &poly.0; - let len = pts.len(); - if len < 3 { - cold_path(); - return Mesh::default(); - } - let mut res = Mesh::new( - Vec::with_capacity(len - 2), - pts.iter() - // TODO Should be able to return a "Mesh2" with points intact - .map(|pt| vertex(pt3(pt.x(), 0.0, pt.y()), ())), - ); - - // A very fast path for convex polygons (O(n) vs O(n²)) - if poly.is_convex() { - res.faces - .extend((2..len).map(|i| tri(0, i - 1, i))); - return res; - } - - let mut clipper = Clipper { - pts: &pts, - ixs: (0..len).collect(), - wnd: poly.winding(), - }; - for n in 0..len * len { - if let Some(tri) = clipper.clip_if_ear(n % clipper.ixs.len()) { - res.faces.push(tri); - if clipper.ixs.is_empty() { - return res; - } - } - } - panic!("triangulation stuck, polygon is likely self-intersecting"); - - /* eprintln!( - "Triangulated {}-gon -> {} triangles in {count} iters ({:.3} ms)", - poly.0.len(), - faces.len(), - start.elapsed().as_secs_f32() * 1000.0 - ); - - debug_assert_eq!(res.faces.len(), pts.len() - 2); - debug_assert_eq!(res.faces.capacity(), pts.len() - 2); - */ -} - -/// Helper type for implementing the ear clipping triangulation algorithm. -struct Clipper<'a, B> { - pts: &'a [Point2], - ixs: Vec, - wnd: Winding, -} - -impl Clipper<'_, B> { - fn clip_if_ear(&mut self, i: usize) -> Option> { - let j = self.next(i); - if self.is_convex(j) && !self.contains_point(j) { - let k = self.next(j); - // Valid ear -> clip it - let res = tri(self.ixs[i], self.ixs[j], self.ixs[k]); - self.ixs.remove(j); - Some(res) - } else { - None - } - } - /// Returns true iff the vertex at ixs[j] is convex, that is, its winding - /// is equal to the winding of the entire polygon. Convex vertices are - /// candidates for clipping, non-convex (aka reflex) vertices never are. - fn is_convex(&self, j: usize) -> bool { - let i = self.prev(j); - let k = self.next(j); - self.winding(i, j, k) == self.wnd - } - - /// Returns the winding of the triangle given by the indices i, j, and k. - /// Assumes that the indices are valid. - fn winding(&self, i: usize, j: usize, k: usize) -> Winding { - let Self { pts, ixs, .. } = &self; - let ij = pts[ixs[j]] - pts[ixs[i]]; - let jk = pts[ixs[k]] - pts[ixs[j]]; - if ij.perp_dot(jk) < 0.0 { - Winding::Cw - } else { - Winding::Ccw - } - } - - /// Returns true iff the triangle at ixs[j] contains any other - /// vertex. - /// Only vertices that are reflex need to be considered, because if *any* - /// vertices are contained, at least one of them must be reflex. - fn contains_point(&self, j: usize) -> bool { - let pts = &self.pts; - let ixs = &self.ixs; - - let i = self.prev(j); - let k = self.next(j); - - // TODO Tri should have contains() - let tri: Tri> = - Tri([i, j, k].map(|h| vertex(pts[ixs[h]], ()))); - - for h in 0..ixs.len() { - if [i, j, k].contains(&h) { - continue; - } - // TODO this should be precomputed - // TODO once all are convex, fast path out - if self.is_convex(h) { - continue; - } - if tri.contains(pts[ixs[h]]) { - return true; - } - } - false - } - - #[inline] - fn next(&self, i: usize) -> usize { - if i + 1 == self.ixs.len() { 0 } else { i + 1 } - } - - #[inline] - fn prev(&self, i: usize) -> usize { - (if i == 0 { self.ixs.len() } else { i }) - 1 - } -} - -#[allow(unused)] -#[cfg(test)] -mod tests { - use super::*; - use retrofire_core::math::pt2; - - #[test] - fn triangulate_tri() { - let pts: [Point2; _] = [pt2(0.0, 0.0), pt2(1.0, 0.0), pt2(1.0, 1.0)]; - let p = Polygon::new(pts); - let t = triangulate(&p); - // FIXME assert_eq!(t, [Tri(pts)]); - } - #[test] - fn triangulate_quad() { - let [a, b, c, d]: [Point2; _] = - [pt2(0.0, 0.0), pt2(1.0, 0.0), pt2(1.0, 1.0), pt2(0.0, 1.0)]; - let p = Polygon::new([a, b, c, d]); - let t = triangulate(&p); - // FIXME assert_eq!(t, [tri(a, b, c), tri(a, c, d)]); - } - #[test] - fn triangulate_concave_quad() { - let [a, b, c, d]: [Point2; _] = - [pt2(0.0, 0.0), pt2(0.5, 0.2), pt2(1.0, 0.0), pt2(0.5, 1.0)]; - let p = Polygon::new([a, b, c, d]); - let t = triangulate(&p); - // FIXME assert_eq!(t, [tri(b, c, d), tri(a, b, d)]); - } - #[test] - fn triangulate_concave_6gon() { - let [a, b, c, d, e, f]: [Point2; _] = [ - pt2(0.0, 0.0), - pt2(1.0, 0.0), - pt2(0.4, 0.8), - pt2(1.0, 1.0), - pt2(0.0, 1.0), - pt2(0.6, 0.2), - ]; - let p = Polygon::new([a, b, c, d, e, f]); - let t = triangulate(&p); - // FIXME assert_eq!(t, [tri(c, d, e), tri(f, a, b), tri(c, e, f), tri(b, c, f)]); - } - #[test] - fn triangulate_self_intersecting() { - let [a, b, c, d]: [Point2; _] = - [pt2(0.0, 0.0), pt2(1.0, 0.0), pt2(0.0, 1.0), pt2(1.0, 1.0)]; - let p = Polygon::new([a, b, c, d]); - let t = triangulate(&p); - // FIXME assert_eq!(t, []); - } -} +pub use tri::triangulate; diff --git a/geom/src/tri.rs b/geom/src/tri.rs new file mode 100644 index 00000000..e4da38a1 --- /dev/null +++ b/geom/src/tri.rs @@ -0,0 +1,197 @@ +use alloc::vec::Vec; +use core::hint::cold_path; + +use retrofire_core::geom::{Polygon, Pos, Tri, Winding, tri}; +use retrofire_core::math::Point2; + +/// Converts a polygon into a triangle mesh that partitions the polygon. +/// +/// In other words, returns a set of triangles that exactly covers the input +/// polygon with no overlap. Given a polygon with *n* vertices, the returned +/// mesh will have exactly *n* vertices and *n*-2 faces. +/// +/// This function is based on the so-called ear-clipping algorithm [TODO ref]. +#[must_use] +pub fn triangulate(poly: &Polygon) -> Vec> +where + V: Pos> + Clone, +{ + let pts = &poly.0; + let len = pts.len(); + if len < 3 { + cold_path(); + return Default::default(); + } + let mut res = Vec::with_capacity(len - 2); + + // A very fast path for convex polygons (O(n) vs O(n²)) + if poly.is_convex() { + res.extend((2..len).map(|i| tri(0, i - 1, i))); + return res; + } + + let mut clipper = Clipper { + points: &pts, + indices: (0..len).collect(), + winding: poly.winding(), + }; + for n in 0..len * len { + if let Some(tri) = clipper.clip_if_ear(n % clipper.indices.len()) { + res.push(tri); + if clipper.indices.is_empty() { + return res; + } + } + } + panic!("triangulation stuck, polygon is likely self-intersecting"); + + /* eprintln!( + "Triangulated {}-gon -> {} triangles in {count} iters ({:.3} ms)", + poly.0.len(), + faces.len(), + start.elapsed().as_secs_f32() * 1000.0 + ); + + debug_assert_eq!(res.faces.len(), pts.len() - 2); + debug_assert_eq!(res.faces.capacity(), pts.len() - 2); + */ +} + +/// Helper type for implementing the ear clipping triangulation algorithm. +struct Clipper<'a, B> { + points: &'a [Point2], + indices: Vec, + winding: Winding, +} + +impl Clipper<'_, B> { + fn clip_if_ear(&mut self, i: usize) -> Option> { + let j = self.next(i); + if self.is_convex(j) && !self.contains_point(j) { + let k = self.next(j); + // Valid ear -> clip it + let res = tri(self.indices[i], self.indices[j], self.indices[k]); + self.indices.remove(j); + Some(res) + } else { + None + } + } + /// Returns true iff the vertex at ixs[j] is convex, that is, its winding + /// is equal to the winding of the entire polygon. Convex vertices are + /// candidates for clipping, non-convex (aka reflex) vertices never are. + fn is_convex(&self, j: usize) -> bool { + let i = self.prev(j); + let k = self.next(j); + self.winding(i, j, k) == self.winding + } + + /// Returns the winding of the triangle given by the indices i, j, and k. + /// Assumes that the indices are valid. + fn winding(&self, i: usize, j: usize, k: usize) -> Winding { + let Self { points: pts, indices: ixs, .. } = &self; + let ij = pts[ixs[j]] - pts[ixs[i]]; + let jk = pts[ixs[k]] - pts[ixs[j]]; + if ij.perp_dot(jk) < 0.0 { + Winding::Cw + } else { + Winding::Ccw + } + } + + /// Returns true iff the triangle at ixs[j] contains any other + /// vertex. + /// Only vertices that are reflex need to be considered, because if *any* + /// vertices are contained, at least one of them must be reflex. + fn contains_point(&self, j: usize) -> bool { + let pts = &self.points; + let ixs = &self.indices; + + let i = self.prev(j); + let k = self.next(j); + + let tri = Tri([i, j, k].map(|h| pts[ixs[h]])); + + for h in 0..ixs.len() { + if [i, j, k].contains(&h) { + continue; + } + // TODO this should be precomputed + // TODO once all are convex, fast path out + if self.is_convex(h) { + continue; + } + if tri.contains(pts[ixs[h]]) { + return true; + } + } + false + } + + #[inline] + fn next(&self, i: usize) -> usize { + if i + 1 == self.indices.len() { + 0 + } else { + i + 1 + } + } + + #[inline] + fn prev(&self, i: usize) -> usize { + (if i == 0 { self.indices.len() } else { i }) - 1 + } +} + +#[allow(unused)] +#[cfg(test)] +mod tests { + use super::*; + use retrofire_core::math::pt2; + + #[test] + fn triangulate_tri() { + let pts: [Point2; _] = [pt2(0.0, 0.0), pt2(1.0, 0.0), pt2(1.0, 1.0)]; + let p = Polygon::new(pts); + let tris = triangulate(&p); + assert_eq!(tris, [tri(0, 1, 2),]); + } + #[test] + fn triangulate_quad() { + let [a, b, c, d]: [Point2; _] = + [pt2(0.0, 0.0), pt2(1.0, 0.0), pt2(1.0, 1.0), pt2(0.0, 1.0)]; + let p = Polygon::new([a, b, c, d]); + let t = triangulate(&p); + // FIXME assert_eq!(t, [tri(a, b, c), tri(a, c, d)]); + } + #[test] + fn triangulate_concave_quad() { + let [a, b, c, d]: [Point2; _] = + [pt2(0.0, 0.0), pt2(0.5, 0.2), pt2(1.0, 0.0), pt2(0.5, 1.0)]; + let p = Polygon::new([a, b, c, d]); + let t = triangulate(&p); + // FIXME assert_eq!(t, [tri(b, c, d), tri(a, b, d)]); + } + #[test] + fn triangulate_concave_6gon() { + let [a, b, c, d, e, f]: [Point2; _] = [ + pt2(0.0, 0.0), + pt2(1.0, 0.0), + pt2(0.4, 0.8), + pt2(1.0, 1.0), + pt2(0.0, 1.0), + pt2(0.6, 0.2), + ]; + let p = Polygon::new([a, b, c, d, e, f]); + let t = triangulate(&p); + // FIXME assert_eq!(t, [tri(c, d, e), tri(f, a, b), tri(c, e, f), tri(b, c, f)]); + } + #[test] + #[should_panic = "triangulation stuck, polygon is likely self-intersecting"] + fn triangulate_self_intersecting() { + let [a, b, c, d]: [Point2; _] = + [pt2(0.0, 0.0), pt2(1.0, 0.0), pt2(0.0, 1.0), pt2(1.0, 1.0)]; + let p = Polygon::new([a, b, c, d]); + let _ = triangulate(&p); + } +} From 89042fcca09505c8172d3049a403463fa6da3193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sun, 28 Jun 2026 23:29:35 +0300 Subject: [PATCH 71/76] Improve Polygon docs, add is_convex() method and FromIterator impl --- core/src/geom/prim.rs | 128 ++++++++++++++++++++++++++-------------- demos/src/bin/bezier.rs | 10 ++-- 2 files changed, 87 insertions(+), 51 deletions(-) diff --git a/core/src/geom/prim.rs b/core/src/geom/prim.rs index 38277617..214f878d 100644 --- a/core/src/geom/prim.rs +++ b/core/src/geom/prim.rs @@ -177,7 +177,7 @@ impl> Tri { P: Clone, P::Diff: Linear, { - P::centroid(&self.0.map(|v| v.pos().clone())) + P::centroid(&self.0.each_ref().map(|v| v.pos().clone())) } } @@ -254,24 +254,6 @@ impl>> Tri

{ t.perp_dot(u) / 2.0 } - /// Returns the (positive) area of `self`. - /// - /// # Examples - /// ``` - /// use retrofire_core::geom::{vertex, Tri}; - /// use retrofire_core::math::pt2; - /// - /// let tri = Tri([ - /// vertex(pt2::<_, ()>(0.0, 0.0), ()), - /// vertex(pt2(0.0, 3.0), ()), - /// vertex(pt2(4.0, 0.0), ()), - /// ]); - /// assert_eq!(tri.area(), 6.0); - /// ``` - pub fn area(&self) -> f32 { - self.signed_area().abs() - } - /// Returns whether the given point is within the bounds of `self`. /// /// # Examples TODO broke @@ -290,11 +272,13 @@ impl>> Tri

{ // compute which side the point is. If it's on the same side of // each line, it is inside the triangle. - let [sign_ab, sign_bc, sign_ca] = self + todo!("Sign method missing in this commit") + + /*let [sign_ab, sign_bc, sign_ca] = self .edges() .map(|e| Edge(e.0.pos(), e.1.pos()).sign(pt)); - sign_ab == sign_bc && sign_bc == sign_ca + sign_ab == sign_bc && sign_bc == sign_ca*/ } } @@ -705,37 +689,91 @@ impl Polygon { }; self.0 .array_windows() - .map(Edge::from) + .map(|[a, b]| Edge(a, b)) .chain(last_first) } } impl Polygon> { /// Returns the vertex winding order of `self`. - pub fn winding(&self) -> Option { - if self.0.len() < 3 { - return None; - } - + /// + /// # Panics + /// If the polygon is degenerate (that is, has fewer than three vertices). + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::{Polygon, Winding}; + /// use retrofire_core::math::{pt2, Point2}; + /// + /// // This is a counter-clockwise polygon: + /// let mut tri = Polygon::::new([ + /// pt2(0.0, 0.0), pt2(3.0, 0.0), pt2(0.0, 2.0)] + /// ); + /// assert_eq!(tri.winding(), Winding::Ccw); + /// + /// // Swapping two vertices reverts the winding order: + /// tri.0.swap(1, 2); + /// assert_eq!(tri.winding(), Winding::Cw); + /// ``` + pub fn winding(&self) -> Winding { // Find (any) vertex Q on the convex hull of the polygon; the leftmost - // one works fine. The winding of the polygon is that of triangle PQR - // where P and R are the vertices adjacent to Q. + // one works fine. The winding of the polygon is equal to that of + // triangle PQR, where P and R are the vertices adjacent to Q. + let pts = &self.0; + let len = pts.len(); + debug_assert!(len >= 3, "degenerate polygon: winding undefined"); - let mut min = (&self.0[0], 0); - for (p, i) in self.0.iter().zip(0..) { - if p.x() < min.0.x() { - min = (p, i); - } - } + let (left_pt, left_i) = zip(pts, 0..) + .min_by(|a, b| a.0.x().total_cmp(&b.0.x())) + .unwrap(); - let b = min.0; - let a = if min.1 == 0 { - self.0[self.0.len() - 1] - } else { - self.0[min.1 - 1] - }; - let c = self.0[(min.1 + 1) % self.0.len()]; + let q = *left_pt; + let p = pts[if left_i == 0 { len } else { left_i } - 1]; + let r = pts[(left_i + 1) % len]; + tri(p, q, r).map(|p| vertex(p, ())).winding() + } + + /// Returns whether `self` is a convex polygon. + /// + /// A polygon is convex iff all of its internal angles are non-reflex, + /// that is, at most 180 degrees. The result of this function does not + /// depend on the winding order of `self`. Note that polygons with angles + /// very close to 180° may be reported as either convex or non-convex, + /// depending on rounding errors. + /// + /// # Panics + /// If the polygon is degenerate (has fewer than three vertices). + /// + /// # Examples + /// ``` + /// use retrofire_core::{geom::Polygon, math::{Point2, pt2}}; + /// + /// let tri = Polygon::::new([ + /// pt2(0.0, 0.0), pt2(2.0, 0.0), pt2(1.0, 2.0)] + /// ); + /// assert!(tri.is_convex()); // Triangles are always convex + /// + /// let quad = Polygon::::new([ + /// pt2(0.0, 0.0), pt2(2.0, 0.0), pt2(1.0, 1.0), pt2(1.0, 2.0) + /// ]); + /// assert!(!quad.is_convex()); // This quad has a concave vertex + /// ``` + #[inline] + pub fn is_convex(&self) -> bool { + let pts = &self.0; + let l = pts.len(); + debug_assert!(l >= 3, "degenerate polygon: convexity undefined"); + + let sign = Self::sign([pts[l - 1], pts[0], pts[1]]); + let wrap = [[pts[l - 2], pts[l - 1], pts[0]]]; + pts[1..] + .array_windows() + .chain(&wrap) + .all(|&abc| sign == Self::sign(abc)) + } - Some(tri(a, *b, c).map(|p| vertex(p, ())).winding()) + #[inline] + fn sign([a, b, c]: [Point2; 3]) -> f32 { + (b - a).perp_dot(c - b).signum() } } @@ -1072,8 +1110,8 @@ impl<'a, T> From<&'a [T; 2]> for Edge<&'a T> { } } -impl FromIterator

for Polygon

{ - fn from_iter>(it: I) -> Self { +impl>> FromIterator for Polygon { + fn from_iter>(it: I) -> Self { Self::new(it) } } diff --git a/demos/src/bin/bezier.rs b/demos/src/bin/bezier.rs index 188cdbf7..8fa95de9 100644 --- a/demos/src/bin/bezier.rs +++ b/demos/src/bin/bezier.rs @@ -29,7 +29,7 @@ fn main() { let pos = Uniform::(min..max); let vel = VectorsOnUnitDisk; - let mut pos_vels: Vec<(Point2, Vec2)> = + let mut _pos_vels: Vec<(Point2, Vec2)> = (pos, vel).samples(rng).take(32).collect(); // Disable some unneeded things @@ -70,11 +70,9 @@ fn main() { let tris = triangulate(&poly); - for tri in tris { - for Edge(&p, &q) in tri.edges() { - let a = vertex(p.to_pt3(), ()); - let b = vertex(q.to_pt3(), ()); - line([a, b], |sl| { + for tri in tris.faces() { + for Edge(&&p, &&q) in tri.edges() { + line([p, q], |sl| { buf[sl.y][sl.xs].fill(0xFFFF); }); } From 2e8a5e2a49339827f3bd9ad3d431755542fee181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sat, 20 Jun 2026 22:58:56 +0300 Subject: [PATCH 72/76] Add Prism solid --- geom/src/solids/prism.rs | 79 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 geom/src/solids/prism.rs diff --git a/geom/src/solids/prism.rs b/geom/src/solids/prism.rs new file mode 100644 index 00000000..c55a68c3 --- /dev/null +++ b/geom/src/solids/prism.rs @@ -0,0 +1,79 @@ +use alloc::vec::Vec; + +use retrofire_core::{ + geom::{Mesh, Normal3, Polygon, tri, vertex}, + math::{Point2, pt3}, +}; + +use crate::{solids::Build, triangulate}; + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Prism { + pub points: Polygon, + pub capped: bool, +} + +impl Prism { + pub fn new(points: I) -> Self + where + I: IntoIterator, + { + Self { + points: points.into_iter().collect(), + capped: true, + } + } + pub fn capped(self, capped: bool) -> Self { + Self { capped, ..self } + } +} + +impl Build<()> for Prism { + fn build(self) -> Mesh<()> { + let verts: Vec<_> = self + .points + .0 + .iter() + .flat_map(|pt| { + [ + vertex(pt3(pt.x(), 0.0, pt.y()), ()), // bottom + vertex(pt3(pt.x(), 1.0, pt.y()), ()), // top + ] + }) + .collect(); + + let mut faces = Vec::new(); + + let l = verts.len(); + for i in (0..l).step_by(2) { + faces.push(tri(i, (i + 1) % l, (i + 3) % l)); + faces.push(tri(i, (i + 3) % l, (i + 2) % l)); + } + + let res = Mesh { faces, verts }; + + if self.capped { + let bottom = triangulate(&self.points); + + let mut top = bottom.clone(); + + for v in &mut top.verts { + v.pos[1] = 1.0; + } + for f in &mut top.faces { + *f = tri(f.0[0], f.0[2], f.0[1]); + } + + return res.merge(bottom).merge(top); + } + res + } +} + +impl Build for Prism { + fn build(self) -> Mesh { + Build::<()>::builder(self) + .with_vertex_normals() + .build() + } +} From 335a5b984bfbe9b70123be47170a5f4e828f10f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sun, 21 Jun 2026 23:46:04 +0300 Subject: [PATCH 73/76] Add prism to solids demo --- core/src/geom/prim.rs | 67 ++++++++++++++++++----------------------- demos/src/bin/solids.rs | 27 ++++++++++++++++- 2 files changed, 56 insertions(+), 38 deletions(-) diff --git a/core/src/geom/prim.rs b/core/src/geom/prim.rs index 214f878d..f2661d74 100644 --- a/core/src/geom/prim.rs +++ b/core/src/geom/prim.rs @@ -272,13 +272,8 @@ impl>> Tri

{ // compute which side the point is. If it's on the same side of // each line, it is inside the triangle. - todo!("Sign method missing in this commit") - - /*let [sign_ab, sign_bc, sign_ca] = self - .edges() - .map(|e| Edge(e.0.pos(), e.1.pos()).sign(pt)); - - sign_ab == sign_bc && sign_bc == sign_ca*/ + let [sign_ab, sign_bc, sign_ca] = self.edges().map(|e| e.sign(pt)); + sign_ab == sign_bc && sign_bc == sign_ca } } @@ -693,7 +688,7 @@ impl Polygon { .chain(last_first) } } -impl Polygon> { +impl>> Polygon { /// Returns the vertex winding order of `self`. /// /// # Panics @@ -723,13 +718,13 @@ impl Polygon> { debug_assert!(len >= 3, "degenerate polygon: winding undefined"); let (left_pt, left_i) = zip(pts, 0..) - .min_by(|a, b| a.0.x().total_cmp(&b.0.x())) + .min_by(|a, b| a.0.pos().x().total_cmp(&b.0.pos().x())) .unwrap(); - let q = *left_pt; - let p = pts[if left_i == 0 { len } else { left_i } - 1]; - let r = pts[(left_i + 1) % len]; - tri(p, q, r).map(|p| vertex(p, ())).winding() + let q = left_pt.pos(); + let p = pts[if left_i == 0 { len } else { left_i } - 1].pos(); + let r = pts[(left_i + 1) % len].pos(); + tri(p, q, r).winding() } /// Returns whether `self` is a convex polygon. @@ -763,37 +758,35 @@ impl Polygon> { let l = pts.len(); debug_assert!(l >= 3, "degenerate polygon: convexity undefined"); - let sign = Self::sign([pts[l - 1], pts[0], pts[1]]); - let wrap = [[pts[l - 2], pts[l - 1], pts[0]]]; - pts[1..] - .array_windows() - .chain(&wrap) - .all(|&abc| sign == Self::sign(abc)) + let sign1 = Self::sign([&pts[l - 2], &pts[l - 1], &pts[0]]); + let sign2 = Self::sign([&pts[l - 1], &pts[0], &pts[1]]); + sign1 == sign2 + && pts[1..] + .array_windows() + .all(|abc| sign1 == Self::sign(abc.each_ref())) } #[inline] - fn sign([a, b, c]: [Point2; 3]) -> f32 { - (b - a).perp_dot(c - b).signum() + fn sign([a, b, c]: [&V; 3]) -> f32 { + (*b.pos() - *a.pos()) + .perp_dot(*c.pos() - *b.pos()) + .signum() } } -impl Edge> { +impl>> Edge { #[inline] - pub const fn normal(&self) -> Vec2 { - let Edge(a, b) = self; - vec2(a.y() - b.y(), b.x() - a.x()) + pub fn normal(&self) -> Vec2 { + let a = self.0.pos(); + let b = self.1.pos(); + (*b - *a).perp() } - /// ASfg - /// - /// # E #[inline] - pub const fn sign(&self, pt: Point2) -> f32 { - let Self(e0, e1) = self; - // Manual sub because of const... - let e0_e1: Vec2 = vec2(e1.x() - e0.x(), e1.y() - e0.y()); - let e0_pt = vec2(pt.x() - e0.x(), pt.y() - e0.y()); - (e0_e1).perp_dot(e0_pt).signum() + pub fn sign(&self, pt: Point2) -> f32 { + let a = *self.0.pos(); + let b = *self.1.pos(); + (b - a).perp_dot(pt - a).signum() } } @@ -871,11 +864,11 @@ impl Pos for Vertex { &self.pos } } -impl Pos for &Vertex { - type Type = P; +impl Pos for &P { + type Type = P::Type; fn pos(&self) -> &Self::Type { - &self.pos + (*self).pos() } } diff --git a/demos/src/bin/solids.rs b/demos/src/bin/solids.rs index 26dbd34c..f2cbda28 100644 --- a/demos/src/bin/solids.rs +++ b/demos/src/bin/solids.rs @@ -5,6 +5,7 @@ use minifb::{Key, KeyRepeat}; use re::prelude::*; +use re::core::geom::Polygon; use re::core::{ geom::{Polyline, Ray}, math::{ProjVec3, color::gray, spline::HermiteSpline}, @@ -130,7 +131,7 @@ fn main() { // Creates the 14 objects exhibited. #[rustfmt::skip] -fn objects_n(res: u32) -> [Mesh; 14] { +fn objects_n(res: u32) -> [Mesh; 15] { let segments = res; let sectors = 2 * res; @@ -141,6 +142,8 @@ fn objects_n(res: u32) -> [Mesh; 14] { let minor_sectors = 2 * res; [ + prism(sectors), + // The five Platonic solids Tetrahedron.build(), Cube { side_len: 1.25 }.build(), @@ -163,6 +166,28 @@ fn objects_n(res: u32) -> [Mesh; 14] { ] } +fn prism(_secs: u32) -> Mesh { + let step = 1; + let points: Vec<_> = (0..10) + .step_by(step as usize) + .map(|a| { + let a = turns(a as f32 / 10.0); + let b = 2.5 * a; + let r = 0.5 + b.sin().powi(20) * 0.0; + + let a = a + 0.1 * turns(r); + polar(r, a).to_cart().to_pt() + }) + .collect(); + + let res = Prism { + points: Polygon::new(points), + capped: true, + }; + + res.build() +} + // Creates a Lathe mesh. fn lathe(secs: u32) -> Mesh { let spline = HermiteSpline::new([ From 57de499677dc944d10314629f250cb67c5d10b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Mon, 29 Jun 2026 00:05:53 +0300 Subject: [PATCH 74/76] Play with Prism in solids demo --- demos/src/bin/solids.rs | 44 +++++++++++++++++++++-------------------- geom/src/lib.rs | 2 +- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/demos/src/bin/solids.rs b/demos/src/bin/solids.rs index f2cbda28..dfc8fada 100644 --- a/demos/src/bin/solids.rs +++ b/demos/src/bin/solids.rs @@ -5,11 +5,10 @@ use minifb::{Key, KeyRepeat}; use re::prelude::*; -use re::core::geom::Polygon; use re::core::{ geom::{Polyline, Ray}, - math::{ProjVec3, color::gray, spline::HermiteSpline}, - render::{Model, cam::Fov, debug, debug::DbgMesh, shader}, + math::{ProjMat3, ProjVec3, color::gray, spline::HermiteSpline}, + render::{self, Model, cam::Fov, debug::DbgMesh, shader}, }; use re::front::{Frame, minifb::Window}; use re::geom::{io::read_obj, solids::*}; @@ -129,7 +128,7 @@ fn main() { }); } -// Creates the 14 objects exhibited. +// Creates the fifteen objects exhibited. #[rustfmt::skip] fn objects_n(res: u32) -> [Mesh; 15] { let segments = res; @@ -167,25 +166,28 @@ fn objects_n(res: u32) -> [Mesh; 15] { } fn prism(_secs: u32) -> Mesh { - let step = 1; - let points: Vec<_> = (0..10) - .step_by(step as usize) + /*let n = 100; + let points: Vec<_> = (0..n) + .map(|a| { + let a = turns(a as f32 / n as f32); + let b = 2.5 * a; + let r = 0.4 + b.sin().powi(4) * 0.6; + + let a = a + 0.1 * turns(r); + polar(r, a).to_cart().to_pt() + }) + .collect();*/ + + let sides = 100; + let pts = turns(0.0) + .vary_to(turns(1.0), sides + 1) + .take(sides as usize) .map(|a| { - let a = turns(a as f32 / 10.0); - let b = 2.5 * a; - let r = 0.5 + b.sin().powi(20) * 0.0; + let r = 1.0 - 0.8 * (1.5 * a).sin().abs().powf(0.5); + polar(r, a + turns(0.1 * r)).to_cart().to_pt() + }); - let a = a + 0.1 * turns(r); - polar(r, a).to_cart().to_pt() - }) - .collect(); - - let res = Prism { - points: Polygon::new(points), - capped: true, - }; - - res.build() + Prism::new(pts).build() } // Creates a Lathe mesh. diff --git a/geom/src/lib.rs b/geom/src/lib.rs index c741406a..51302b1b 100644 --- a/geom/src/lib.rs +++ b/geom/src/lib.rs @@ -10,6 +10,6 @@ pub mod isect; pub mod solids; mod tri; - +s pub use isect::Intersect; pub use tri::triangulate; From ec94a94a8a502dd7b04b4acc0ca1d08ef6adafda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 16 Jul 2026 22:27:45 +0300 Subject: [PATCH 75/76] Add size hint to Vary iterator --- core/src/math/vary.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/src/math/vary.rs b/core/src/math/vary.rs index 71609a85..0e3eb6a3 100644 --- a/core/src/math/vary.rs +++ b/core/src/math/vary.rs @@ -152,6 +152,15 @@ impl Iterator for Iter { let new = self.val.step(&self.step); Some(mem::replace(&mut self.val, new)) } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + if let Some(n) = self.n { + (n as usize, Some(n as usize)) + } else { + (0, None) + } + } } #[cfg(test)] From b184185394b929ebb41ef32db8bfda2fb3029440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 16 Jul 2026 22:29:00 +0300 Subject: [PATCH 76/76] WIP Change Prism to take Parametrics like Lathe --- core/src/geom/mesh.rs | 6 +- demos/src/bin/solids.rs | 32 ++++----- geom/src/solids/prism.rs | 145 ++++++++++++++++++++++++++------------- geom/src/tri.rs | 24 +++---- 4 files changed, 124 insertions(+), 83 deletions(-) diff --git a/core/src/geom/mesh.rs b/core/src/geom/mesh.rs index d5f8e3e6..7f18b229 100644 --- a/core/src/geom/mesh.rs +++ b/core/src/geom/mesh.rs @@ -226,8 +226,10 @@ impl Builder { let face_normals = faces.iter().map(|tri| { // TODO If n-gonal faces are supported some day, the cross // product is not proportional to area anymore - let [a, b, c] = tri.map(|i| verts[i].pos).0; - (b - a).cross(&(c - a)).to() + //let [a, b, c] = tri.map(|i| verts[i].pos).0; + //(b - a).cross(&(c - a)).to() + + tri.map(|i| &verts[i]).normal() }); // ...initialize vertex normals to zero... let mut verts: Vec<_> = verts diff --git a/demos/src/bin/solids.rs b/demos/src/bin/solids.rs index dfc8fada..23b5eef8 100644 --- a/demos/src/bin/solids.rs +++ b/demos/src/bin/solids.rs @@ -5,10 +5,11 @@ use minifb::{Key, KeyRepeat}; use re::prelude::*; +use re::core::geom::Polygon; use re::core::{ geom::{Polyline, Ray}, math::{ProjMat3, ProjVec3, color::gray, spline::HermiteSpline}, - render::{self, Model, cam::Fov, debug::DbgMesh, shader}, + render::{Model, cam::Fov, debug, debug::DbgMesh, shader}, }; use re::front::{Frame, minifb::Window}; use re::geom::{io::read_obj, solids::*}; @@ -16,7 +17,7 @@ use re::geom::{io::read_obj, solids::*}; #[derive(Default)] struct State { lod: u32, - objects: [Mesh; 14], + objects: [Mesh; 15], debug_mesh: DbgMesh, debug_flags: [bool; 6], @@ -165,29 +166,20 @@ fn objects_n(res: u32) -> [Mesh; 15] { ] } -fn prism(_secs: u32) -> Mesh { - /*let n = 100; - let points: Vec<_> = (0..n) - .map(|a| { - let a = turns(a as f32 / n as f32); - let b = 2.5 * a; - let r = 0.4 + b.sin().powi(4) * 0.6; - - let a = a + 0.1 * turns(r); - polar(r, a).to_cart().to_pt() - }) - .collect();*/ - - let sides = 100; +fn prism(secs: u32) -> Mesh { let pts = turns(0.0) - .vary_to(turns(1.0), sides + 1) - .take(sides as usize) + .vary_to(turns(1.0), secs + 1) + .take(secs as usize) .map(|a| { - let r = 1.0 - 0.8 * (1.5 * a).sin().abs().powf(0.5); + let r = 1.0 - 0.8 * (1.5 * a).sin().abs(); polar(r, a + turns(0.1 * r)).to_cart().to_pt() }); - Prism::new(pts).build() + let poly = Polygon::new(pts).with_vertex_normals(); + + Build::builder(Prism::new(poly.0.len(), poly)) + .transform(&translate((0.0, -0.5, 0.0)).to()) + .build() } // Creates a Lathe mesh. diff --git a/geom/src/solids/prism.rs b/geom/src/solids/prism.rs index c55a68c3..5dd2f052 100644 --- a/geom/src/solids/prism.rs +++ b/geom/src/solids/prism.rs @@ -1,79 +1,132 @@ use alloc::vec::Vec; -use retrofire_core::{ - geom::{Mesh, Normal3, Polygon, tri, vertex}, - math::{Point2, pt3}, +use retrofire_core::geom::{ + Mesh, Normal2, Normal3, Polygon, Pos, Tri, Vertex2, Vertex3, tri, vertex, }; +use retrofire_core::math::{Parametric, Point2, Vary, Vec3, param, pt3, vec3}; use crate::{solids::Build, triangulate}; #[derive(Clone, Debug, Default, PartialEq)] -pub struct Prism { - pub points: Polygon, +pub struct Prism

{ + pub points: P, + pub sides: usize, pub capped: bool, } -impl Prism { - pub fn new(points: I) -> Self +impl

Prism

{ + pub fn new>(sides: usize, points: P) -> Self where - I: IntoIterator, + P: Parametric, { - Self { - points: points.into_iter().collect(), - capped: true, - } + Self { points, sides, capped: true } } pub fn capped(self, capped: bool) -> Self { Self { capped, ..self } } -} -impl Build<()> for Prism { - fn build(self) -> Mesh<()> { - let verts: Vec<_> = self - .points - .0 - .iter() - .flat_map(|pt| { - [ - vertex(pt3(pt.x(), 0.0, pt.y()), ()), // bottom - vertex(pt3(pt.x(), 1.0, pt.y()), ()), // top - ] - }) - .collect(); + fn make_caps( + self, + poly: &Polygon, + verts: &mut Vec>, + faces: &mut Vec>, + ) { + let cap_faces = triangulate(&poly); - let mut faces = Vec::new(); + let bot_start = verts.len(); + for i in 0..poly.0.len() { + verts.push(verts[i]); + } + // top cap verts + let top_start = verts.len(); + for i in poly.0.len()..2 * poly.0.len() { + verts.push(verts[i]); + } - let l = verts.len(); - for i in (0..l).step_by(2) { - faces.push(tri(i, (i + 1) % l, (i + 3) % l)); - faces.push(tri(i, (i + 3) % l, (i + 2) % l)); + for tri in &cap_faces { + faces.push(tri.map(|i| bot_start + i)); + } + for Tri([i, j, k]) in cap_faces { + // Invert winding order + faces.push(tri(i, k, j).map(|l| top_start + l)); } + } +} - let res = Mesh { faces, verts }; +impl>> Build<()> for Prism

{ + fn build(self) -> Mesh<()> { + let pts = param::iter(&self.points, self.sides); - if self.capped { - let bottom = triangulate(&self.points); + let bottom_verts = pts + .clone() + .map(|pt| vertex(pt3(pt.pos().x(), 0.0, pt.pos().y()), ())); + let top_verts = + pts.map(|pt| vertex(pt3(pt.pos().x(), 1.0, pt.pos().y()), ())); - let mut top = bottom.clone(); + // Bottom verts = 0..pts.len + // Top verts = pts.len..2*pts.len + let mut verts: Vec<_> = bottom_verts.chain(top_verts).collect(); - for v in &mut top.verts { - v.pos[1] = 1.0; - } - for f in &mut top.faces { - *f = tri(f.0[0], f.0[2], f.0[1]); + let mut faces = Vec::with_capacity(verts.len()); + + let len = self.sides; + for i in 0..len { + let j = (i + 1) % len; + let k = i + len; + let l = j + len; + if i % 2 == 0 { + // k - l + // | / | + // i - j + faces.push(tri(i, k, l)); + faces.push(tri(i, l, j)); + } else { + // k - l + // | \ | + // i - j + faces.push(tri(i, k, j)); + faces.push(tri(j, k, l)); } + } - return res.merge(bottom).merge(top); + if self.capped { + //self.make_caps(&mut verts, &mut faces); } - res + + Mesh { faces, verts } } } -impl Build for Prism { +impl>> Build for Prism

{ fn build(self) -> Mesh { - Build::<()>::builder(self) - .with_vertex_normals() - .build() + let l = self.sides; + let pts: Vec<_> = 0f32 + .vary_to(1.0, l as u32) + .map(|t| self.points.eval(t)) + .collect(); + + let mut verts: Vec<_> = (0..l) + .map(|i| [i, (i + 1) % l, (i + 2) % l]) + .map(|ijk| ijk.map(|i| pts[i].pos)) + .flat_map(|[a, b, c]| { + let n = ((b - a).perp() + (c - b).perp()).normalize_or_zero(); + let n = -vec3(n.x(), 0.0, n.y()); + [ + vertex(pt3(b.x(), 0.0, b.y()), n), // bottom + vertex(pt3(b.x(), 1.0, b.y()), n), // top + ] + }) + .collect(); + + let mut faces = Vec::with_capacity(verts.len()); + + todo!(); + + if self.capped { + todo!() + //self.make_caps_n(&mut verts, &mut faces); + } + + Mesh { faces, verts } } } diff --git a/geom/src/tri.rs b/geom/src/tri.rs index e4da38a1..44132109 100644 --- a/geom/src/tri.rs +++ b/geom/src/tri.rs @@ -16,8 +16,8 @@ pub fn triangulate(poly: &Polygon) -> Vec> where V: Pos> + Clone, { - let pts = &poly.0; - let len = pts.len(); + let points = &poly.0; + let len = points.len(); if len < 3 { cold_path(); return Default::default(); @@ -31,7 +31,7 @@ where } let mut clipper = Clipper { - points: &pts, + points: points.as_slice(), indices: (0..len).collect(), winding: poly.winding(), }; @@ -58,13 +58,13 @@ where } /// Helper type for implementing the ear clipping triangulation algorithm. -struct Clipper<'a, B> { - points: &'a [Point2], +struct Clipper<'a, V> { + points: &'a [V], indices: Vec, winding: Winding, } -impl Clipper<'_, B> { +impl>> Clipper<'_, V> { fn clip_if_ear(&mut self, i: usize) -> Option> { let j = self.next(i); if self.is_convex(j) && !self.contains_point(j) { @@ -90,13 +90,7 @@ impl Clipper<'_, B> { /// Assumes that the indices are valid. fn winding(&self, i: usize, j: usize, k: usize) -> Winding { let Self { points: pts, indices: ixs, .. } = &self; - let ij = pts[ixs[j]] - pts[ixs[i]]; - let jk = pts[ixs[k]] - pts[ixs[j]]; - if ij.perp_dot(jk) < 0.0 { - Winding::Cw - } else { - Winding::Ccw - } + tri(&pts[ixs[i]], &pts[ixs[j]], &pts[ixs[k]]).winding() } /// Returns true iff the triangle at ixs[j] contains any other @@ -110,7 +104,7 @@ impl Clipper<'_, B> { let i = self.prev(j); let k = self.next(j); - let tri = Tri([i, j, k].map(|h| pts[ixs[h]])); + let tri = Tri([i, j, k].map(|h| *pts[ixs[h]].pos())); for h in 0..ixs.len() { if [i, j, k].contains(&h) { @@ -121,7 +115,7 @@ impl Clipper<'_, B> { if self.is_convex(h) { continue; } - if tri.contains(pts[ixs[h]]) { + if tri.contains(pts[ixs[h]].pos().clone()) { return true; } }