build: replace premake with a hand-written Makefile; optimize FFT

Build system
- Add a single hand-written Makefile (GNU make + gcc, pure C). Builds
  raylib from vendored source and links rspektrum with no premake/lua.
  Targets: all/run/test/bench/clean; release default, DEBUG=1 for debug;
  ARCH overridable (defaults to -march=x86-64-v3).
- Remove premake entirely: rspektrum.make, raylib.make, build/premake5*
  binaries, build/premake5.lua, build/ecc/*. The generated top-level
  Makefile was gitignored, so hand-edits to it were silently lost.
- Vendor raylib src/ into the repo (was gitignored -> fresh clones could
  not build). Commit only src/ (~16MB); examples/projects stay local.
  Verified: a build from the git-tracked tree alone succeeds offline.
- Release flags bumped to -O3 -ffast-math with a portable arch baseline
  (x86-64-v3 = AVX2+FMA on x64, SSE2 on x86). Confirmed FMA/AVX codegen
  in fft.o.

FFT optimization (src/fft.c)
- Precompute twiddle factors and the bit-reversal permutation once per
  size, cached as a small plan table (FFTW's idea, lightweight). Removes
  the per-butterfly cexpf() and per-element bit-twiddling that dominated.
- 3.6x faster on the mlnl_samples.wav STFT workload (2048-pt, -O2 same
  flags both sides): 81us -> 22us per FFT. With the new -O3/-ffast-math/
  AVX2 release flags stacked: ~15us (5.5x vs the old -O2 baseline).
- Verified vs a double-precision reference DFT: 1e-6 relative error,
  round-trip 2.4e-7. Drop-in: same FFT() signature.

Tests/bench (bench/)
- fft_verify.c: FFT vs reference DFT + round-trip check (make test).
- fft_bench.c: times the real STFT workload (make bench).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 21:13:49 -07:00
parent e900caad1d
commit 849306d65e
187 changed files with 351146 additions and 1031 deletions
+64 -10
View File
@@ -1,37 +1,91 @@
// fft.c - Radix-2 Cooley-Tukey FFT
//
// Twiddle factors and the bit-reversal permutation are precomputed per FFT size
// and cached (FFTW's "plan" idea, kept lightweight): the first call for a size
// builds the tables, every later call just reads them. This removes the per-
// butterfly cexpf() and the per-element bit-twiddling that dominated the old
// straight-line version.
#include "fft.h"
#include <math.h>
#include <complex.h>
#include <stdbool.h>
#include <stdlib.h>
static void BitReverseCopy(float complex* input, float complex* output, int n)
// Cache of plans, keyed by transform size. FFT_SIZE_MAX is 2048, so a handful
// of distinct power-of-two sizes ever appear; a tiny fixed table is plenty.
#define FFT_MAX_PLANS 16
typedef struct {
int n; // transform size (0 = empty slot)
int* rev; // bit-reversal permutation, length n
float complex* twiddle; // forward twiddles W_n^k, length n/2
} FFTPlan;
static FFTPlan g_plans[FFT_MAX_PLANS];
static FFTPlan* GetPlan(int n)
{
for (int i = 0; i < FFT_MAX_PLANS; i++)
if (g_plans[i].n == n) return &g_plans[i];
// Find a free slot. (Sizes are few and long-lived, so no eviction needed.)
FFTPlan* slot = NULL;
for (int i = 0; i < FFT_MAX_PLANS; i++)
if (g_plans[i].n == 0) { slot = &g_plans[i]; break; }
if (!slot) slot = &g_plans[0]; // pathological fallback: reuse slot 0
free(slot->rev);
free(slot->twiddle);
int bits = 0, temp = n;
while (temp > 1) { bits++; temp >>= 1; }
slot->rev = (int*)malloc((size_t)n * sizeof(int));
for (int i = 0; i < n; i++) {
int j = 0, k = i;
for (int b = 0; b < bits; b++) { j = (j << 1) | (k & 1); k >>= 1; }
output[j] = input[i];
slot->rev[i] = j;
}
// Forward twiddles W_n^k = exp(-2*pi*i*k/n) for k in [0, n/2).
slot->twiddle = (float complex*)malloc((size_t)(n / 2) * sizeof(float complex));
for (int k = 0; k < n / 2; k++) {
float ang = -2.0f * (float)M_PI * k / n;
slot->twiddle[k] = cosf(ang) + sinf(ang) * I;
}
slot->n = n;
return slot;
}
void FFT(float complex* input, float complex* output, int n, bool inverse)
{
if (n <= 1) { output[0] = input[0]; return; }
BitReverseCopy(input, output, n);
for (int stage = 1; stage < n; stage *= 2) {
int step = stage * 2;
float angleStep = (inverse ? 2.0f : -2.0f) * (float)M_PI / step;
for (int k = 0; k < stage; k++) {
float complex twiddle = cexpf(I * angleStep * k);
FFTPlan* plan = GetPlan(n);
// Bit-reversal permutation via the precomputed table.
for (int i = 0; i < n; i++) output[plan->rev[i]] = input[i];
// Butterflies. The cached twiddle table holds W_n^k at full resolution;
// for a stage of length `step`, the needed root W_step^k == W_n^(k*n/step),
// so we index the table with a per-stage stride. For the inverse transform
// we conjugate the same factor (W_n^-k), avoiding a second table.
for (int step = 2; step <= n; step *= 2) {
int half = step / 2;
int stride = n / step; // table index multiplier for this stage
for (int k = 0; k < half; k++) {
float complex w = plan->twiddle[k * stride];
if (inverse) w = conjf(w);
for (int i = k; i < n; i += step) {
int j = i + stage;
float complex t = output[j] * twiddle;
int j = i + half;
float complex t = output[j] * w;
output[j] = output[i] - t;
output[i] = output[i] + t;
}
}
}
if (inverse) for (int i = 0; i < n; i++) output[i] /= n;
}