FastLED 3.10.9
Loading...
Searching...
No Matches
AutoResearchAnimartrixBench.h
Go to the documentation of this file.
1// AutoResearchAnimartrixBench.h
2//
3// Animartrix-representative Perlin-noise benchmark: scalar float (`fl::pnoise`)
4// vs s16x16 fixed-point (`fl::perlin_i16_optimized::pnoise2d`).
5//
6// Why Perlin noise: it's the workhorse of every Animartrix effect — every
7// frame, every pixel goes through one or more `pnoise` calls to drive the
8// polar-coordinate field that shapes the pattern. If a fixed-point variant
9// wins here, it wins in Animartrix.
10//
11// Why s16x16 (and not s8x8): Animartrix's fade/lerp/grad inner loops need
12// the precision of Q16.16 to match the float reference. The s8x8 Q8.8
13// type loses too many bits in the multiplications inside the gradient
14// step. This benchmark mirrors what the FastLED tree already ships
15// (`fl/fx/2d/animartrix_detail/perlin_i16_optimized.cpp.hpp`).
16
17#pragma once
18
19#include <FastLED.h>
20#include "fl/stl/chrono.h"
24#include "fl/stl/singleton.h"
25
26namespace autoresearch {
28
29// Volatile sink to defeat dead-code elimination on both the float and
30// the i16 outputs. Same trick the SIMD multiply benchmark uses.
32 volatile int32_t sink;
33 int32_t fade_lut[257];
34
38};
39
43
45 int64_t iterations;
46 int64_t pnoise_float_us; // fl::pnoise(x, y, 0) total wall time
47 int64_t pnoise_i16_us; // fl::perlin_i16_optimized::pnoise2d total wall time
48 // The same coordinate grid is fed to both versions so the comparison
49 // measures purely the arithmetic cost of float vs i16 fixed-point.
50};
51
52// Sweep a 2D coordinate grid the same way an Animartrix render pass
53// would (one Perlin lookup per output pixel). 16*16 = 256 pixels per
54// outer iter, matching a 16x16 panel — small enough that the compiler
55// won't unroll the world, large enough that any per-iteration fixed
56// overhead is amortized.
59 r.iterations = iters;
60
61 // Init the i16 implementation's fade lookup table once, off the
62 // benchmark clock. Function-local static so the linter's
63 // static-in-header rule stays happy and C++11's thread-safe init
64 // guarantees a single initialization across calls. The float path
65 // has no equivalent setup.
67 const int32_t* fade_lut = state.fade_lut;
68
69 constexpr int GRID = 16; // 16x16 pixel pass per iteration
70 constexpr float STEP_F = 0.05f; // Animartrix-typical pixel step in noise space
71 // Mirror in fixed-point: 0.05 in Q16.16 = 0.05 * 65536 ≈ 3277
72 constexpr int32_t STEP_I = static_cast<int32_t>(0.05f * 65536.0f);
73
74 // ── Float pnoise ──────────────────────────────────────────────
75 {
76 float ax = 0.0f, ay = 0.0f;
77 int32_t sink = 0;
78 uint32_t t0 = fl::micros();
79 for (int it = 0; it < iters; it++) {
80 // Slowly drift the origin across the iter so the compiler
81 // can't pre-compute. Same pattern Animartrix uses (the
82 // origin advances with `time_speed * dt` each frame).
83 ax += 0.011f;
84 ay += 0.013f;
85 for (int row = 0; row < GRID; row++) {
86 float y = ay + row * STEP_F;
87 for (int col = 0; col < GRID; col++) {
88 float x = ax + col * STEP_F;
89 float n = fl::pnoise(x, y, 0.0f);
90 // Map [-1, 1] → int8 the way Animartrix's output
91 // stage does. Sink to defeat DCE.
92 sink += static_cast<int32_t>(n * 127.0f);
93 }
94 }
95 }
96 uint32_t t1 = fl::micros();
97 state.sink = sink;
98 r.pnoise_float_us = static_cast<int64_t>(t1 - t0);
99 }
100
101 // ── i16 fixed-point pnoise2d ──────────────────────────────────
102 {
103 int32_t ax_i = 0, ay_i = 0;
104 // Q16.16 increment for the slow drift (matches 0.011, 0.013 in float)
105 constexpr int32_t DRIFT_X = static_cast<int32_t>(0.011f * 65536.0f);
106 constexpr int32_t DRIFT_Y = static_cast<int32_t>(0.013f * 65536.0f);
107 int32_t sink = 0;
108 uint32_t t0 = fl::micros();
109 for (int it = 0; it < iters; it++) {
110 ax_i += DRIFT_X;
111 ay_i += DRIFT_Y;
112 for (int row = 0; row < GRID; row++) {
113 int32_t y_i = ay_i + row * STEP_I;
114 for (int col = 0; col < GRID; col++) {
115 int32_t x_i = ax_i + col * STEP_I;
116 // pnoise2d_raw returns i32 Q16.16. Scale to int8
117 // the same way the float path does — divide by
118 // (HP_ONE / 127) so the magnitudes line up.
120 x_i, y_i, fade_lut, fl::PERLIN_NOISE);
121 sink += n >> 9; // crude scale; the actual op cost,
122 // not the value, is what we measure
123 }
124 }
125 }
126 uint32_t t1 = fl::micros();
127 state.sink = sink;
128 r.pnoise_i16_us = static_cast<int64_t>(t1 - t0);
129 }
130
131 return r;
132}
133
134} // namespace animartrix_check
135} // namespace autoresearch
int y
Definition simple.h:93
int x
Definition simple.h:92
TestState state
FastLED chrono implementation - duration types for time measurements.
static T & instance() FL_NO_EXCEPT
Definition singleton.h:81
PerlinBenchResult runPerlinBenchmark(int iters)
FASTLED_FORCE_INLINE float pnoise(float x, float y, float z) FL_NO_EXCEPT
fl::u32 micros() FL_NO_EXCEPT
Universal microsecond timer - returns microseconds since system startup.
static void init_fade_lut(fl::i32 *table) FL_NO_EXCEPT
static fl::i32 pnoise2d_raw(fl::i32 fx_raw, fl::i32 fy_raw, const fl::i32 *fade_lut, const fl::u8 *perm) FL_NO_EXCEPT