FastLED 3.10.8
Loading...
Searching...
No Matches
wave_simulation_real.cpp.hpp
Go to the documentation of this file.
1// Based on works and code by Shawn Silverman.
2
3#include "fl/stl/stdint.h"
4
5#include "fl/math/math.h"
7#include "fl/stl/noexcept.h"
8
9namespace fl {
10
11// Define Q15 conversion constants.
12// #define FIXED_SCALE (1 << 15) // 32768: 1.0 in Q15
13#define INT16_POS (32767) // Maximum value for i16
14#define INT16_NEG (-32768) // Minimum value for i16
15
16namespace wave_detail { // Anonymous namespace for internal linkage
17// Convert float to fixed Q15.
18// i16 float_to_fixed(float f) { return (i16)(f * FIXED_SCALE); }
19
21 f = fl::clamp(f, -1.0f, 1.0f);
22 if (f < 0.0f) {
23 return static_cast<i16>(f * 32768.0f);
24 } else {
25 return (i16)(f * INT16_POS); // Round to nearest
26 }
27}
28
29// Convert fixed Q15 to float.
31 // return ((float)f) / FIXED_SCALE;
32 if (f < 0) {
33 return static_cast<float>(f) / 32768.0f; // Negative values
34 } else {
35 return ((float)f) / INT16_POS; // Positive values
36 }
37}
38
39// // Multiply two Q15 fixed point numbers.
40// i16 fixed_mul(i16 a, i16 b) {
41// return (i16)(((i32)a * b) >> 15);
42// }
43
44// Precompute the Q15 damping decay factor for a power-of-two exponent.
45// The damping update is `f_new = f * (1 - 1/2^damp)`, equivalent to the
46// arithmetic-shift form `f -= f >> damp` (modulo 1-LSB rounding). Caching
47// it as Q15 here lets the kernel use a single Q15 multiply per cell and
48// opens the door to non-power-of-two damping if the public API ever
49// needs it (today it's still int-only). damp <= 0 means no decay.
51 if (damp <= 0) return INT16_POS; // ~1.0 — no decay
52 // At this exponent the decay rounds to 1.0f; avoid an oversized shift.
53 if (damp >= 32) return INT16_POS;
54 const float decay = 1.0f - 1.0f / static_cast<float>(u32(1) << damp);
55 return float_to_fixed(decay);
56}
57} // namespace wave_detail
58
61 : length(len),
62 grid1(length + 2), // Initialize vector with correct size
63 grid2(length + 2), // Initialize vector with correct size
64 whichGrid(0),
65 // CFL stability bound for the explicit leapfrog 5-point stencil in 1D
66 // is C^2 <= 1. Negative speed has no physical meaning. Clamp at the
67 // boundary to keep the Q15 fixed-point kernel both stable and within
68 // i32 overflow margins (paired with the i64 promote in update()).
69 mCourantSq(wave_detail::float_to_fixed(fl::clamp(courantSq, 0.0f, 1.0f))),
72 // Additional initialization can be added here if needed.
73}
74
76 // See constructor for clamp rationale.
77 mCourantSq = wave_detail::float_to_fixed(fl::clamp(something, 0.0f, 1.0f));
78}
79
84
86
90
92 if (x >= length) {
93 FL_WARN("Out of range.");
94 return 0;
95 }
96 const i16 *curr = (whichGrid == 0) ? grid1.data() : grid2.data();
97 return curr[x + 1];
98}
99
101 if (x >= length) {
102 FL_WARN("Out of range.");
103 return 0;
104 }
105 const i16 *prev = (whichGrid == 0) ? grid2.data() : grid1.data();
106 return prev[x + 1];
107}
108
110 if (x >= length) {
111 FL_WARN("Out of range.");
112 return 0.0f;
113 }
114 // Retrieve value from the active grid (offset by 1 for boundary).
115 const i16 *curr = (whichGrid == 0) ? grid1.data() : grid2.data();
116 return wave_detail::fixed_to_float(curr[x + 1]);
117}
118
119bool WaveSimulation1D_Real::has(fl::size x) const FL_NO_EXCEPT { return (x < length); }
120
122 if (x >= length) {
123 FL_WARN("warning X value too high");
124 return;
125 }
126 i16 *curr = (whichGrid == 0) ? grid1.data() : grid2.data();
128}
129
131 i16 *curr = (whichGrid == 0) ? grid1.data() : grid2.data();
132 i16 *next = (whichGrid == 0) ? grid2.data() : grid1.data();
133
134 // Update boundaries with a Neumann (zero-gradient) condition:
135 curr[0] = curr[1];
136 curr[length + 1] = curr[length];
137
138 i32 mCourantSq32 = static_cast<i32>(mCourantSq);
139 // Hoist the lower-saturation bound: in half-duplex mode the new value
140 // must be >= 0 (negative parts of the wave are clipped); in full-duplex
141 // mode the full Q15 negative range is allowed. Folding this into the
142 // per-cell clamp via fl::clamp lets us drop the dedicated second pass
143 // that used to walk the whole grid after the update loop.
144 const i32 q15_min = mHalfDuplex ? 0 : -32768;
145 // Iterate over each inner cell.
146 for (fl::size i = 1; i < length + 1; i++) {
147 // Compute the 1D Laplacian:
148 // lap = curr[i+1] - 2 * curr[i] + curr[i-1]
149 i32 lap =
150 (i32)curr[i + 1] - (i32)curr[i] * 2 + curr[i - 1];
151
152 // Multiply the Laplacian by the simulation speed using Q15 arithmetic.
153 // Promote to i64 before the multiply. With the 1D CFL clamp at 1.0,
154 // max |mCourantSq32| = 32767 and max |lap| ~131,070 (saturated
155 // alternating cells) give a worst-case product of ~4.3e9 — past
156 // i32 max (2.15e9). The i64 promote also acts as a safety net if
157 // the clamp is ever regressed; pre-clamp the same product could
158 // already reach ~4.3e9 in 1D and ~8.6e9 in 2D.
159 i32 term = static_cast<i32>(
160 (static_cast<i64>(mCourantSq32) * lap) >> 15);
161
162 // Compute the new value:
163 // f = -next[i] + 2 * curr[i] + term
164 i32 f = -(i32)next[i] + (i32)curr[i] * 2 + term;
165
166 // Apply damping: use a precomputed Q15 multiplier in place of the
167 // arithmetic shift. Functionally equivalent for power-of-two damp
168 // (modulo 1-LSB rounding) but generalizes cleanly to non-power-of-
169 // two damping if the public API ever needs it. On Cortex-M4+ this
170 // lowers to a single smmlsr/smulwb; on AVR it's a 16x32 multiply
171 // (~30 cycles) — same ballpark as the shift but cleaner semantics.
172 f = static_cast<i32>(
173 (static_cast<i64>(f) * mDampDecayQ15) >> 15);
174
175 // Clamp f into [q15_min, 32767] in a single step. q15_min is 0 when
176 // half-duplex is on, -32768 otherwise. This subsumes both the Q15
177 // saturation clamp and the post-pass that used to zero negatives.
178 next[i] = static_cast<i16>(fl::clamp(f, q15_min, static_cast<i32>(32767)));
179 }
180
181 // Toggle the active grid.
182 whichGrid ^= 1;
183}
184
186 float speed, float dampening) FL_NO_EXCEPT
187 : width(W), height(H), stride(W + 2),
188 grid1((W + 2) * (H + 2)),
189 grid2((W + 2) * (H + 2)), whichGrid(0),
190 // CFL stability bound for the explicit leapfrog 5-point stencil in 2D
191 // is C^2 <= 1/2. Negative speed has no physical meaning. Clamp at the
192 // boundary to keep the Q15 fixed-point kernel both stable and within
193 // i32 overflow margins (paired with the i64 promote in update()).
195 // Dampening exponent; e.g., 6 means a factor of 2^6 = 64.
196 mDampening(static_cast<int>(dampening)),
198
199// PSRAM-backed overload. Identical to the SRAM constructor above but
200// constructs grid1 / grid2 with the PSRAM memory resource so the
201// (potentially large) cell buffers don't compete with SRAM. The grids
202// then `resize()` to the requested cell count from PSRAM.
204 u32 W, u32 H,
205 float speed,
207 : width(W), height(H), stride(W + 2),
211 mDampening(static_cast<int>(dampening)),
213 grid1.resize((W + 2) * (H + 2));
214 grid2.resize((W + 2) * (H + 2));
215}
216
218 // See constructor for clamp rationale.
219 mCourantSq = wave_detail::float_to_fixed(fl::clamp(something, 0.0f, 0.5f));
220}
221
226
228
232
233float WaveSimulation2D_Real::getf(fl::size x, fl::size y) const FL_NO_EXCEPT {
234 if (x >= width || y >= height) {
235 FL_WARN("Out of range: " << x << ", " << y);
236 return 0.0f;
237 }
238 const i16 *curr = (whichGrid == 0 ? grid1.data() : grid2.data());
239 return wave_detail::fixed_to_float(curr[(y + 1) * stride + (x + 1)]);
240}
241
242i16 WaveSimulation2D_Real::geti16(fl::size x, fl::size y) const FL_NO_EXCEPT {
243 if (x >= width || y >= height) {
244 FL_WARN("Out of range: " << x << ", " << y);
245 return 0;
246 }
247 const i16 *curr = (whichGrid == 0 ? grid1.data() : grid2.data());
248 return curr[(y + 1) * stride + (x + 1)];
249}
250
252 if (x >= width || y >= height) {
253 FL_WARN("Out of range: " << x << ", " << y);
254 return 0;
255 }
256 const i16 *prev = (whichGrid == 0 ? grid2.data() : grid1.data());
257 return prev[(y + 1) * stride + (x + 1)];
258}
259
260bool WaveSimulation2D_Real::has(fl::size x, fl::size y) const FL_NO_EXCEPT {
261 return (x < width && y < height);
262}
263
264void WaveSimulation2D_Real::setf(fl::size x, fl::size y, float value) FL_NO_EXCEPT {
266 return seti16(x, y, v);
267}
268
269void WaveSimulation2D_Real::seti16(fl::size x, fl::size y, i16 value) FL_NO_EXCEPT {
270 if (x >= width || y >= height) {
271 FL_WARN("Out of range: " << x << ", " << y);
272 return;
273 }
274 i16 *curr = (whichGrid == 0 ? grid1.data() : grid2.data());
275 curr[(y + 1) * stride + (x + 1)] = value;
276}
277
279 i16 *curr = (whichGrid == 0 ? grid1.data() : grid2.data());
280 i16 *next = (whichGrid == 0 ? grid2.data() : grid1.data());
281
282 // Update horizontal boundaries.
283 for (fl::size j = 0; j < height + 2; ++j) {
284 if (mXCylindrical) {
285 curr[j * stride + 0] = curr[j * stride + width];
286 curr[j * stride + (width + 1)] = curr[j * stride + 1];
287 } else {
288 curr[j * stride + 0] = curr[j * stride + 1];
289 curr[j * stride + (width + 1)] = curr[j * stride + width];
290 }
291 }
292
293 // Update vertical boundaries.
294 for (fl::size i = 0; i < width + 2; ++i) {
295 curr[0 * stride + i] = curr[1 * stride + i];
296 curr[(height + 1) * stride + i] = curr[height * stride + i];
297 }
298
299 i32 mCourantSq32 = static_cast<i32>(mCourantSq);
300 // Hoist the lower-saturation bound — see WaveSimulation1D_Real::update()
301 // for the rationale. Fold half-duplex into the per-cell clamp instead
302 // of running a second pass over the grid (especially expensive on
303 // PSRAM-backed grids).
304 const i32 q15_min = mHalfDuplex ? 0 : -32768;
305
306 // Update each inner cell. Per-row hoist: lift j*stride out of the inner
307 // loop, set up row pointers to curr/next/above/below, and mark them
308 // FL_RESTRICT_PARAM so the optimizer knows curr and next don't alias.
309 // This gives the compiler a clean dependency picture across iterations
310 // (it can vectorize / interleave loads without re-deriving the address).
311 //
312 // Outer-level branch on the stencil keeps the inner loop branchless;
313 // the two kernels are otherwise identical (Q15 multiply, damping,
314 // clamp). FivePoint is the backward-compatible default; the wrapper
315 // class WaveSimulation2D auto-selects NinePointIsotropic at high
316 // super-sample factors where the anisotropy of the 5-point stencil
317 // becomes visually obvious.
318 const bool nine_point = (mStencil == LaplacianStencil::NinePointIsotropic);
319 for (fl::size j = 1; j <= height; ++j) {
320 const fl::size row = j * stride;
321 const i16* FL_RESTRICT_PARAM row_curr = curr + row;
322 const i16* FL_RESTRICT_PARAM row_above = curr + (row - stride);
323 const i16* FL_RESTRICT_PARAM row_below = curr + (row + stride);
324 i16* FL_RESTRICT_PARAM row_next = next + row;
325 for (fl::size i = 1; i <= width; ++i) {
326 const i32 c = row_curr[i];
327 i32 laplacian;
328 if (nine_point) {
329 // 9-point isotropic Laplacian, scaled up by 6 to keep
330 // everything in integer arithmetic:
331 // 6 * lap = (NW+NE+SW+SE) + 4*(N+S+E+W) - 20*C
332 // The /6 is folded into the term computation below so we
333 // pay it once per cell rather than four times.
334 const i32 diag = (i32)row_above[i - 1] + row_above[i + 1] +
335 row_below[i - 1] + row_below[i + 1];
336 const i32 nbr = (i32)row_above[i] + row_below[i] +
337 row_curr[i - 1] + row_curr[i + 1];
338 laplacian = diag + nbr * 4 - 20 * c;
339 } else {
340 // Standard 5-point Laplacian: N + S + E + W - 4*C.
341 laplacian = (i32)row_curr[i + 1] + row_curr[i - 1] +
342 row_above[i] + row_below[i] - c * 4;
343 }
344 // Promote to i64 before the multiply. With the 2D CFL clamp at
345 // 0.5, the 5-point worst case product is ~4.3e9 and the
346 // 9-point (scaled by 6, so |lap| ~6x larger) is ~2.6e10 — both
347 // past i32 max (2.15e9). The i64 promote also acts as a safety
348 // net if the clamp is ever regressed.
349 i64 product = static_cast<i64>(mCourantSq32) * laplacian;
350 i32 term;
351 if (nine_point) {
352 // Undo the x6 scaling on the 9-point Laplacian here.
353 // Integer division by a compile-time-constant 6 is lowered
354 // to a reciprocal-multiply on Cortex-M4+ and to a small
355 // shift+add on AVR; either way it's well under the cost
356 // of computing the Laplacian itself.
357 term = static_cast<i32>((product >> 15) / 6);
358 } else {
359 term = static_cast<i32>(product >> 15);
360 }
361 // f = -next[index] + 2 * curr[index] + mCourantSq * laplacian.
362 i32 f = -(i32)row_next[i] + c * 2 + term;
363
364 // Apply damping with the precomputed Q15 decay multiplier —
365 // see the 1D update for the rationale.
366 f = static_cast<i32>(
367 (static_cast<i64>(f) * mDampDecayQ15) >> 15);
368
369 // Clamp f into [q15_min, 32767] in a single step — subsumes
370 // both the Q15 saturation clamp and the half-duplex zero pass.
371 row_next[i] = static_cast<i16>(fl::clamp(f, q15_min, static_cast<i32>(32767)));
372 }
373 }
374
375 // Swap the roles of the grids.
376 whichGrid ^= 1;
377}
378
379} // namespace fl
uint16_t speed
Definition Noise.ino:72
static const int H
Definition PerfDisc.ino:21
static const int W
Definition PerfDisc.ino:20
bool has(fl::size x) const FL_NO_EXCEPT
int getDampenening() const FL_NO_EXCEPT
void setSpeed(float something) FL_NO_EXCEPT
void setDampening(int damp) FL_NO_EXCEPT
WaveSimulation1D_Real(u32 length, float speed=0.16f, int dampening=6) FL_NO_EXCEPT
float getf(fl::size x) const FL_NO_EXCEPT
float getSpeed() const FL_NO_EXCEPT
i16 geti16Previous(fl::size x) const FL_NO_EXCEPT
void set(fl::size x, float value) FL_NO_EXCEPT
i16 geti16(fl::size x) const FL_NO_EXCEPT
bool has(fl::size x, fl::size y) const FL_NO_EXCEPT
void setSpeed(float something) FL_NO_EXCEPT
void seti16(fl::size x, fl::size y, i16 value) FL_NO_EXCEPT
i16 geti16(fl::size x, fl::size y) const FL_NO_EXCEPT
WaveSimulation2D_Real(u32 W, u32 H, float speed=0.16f, float dampening=6.0f) FL_NO_EXCEPT
void setf(fl::size x, fl::size y, float value) FL_NO_EXCEPT
int getDampenening() const FL_NO_EXCEPT
void setDampening(int damp) FL_NO_EXCEPT
i16 geti16Previous(fl::size x, fl::size y) const FL_NO_EXCEPT
float getf(fl::size x, fl::size y) const FL_NO_EXCEPT
float getSpeed() const FL_NO_EXCEPT
#define FL_RESTRICT_PARAM
fl::UISlider length("Length", 1.0f, 0.0f, 1.0f, 0.01f)
fl::UISlider dampening("Dampening", 6.0f, 0.0f, 10.0f, 0.1f)
#define FL_WARN(...)
Definition log.h:437
float fixed_to_float(i16 f) FL_NO_EXCEPT
i16 float_to_fixed(float f) FL_NO_EXCEPT
i16 compute_damp_decay_q15(int damp) FL_NO_EXCEPT
u8 u8 fract8 blur_amount FL_NO_EXCEPT
Definition blur.h:186
constexpr int type_rank< T >::value
u8 u8 height
Definition blur.h:186
constexpr enable_if< is_fixed_point< T >::value, T >::type clamp(T x, T lo, T hi) FL_NO_EXCEPT
u8 width
Definition blur.h:186
memory_resource * psram_memory_resource() FL_NO_EXCEPT
Get the PSRAM memory resource (wraps PSRamAllocate / PSRamDeallocate).
Base definition for an LED controller.
Definition crgb.hpp:179
fl::i64 i64
Definition stdint.h:221
#define INT16_POS