Skip to main content

handwired_rs/
springs.rs

1//! Springs, ported from handwired's `emitSpring`, `emitApplySpringMatrix` and
2//! `emitAnimatedObject`. The host writes each evaluation's step matrix into
3//! source cells (see [`crate::timing`]), and the circuit applies it.
4
5use crate::circuit::{Circuit, OpKind};
6use crate::emit::{emit_relu, emit_select};
7use crate::wire::{Lin, Term, Wire};
8
9fn has_finite_range(circuit: &Circuit, wire: Wire) -> bool {
10    let range = circuit.range(wire);
11    (range.min as f32).is_finite() && (range.max as f32).is_finite()
12}
13
14fn emit_absolute(circuit: &mut Circuit, value: Wire) -> Wire {
15    let positive = emit_relu(circuit, value);
16    let negated = circuit.affine(-value);
17    let negative = emit_relu(circuit, negated);
18    circuit.affine(positive + negative)
19}
20
21fn emit_clamp_zero_one(circuit: &mut Circuit, value: Wire, unit: Wire) -> Wire {
22    let positive = emit_relu(circuit, value);
23    let over = circuit.affine(value - unit);
24    let above_one = emit_relu(circuit, over);
25    let result = circuit.affine(positive - above_one);
26    circuit.assume_range(result, 0.0, 1.0);
27    result
28}
29
30/// A spring's next state after four explicit semi-implicit Euler steps, with every constant a wire.
31#[derive(Copy, Clone, Debug)]
32pub struct SpringState {
33    pub position: Wire,
34    pub velocity: Wire,
35    /// 0 once the spring is within both settle thresholds (scaled), 1 while it moves, and between them near the
36    /// thresholds.
37    pub active: Wire,
38}
39
40/// Four steps of `velocity += (-stiffness × (position - destination) - damping × velocity) × timestep;
41/// position += velocity × timestep`, then a soft settle: `active` is
42/// `clamp01(relu(|position - destination| - settle_distance) × distance_scale + relu(|velocity| - settle_speed) ×
43/// speed_scale)`, the position moves to the destination as `active` goes to 0, and the velocity scales by it.
44/// `unit` must hold 1. [`emit_animated_object`] is the exact, matrix-stepped form.
45#[allow(clippy::too_many_arguments)]
46pub fn emit_spring(
47    circuit: &mut Circuit,
48    position_source: Wire,
49    destination: Wire,
50    velocity_source: Wire,
51    stiffness: Wire,
52    damping: Wire,
53    timestep: Wire,
54    settle_distance: Wire,
55    settle_speed: Wire,
56    settle_distance_scale: Wire,
57    settle_speed_scale: Wire,
58    unit: Wire,
59) -> SpringState {
60    let (mut position, mut velocity) = (position_source, velocity_source);
61    for _ in 0..4 {
62        let displacement = circuit.affine(position - destination);
63        let spring_force = circuit.multiply(stiffness, displacement);
64        let damping_force = circuit.multiply(damping, velocity);
65        let acceleration = circuit.affine(-Lin::from(spring_force) - damping_force);
66        let velocity_change = circuit.multiply(acceleration, timestep);
67        velocity = circuit.affine(velocity + velocity_change);
68        let position_change = circuit.multiply(velocity, timestep);
69        position = circuit.affine(position + position_change);
70    }
71    let offset = circuit.affine(position - destination);
72    let distance = emit_absolute(circuit, offset);
73    let distance_over = circuit.affine(distance - settle_distance);
74    let distance_above = emit_relu(circuit, distance_over);
75    let speed = emit_absolute(circuit, velocity);
76    let speed_over = circuit.affine(speed - settle_speed);
77    let speed_above = emit_relu(circuit, speed_over);
78    let scaled_distance = circuit.multiply(distance_above, settle_distance_scale);
79    let scaled_speed = circuit.multiply(speed_above, settle_speed_scale);
80    let combined = circuit.affine(scaled_distance + scaled_speed);
81    let active = emit_clamp_zero_one(circuit, combined, unit);
82    SpringState {
83        position: emit_select(circuit, active, destination, position),
84        velocity: circuit.multiply(active, velocity),
85        active,
86    }
87}
88
89/// The 4 cells of a spring matrix that the host writes before each evaluation, e.g.
90/// `spring_matrix_power(spring_step_matrix(constants, 4.0), steps)` from [`crate::timing`]:
91/// `displacement' = m[0] × displacement + m[1] × velocity`, `velocity' = m[2] × displacement + m[3] × velocity`.
92pub type SpringMatrixWires = [Wire; 4];
93
94/// One spring's displacement from its destination and its velocity after every step the matrix describes, e.g.
95/// all of one evaluation's 4 ms steps at once. The matrix cells need finite declared ranges, e.g.
96/// ±[`spring_matrix_bound`](crate::timing::spring_matrix_bound), so a host that writes an overflowing matrix fails
97/// the source range check. Each product goes through `multiply`, so the backward pass differentiates whole
98/// products. Cost: 14 operations in 3 layers.
99pub fn emit_apply_spring_matrix(
100    circuit: &mut Circuit,
101    matrix: SpringMatrixWires,
102    displacement: Wire,
103    velocity: Wire,
104) -> (Wire, Wire) {
105    for entry in matrix {
106        assert!(
107            has_finite_range(circuit, entry),
108            "spring matrix wires need finite declared ranges, e.g. ±spring_matrix_bound(constants)"
109        );
110    }
111    let products = [
112        circuit.multiply(matrix[0], displacement),
113        circuit.multiply(matrix[1], velocity),
114        circuit.multiply(matrix[2], displacement),
115        circuit.multiply(matrix[3], velocity),
116    ];
117    let next_displacement = circuit.round_affine(products[0] + products[1]);
118    let next_velocity = circuit.round_affine(products[2] + products[3]);
119    (next_displacement, next_velocity)
120}
121
122/// One spring of an [`emit_animated_object`].
123#[derive(Copy, Clone, Debug)]
124pub struct AnimatedSpring {
125    pub matrix: SpringMatrixWires,
126    /// The displacement from `destination_during_steps`, usually a state cell the previous evaluation wrote back.
127    pub displacement: Wire,
128    pub velocity: Wire,
129    /// The destination from this evaluation's time on, after its events.
130    pub destination: Wire,
131    /// The destination the stored displacement is measured from, in effect during this evaluation's steps,
132    /// usually the destination the previous evaluation wrote back. `None` when the evaluation can't change the
133    /// destination; the steps then pull toward `destination`.
134    pub destination_during_steps: Option<Wire>,
135}
136
137#[derive(Copy, Clone, Debug)]
138pub struct AnimatedObjectOptions {
139    /// An exact bit: 1 when this evaluation ends at a fixed simulated settle time
140    /// ([`PlannedEvaluation::settle_check`](crate::timing::PlannedEvaluation::settle_check)). The object only
141    /// settles then, so settling doesn't depend on the frame rate.
142    pub settle_check: Wire,
143    /// An exact bit that pins every spring onto its destination with zero velocity, e.g. while dragging, or a
144    /// planned snap.
145    pub snap: Option<Wire>,
146    /// Settle thresholds, 0.01 px and 0.01 px/s by default; each at least 2^-22.
147    pub settle_distance: f64,
148    pub settle_speed: f64,
149}
150
151impl AnimatedObjectOptions {
152    pub fn new(settle_check: Wire) -> AnimatedObjectOptions {
153        AnimatedObjectOptions { settle_check, snap: None, settle_distance: 0.01, settle_speed: 0.01 }
154    }
155}
156
157#[derive(Clone, Debug)]
158pub struct AnimatedObject {
159    /// Each spring's displacement from `destination` after this evaluation: write it back, with `destination` as
160    /// the next evaluation's `destination_during_steps`.
161    pub displacements: Vec<Wire>,
162    pub velocities: Vec<Wire>,
163    /// What to draw, `destination + displacement`, rounded once. Not state, so the rounding never builds up.
164    pub positions: Vec<Wire>,
165    /// 1 when this evaluation checks settling and every spring is within both thresholds, or when every spring is
166    /// exactly at rest (every displacement and velocity ±0).
167    pub settled: Wire,
168    /// 1 while the object keeps moving: not settled and not snapped. A host stops its clock once every object's
169    /// active bit is 0.
170    pub active: Wire,
171}
172
173/// Springs of one animated object, e.g. a window's x, y, width, height and scale, stepped by their matrices and
174/// settled together with one exact bit. Each spring stores its displacement from its destination rather than a
175/// position, so FP32 rounds the state at the size of the displacement, finely near rest even far from the origin.
176/// When the object settles or snaps, every displacement and velocity becomes 0, up to the sign of zero.
177///
178/// For each spring, `(displacement', velocity') = matrix × (displacement, velocity)`, then
179/// `displacement'' = f(f(destination_during_steps - destination) + displacement')`, so an unchanged destination
180/// adds exactly 0. The exceedance, the sum of `relu(|displacement''| - distance)` and `relu(|velocity'| - speed)`,
181/// is 0 exactly when every spring is within both thresholds; `settled = boolean(relu(0.5 × settle_check + 0.5 ×
182/// at_rest - 2^20 × exceedance))`.
183pub fn emit_animated_object(
184    circuit: &mut Circuit,
185    springs: &[AnimatedSpring],
186    options: AnimatedObjectOptions,
187) -> AnimatedObject {
188    assert!(!springs.is_empty(), "an animated object needs at least one spring");
189    let settle_distance = options.settle_distance as f32 as f64;
190    let settle_speed = options.settle_speed as f32 as f64;
191    for threshold in [settle_distance, settle_speed] {
192        assert!(
193            threshold.is_finite() && threshold >= 2f64.powi(-22),
194            "settle thresholds must be finite and at least 2^-22, got {threshold}"
195        );
196    }
197    for bit in std::iter::once(options.settle_check).chain(options.snap) {
198        let range = circuit.range(bit);
199        assert!(range.min >= 0.0 && range.max <= 1.0, "settle_check and snap must be declared within 0..1");
200    }
201    let mut exceedance_terms: Vec<Term> = Vec::new();
202    let mut rest_terms: Vec<Term> = Vec::new();
203    let mut stepped = Vec::with_capacity(springs.len());
204    for spring in springs {
205        let (next_displacement, next_velocity) =
206            emit_apply_spring_matrix(circuit, spring.matrix, spring.displacement, spring.velocity);
207        // Its own operation, so each threshold ReLU reads one FP32 value.
208        let displacement = match spring.destination_during_steps {
209            Some(during) if during != spring.destination => circuit
210                .round_affine(Lin::new(vec![(during, 1.0), (spring.destination, -1.0), (next_displacement, 1.0)], 0.0)),
211            _ => next_displacement,
212        };
213        for (value, threshold) in [(displacement, settle_distance), (next_velocity, settle_speed)] {
214            let positive_part = circuit.reglu(1.0, value);
215            let negative_part = circuit.reglu(1.0, -value);
216            // relu(|value| - threshold), with |value| = relu(value) + relu(-value).
217            exceedance_terms.push((circuit.reglu(1.0, positive_part + negative_part - threshold), 1.0));
218            rest_terms.push((positive_part, 2f64.powi(127)));
219            rest_terms.push((negative_part, 2f64.powi(127)));
220        }
221        stepped.push((displacement, next_velocity));
222    }
223    let exceedance = circuit.round_affine(Lin::new(exceedance_terms, 0.0));
224    // Each lifted term is 0, at least 2^127 × 2^-149 = 2^-22, or infinite, so relu(1 - 2^22 × lifted rest) is the
225    // at-rest bit. Kept as written: combining a repeated part would give 2^128, which isn't an FP32 value.
226    let lifted_rest = circuit.append(OpKind::Reglu, Lin::constant(1.0), Lin::new(rest_terms, 0.0), true, None);
227    let at_rest = circuit.reglu(1.0, Lin::new(vec![(lifted_rest, -(2f64.powi(22)))], 1.0));
228    // The settle check and rest bits first, so the partial sum is exactly 0, 0.5 or 1 before the exceedance.
229    let score_terms = vec![(options.settle_check, 0.5), (at_rest, 0.5), (exceedance, -(2f64.powi(20)))];
230    let settled_score = circuit.append(OpKind::Reglu, Lin::constant(1.0), Lin::new(score_terms, 0.0), true, None);
231    circuit.mark_rounding_point(settled_score);
232    let settled = circuit.boolean(settled_score);
233    let not_settled = circuit.round_affine(1.0 - settled);
234    let active = match options.snap {
235        None => not_settled,
236        Some(snap) => {
237            let not_snapped = circuit.round_affine(1.0 - snap);
238            circuit.multiply(not_settled, not_snapped)
239        }
240    };
241    let displacements: Vec<Wire> =
242        stepped.iter().map(|&(displacement, _)| circuit.multiply(displacement, active)).collect();
243    let velocities = stepped.iter().map(|&(_, velocity)| circuit.multiply(velocity, active)).collect();
244    let positions = springs
245        .iter()
246        .zip(&displacements)
247        .map(|(spring, &displacement)| circuit.round_affine(spring.destination + displacement))
248        .collect();
249    AnimatedObject { displacements, velocities, positions, settled, active }
250}