Skip to main content

solium_effects/
ffi.rs

1//! A C ABI over the deformations, so they can be called from outside Rust.
2//!
3//! This exists for the preview: the engine is built for `wasm32` and driven
4//! from the page, which means the mesh you watch bend in a browser is the one
5//! `warp::mesh_part` builds for a real window. A reimplementation in JavaScript
6//! would drift the first time either changed, and the drift would be
7//! invisible -- the page would still bend something plausible.
8//!
9//! A grid is written into a fixed scratch buffer rather than allocated, the
10//! same way `solium_layout::ffi` publishes its rectangles: the crate has no
11//! dependencies and wants none, and a wasm module that never allocates is a
12//! wasm module with nothing to leak.
13
14use crate::{Axis, Deform, Rect};
15
16/// The most points a grid will report, as `x, y` pairs.
17///
18/// Bigger than any grid [`Deform::segments`] asks for today -- a genie is 49
19/// by 9 -- with room for an effect that wants a finer one in both directions
20/// before this has to be thought about again.
21pub const CAPACITY: usize = 64 * 64;
22
23static mut SCRATCH: [f64; CAPACITY * 2] = [0.0; CAPACITY * 2];
24
25/// Where the last grid was written.
26#[expect(unsafe_code, reason = "exporting a C symbol and its scratch buffer")]
27#[unsafe(no_mangle)]
28pub extern "C" fn solium_effect_scratch() -> *const f64 {
29    &raw const SCRATCH as *const f64
30}
31
32#[expect(unsafe_code, reason = "exporting a C symbol")]
33#[unsafe(no_mangle)]
34pub extern "C" fn solium_effect_capacity() -> u32 {
35    CAPACITY as u32
36}
37
38/// How many effects there are, so a caller can enumerate them.
39#[expect(unsafe_code, reason = "exporting a C symbol for the preview")]
40#[unsafe(no_mangle)]
41pub extern "C" fn solium_effect_count() -> u32 {
42    Deform::all().len() as u32
43}
44
45/// How many axes there are.
46#[expect(unsafe_code, reason = "exporting a C symbol for the preview")]
47#[unsafe(no_mangle)]
48pub extern "C" fn solium_axis_count() -> u32 {
49    Axis::all().len() as u32
50}
51
52/// The effect at `index` in [`Deform::all`], with the parameters given.
53///
54/// Out of range falls back to the first effect rather than refusing: the page
55/// can be older than the engine embedded in it, and a preview that renders
56/// the wrong effect is a better failure than one that renders nothing.
57fn effect(index: u32, axis: u32, progress: f64, spread: f64) -> Deform {
58    let axis = Axis::all()
59        .get(axis as usize)
60        .map_or_else(Axis::default, |(_, axis)| *axis);
61    // Destructured rather than indexed: the array's length is in its type, so
62    // "there is a first effect" is checked by the compiler and not by a bound.
63    let [(_, first), ..] = Deform::all();
64    match Deform::all()
65        .get(index as usize)
66        .map_or(first, |(_, effect)| *effect)
67    {
68        Deform::Genie { .. } => Deform::Genie {
69            progress: crate::number(progress),
70            spread: crate::number(spread),
71            axis,
72        },
73    }
74}
75
76/// How many columns the grid for this effect has. Points across is one more.
77#[expect(unsafe_code, reason = "exporting a C symbol for the preview")]
78#[unsafe(no_mangle)]
79pub extern "C" fn solium_effect_columns(index: u32, axis: u32) -> u32 {
80    effect(index, axis, 0.0, 0.0).segments().0
81}
82
83/// How many rows the grid for this effect has. Points down is one more.
84#[expect(unsafe_code, reason = "exporting a C symbol for the preview")]
85#[unsafe(no_mangle)]
86pub extern "C" fn solium_effect_rows(index: u32, axis: u32) -> u32 {
87    effect(index, axis, 0.0, 0.0).segments().1
88}
89
90/// Deform a window into a target and write the whole grid out.
91///
92/// Returns how many points were written. They are in row-major order --
93/// `columns + 1` across, `rows + 1` down -- which is the order
94/// `warp::mesh_part` walks, so a page drawing quads out of this is drawing the
95/// compositor's own cells.
96#[expect(unsafe_code, reason = "exporting a C symbol for the preview")]
97#[unsafe(no_mangle)]
98pub extern "C" fn solium_effect_grid(
99    index: u32,
100    axis: u32,
101    progress: f64,
102    spread: f64,
103    from_x: f64,
104    from_y: f64,
105    from_w: f64,
106    from_h: f64,
107    to_x: f64,
108    to_y: f64,
109    to_w: f64,
110    to_h: f64,
111) -> u32 {
112    let deform = effect(index, axis, progress, spread);
113    let from = Rect::new(from_x, from_y, from_w, from_h);
114    let to = Rect::new(to_x, to_y, to_w, to_h);
115    let (columns, rows) = deform.segments();
116
117    let mut written = 0_usize;
118    for row in 0..=rows {
119        let v = f64::from(row) / f64::from(rows);
120        for column in 0..=columns {
121            if written >= CAPACITY {
122                break;
123            }
124            let u = f64::from(column) / f64::from(columns);
125            let (x, y) = deform.place(from, to, u, v);
126            publish(written, x, y);
127            written += 1;
128        }
129    }
130    written as u32
131}
132
133/// Put one point into the scratch buffer.
134#[expect(unsafe_code, reason = "writing the scratch buffer the caller reads")]
135fn publish(index: usize, x: f64, y: f64) {
136    if index >= CAPACITY {
137        return;
138    }
139    let scratch = &raw mut SCRATCH;
140    // SAFETY: single-threaded by construction -- this is called from a wasm
141    // module with one thread, never from the compositor, and `index` is
142    // checked against the buffer's length immediately above. Written through a
143    // raw pointer rather than a `&mut` to the static, so no reference to it
144    // exists that a concurrent read could alias.
145    unsafe {
146        (*scratch)[index * 2] = x;
147        (*scratch)[index * 2 + 1] = y;
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    /// The page must not be able to see a different answer than the
156    /// compositor does.
157    #[expect(unsafe_code, reason = "reading the scratch buffer the page reads")]
158    #[test]
159    fn the_exported_surface_agrees_with_the_engine() {
160        let from = Rect::new(0.0, 0.0, 800.0, 600.0);
161        let to = Rect::new(400.0, 900.0, 64.0, 32.0);
162
163        for index in 0..solium_effect_count() {
164            for axis in 0..solium_axis_count() {
165                let deform = effect(index, axis, 0.4, 1.2);
166                let (columns, rows) = deform.segments();
167                assert_eq!(solium_effect_columns(index, axis), columns);
168                assert_eq!(solium_effect_rows(index, axis), rows);
169
170                let written = solium_effect_grid(
171                    index, axis, 0.4, 1.2, from.x, from.y, from.w, from.h, to.x, to.y, to.w, to.h,
172                );
173                assert_eq!(written, (columns + 1) * (rows + 1));
174
175                // Read back the way the page does, and compare against the
176                // engine called directly.
177                let base = solium_effect_scratch();
178                for row in 0..=rows {
179                    for column in 0..=columns {
180                        let at = (row * (columns + 1) + column) as usize;
181                        let (u, v) = (
182                            f64::from(column) / f64::from(columns),
183                            f64::from(row) / f64::from(rows),
184                        );
185                        let (x, y) = deform.place(from, to, u, v);
186                        // SAFETY: `at` is below `written`, which the assertion
187                        // above pins to the grid this loop walks, and the
188                        // buffer is not written while it is read.
189                        let (got_x, got_y) = unsafe { (*base.add(at * 2), *base.add(at * 2 + 1)) };
190                        assert!((got_x - x).abs() < f64::EPSILON, "point {at} x");
191                        assert!((got_y - y).abs() < f64::EPSILON, "point {at} y");
192                    }
193                }
194            }
195        }
196    }
197
198    #[test]
199    fn an_out_of_range_effect_falls_back_rather_than_panicking() {
200        // Reachable from a page that is out of date with the engine.
201        assert_eq!(effect(9999, 9999, 0.5, 1.0), effect(0, 0, 0.5, 1.0));
202    }
203
204    /// A grid never runs past the buffer the page reads out of.
205    #[test]
206    fn the_grid_fits_the_buffer_it_is_published_in() {
207        for index in 0..solium_effect_count() {
208            for axis in 0..solium_axis_count() {
209                let (columns, rows) = effect(index, axis, 0.0, 0.0).segments();
210                assert!(
211                    ((columns + 1) * (rows + 1)) as usize <= CAPACITY,
212                    "{}x{} points does not fit {CAPACITY}",
213                    columns + 1,
214                    rows + 1
215                );
216            }
217        }
218    }
219}