#include <Arduino.h>
#include <FastLED.h>
// ── Matrix configuration ──────────────────────────────────────────────────
static constexpr uint8_t kMatrixWidth = 32;
static constexpr uint8_t kMatrixHeight = 32;
static constexpr int kLedCount = kMatrixWidth * kMatrixHeight;
static constexpr bool kMatrixSerpentine = false; // false = raster scan, true = serpentine
#define DATA_PIN 3
// ── Pixel buffer ──────────────────────────────────────────────────────────
// Both implementations share one buffer and use row-major indexing.
// They're benchmarked sequentially.
CRGB leds[kLedCount];
// Required by graphics_prims.h (silences per-pixel debug prints)
static constexpr bool kDebugSerial = false;
static constexpr bool kFrameTimings = false;
// ── Original ("mine") implementation ─────────────────────────────────────
// graphics.h — pixel helpers: pixelIndex, addPixelXY, etc.
// graphics_prims.h — drawRing / drawDisc / drawLine / drawThickLine
#include "graphics.h"
#include "graphics_prims.h"
// ── FastLED canvas implementation ─────────────────────────────────────────
#include "fl/gfx/canvas.h"
#include "fl/gfx/primitives.h"
// ── Constants for demo ───────────────────────────────────────────────────
static const coord kTwoPi = coord::from_raw(411774); // 2π = 6.2831853
// ── Benchmark parameters ──────────────────────────────────────────────────
// How many draw calls to average over. Enough that micros() noise washes out.
static constexpr int kIter = 32;
// Centre of the 32×32 matrix
static const coord kCX = coord::from_raw(16 * coord::SCALE);
static const coord kCY = coord::from_raw(16 * coord::SCALE);
// ── Test matrix ───────────────────────────────────────────────────────────
struct RingTestCase {
const char* label;
coord cx, cy; // center coordinates
coord r; // inner radius
coord t; // thickness
};
struct LineTestCase {
const char* label;
coord x0, y0; // start point
coord x1, y1; // end point
coord thickness; // for thick lines (0 = thin line)
};
static const RingTestCase kRingCases[] = {
{ "r= 3 t=1", kCX, kCY, coord::from_raw( 3 * coord::SCALE), coord::from_raw(1 * coord::SCALE) },
{ "r= 3 t=2", kCX, kCY, coord::from_raw( 3 * coord::SCALE), coord::from_raw(2 * coord::SCALE) },
{ "r= 6 t=1", kCX, kCY, coord::from_raw( 6 * coord::SCALE), coord::from_raw(1 * coord::SCALE) },
{ "r= 6 t=3", kCX, kCY, coord::from_raw( 6 * coord::SCALE), coord::from_raw(3 * coord::SCALE) },
{ "r=10 t=1", kCX, kCY, coord::from_raw(10 * coord::SCALE), coord::from_raw(1 * coord::SCALE) },
{ "r=10 t=4", kCX, kCY, coord::from_raw(10 * coord::SCALE), coord::from_raw(4 * coord::SCALE) },
{ "r=13 t=1", kCX, kCY, coord::from_raw(13 * coord::SCALE), coord::from_raw(1 * coord::SCALE) },
{ "r=13 t=5", kCX, kCY, coord::from_raw(13 * coord::SCALE), coord::from_raw(5 * coord::SCALE) },
{ "r=20 t=1", coord::from_raw(0 * coord::SCALE), coord::from_raw(0 * coord::SCALE), coord::from_raw(20 * coord::SCALE), coord::from_raw(1 * coord::SCALE) },
};
static const LineTestCase kLineCases[] = {
{ "len=10 t=0", coord::from_raw(11 * coord::SCALE), coord::from_raw(16 * coord::SCALE), coord::from_raw(21 * coord::SCALE), coord::from_raw(16 * coord::SCALE), coord{} }, // horizontal thin
{ "len=10 t=2", coord::from_raw(11 * coord::SCALE), coord::from_raw(16 * coord::SCALE), coord::from_raw(21 * coord::SCALE), coord::from_raw(16 * coord::SCALE), coord::from_raw(2 * coord::SCALE) }, // horizontal thick
{ "len=14 t=0", coord::from_raw(16 * coord::SCALE), coord::from_raw(9 * coord::SCALE), coord::from_raw(16 * coord::SCALE), coord::from_raw(23 * coord::SCALE), coord{} }, // vertical thin
{ "len=14 t=3", coord::from_raw(16 * coord::SCALE), coord::from_raw(9 * coord::SCALE), coord::from_raw(16 * coord::SCALE), coord::from_raw(23 * coord::SCALE), coord::from_raw(3 * coord::SCALE) }, // vertical thick
{ "len=20 t=0", coord::from_raw(6 * coord::SCALE), coord::from_raw(6 * coord::SCALE), coord::from_raw(26 * coord::SCALE), coord::from_raw(26 * coord::SCALE), coord{} }, // diagonal thin
{ "len=20 t=4", coord::from_raw(6 * coord::SCALE), coord::from_raw(6 * coord::SCALE), coord::from_raw(26 * coord::SCALE), coord::from_raw(26 * coord::SCALE), coord::from_raw(4 * coord::SCALE) }, // diagonal thick (45°)
{ "len=50 t=0", coord{}, coord{}, coord::from_raw(50 * coord::SCALE), coord::from_raw(10 * coord::SCALE), coord{} }, // shallow angle thin (11.3°), mostly off-screen
{ "len=30 t=2", coord::from_raw(5 * coord::SCALE), coord::from_raw(10 * coord::SCALE), coord::from_raw(35 * coord::SCALE), coord::from_raw(20 * coord::SCALE), coord::from_raw(2 * coord::SCALE) }, // longer shallow angle thick (18.4°)
};
static constexpr uint8_t kNumRingCases = sizeof(kRingCases) / sizeof(kRingCases[0]);
static constexpr uint8_t kNumLineCases = sizeof(kLineCases) / sizeof(kLineCases[0]);
// ── Demo clock functions ──────────────────────────────────────────────────
static uint8_t demoClockOrig(coord cx, coord cy, coord r, uint32_t ms) {
static constexpr uint32_t kSecPeriod = 6000u;
static constexpr uint32_t kMinPeriod = 72000u;
static constexpr uint32_t kHrPeriod = 864000u;
static const coord kOffset = coord::from_raw(102943); // π/2 = 1.5707963, East→North offset
// angle = (ms % period) * 2π / period — pure integer arithmetic
coord secA = coord::from_raw((uint32_t)(ms % kSecPeriod) * (uint32_t)kTwoPi.raw() / kSecPeriod) - kOffset;
coord minA = coord::from_raw((int32_t)((uint64_t)(ms % kMinPeriod) * (uint32_t)kTwoPi.raw() / kMinPeriod)) - kOffset;
coord hrA = coord::from_raw((int32_t)((uint64_t)(ms % kHrPeriod) * (uint32_t)kTwoPi.raw() / kHrPeriod)) - kOffset;
// Zoom: slow sine (period 20 s). When positive, zoom in on the second-hand tip.
coord zPhase = coord::from_raw((int32_t)((uint64_t)(ms % 20000u) * (uint32_t)kTwoPi.raw() / 20000u));
coord zSin = fl::s16x16::sin(zPhase);
// zoom ∈ [1.0, 2.5]: 1 + max(0, zSin) * 1.5
coord zoom = coord::from_raw(65536) + (zSin.raw() > 0 ? zSin * coord::from_raw(98304) : coord{}); // 1.0 + max(0,zSin)*1.5
// Second hand trig (needed for both tip tracking and drawing)
coord sc = fl::s16x16::cos(secA), ss = fl::s16x16::sin(secA);
coord tipX = cx + sc * r;
coord tipY = cy + ss * r;
// Camera blends linearly from clock centre (zoom=1) to tip (zoom=max).
coord zoomFrac = (zoom - coord::from_raw(65536)) / coord::from_raw(98304); // (zoom-1)/1.5
coord camX = cx + (tipX - cx) * zoomFrac;
coord camY = cy + (tipY - cy) * zoomFrac;
// screen = displayCentre + (world - cam) * zoom
coord eff_cx = cx + (cx - camX) * zoom;
coord eff_cy = cy + (cy - camY) * zoom;
coord eff_r = r * zoom;
coord eff_zoom = zoom;
// Face ring
drawRing(eff_cx, eff_cy, eff_r, eff_zoom, CRGB(40, 40, 40)); // thickness = 1.0 * eff_zoom
// 12 tick marks; major ticks (every 3rd) are longer and brighter
for (uint8_t i = 0; i < 12; ++i) {
coord ta = coord::from_raw((int32_t)i * (kTwoPi.raw() / 12)) - kOffset;
coord tc = fl::s16x16::cos(ta), ts = fl::s16x16::sin(ta);
bool major = (i % 3 == 0);
coord inner = eff_r * coord::from_raw(major ? 47185 : 53739); // *0.72 or *0.82
drawLine(eff_cx + tc * inner, eff_cy + ts * inner,
eff_cx + tc * eff_r, eff_cy + ts * eff_r,
major ? CRGB(200, 200, 200) : CRGB(80, 80, 80));
}
// Hour hand: thick and short
{
coord hc = fl::s16x16::cos(hrA), hs = fl::s16x16::sin(hrA);
coord hLen = eff_r * coord::from_raw(36044); // *0.55
drawThickLine(eff_cx, eff_cy, eff_cx + hc * hLen, eff_cy + hs * hLen,
coord::from_raw(163840) * eff_zoom, CRGB(200, 200, 200)); // thick=2.5
}
// Minute hand: medium thickness, nearly full length
{
coord mc = fl::s16x16::cos(minA), ms_v = fl::s16x16::sin(minA);
coord mLen = eff_r * coord::from_raw(52428); // *0.80
drawThickLine(eff_cx, eff_cy, eff_cx + mc * mLen, eff_cy + ms_v * mLen,
coord::from_raw(98304) * eff_zoom, CRGB::White); // thick=1.5
}
// Second hand: thin red line + short counterbalance tail
{
coord tail = eff_r * coord::from_raw(16384); // *0.25 (exact)
drawLine(eff_cx - sc * tail, eff_cy - ss * tail,
eff_cx + sc * eff_r, eff_cy + ss * eff_r, CRGB(255, 40, 40));
}
// Centre cap
drawDisc(eff_cx, eff_cy, coord::from_raw(98304) * eff_zoom, CRGB(220, 220, 220)); // r=1.5
return 255;
}
static uint8_t demoClockCanvas(coord cx, coord cy, coord r, uint32_t ms) {
static constexpr uint32_t kSecPeriod = 6000u;
static constexpr uint32_t kMinPeriod = 72000u;
static constexpr uint32_t kHrPeriod = 864000u;
static const coord kOffset = coord::from_raw(102943); // π/2 = 1.5707963, East→North offset
// angle = (ms % period) * 2π / period — pure integer arithmetic
coord secA = coord::from_raw((uint32_t)(ms % kSecPeriod) * (uint32_t)kTwoPi.raw() / kSecPeriod) - kOffset;
coord minA = coord::from_raw((int32_t)((uint64_t)(ms % kMinPeriod) * (uint32_t)kTwoPi.raw() / kMinPeriod)) - kOffset;
coord hrA = coord::from_raw((int32_t)((uint64_t)(ms % kHrPeriod) * (uint32_t)kTwoPi.raw() / kHrPeriod)) - kOffset;
// Zoom: slow sine (period 20 s). When positive, zoom in on the second-hand tip.
coord zPhase = coord::from_raw((int32_t)((uint64_t)(ms % 20000u) * (uint32_t)kTwoPi.raw() / 20000u));
coord zSin = fl::s16x16::sin(zPhase);
// zoom ∈ [1.0, 2.5]: 1 + max(0, zSin) * 1.5
coord zoom = coord::from_raw(65536) + (zSin.raw() > 0 ? zSin * coord::from_raw(98304) : coord{}); // 1.0 + max(0,zSin)*1.5
// Second hand trig (needed for both tip tracking and drawing)
coord sc = fl::s16x16::cos(secA), ss = fl::s16x16::sin(secA);
coord tipX = cx + sc * r;
coord tipY = cy + ss * r;
// Camera blends linearly from clock centre (zoom=1) to tip (zoom=max).
coord zoomFrac = (zoom - coord::from_raw(65536)) / coord::from_raw(98304); // (zoom-1)/1.5
coord camX = cx + (tipX - cx) * zoomFrac;
coord camY = cy + (tipY - cy) * zoomFrac;
// screen = displayCentre + (world - cam) * zoom
coord eff_cx = cx + (cx - camX) * zoom;
coord eff_cy = cy + (cy - camY) * zoom;
coord eff_r = r * zoom;
coord eff_zoom = zoom;
// Create canvas
fl::span<CRGB> canvasSpan(leds, kLedCount);
fl::gfx::Canvas<CRGB> canvas(canvasSpan, kMatrixWidth, kMatrixHeight);
// Face ring
canvas.drawRing(CRGB(40, 40, 40), eff_cx, eff_cy, eff_r, eff_zoom); // thickness = 1.0 * eff_zoom
// 12 tick marks; major ticks (every 3rd) are longer and brighter
for (uint8_t i = 0; i < 12; ++i) {
coord ta = coord::from_raw((int32_t)i * (kTwoPi.raw() / 12)) - kOffset;
coord tc = fl::s16x16::cos(ta), ts = fl::s16x16::sin(ta);
bool major = (i % 3 == 0);
coord inner = eff_r * coord::from_raw(major ? 47185 : 53739); // *0.72 or *0.82
canvas.drawLine(major ? CRGB(200, 200, 200) : CRGB(80, 80, 80),
(eff_cx + tc * inner), (eff_cy + ts * inner),
(eff_cx + tc * eff_r), (eff_cy + ts * eff_r));
}
// Hour hand: thick and short
{
coord hc = fl::s16x16::cos(hrA), hs = fl::s16x16::sin(hrA);
coord hLen = eff_r * coord::from_raw(36044); // *0.55
coord hThick = coord::from_raw(163840) * eff_zoom; // thick=2.5
canvas.drawStrokeLine(CRGB(200, 200, 200), eff_cx, eff_cy,
(eff_cx + hc * hLen), (eff_cy + hs * hLen), hThick);
}
// Minute hand: medium thickness, nearly full length
{
coord mc = fl::s16x16::cos(minA), ms_v = fl::s16x16::sin(minA);
coord mLen = eff_r * coord::from_raw(52428); // *0.80
coord mThick = coord::from_raw(98304) * eff_zoom; // thick=1.5
canvas.drawStrokeLine(CRGB::White, eff_cx, eff_cy,
(eff_cx + mc * mLen), (eff_cy + ms_v * mLen), mThick);
}
// Second hand: thin red line + short counterbalance tail
{
coord tail = eff_r * coord::from_raw(16384); // *0.25 (exact)
canvas.drawLine(CRGB(255, 40, 40), (eff_cx - sc * tail), (eff_cy - ss * tail),
(eff_cx + sc * eff_r), (eff_cy + ss * eff_r));
}
// Centre cap
coord capR = coord::from_raw(98304) * eff_zoom; // r=1.5
canvas.drawDisc(CRGB(220, 220, 220), eff_cx, eff_cy, capR);
return 255;
}
// ── Helpers ───────────────────────────────────────────────────────────────
// Print a right-justified decimal number in a fixed-width field.
static void printPadded(uint32_t val, uint8_t width) {
char buf[11];
uint8_t len = 0;
if (val == 0) { buf[len++] = '0'; }
else { uint32_t v = val; while (v) { buf[len++] = '0' + (v % 10); v /= 10; } }
// buf holds digits in reverse
for (uint8_t i = len; i < width; ++i) Serial.print(' ');
for (int8_t i = len - 1; i >= 0; --i) Serial.print(buf[i]);
}
void setup() {
Serial.begin(2000000);
FastLED.addLeds<WS2812B, DATA_PIN, GRB>(leds, kLedCount);
Serial.println(F("=== drawRing & drawDisc benchmark: orig vs FastLED canvas ==="));
Serial.println(F("Board: Arduino Mega (AVR 16 MHz)"));
Serial.println(F("Matrix: 32x32"));
Serial.print( F("Iterations per case: ")); Serial.println(kIter);
Serial.println(F(""));
Serial.println(F(" │ Ring │ Disc"));
Serial.println(F(" case │ orig canvas % │ orig canvas %"));
Serial.println(F("────────────┼────────────────────────┼───────────────────────"));
// Build a Canvas wrapping the shared pixel buffer
fl::span<CRGB> canvasSpan(leds, kLedCount);
fl::gfx::Canvas<CRGB> canvas(canvasSpan, kMatrixWidth, kMatrixHeight);
// Benchmark rings and discs
for (uint8_t i = 0; i < kNumRingCases; ++i) {
const RingTestCase& tc = kRingCases[i];
// ── Benchmark original drawRing ──────────────────────────────────
::memset(leds, 0, sizeof(leds));
drawRing(tc.cx, tc.cy, tc.r, tc.t, CRGB::White);
FastLED.show();
uint32_t t0 = micros();
for (int j = 0; j < kIter; ++j)
drawRing(tc.cx, tc.cy, tc.r, tc.t, CRGB::White);
uint32_t ringOrigUs = (micros() - t0) / kIter;
// ── Benchmark FastLED canvas drawRing ────────────────────────────
::memset(leds, 0, sizeof(leds));
canvas.drawRing(CRGB::White, tc.cx, tc.cy, tc.r, tc.t);
FastLED.show();
uint32_t t1 = micros();
for (int j = 0; j < kIter; ++j)
canvas.drawRing(CRGB::White, tc.cx, tc.cy, tc.r, tc.t);
uint32_t ringCanvasUs = (micros() - t1) / kIter;
// ── Benchmark original drawDisc ──────────────────────────────────
coord discRadius = tc.r + tc.t; // outer radius of ring
::memset(leds, 0, sizeof(leds));
drawDisc(tc.cx, tc.cy, discRadius, CRGB::White);
FastLED.show();
uint32_t t2 = micros();
for (int j = 0; j < kIter; ++j)
drawDisc(tc.cx, tc.cy, discRadius, CRGB::White);
uint32_t discOrigUs = (micros() - t2) / kIter;
// ── Benchmark FastLED canvas drawDisc ────────────────────────────
::memset(leds, 0, sizeof(leds));
canvas.drawDisc(CRGB::White, tc.cx, tc.cy, discRadius);
FastLED.show();
uint32_t t3 = micros();
for (int j = 0; j < kIter; ++j)
canvas.drawDisc(CRGB::White, tc.cx, tc.cy, discRadius);
uint32_t discCanvasUs = (micros() - t3) / kIter;
// ── Print result row ─────────────────────────────────────────────
Serial.print(F(" "));
Serial.print(tc.label);
Serial.print(F(" │ "));
// Ring results
printPadded(ringOrigUs, 5); Serial.print(F("us "));
printPadded(ringCanvasUs, 5); Serial.print(F("us "));
uint32_t ringPct = (ringCanvasUs * 100ul) / ringOrigUs;
printPadded(ringPct, 4); Serial.print(F("% │ "));
// Disc results
printPadded(discOrigUs, 5); Serial.print(F("us "));
printPadded(discCanvasUs, 5); Serial.print(F("us "));
uint32_t discPct = (discCanvasUs * 100ul) / discOrigUs;
printPadded(discPct, 4); Serial.print('%');
Serial.println();
}
Serial.println(F("────────────┴────────────────────────┴───────────────────────"));
Serial.println(F("Note: % < 100 means canvas is faster"));
Serial.println(F(""));
Serial.println(F("=== drawLine benchmark: orig vs FastLED canvas ==="));
Serial.println(F(""));
Serial.println(F(" case │ orig canvas %"));
Serial.println(F("────────────┼────────────────────────"));
// Benchmark lines
for (uint8_t i = 0; i < kNumLineCases; ++i) {
const LineTestCase& tc = kLineCases[i];
// ── Benchmark original drawLine/drawThickLine ──────────────────────
::memset(leds, 0, sizeof(leds));
if (tc.thickness.raw() == 0) {
drawLine(tc.x0, tc.y0, tc.x1, tc.y1, CRGB::White);
} else {
drawThickLine(tc.x0, tc.y0, tc.x1, tc.y1, tc.thickness, CRGB::White);
}
FastLED.show();
uint32_t t0 = micros();
for (int j = 0; j < kIter; ++j) {
if (tc.thickness.raw() == 0) {
drawLine(tc.x0, tc.y0, tc.x1, tc.y1, CRGB::White);
} else {
drawThickLine(tc.x0, tc.y0, tc.x1, tc.y1, tc.thickness, CRGB::White);
}
}
uint32_t lineOrigUs = (micros() - t0) / kIter;
// ── Benchmark FastLED canvas drawLine ──────────────────────────────
::memset(leds, 0, sizeof(leds));
if (tc.thickness.raw() == 0) {
canvas.drawLine(CRGB::White, tc.x0, tc.y0, tc.x1, tc.y1);
} else {
canvas.drawStrokeLine(CRGB::White, tc.x0, tc.y0, tc.x1, tc.y1, tc.thickness);
}
FastLED.show();
uint32_t t1 = micros();
for (int j = 0; j < kIter; ++j) {
if (tc.thickness.raw() == 0) {
canvas.drawLine(CRGB::White, tc.x0, tc.y0, tc.x1, tc.y1);
} else {
canvas.drawStrokeLine(CRGB::White, tc.x0, tc.y0, tc.x1, tc.y1, tc.thickness);
}
}
uint32_t lineCanvasUs = (micros() - t1) / kIter;
// ── Print result row ─────────────────────────────────────────────
Serial.print(F(" "));
Serial.print(tc.label);
Serial.print(F(" │ "));
// Line results
printPadded(lineOrigUs, 5); Serial.print(F("us "));
printPadded(lineCanvasUs, 5); Serial.print(F("us "));
uint32_t linePct = (lineCanvasUs * 100ul) / lineOrigUs;
printPadded(linePct, 4); Serial.print('%');
Serial.println();
}
Serial.println(F("────────────┴────────────────────────"));
Serial.println(F("Note: % < 100 means canvas is faster"));
Serial.println(F(""));
Serial.println(F("=== Clock demo: each implementation runs for 20 seconds with timing stats ==="));
}
void loop() {
static uint8_t currentImpl = 0; // 0 = original, 1 = canvas
static uint32_t cycleStartMs = 0;
static uint32_t frameCount = 0;
static uint32_t minRenderUs = UINT32_MAX;
static uint32_t maxRenderUs = 0;
static uint32_t totalRenderUs = 0;
static bool printedStats = false;
static bool initialized = false;
if (!initialized) {
cycleStartMs = millis();
initialized = true;
}
uint32_t ms = millis();
coord cx = coord::from_raw(16 * coord::SCALE);
coord cy = coord::from_raw(16 * coord::SCALE);
coord r = coord::from_raw(13 * coord::SCALE);
// Check if we need to switch implementations (every 20 seconds)
if (ms - cycleStartMs >= 20000) {
if (!printedStats) {
// Print statistics for the completed cycle
uint32_t avgRenderUs = totalRenderUs / frameCount;
Serial.print(currentImpl == 0 ? F("Original") : F("Canvas"));
Serial.print(F(" cycle stats: frames="));
Serial.print(frameCount);
Serial.print(F(" min="));
Serial.print(minRenderUs);
Serial.print(F("us max="));
Serial.print(maxRenderUs);
Serial.print(F("us avg="));
Serial.print(avgRenderUs);
Serial.println(F("us"));
printedStats = true;
// Immediately switch to next implementation
currentImpl = (currentImpl + 1) % 2;
cycleStartMs = ms;
frameCount = 0;
minRenderUs = UINT32_MAX;
maxRenderUs = 0;
totalRenderUs = 0;
printedStats = false;
}
} else {
// Normal rendering cycle
FastLED.clear();
// Time the render (excluding FastLED.show())
uint32_t renderStart = micros();
if (currentImpl == 0) {
demoClockOrig(cx, cy, r, ms);
} else {
demoClockCanvas(cx, cy, r, ms);
}
uint32_t renderUs = micros() - renderStart;
// Update statistics
frameCount++;
if (renderUs < minRenderUs) minRenderUs = renderUs;
if (renderUs > maxRenderUs) maxRenderUs = renderUs;
totalRenderUs += renderUs;
FastLED.show();
}
}
FPS: 0
Power: 0.00W
Power: 0.00W