From fc8103877b5031767d17c0b65b86f218688d6d25 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 23 Feb 2026 23:20:51 +0000 Subject: [PATCH 01/17] Add bindings for the screenTint parameter. --- inox2d/src/formats/payload.rs | 3 +++ inox2d/src/params.rs | 23 ++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/inox2d/src/formats/payload.rs b/inox2d/src/formats/payload.rs index cb25138..6543bbf 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -464,6 +464,9 @@ fn deserialize_binding_values(param_name: &str, values: &[JsonValue]) -> InoxPar BindingValues::Deform(Matrix2d::from_slice_vecs(&parsed, true)?) } + "screenTint.r" => BindingValues::ScreenTintR(deserialize_inner_binding_values(values)?), + "screenTint.g" => BindingValues::ScreenTintG(deserialize_inner_binding_values(values)?), + "screenTint.b" => BindingValues::ScreenTintB(deserialize_inner_binding_values(values)?), // TODO "opacity" => BindingValues::Opacity, param_name => return Err(InoxParseError::UnknownParamName(param_name.to_owned())), diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 358af1c..34c19b1 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -8,7 +8,7 @@ use crate::math::{ matrix::Matrix2d, }; use crate::node::{ - components::{DeformSource, DeformStack, Mesh, TransformStore, ZSort}, + components::{DeformSource, DeformStack, Drawable, Mesh, TransformStore, ZSort}, InoxNodeUuid, }; use crate::puppet::{InoxNodeTree, Puppet, World}; @@ -32,6 +32,9 @@ pub enum BindingValues { TransformRY(Matrix2d), TransformRZ(Matrix2d), Deform(Matrix2d>), + ScreenTintR(Matrix2d), + ScreenTintG(Matrix2d), + ScreenTintB(Matrix2d), // TODO Opacity, } @@ -228,6 +231,24 @@ impl Param { .expect("Nodes being deformed must have a DeformStack component.") .push(DeformSource::Param(self.uuid), Deform::Direct(direct_deform)); } + BindingValues::ScreenTintR(ref matrix) => { + let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); + + comps.get_mut::(binding.node).unwrap().blending.screen_tint.x += + bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + } + BindingValues::ScreenTintG(ref matrix) => { + let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); + + comps.get_mut::(binding.node).unwrap().blending.screen_tint.y += + bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + } + BindingValues::ScreenTintB(ref matrix) => { + let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); + + comps.get_mut::(binding.node).unwrap().blending.screen_tint.z += + bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + } // TODO BindingValues::Opacity => {} } From 2b351dba208f77471ebc6cb4265984f2f603f5ef Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 23 Feb 2026 23:43:19 +0000 Subject: [PATCH 02/17] Implement opacity bindings --- inox2d/src/formats/payload.rs | 3 +-- inox2d/src/params.rs | 11 +++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/inox2d/src/formats/payload.rs b/inox2d/src/formats/payload.rs index 6543bbf..aa1d2d4 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -467,8 +467,7 @@ fn deserialize_binding_values(param_name: &str, values: &[JsonValue]) -> InoxPar "screenTint.r" => BindingValues::ScreenTintR(deserialize_inner_binding_values(values)?), "screenTint.g" => BindingValues::ScreenTintG(deserialize_inner_binding_values(values)?), "screenTint.b" => BindingValues::ScreenTintB(deserialize_inner_binding_values(values)?), - // TODO - "opacity" => BindingValues::Opacity, + "opacity" => BindingValues::Opacity(deserialize_inner_binding_values(values)?), param_name => return Err(InoxParseError::UnknownParamName(param_name.to_owned())), }) } diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 34c19b1..82edf0c 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -35,8 +35,7 @@ pub enum BindingValues { ScreenTintR(Matrix2d), ScreenTintG(Matrix2d), ScreenTintB(Matrix2d), - // TODO - Opacity, + Opacity(Matrix2d), } #[derive(Debug, Clone)] @@ -249,8 +248,12 @@ impl Param { comps.get_mut::(binding.node).unwrap().blending.screen_tint.z += bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } - // TODO - BindingValues::Opacity => {} + BindingValues::Opacity(ref matrix) => { + let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); + + comps.get_mut::(binding.node).unwrap().blending.opacity += + bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + } } } } From b811bd1ab5c3e353760dff19fba5857ace8ab33d Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Wed, 25 Feb 2026 22:59:32 +0000 Subject: [PATCH 03/17] Implement tint parameter bindings --- inox2d/src/formats/payload.rs | 3 +++ inox2d/src/params.rs | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/inox2d/src/formats/payload.rs b/inox2d/src/formats/payload.rs index aa1d2d4..37f4b1a 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -464,6 +464,9 @@ fn deserialize_binding_values(param_name: &str, values: &[JsonValue]) -> InoxPar BindingValues::Deform(Matrix2d::from_slice_vecs(&parsed, true)?) } + "tint.r" => BindingValues::TintR(deserialize_inner_binding_values(values)?), + "tint.g" => BindingValues::TintG(deserialize_inner_binding_values(values)?), + "tint.b" => BindingValues::TintB(deserialize_inner_binding_values(values)?), "screenTint.r" => BindingValues::ScreenTintR(deserialize_inner_binding_values(values)?), "screenTint.g" => BindingValues::ScreenTintG(deserialize_inner_binding_values(values)?), "screenTint.b" => BindingValues::ScreenTintB(deserialize_inner_binding_values(values)?), diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 82edf0c..d13b67b 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -32,6 +32,9 @@ pub enum BindingValues { TransformRY(Matrix2d), TransformRZ(Matrix2d), Deform(Matrix2d>), + TintR(Matrix2d), + TintG(Matrix2d), + TintB(Matrix2d), ScreenTintR(Matrix2d), ScreenTintG(Matrix2d), ScreenTintB(Matrix2d), @@ -230,6 +233,24 @@ impl Param { .expect("Nodes being deformed must have a DeformStack component.") .push(DeformSource::Param(self.uuid), Deform::Direct(direct_deform)); } + BindingValues::TintR(ref matrix) => { + let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); + + comps.get_mut::(binding.node).unwrap().blending.tint.x += + bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + } + BindingValues::TintG(ref matrix) => { + let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); + + comps.get_mut::(binding.node).unwrap().blending.tint.y += + bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + } + BindingValues::TintB(ref matrix) => { + let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); + + comps.get_mut::(binding.node).unwrap().blending.tint.z += + bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + } BindingValues::ScreenTintR(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); From 8d1e4b709a92f97a7728b887d46cdff38758570a Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Wed, 4 Mar 2026 00:25:18 +0000 Subject: [PATCH 04/17] Support Z translation binding --- inox2d/src/formats/payload.rs | 1 + inox2d/src/params.rs | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/inox2d/src/formats/payload.rs b/inox2d/src/formats/payload.rs index 37f4b1a..5808300 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -446,6 +446,7 @@ fn deserialize_binding_values(param_name: &str, values: &[JsonValue]) -> InoxPar "zSort" => BindingValues::ZSort(deserialize_inner_binding_values(values)?), "transform.t.x" => BindingValues::TransformTX(deserialize_inner_binding_values(values)?), "transform.t.y" => BindingValues::TransformTY(deserialize_inner_binding_values(values)?), + "transform.t.z" => BindingValues::TransformTZ(deserialize_inner_binding_values(values)?), "transform.s.x" => BindingValues::TransformSX(deserialize_inner_binding_values(values)?), "transform.s.y" => BindingValues::TransformSY(deserialize_inner_binding_values(values)?), "transform.r.x" => BindingValues::TransformRX(deserialize_inner_binding_values(values)?), diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index d13b67b..ebc888b 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -26,6 +26,7 @@ pub enum BindingValues { ZSort(Matrix2d), TransformTX(Matrix2d), TransformTY(Matrix2d), + TransformTZ(Matrix2d), TransformSX(Matrix2d), TransformSY(Matrix2d), TransformRX(Matrix2d), @@ -147,6 +148,16 @@ impl Param { .translation .y += bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } + BindingValues::TransformTZ(ref matrix) => { + let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); + + comps + .get_mut::(binding.node) + .unwrap() + .relative + .translation + .z += bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + } BindingValues::TransformSX(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); From 7ca6a435dea11190550e76576d09dd807ae661b4 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 8 Mar 2026 20:20:09 +0000 Subject: [PATCH 05/17] Reset all drawable parameters every frame. --- inox2d/src/formats/payload.rs | 8 ++++---- inox2d/src/node/components.rs | 22 ++++++++++++++++++++++ inox2d/src/render.rs | 4 ++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/inox2d/src/formats/payload.rs b/inox2d/src/formats/payload.rs index 5808300..dd0cb53 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -176,8 +176,8 @@ fn deserialize_simple_physics(obj: JsonObject) -> InoxParseResult } fn deserialize_drawable(obj: JsonObject) -> InoxParseResult { - Ok(Drawable { - blending: Blending { + Ok(Drawable::new( + Blending { mode: match obj.get_str("blend_mode")? { "Normal" => BlendMode::Normal, "Multiply" => BlendMode::Multiply, @@ -192,7 +192,7 @@ fn deserialize_drawable(obj: JsonObject) -> InoxParseResult { screen_tint: obj.get_vec3("screenTint").unwrap_or(vec3(0.0, 0.0, 0.0)), opacity: obj.get_f32("opacity").unwrap_or(1.0), }, - masks: { + { if let Ok(masks) = obj.get_list("masks") { Some(Masks { threshold: obj.get_f32("mask_threshold").unwrap_or(0.5), @@ -209,7 +209,7 @@ fn deserialize_drawable(obj: JsonObject) -> InoxParseResult { None } }, - }) + )) } fn deserialize_mesh(obj: JsonObject) -> InoxParseResult { diff --git a/inox2d/src/node/components.rs b/inox2d/src/node/components.rs index 882df76..c8750ae 100644 --- a/inox2d/src/node/components.rs +++ b/inox2d/src/node/components.rs @@ -30,10 +30,32 @@ pub struct Composite {} /// If has this as a component, the node should render something pub struct Drawable { pub blending: Blending, + + /// The original parameters for this drawable. + /// Copied over `blending` on reset. + initial_blending: Blending, + /// If Some, the node should consider masking when rendering pub masks: Option, } +impl Drawable { + pub fn new(blending: Blending, masks: Option) -> Self { + Self { + blending, + initial_blending: blending, + masks, + } + } + + /// Reset the drawable back to the initial configuration set when `new` + /// was called. + pub fn reset(&mut self) { + self.blending = self.initial_blending; + } +} + +#[derive(Copy, Clone)] pub struct Blending { pub mode: BlendMode, pub tint: Vec3, diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index c77fe74..f95107f 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -129,6 +129,10 @@ impl RenderCtx { if let Some(deform_stack) = comps.get_mut::(node.uuid) { deform_stack.reset(); } + + if let Some(drawable) = comps.get_mut::(node.uuid) { + drawable.reset(); + } } } From d3d8efad61dcf9ba7e0cd8294c86587a0adf908e Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 8 Mar 2026 20:30:24 +0000 Subject: [PATCH 06/17] Reset all drawable parameters every frame. --- inox2d/src/render.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index f95107f..0cb1826 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -5,7 +5,7 @@ use std::collections::HashSet; use std::mem::swap; use crate::node::{ - components::{DeformStack, Mask, Masks, ZSort}, + components::{DeformStack, Drawable, Mask, Masks, ZSort}, drawables::{CompositeComponents, DrawableKind, TexturedMeshComponents}, InoxNodeUuid, }; From 62537d6e91e9ad4a519e2e62a3c311746e37450f Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sun, 8 Mar 2026 20:21:57 +0000 Subject: [PATCH 07/17] Interpolating within an out range with no delta should not turn the target parameter into a NaN. --- inox2d/src/math/interp.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/inox2d/src/math/interp.rs b/inox2d/src/math/interp.rs index 85c8e8c..76823d8 100644 --- a/inox2d/src/math/interp.rs +++ b/inox2d/src/math/interp.rs @@ -67,7 +67,15 @@ fn interpolate_linear(t: f32, range_in: InterpRange, range_out: InterpRange range_in.end, ); - (t - range_in.beg) * (range_out.end - range_out.beg) / (range_in.end - range_in.beg) + range_out.beg + let range_out_delta = range_out.end - range_out.beg; + let range_in_delta = range_in.end - range_in.beg; + if range_out_delta == 0.0 { + // Calculus teachers HATE this ONE WEIRD TRICK to getting rid of + // divide-by-zero errors in your code! + return range_out.beg; + } + + (t - range_in.beg) * range_out_delta / range_in_delta + range_out.beg } #[inline] From 7738ef2971948c80b456fa3d6fca039aaa5af318 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 9 Mar 2026 23:06:24 +0000 Subject: [PATCH 08/17] Tint, screen tint, and opacity are applied multiplicatively. --- inox2d/src/params.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index ebc888b..f246b89 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -247,43 +247,43 @@ impl Param { BindingValues::TintR(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.tint.x += + comps.get_mut::(binding.node).unwrap().blending.tint.x *= bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::TintG(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.tint.y += + comps.get_mut::(binding.node).unwrap().blending.tint.y *= bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::TintB(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.tint.z += + comps.get_mut::(binding.node).unwrap().blending.tint.z *= bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::ScreenTintR(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.screen_tint.x += + comps.get_mut::(binding.node).unwrap().blending.screen_tint.x *= bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::ScreenTintG(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.screen_tint.y += + comps.get_mut::(binding.node).unwrap().blending.screen_tint.y *= bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::ScreenTintB(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.screen_tint.z += + comps.get_mut::(binding.node).unwrap().blending.screen_tint.z *= bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::Opacity(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.opacity += + comps.get_mut::(binding.node).unwrap().blending.opacity *= bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } } From 59205fb6d4d58f27a94b624fb53069ffe70bc47c Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Mon, 9 Mar 2026 23:20:02 +0000 Subject: [PATCH 09/17] Well, ok, the screenTint is also multiplied I guess? --- inox2d/src/params.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index f246b89..a0d51e5 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -265,19 +265,19 @@ impl Param { BindingValues::ScreenTintR(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.screen_tint.x *= + comps.get_mut::(binding.node).unwrap().blending.screen_tint.x += bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::ScreenTintG(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.screen_tint.y *= + comps.get_mut::(binding.node).unwrap().blending.screen_tint.y += bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::ScreenTintB(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - comps.get_mut::(binding.node).unwrap().blending.screen_tint.z *= + comps.get_mut::(binding.node).unwrap().blending.screen_tint.z += bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::Opacity(ref matrix) => { From 04084ba53a74ebf64afb0489996858c6128d1263 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Thu, 12 Mar 2026 13:52:25 +0000 Subject: [PATCH 10/17] We fixed the bug with bindings at zero, so don't skip them. --- inox2d/src/params.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index a0d51e5..8dd46e9 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -324,10 +324,7 @@ impl ParamCtx { pub(crate) fn apply(&self, params: &HashMap, nodes: &InoxNodeTree, comps: &mut World) { // a correct implementation should not care about the order of `.apply()` for (param_name, val) in self.values.iter() { - // TODO: a correct implementation should not fail on param value (0, 0) - if *val != Vec2::ZERO { - params.get(param_name).unwrap().apply(*val, nodes, comps); - } + params.get(param_name).unwrap().apply(*val, nodes, comps); } } } From e8080e33792a72040ccf2658e84c0cc786c8b019 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Thu, 12 Mar 2026 13:54:45 +0000 Subject: [PATCH 11/17] Allow retrieving param values --- inox2d/src/params.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 8dd46e9..8b87a23 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -320,6 +320,14 @@ impl ParamCtx { } } + pub fn get(&self, param_name: &str) -> Result { + if let Some(value) = self.values.get(param_name) { + Ok(*value) + } else { + Err(SetParamError::NoParameterNamed(param_name.to_string())) + } + } + /// Modify components as specified by all params. Must be called ONCE per frame. pub(crate) fn apply(&self, params: &HashMap, nodes: &InoxNodeTree, comps: &mut World) { // a correct implementation should not care about the order of `.apply()` From 1231edcb0d2a7df5f6cec1d0dad3d76473f8c3ae Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Wed, 27 May 2026 17:51:30 +0000 Subject: [PATCH 12/17] Add a facility for parallel ECS world access. We do not use any run-time locks. Instead, we rely on partitioning: independent threads may only access data relating to nodes that they are currently assigned. The world itself is responsible for splitting itself into disjoint subsets and handing them out to different parallel tasks. The primary entry point to parallel execution is World.par_iter. This takes a list of nodes and a callback to execute in parallel. The list of nodes is partitioned into chunks and each chunk is made into a Partition that borrows the world and provides mutable access to only the nodes in its own list. --- inox2d/src/puppet/world.rs | 144 +++++++++++++++++++++++++++++++------ 1 file changed, 124 insertions(+), 20 deletions(-) diff --git a/inox2d/src/puppet/world.rs b/inox2d/src/puppet/world.rs index ebad444..b56e2ee 100644 --- a/inox2d/src/puppet/world.rs +++ b/inox2d/src/puppet/world.rs @@ -1,7 +1,13 @@ use std::any::TypeId; -use std::collections::HashMap; +use std::cell::UnsafeCell; +use std::cmp::max; +use std::collections::{HashMap, HashSet}; +use std::hash::RandomState; use std::mem::{size_of, transmute, ManuallyDrop, MaybeUninit}; +use rayon::current_num_threads; +use rayon::prelude::*; + use super::InoxNodeUuid; // to keep the provenance of the pointer in Vec (or any data struct that contains pointers), @@ -27,11 +33,11 @@ impl Drop for AnyVec { impl AnyVec { // Self is inherently Send + Sync as a pack of bytes regardless of inner type, which is bad pub fn new() -> Self { - let vec = ManuallyDrop::new(Vec::::new()); + let vec = ManuallyDrop::new(Vec::>::new()); Self { // SAFETY: ManuallyDrop guaranteed to have same bit layout as inner, and inner is a proper Vec // provenance considerations present, see comment for VecBytes - vec_bytes: unsafe { transmute::>, VecBytes>(vec) }, + vec_bytes: unsafe { transmute::>>, VecBytes>(vec) }, // SAFETY: only to be called once at end of lifetime, and vec_bytes contain a valid Vec throughout self lifetime drop: |vec_bytes| unsafe { let vec: Vec = transmute(*vec_bytes); @@ -42,12 +48,12 @@ impl AnyVec { } /// T MUST be the same as in new::() for a same instance - pub unsafe fn downcast_unchecked(&self) -> &Vec { + pub unsafe fn downcast_unchecked(&self) -> &Vec> { transmute(&self.vec_bytes) } /// T MUST be the same as in new::() for a same instance - pub unsafe fn downcast_mut_unchecked(&mut self) -> &mut Vec { + pub unsafe fn downcast_mut_unchecked(&mut self) -> &mut Vec> { transmute(&mut self.vec_bytes) } } @@ -80,7 +86,7 @@ impl World { debug_assert!(!pair.1.contains_key(&node),); pair.1.insert(node, column.len()); - column.push(v); + column.push(UnsafeCell::new(v)); } pub fn get(&self, node: InoxNodeUuid) -> Option<&T> { @@ -97,7 +103,7 @@ impl World { }; debug_assert!(index < column.len()); // SAFETY: what has been inserted into pair.1 should be a valid index - Some(unsafe { column.get_unchecked(index) }) + Some(unsafe { &*column.get_unchecked(index).get() }) } pub fn get_mut(&mut self, node: InoxNodeUuid) -> Option<&mut T> { @@ -114,7 +120,31 @@ impl World { }; debug_assert!(index < column.len()); // SAFETY: what has been inserted into pair.1 should be a valid index - Some(unsafe { column.get_unchecked_mut(index) }) + // SAFETY: since we own a &mut self, no other &mut references can exist + // to any other world cell + Some(unsafe { &mut *column.get_unchecked_mut(index).get() }) + } + + /// # Safety + /// + /// All safety requirements of `UnsafeCell` apply. Caller must ensure no + /// two aliasing mutable references to the same (node, T) pair exist at the + /// same time. + pub unsafe fn get_interior_mut(&self, node: InoxNodeUuid) -> Option<&mut T> { + let pair = match self.columns.get(&TypeId::of::()) { + Some(c) => c, + None => return None, + }; + // SAFETY: AnyVec in pair must be of type T, enforced by hashing + let column = unsafe { pair.0.downcast_unchecked() }; + + let index = match pair.1.get(&node) { + Some(i) => *i, + None => return None, + }; + debug_assert!(index < column.len()); + // SAFETY: what has been inserted into pair.1 should be a valid index + Some(unsafe { &mut *column.get_unchecked(index).get() }) } /// # Safety @@ -124,7 +154,7 @@ impl World { let pair = self.columns.get(&TypeId::of::()).unwrap_unchecked(); let index = *pair.1.get(&node).unwrap_unchecked(); - pair.0.downcast_unchecked().get_unchecked(index) + &*pair.0.downcast_unchecked().get_unchecked(index).get() } /// # Safety @@ -134,7 +164,27 @@ impl World { let pair = self.columns.get_mut(&TypeId::of::()).unwrap_unchecked(); let index = *pair.1.get(&node).unwrap_unchecked(); - pair.0.downcast_mut_unchecked().get_unchecked_mut(index) + &mut *pair.0.downcast_mut_unchecked().get_unchecked_mut(index).get() + } + + pub fn par_iter(&mut self, nodes: &[InoxNodeUuid], your_fn: F) + where + F: for<'a> Fn(Partition<'a>) + Sync, + { + // SAFETY: We must filter duplicates from `nodes` to ensure disjoint + // partitions. + // + // NOTE: For some reason type inference fails and we have to manually + // tell Rust to use the standard state type. + let nodes_set: HashSet = HashSet::from_iter(nodes.iter().map(|n| *n)); + let nodes_count = nodes_set.len(); + let batch_size = max(nodes_count / current_num_threads(), 1); + + let nodes_clean: Vec<_> = nodes_set.into_iter().collect(); + + nodes_clean + .par_chunks(batch_size) + .for_each(|nodes| your_fn(unsafe { Partition::new_unchecked(nodes, &self) })) } } @@ -144,10 +194,58 @@ impl Default for World { } } +/// A subset of the World that is partitioned by node UUID. +/// +/// Partitions will allow access to the specific UUIDs only, with all other +/// access to nodes failing. This is intended as a way to partition +/// multithreaded writes to the ECS world by node. +pub struct Partition<'a> { + nodes: &'a [InoxNodeUuid], + data: &'a World, +} + +impl<'a> Partition<'a> { + /// Create a new world partition. + /// + /// SAFETY: Callers must ensure that all live partitions of the given World + /// reference disjoint node IDs before handing them to safe code. + pub unsafe fn new_unchecked(nodes: &'a [InoxNodeUuid], data: &'a World) -> Self { + Self { nodes, data } + } + + pub fn get(&self, node: InoxNodeUuid) -> Option<&T> { + // Note: the node scan is still necessary as some other partition may + // be handing out &mut Ts + if self.contains(node) { + self.data.get(node) + } else { + None + } + } + + pub fn get_mut(&self, node: InoxNodeUuid) -> Option<&mut T> { + if self.contains(node) { + unsafe { self.data.get_interior_mut(node) } + } else { + None + } + } + + /// Retrieve the list of nodes present in the partition. + pub fn nodes(&self) -> impl Iterator + use<'_> { + self.nodes.iter().copied() + } + + pub fn contains(&self, node: InoxNodeUuid) -> bool { + self.nodes.iter().find(|n| **n == node).is_some() + } +} + #[cfg(test)] mod tests { mod any_vec { use super::super::AnyVec; + use std::cell::UnsafeCell; #[test] fn new_and_drop_empty() { @@ -180,16 +278,22 @@ mod tests { let mut any_vec = AnyVec::new::(); unsafe { - any_vec.downcast_mut_unchecked().push(Data { int: 0, c: b'A' }); - any_vec.downcast_mut_unchecked().push(Data { int: 1, c: b'B' }); - any_vec.downcast_mut_unchecked().push(Data { int: 2, c: b'C' }); - - assert_eq!(any_vec.downcast_unchecked::()[0], Data { int: 0, c: b'A' }); - assert_eq!(any_vec.downcast_unchecked::()[1], Data { int: 1, c: b'B' }); - - any_vec.downcast_mut_unchecked::()[2].c = b'D'; - - assert_eq!(any_vec.downcast_unchecked::()[2], Data { int: 2, c: b'D' }); + any_vec + .downcast_mut_unchecked() + .push(UnsafeCell::new(Data { int: 0, c: b'A' })); + any_vec + .downcast_mut_unchecked() + .push(UnsafeCell::new(Data { int: 1, c: b'B' })); + any_vec + .downcast_mut_unchecked() + .push(UnsafeCell::new(Data { int: 2, c: b'C' })); + + assert_eq!(*any_vec.downcast_unchecked::()[0].get(), Data { int: 0, c: b'A' }); + assert_eq!(*any_vec.downcast_unchecked::()[1].get(), Data { int: 1, c: b'B' }); + + (*any_vec.downcast_mut_unchecked::()[2].get()).c = b'D'; + + assert_eq!(*any_vec.downcast_unchecked::()[2].get(), Data { int: 2, c: b'D' }); } } } From 65e275516eb5ee0b0f8f14ecf3c10daf70a77b80 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Wed, 27 May 2026 18:01:21 +0000 Subject: [PATCH 13/17] Use the aforementioned parallel API to apply parameters in parallel. This reduces param application time on myself from 2.706ms to 0.876ms. --- inox2d/src/params.rs | 31 +++++++++++++++++++++++++------ inox2d/src/puppet.rs | 2 +- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 8b87a23..9e5cde8 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use glam::{vec2, Vec2}; @@ -11,7 +11,7 @@ use crate::node::{ components::{DeformSource, DeformStack, Drawable, Mesh, TransformStore, ZSort}, InoxNodeUuid, }; -use crate::puppet::{InoxNodeTree, Puppet, World}; +use crate::puppet::{InoxNodeTree, Partition, Puppet, World}; /// Parameter binding to a node. This allows to animate a node based on the value of the parameter that owns it. pub struct Binding { @@ -76,12 +76,17 @@ pub struct Param { } impl Param { + /// Iterate all bindings + pub(crate) fn iter_bindings(&self) -> impl Iterator { + self.bindings.iter() + } + /// Internal function that modifies puppet components according to one param set. /// Must be only called ONCE per frame to ensure correct behavior. /// /// End users may repeatedly apply a same parameter for multiple times in between frames, /// but other facilities should be present to make sure this `apply()` is only called once per parameter. - pub(crate) fn apply(&self, val: Vec2, nodes: &InoxNodeTree, comps: &mut World) { + pub(crate) fn apply(&self, val: Vec2, nodes: &InoxNodeTree, comps: &Partition<'_>) { let val = val.clamp(self.min, self.max); let val_normed = (val - self.min) / (self.max - self.min); @@ -114,6 +119,10 @@ impl Param { // Apply offset on each binding for binding in &self.bindings { + if !comps.contains(binding.node) { + continue; + } + let range_in = InterpRange::new( vec2(self.axis_points.x[x_mindex], self.axis_points.y[y_mindex]), vec2(self.axis_points.x[x_maxdex], self.axis_points.y[y_maxdex]), @@ -330,10 +339,20 @@ impl ParamCtx { /// Modify components as specified by all params. Must be called ONCE per frame. pub(crate) fn apply(&self, params: &HashMap, nodes: &InoxNodeTree, comps: &mut World) { - // a correct implementation should not care about the order of `.apply()` - for (param_name, val) in self.values.iter() { - params.get(param_name).unwrap().apply(*val, nodes, comps); + let mut bindings_bucket = HashSet::new(); + + for (param_name, _val) in self.values.iter() { + for node in params.get(param_name).unwrap().iter_bindings().map(|b| b.node) { + bindings_bucket.insert(node); + } } + + let accessed_nodes: Vec<_> = bindings_bucket.iter().copied().collect(); + comps.par_iter(accessed_nodes.as_slice(), |comps| { + for (param_name, val) in self.values.iter() { + params.get(param_name).unwrap().apply(*val, nodes, &comps); + } + }) } } diff --git a/inox2d/src/puppet.rs b/inox2d/src/puppet.rs index 7cdcbfa..4b4d965 100644 --- a/inox2d/src/puppet.rs +++ b/inox2d/src/puppet.rs @@ -13,7 +13,7 @@ use crate::render::RenderCtx; use meta::PuppetMeta; use transforms::TransformCtx; pub use tree::InoxNodeTree; -pub use world::World; +pub use world::{Partition, World}; /// Inochi2D puppet. pub struct Puppet { From eb45316e901628650a027cd6848400d22eca3292 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Wed, 27 May 2026 19:48:51 +0000 Subject: [PATCH 14/17] Allow creating world partitions manually. Calling `as_partition` returns a Partition covering the entire world. You can then split the partitions into pieces and process them in parallel. Additionally, for soundness, `Partition.get_mut()` now requires a mutable borrow. --- inox2d/Cargo.toml | 2 + inox2d/src/params.rs | 6 +- inox2d/src/puppet/world.rs | 206 +++++++++++++++++++++++++++++++++++-- 3 files changed, 200 insertions(+), 14 deletions(-) diff --git a/inox2d/Cargo.toml b/inox2d/Cargo.toml index b4f122b..ec52154 100644 --- a/inox2d/Cargo.toml +++ b/inox2d/Cargo.toml @@ -21,6 +21,8 @@ owo-colors = { version = "4.0.0", optional = true } simple-tga-reader = "0.1.0" thiserror = "1.0.39" tracing = "0.1.37" +parking_lot = "0.12.5" +rayon = "1.12.0" [dev-dependencies] clap = { version = "4.1.8", features = ["derive"] } diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 9e5cde8..49d27d9 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -86,7 +86,7 @@ impl Param { /// /// End users may repeatedly apply a same parameter for multiple times in between frames, /// but other facilities should be present to make sure this `apply()` is only called once per parameter. - pub(crate) fn apply(&self, val: Vec2, nodes: &InoxNodeTree, comps: &Partition<'_>) { + pub(crate) fn apply(&self, val: Vec2, nodes: &InoxNodeTree, comps: &mut Partition<'_>) { let val = val.clamp(self.min, self.max); let val_normed = (val - self.min) / (self.max - self.min); @@ -348,9 +348,9 @@ impl ParamCtx { } let accessed_nodes: Vec<_> = bindings_bucket.iter().copied().collect(); - comps.par_iter(accessed_nodes.as_slice(), |comps| { + comps.par_iter(accessed_nodes.as_slice(), |mut comps| { for (param_name, val) in self.values.iter() { - params.get(param_name).unwrap().apply(*val, nodes, &comps); + params.get(param_name).unwrap().apply(*val, nodes, &mut comps); } }) } diff --git a/inox2d/src/puppet/world.rs b/inox2d/src/puppet/world.rs index b56e2ee..606cc95 100644 --- a/inox2d/src/puppet/world.rs +++ b/inox2d/src/puppet/world.rs @@ -1,4 +1,5 @@ use std::any::TypeId; +use std::borrow::Cow; use std::cell::UnsafeCell; use std::cmp::max; use std::collections::{HashMap, HashSet}; @@ -184,7 +185,15 @@ impl World { nodes_clean .par_chunks(batch_size) - .for_each(|nodes| your_fn(unsafe { Partition::new_unchecked(nodes, &self) })) + .for_each(|nodes| your_fn(unsafe { Partition::new_unchecked(Some(nodes.into()), None, &self) })) + } + + pub fn as_partition(&mut self) -> Partition<'_> { + Partition { + nodes: None, + components: None, + data: self, + } } } @@ -194,13 +203,21 @@ impl Default for World { } } -/// A subset of the World that is partitioned by node UUID. +/// A subset of the World that is partitioned by node UUID or component type. +/// +/// Partitions will allow access to the specific UUIDs or components only, with +/// all other access to nodes failing. This is intended as a way to partition +/// multithreaded writes to the ECS world. +/// +/// SAFETY CONSIDERATIONS: /// -/// Partitions will allow access to the specific UUIDs only, with all other -/// access to nodes failing. This is intended as a way to partition -/// multithreaded writes to the ECS world by node. +/// Partitions must only be created in functions that have mutable access to +/// the underlying World, in such a way that the Partition's lifetime is bound +/// to that mutable borrow. While we do not store a mutable pointer, we do +/// mutate the underlying world. pub struct Partition<'a> { - nodes: &'a [InoxNodeUuid], + nodes: Option>, + components: Option>, data: &'a World, } @@ -209,8 +226,16 @@ impl<'a> Partition<'a> { /// /// SAFETY: Callers must ensure that all live partitions of the given World /// reference disjoint node IDs before handing them to safe code. - pub unsafe fn new_unchecked(nodes: &'a [InoxNodeUuid], data: &'a World) -> Self { - Self { nodes, data } + pub unsafe fn new_unchecked( + nodes: Option>, + components: Option>, + data: &'a World, + ) -> Self { + Self { + nodes, + components, + data, + } } pub fn get(&self, node: InoxNodeUuid) -> Option<&T> { @@ -223,7 +248,7 @@ impl<'a> Partition<'a> { } } - pub fn get_mut(&self, node: InoxNodeUuid) -> Option<&mut T> { + pub fn get_mut(&mut self, node: InoxNodeUuid) -> Option<&mut T> { if self.contains(node) { unsafe { self.data.get_interior_mut(node) } } else { @@ -232,12 +257,88 @@ impl<'a> Partition<'a> { } /// Retrieve the list of nodes present in the partition. + /// + /// If the world is not partitioned by node, then this yields an empty + /// array. To check if a particular node is part of the partition, use + /// `.contains()`. pub fn nodes(&self) -> impl Iterator + use<'_> { - self.nodes.iter().copied() + self.nodes.iter().flat_map(|n| n.iter()).copied() } + /// Determine if a particular node is contained in the partition. pub fn contains(&self, node: InoxNodeUuid) -> bool { - self.nodes.iter().find(|n| **n == node).is_some() + self.nodes.is_none() || self.nodes().find(|n| *n == node).is_some() + } + + /// Determine if a particular component is contained in the partition. + pub fn contains_component(&self) -> bool { + self.components.is_none() + || self + .components + .as_ref() + .unwrap() + .iter() + .find(|c| **c == TypeId::of::()) + .is_some() + } + + /// Determine if a particular component's TypeId is contained in the + /// partition. + pub fn contains_type_id(&self, type_id: TypeId) -> bool { + if let Some(components) = &self.components { + components.iter().find(|c| **c == type_id).is_some() + } else { + true + } + } + + /// Split the given partition by a particular component. + /// + /// The first partition returned will be limited to the given component. + /// The second will contain all other components in this partition. + /// + /// In the event that the requested component is not present, the first + /// partition will be empty (no components), and the second partition will + /// contain the same components as this partition. + pub fn split_by_component(self) -> (Partition<'a>, Partition<'a>) { + let c = TypeId::of::(); + let has_c = if let Some(components_partition) = self.components.as_ref() { + components_partition.iter().find(|other_c| **other_c == c).is_some() + } else { + //None signals no restriction + true + }; + + let c_list = if has_c { Cow::Owned(vec![c]) } else { Cow::Owned(vec![]) }; + + let not_c = if has_c { + if let Some(components_partition) = self.components.as_ref() { + Cow::Owned( + components_partition + .iter() + .filter(|other_c| **other_c != c) + .copied() + .collect(), + ) + } else { + Cow::Owned(self.data.columns.keys().copied().collect()) + } + } else { + self.components.unwrap() + }; + + ( + Partition { + nodes: self.nodes.clone(), + components: Some(c_list), + data: self.data, + }, + Partition { + nodes: self.nodes, + components: Some(not_c), + data: self.data, + }, + ) } } @@ -360,4 +461,87 @@ mod tests { } } } + + mod partition { + use crate::node::InoxNodeUuid; + + use super::super::World; + use std::any::TypeId; + + struct ComponentA {} + + struct ComponentB {} + + struct ComponentC {} + + #[test] + fn disjoint_component_partitions() { + let mut world = World::new(); + + let partition = world.as_partition(); + + assert_eq!(partition.nodes.as_ref(), None); + assert_eq!(partition.components.as_deref(), None); + + let (partition_a, partition_b) = world.as_partition().split_by_component::(); + + assert_eq!(partition_a.nodes.as_ref(), None); + assert_eq!(partition_b.nodes.as_ref(), None); + assert_eq!( + partition_a.components.as_deref(), + Some([TypeId::of::()].as_slice()) + ); + assert_eq!(partition_b.components.as_deref(), Some([].as_slice())); + + world.add(InoxNodeUuid(0), ComponentB {}); + world.add(InoxNodeUuid(0), ComponentC {}); + + let (partition_a, partition_b) = world.as_partition().split_by_component::(); + + assert_eq!(partition_a.nodes.as_ref(), None); + assert_eq!(partition_b.nodes.as_ref(), None); + assert_eq!( + partition_a.components.as_deref(), + Some([TypeId::of::()].as_slice()) + ); + assert!(partition_b + .components + .as_deref() + .unwrap() + .contains(&TypeId::of::())); + assert!(partition_b + .components + .as_deref() + .unwrap() + .contains(&TypeId::of::())); + + let (partition_c, partition_d) = partition_a.split_by_component::(); + + assert_eq!(partition_c.nodes.as_ref(), None); + assert_eq!(partition_d.nodes.as_ref(), None); + assert_eq!(partition_c.components.as_deref(), Some([].as_slice())); + assert_eq!( + partition_d.components.as_deref(), + Some([TypeId::of::()].as_slice()) + ); + + let (partition_c, partition_d) = partition_b.split_by_component::(); + + assert_eq!(partition_c.nodes.as_ref(), None); + assert_eq!(partition_d.nodes.as_ref(), None); + assert_eq!( + partition_c.components.as_deref(), + Some([TypeId::of::()].as_slice()) + ); + assert_eq!( + partition_d.components.as_deref(), + Some([TypeId::of::()].as_slice()) + ); + + let partition = world.as_partition(); + + assert_eq!(partition.nodes.as_ref(), None); + assert_eq!(partition.components.as_deref(), None); + } + } } From 3258fe69c42c8e586decf70d39bc97618f9d4f4d Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Thu, 28 May 2026 20:11:04 +0000 Subject: [PATCH 15/17] Add a trait for accessing ECS components from either a World or Partition. Also, add some other ways to parallel access the ECS world. --- inox2d/src/node/drawables.rs | 4 +- inox2d/src/params.rs | 2 +- inox2d/src/puppet/world.rs | 129 +++++++++++++++++++++++++----- inox2d/src/render/deform_stack.rs | 4 +- 4 files changed, 114 insertions(+), 25 deletions(-) diff --git a/inox2d/src/node/drawables.rs b/inox2d/src/node/drawables.rs index 451f09b..6f935de 100644 --- a/inox2d/src/node/drawables.rs +++ b/inox2d/src/node/drawables.rs @@ -4,7 +4,7 @@ use crate::node::{ components::{Composite, Drawable, Mesh, TexturedMesh, TransformStore}, InoxNodeUuid, }; -use crate::puppet::World; +use crate::puppet::Query; /// Possible component combinations of a renderable node. /// @@ -38,7 +38,7 @@ impl<'comps> DrawableKind<'comps> { /// `None` if node not renderable. /// /// If `check`, will send a warning to `tracing` if component combination non-standard for a supposed-to-be Drawable node. - pub(crate) fn new(id: InoxNodeUuid, comps: &'comps World, check: bool) -> Option { + pub(crate) fn new(id: InoxNodeUuid, comps: &'comps impl Query, check: bool) -> Option { let drawable = match comps.get::(id) { Some(drawable) => drawable, None => return None, diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 49d27d9..6d2ff73 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -348,7 +348,7 @@ impl ParamCtx { } let accessed_nodes: Vec<_> = bindings_bucket.iter().copied().collect(); - comps.par_iter(accessed_nodes.as_slice(), |mut comps| { + comps.with_node_partitions(accessed_nodes.as_slice(), |mut comps, _| { for (param_name, val) in self.values.iter() { params.get(param_name).unwrap().apply(*val, nodes, &mut comps); } diff --git a/inox2d/src/puppet/world.rs b/inox2d/src/puppet/world.rs index 606cc95..78796f8 100644 --- a/inox2d/src/puppet/world.rs +++ b/inox2d/src/puppet/world.rs @@ -168,26 +168,7 @@ impl World { &mut *pair.0.downcast_mut_unchecked().get_unchecked_mut(index).get() } - pub fn par_iter(&mut self, nodes: &[InoxNodeUuid], your_fn: F) - where - F: for<'a> Fn(Partition<'a>) + Sync, - { - // SAFETY: We must filter duplicates from `nodes` to ensure disjoint - // partitions. - // - // NOTE: For some reason type inference fails and we have to manually - // tell Rust to use the standard state type. - let nodes_set: HashSet = HashSet::from_iter(nodes.iter().map(|n| *n)); - let nodes_count = nodes_set.len(); - let batch_size = max(nodes_count / current_num_threads(), 1); - - let nodes_clean: Vec<_> = nodes_set.into_iter().collect(); - - nodes_clean - .par_chunks(batch_size) - .for_each(|nodes| your_fn(unsafe { Partition::new_unchecked(Some(nodes.into()), None, &self) })) - } - + /// Yield a partition that refers to the total contents of the world. pub fn as_partition(&mut self) -> Partition<'_> { Partition { nodes: None, @@ -195,6 +176,32 @@ impl World { data: self, } } + + /// Do parallel work on the given world. + /// + /// This function splits the world node-wise into chunks and provides + /// each partition to the provided callback in a separate thread. + pub fn with_node_partitions(&mut self, nodes: &[InoxNodeUuid], your_fn: F) + where + F: for<'b> Fn(Partition<'b>, &[InoxNodeUuid]) + Sync, + { + self.as_partition().with_node_partitions(nodes, your_fn) + } + + /// Do parallel work on the given world and return a list of values. + /// + /// One value will be returned per invocation of your function; you will + /// need to fold them yourself. + /// + /// This function splits the world node-wise into chunks and provides + /// each partition to the provided callback in a separate thread. + pub fn with_node_partitions_map(&mut self, nodes: &[InoxNodeUuid], your_fn: F) -> Vec + where + F: for<'b> Fn(Partition<'b>, &[InoxNodeUuid]) -> T + Sync, + T: Send, + { + self.as_partition().with_node_partitions_map(nodes, your_fn) + } } impl Default for World { @@ -340,6 +347,88 @@ impl<'a> Partition<'a> { }, ) } + + /// Do parallel work on the given partition. + /// + /// This function splits the partition node-wise into chunks and provides + /// each split of the partition to the provided callback in a separate + /// thread. + pub fn with_node_partitions(&mut self, nodes: &[InoxNodeUuid], your_fn: F) + where + F: for<'b> Fn(Partition<'b>, &[InoxNodeUuid]) + Sync, + { + self.with_node_partitions_map(nodes, your_fn); + } + + /// Do parallel work on the given partition and return a list of values. + /// + /// One value will be returned per invocation of your function; you will + /// need to fold them yourself. + /// + /// This function splits the partition node-wise into chunks and provides + /// each split of the partition to the provided callback in a separate + /// thread. + pub fn with_node_partitions_map(&mut self, nodes: &[InoxNodeUuid], your_fn: F) -> Vec + where + F: for<'b> Fn(Partition<'b>, &[InoxNodeUuid]) -> T + Sync, + T: Send, + { + // SAFETY: We must filter duplicates from `nodes` to ensure disjoint + // partitions. + // + // NOTE: For some reason type inference fails and we have to manually + // tell Rust to use the standard state type. + let nodes_set: HashSet = HashSet::from_iter(nodes.iter().copied()); + let nodes_clean: Vec<_> = if let Some(nodes) = &self.nodes { + // If we are already in a node partition, we must also intersect + // with our own node partition. + let outer_set: HashSet = HashSet::from_iter(nodes.iter().copied()); + + nodes_set.intersection(&outer_set).copied().collect() + } else { + nodes_set.into_iter().collect() + }; + + let nodes_count = nodes_clean.len(); + let batch_size = max(nodes_count / current_num_threads(), 1); + + nodes_clean + .par_chunks(batch_size) + .map(|nodes| { + your_fn( + unsafe { Partition::new_unchecked(Some(nodes.into()), None, &self.data) }, + nodes, + ) + }) + .collect() + } +} + +/// Represents any type that can retrieve components for a given node. +pub trait Query { + fn get(&self, node: InoxNodeUuid) -> Option<&T>; + + fn get_mut(&mut self, node: InoxNodeUuid) -> Option<&mut T>; +} + +impl Query for World { + fn get(&self, node: InoxNodeUuid) -> Option<&T> { + self.get(node) + } + + fn get_mut(&mut self, node: InoxNodeUuid) -> Option<&mut T> { + self.get_mut(node) + } +} + +impl Query for Partition<'_> { + fn get(&self, node: InoxNodeUuid) -> Option<&T> { + self.get(node) + } + + fn get_mut(&mut self, node: InoxNodeUuid) -> Option<&mut T> { + self.get_mut(node) + } } #[cfg(test)] diff --git a/inox2d/src/render/deform_stack.rs b/inox2d/src/render/deform_stack.rs index 037a23d..792a99d 100644 --- a/inox2d/src/render/deform_stack.rs +++ b/inox2d/src/render/deform_stack.rs @@ -5,7 +5,7 @@ use glam::Vec2; use crate::math::deform::{linear_combine, Deform}; use crate::node::components::{DeformSource, DeformStack}; -use crate::puppet::{InoxNodeTree, World}; +use crate::puppet::{InoxNodeTree, Query}; impl DeformStack { pub(crate) fn new(deform_len: usize) -> Self { @@ -23,7 +23,7 @@ impl DeformStack { } /// Combine the deformations received so far according to some rules, and write to the result - pub(crate) fn combine(&self, _nodes: &InoxNodeTree, _node_comps: &World, result: &mut [Vec2]) { + pub(crate) fn combine(&self, _nodes: &InoxNodeTree, _node_comps: &impl Query, result: &mut [Vec2]) { if result.len() != self.deform_len { panic!("Required output deform dimensions different from what DeformStack is initialized with.") } From c4b57035886dd38befb48e4c0ccbced3b5692510 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sat, 30 May 2026 18:26:46 +0000 Subject: [PATCH 16/17] Restore publication of the new Query type that we lost during the rebase. --- inox2d/src/puppet.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inox2d/src/puppet.rs b/inox2d/src/puppet.rs index 4b4d965..196c886 100644 --- a/inox2d/src/puppet.rs +++ b/inox2d/src/puppet.rs @@ -13,7 +13,7 @@ use crate::render::RenderCtx; use meta::PuppetMeta; use transforms::TransformCtx; pub use tree::InoxNodeTree; -pub use world::{Partition, World}; +pub use world::{Partition, Query, World}; /// Inochi2D puppet. pub struct Puppet { From e9483eed6fd9d2df1ad8a12dc7a49b8f3e577a03 Mon Sep 17 00:00:00 2001 From: Arcturus Emrys Date: Sat, 30 May 2026 18:30:08 +0000 Subject: [PATCH 17/17] Also, apparently this wasn't already Debug? --- inox2d/src/node.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inox2d/src/node.rs b/inox2d/src/node.rs index 432f3dc..ae92411 100644 --- a/inox2d/src/node.rs +++ b/inox2d/src/node.rs @@ -3,7 +3,7 @@ pub mod drawables; use crate::math::transform::TransformOffset; -#[derive(Clone, Copy, Hash, Eq, PartialEq)] +#[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)] #[repr(transparent)] pub struct InoxNodeUuid(pub(crate) u32);