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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user