Created
April 4, 2021 08:55
-
-
Save FeepingCreature/f2ba56ed4945c14ea8913f25d1dcc352 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module fflame; | |
macro import cx.macros.listcomprehension; | |
import helpers; | |
import sdl; | |
extern(C) float logf(float); | |
extern(C) float fabsf(float); | |
extern(C) float expf(float); | |
extern(C) float sinf(float); | |
extern(C) float cosf(float); | |
extern(C) float tanf(float); | |
// extern(C) float atan2f(float y, float x); | |
extern(C) float sqrtf(float); | |
extern(C) float powf(float, float); | |
extern(C) void print(string); | |
extern(C) string cxruntime_itoa(int i); | |
extern(C) string cxruntime_ftoa(float f); | |
extern(C) void srand(int seed); | |
extern(C) int rand(); | |
extern(C) char* toStringz(string); | |
extern(C) void assert(bool); | |
extern(C) long atol(char* ptr); | |
string itoa(int i) { return cxruntime_itoa(i); } | |
string ftoa(float f) { return cxruntime_ftoa(f); } | |
float sin(float f) { return sinf(f); } | |
float cos(float f) { return cosf(f); } | |
float tan(float f) { return tanf(f); } | |
// float atan2(float y, float x) { return atan2f(y, x); } | |
// see http://dspguru.com/dsp/tricks/fixed-point-atan2-with-self-normalization/ thanks SO | |
float atan2(float y, float x) { | |
float coeff_1 = 3.1415926537/4; | |
float coeff_2 = 3*coeff_1; | |
float abs_y = fabsf(y)+0.000001; | |
float angle; | |
if (x >= 0) { | |
float r = (x - abs_y) / (x + abs_y); | |
angle = coeff_1 - coeff_1 * r; | |
} else { | |
float r = (x + abs_y) / (abs_y - x); | |
angle = coeff_2 - coeff_1 * r; | |
} | |
if (y < 0) return -angle; | |
else return angle; | |
} | |
float sqrt(float f) { return sqrtf(f); } | |
float max(float a, float b) { if (a > b) return a; return b; } | |
float min(float a, float b) { if (a < b) return a; return b; } | |
struct Point | |
{ | |
float x; float y; | |
void weightAdd(Point p2, float weight) { | |
this.x += p2.x * weight; | |
this.y += p2.y * weight; | |
} | |
} | |
AffineTransform makeAffineTransform(float a, float b, float c, float d, float e, float f, int symmetry) { | |
AffineTransform res; | |
res.a = a; res.b = b; res.c = c; res.d = d; res.e = e; res.f = f; | |
res.symmetry = symmetry; | |
if (symmetry) { | |
res.syms = new float[](symmetry); | |
res.symc = new float[](symmetry); | |
for (int i <- 0 .. symmetry) { | |
float tau = 2 * 3.1415926538; | |
float angle = i * tau / symmetry; | |
res.syms[i] = sin(angle); | |
res.symc[i] = cos(angle); | |
} | |
} | |
return res; | |
} | |
float randvary(float v, float f) { | |
float interp; | |
if (rand() % 4 != 0) { | |
return v + sin(f * randf) * randf; | |
} | |
return v * (1 - f) + randnf * f; | |
} | |
struct AffineTransform | |
{ | |
float a; float b; float c; | |
float d; float e; float f; | |
int symmetry; | |
float[] syms; float[] symc; | |
int ii; | |
Point apply(Point p) { | |
p = Point(a * p.x + b * p.y + c, d * p.x + e * p.y + f); | |
if (symmetry) { | |
int i = ii % symmetry; | |
ii += 1; | |
p = Point(p.x * symc[i] - p.y * syms[i], p.x * syms[i] + p.y * symc[i]); | |
} | |
return p; | |
} | |
void vary(float f) { | |
a = a.randvary(f); | |
b = b.randvary(f); | |
c = c.randvary(f); | |
d = d.randvary(f); | |
e = e.randvary(f); | |
this.f = this.f.randvary(f); | |
} | |
} | |
struct Function | |
{ | |
AffineTransform affine; | |
int variation; | |
Point apply(Point p) { | |
p = affine.apply(p); | |
float r2() { return p.x * p.x + p.y * p.y; } | |
float r() { return sqrt(r2); } | |
float angle() { return atan2(p.y, p.x); } | |
float pi = 3.1415926537; | |
int variation = variation; | |
if (variation == 0) { | |
return p; | |
} else if (variation == 1) { | |
return Point(sin(p.x), sin(p.y)); | |
} else if (variation == 2) { | |
float f = 1/r2; | |
return Point(f * p.x, f * p.y); | |
} else if (variation == 3) { | |
float sr = sin(r2), cr = cos(r2); | |
return Point(p.x * sr - p.y * cr, p.x * cr + p.y * sr); | |
} else if (variation == 4) { | |
float dr = 1 / sqrt(r2); | |
return Point(dr * (p.x - p.y) * (p.x + p.y), dr * 2 * p.x * p.y); | |
} else if (variation == 5) { | |
return Point(angle / pi, r - 1); | |
} else if (variation == 6) { | |
float a = angle, r = r; | |
return Point(r * sin(a + r), r * cos(a - r)); | |
} else if (variation == 7) { | |
float a = angle, r = r; | |
return Point(r * sin(a * r), -r * cos(a * r)); | |
} else if (variation == 8) { | |
float f = angle / pi; | |
return Point(f * sin(pi * r), f * cos(pi * r)); | |
} else if (variation == 9) { | |
float a = angle, r = r, dr = 1 / r; | |
return Point(dr * (cos(a) + sin(r)), dr * (sin(a) - cos(r))); | |
} else if (variation == 10) { | |
float a = angle, r = r; | |
return Point(sin(a) / r, r * cos(a)); | |
} else if (variation == 11) { | |
float a = angle, r = r; | |
return Point(sin(a) * cos(r), cos(a) * sin(r)); | |
} else if (variation == 12) { | |
float a = angle, r = r; | |
float p0 = sin(a + r), p1 = cos(a - r); | |
float p03 = p0 * p0 * p0, p13 = p1 * p1 * p1; | |
return Point(r * (p03 + p13), r * (p03 - p13)); | |
} else if (variation == 13) { | |
float sqr = sqrt(r), a = angle; | |
// TODO what is this | |
float omik = 0; | |
return Point(sqr * cos(a / 2 + omik), sqr * sin(angle / 2 + omik)); | |
} else if (variation == 14) { | |
if (p.x > 0) { | |
if (p.y > 0) return p; | |
else return Point(p.x, p.y / 2); | |
} else { | |
if (p.y > 0) return Point(2 * p.x, p.y); | |
else return Point(2 * p.x, p.y / 2); | |
} | |
} | |
} | |
void vary(float f) { | |
affine.vary(f); | |
} | |
} | |
struct MetaEntry | |
{ | |
float weight; | |
Function f; | |
void vary(float f) { | |
weight = weight.randvary(f); | |
this.f.vary(f); | |
} | |
} | |
struct Metafunction | |
{ | |
MetaEntry[] entries; | |
AffineTransform affine; | |
Color color; | |
Point apply(Point p) { | |
Point result; | |
for (int i <- 0 .. entries.length) { | |
result.weightAdd(entries[i].f.apply(p), entries[i].weight); | |
} | |
return affine.apply(result); | |
} | |
void vary(float f) { | |
[entry.vary(f) for entry in entries]; | |
affine.vary(f); | |
} | |
} | |
class ChaosGame | |
{ | |
Metafunction[] functions; | |
Metafunction finalFunction; | |
int rstate; | |
this() { rstate = rand; } | |
void apply(RangeCache cache, long steps) { | |
Point p = Point(randf, randf); | |
Color color = Color(0, 0, 0); | |
for (long l <- 0..steps) { | |
// stolen from SO | |
rstate = (214013*rstate+2531011); | |
int randval = (rstate / 0x10000)&0x7FFF; | |
int index = cast(int)(randval % functions.length); | |
auto fun = functions[index]; | |
p = functions[index].apply(p); | |
color = color.mix(fun.color, 0.5); | |
p = finalFunction.apply(p); | |
color = color.mix(finalFunction.color, 0.5); | |
cache.pset(p, color); | |
} | |
} | |
void vary(float f) { | |
[fun.vary(f) for fun in functions]; | |
finalFunction.vary(f); | |
} | |
} | |
struct Color | |
{ | |
float r; | |
float g; | |
float b; | |
Color mix(Color other, float factor) | |
{ | |
return Color( | |
r * (1 - factor) + other.r * factor, | |
g * (1 - factor) + other.g * factor, | |
b * (1 - factor) + other.b * factor); | |
} | |
} | |
struct WeightedColor | |
{ | |
float r; | |
float g; | |
float b; | |
float i; | |
} | |
class Buffer | |
{ | |
int width; int height; | |
long steps; | |
WeightedColor[] back; | |
this(this.width, this.height) { | |
this.back = new WeightedColor[](width * height); | |
for (int i <- 0 .. this.back.length) | |
this.back[i] = WeightedColor(0, 0, 0, 0); | |
} | |
void writePixel(int offset, Color color) { | |
auto ptr = &back[offset]; | |
ptr.r += color.r; | |
ptr.g += color.g; | |
ptr.b += color.b; | |
ptr.i += 1; | |
} | |
void paint(void* surface) { | |
// float adjf = 1 / powf(cast(int) (steps / (width * height)), 0.25); | |
float adjf = 2.0 / cast(int) ((steps + width * height) / (width * height)); | |
for (int y = 0; y < height; y += 1) { | |
for (int x = 0; x < width; x += 1) { | |
auto color = this.back[y * width + x]; | |
float i = color.i * adjf; | |
float i = logf(i + 1.01) / (i + 0.01); | |
// float r = max(1 - color.r * i, 0); | |
// float g = max(1 - color.g * i, 0); | |
// float b = max(1 - color.b * i, 0); | |
float r = min(color.r * i * adjf, 1); | |
float g = min(color.g * i * adjf, 1); | |
float b = min(color.b * i * adjf, 1); | |
int rgb = | |
(cast(int)(r * 0xff) & 0xff * 0x10000) | |
+ (cast(int)(g * 0xff) & 0xff * 0x100) | |
+ (cast(int)(b * 0xff) & 0xff * 0x1); | |
// TODO direct write | |
auto rect = SDL_Rect(x, y, 1, 1); | |
SDL_FillRect(surface, &rect, rgb); | |
} | |
} | |
} | |
} | |
float randf() { | |
return rand * 1.0 / 2147483647; | |
} | |
float randnf() { | |
return randf * 2.0 - 1.0; | |
} | |
AffineTransform randAffine() { | |
int symmetry = 0; | |
if (rand() % 10 == 0) { | |
symmetry = rand() % 5 + 2; | |
} | |
return makeAffineTransform(randnf, randnf, randnf, randnf, randnf, randnf, symmetry); | |
} | |
Function randFun() { | |
return Function(randAffine, rand % 15); | |
} | |
struct pthread_mutex_t | |
{ | |
// __SIZEOF_PTHREAD_MUTEX_T is like 40 max | |
long a; long b; long c; long d; long e; | |
} | |
extern(C) int pthread_mutex_init(pthread_mutex_t* mutex, void* attr); | |
extern(C) int pthread_mutex_destroy(pthread_mutex_t* mutex); | |
extern(C) int pthread_mutex_lock(pthread_mutex_t* mutex); | |
extern(C) int pthread_mutex_unlock(pthread_mutex_t* mutex); | |
class Mutex | |
{ | |
pthread_mutex_t mutex; | |
this() { pthread_mutex_init(&mutex, null); } | |
void lock() { pthread_mutex_lock(&mutex); } | |
void unlock() { pthread_mutex_unlock(&mutex); } | |
} | |
struct pthread_cond_t | |
{ | |
// __SIZEOF_PTHREAD_MUTEX_T is like 48? | |
long a; long b; long c; long d; long e; long f; | |
} | |
extern(C) int pthread_cond_init(pthread_cond_t*, void* attr); | |
extern(C) int pthread_cond_destroy(pthread_cond_t*); | |
extern(C) int pthread_cond_wait(pthread_cond_t*, pthread_mutex_t*); | |
extern(C) int pthread_cond_broadcast(pthread_cond_t*); | |
extern(C) int pthread_cond_signal(pthread_cond_t*); | |
class CondVar | |
{ | |
pthread_cond_t cond; | |
this() { pthread_cond_init(&cond, null); } | |
void wait(Mutex mutex) { pthread_cond_wait(&cond, &mutex.mutex); } | |
void signal() { pthread_cond_signal(&cond); } | |
void broadcast() { pthread_cond_broadcast(&cond); } | |
} | |
template Waitable(T) | |
{ | |
class Waitable | |
{ | |
Mutex mutex; | |
CondVar signal; | |
T value; | |
this(this.value) { | |
this.mutex = new Mutex; | |
this.signal = new CondVar; | |
} | |
void set(T value) { | |
mutex.lock; | |
this.value = value; | |
signal.broadcast; | |
mutex.unlock; | |
} | |
void update(T delegate(T) action) { | |
mutex.lock; | |
this.value = action(this.value); | |
signal.broadcast; | |
mutex.unlock; | |
} | |
// TODO move into waitFor | |
T id(T value) { return value; } | |
void waitFor(bool delegate(T) condition) { | |
waitReact(condition, &id); | |
} | |
void waitReact(bool delegate(T) condition, T delegate(T) react) { | |
mutex.lock; | |
while (true) { | |
if (condition(this.value)) { | |
this.value = react(this.value); | |
signal.broadcast; | |
mutex.unlock; | |
return; | |
} | |
signal.wait(mutex); | |
} | |
} | |
} | |
} | |
class Semaphore | |
{ | |
Waitable!int waitable; | |
this(int i) { this.waitable = new Waitable!int(i); } | |
void acquire() { | |
bool greaterZero(int i) { return i > 0; } | |
int decrement(int i) { return i - 1; } | |
waitable.waitReact(&greaterZero, &decrement); | |
} | |
void release() { | |
int increment(int i) { return i + 1; } | |
waitable.update(&increment); | |
} | |
} | |
abstract class Task | |
{ | |
abstract void run() { assert(false); } | |
} | |
struct pthread_t | |
{ | |
// __SIZEOF_PTHREAD_T is 8, I think? | |
long a; | |
} | |
void thread_runner(void* arg) { | |
auto dg = *(cast(void delegate()*) arg); | |
dg(); | |
} | |
extern(C) int pthread_create(pthread_t* thread, void* attr, void function(void*) start_routine, void* arg); | |
class Thread | |
{ | |
pthread_t thr; | |
void delegate() run; | |
this(this.run) { } | |
void start() { | |
pthread_create(&thr, null, &thread_runner, &run); | |
} | |
} | |
class ThreadPool | |
{ | |
Mutex mutex; | |
Task[] tasks; | |
Thread[] threads; | |
int queuedTasks; | |
Semaphore waitingTasks; | |
Semaphore doneTasks; | |
this(int i) { | |
this.mutex = new Mutex; | |
this.waitingTasks = new Semaphore(0); | |
this.doneTasks = new Semaphore(0); | |
for (int j <- 0..i) { | |
auto thread = new Thread(&run); | |
thread.start; | |
threads ~= thread; | |
} | |
} | |
void run() { | |
while (true) { | |
// TODO why is this broken, probably double calls getTask | |
// getTask.run; | |
auto task = getTask; | |
task.run; | |
this.doneTasks.release; | |
} | |
} | |
void waitComplete(void delegate(float) progress) { | |
this.mutex.lock; | |
int tasks = this.queuedTasks; | |
this.queuedTasks = 0; | |
this.mutex.unlock; | |
for (int i <- 0..tasks) { | |
this.doneTasks.acquire; | |
progress((i + 1) * 1.0 / tasks); | |
} | |
} | |
void addTask(Task task) { | |
mutex.lock; | |
tasks ~= task; | |
this.queuedTasks += 1; | |
mutex.unlock; | |
this.waitingTasks.release; | |
} | |
Task getTask() { | |
this.waitingTasks.acquire; | |
mutex.lock; | |
auto ret = tasks[$ - 1]; | |
tasks = tasks[0 .. $ - 1]; | |
mutex.unlock; | |
return ret; | |
} | |
} | |
Metafunction randMetafun() { | |
MetaEntry[] entries; | |
float weightsum; | |
for (int i <- 0 .. rand() % 3 + 2) { | |
float weight = randf; | |
entries ~= MetaEntry(weight, randFun); | |
weightsum += weight; | |
} | |
for (int i <- 0 .. entries.length) { | |
entries[i].weight /= weightsum; | |
} | |
int r = rand(); | |
return Metafunction(entries, randAffine, Color(randf, randf, randf)); | |
} | |
class RenderGame : Task | |
{ | |
ChaosGame game; | |
RangeCache cache; | |
Buffer buffer; | |
long steps; | |
this(this.game, this.buffer, this.steps) { | |
this.cache = new RangeCache(buffer); | |
} | |
override void run() { | |
game.apply(cache, steps); | |
cache.flush; | |
} | |
} | |
struct CacheEntry | |
{ | |
Color color; | |
int offset; | |
} | |
class RangeCache | |
{ | |
Buffer buffer; | |
bool cache; | |
int width; int height; | |
int sections; | |
int sectionLength; | |
int sectionCovers; | |
CacheEntry[] ranges; | |
int[] offsets; | |
float ratio; | |
float hwidth; float hheight; | |
long steps; | |
this(this.buffer) { | |
this.width = buffer.width; | |
this.height = buffer.height; | |
this.ratio = height * 1.0 / width; | |
this.hwidth = width / 2; | |
this.hheight = height / 2; | |
// TODO how to balance | |
if (true || width * height < 4194304) { | |
return; | |
} | |
this.cache = true; | |
this.sectionCovers = 16384; | |
print("w " ~ itoa(width) ~ " h " ~ itoa(height) ~ " covers 16384?"); | |
assert(width * height % sectionCovers == 0); | |
this.sections = width * height / sectionCovers; | |
this.offsets = new int[](sections); | |
this.ranges = new CacheEntry[](this.sections * 32); | |
// this.sectionLength = ranges.length / sections; | |
// print("range " ~ itoa(cast(int) this.ranges.length) ~ " over " ~ itoa(this.sections)); | |
assert(this.ranges.length % this.sections == 0); | |
this.sectionLength = cast(int) (this.ranges.length / this.sections); | |
} | |
void pset(Point p, Color color) { | |
this.steps += 1; | |
int x = cast(int)((p.x * ratio + 1) * hwidth), y = cast(int)((p.y + 1) * hheight); | |
if (x < 0 || x >= width || y < 0 || y >= height) return; | |
int offset = y * width + x; | |
// Does this actually do anything ... ? | |
if (!this.cache) { | |
this.buffer.writePixel(offset, color); | |
return; | |
} | |
int section = offset / 16384; // TODO right shift | |
if (offsets[section] == sectionLength) | |
{ | |
flushSection(section); | |
} | |
ranges[section * sectionLength + offsets[section]] = CacheEntry(color, offset); | |
offsets[section] += 1; | |
} | |
void flushSection(int section) { | |
for (int i = 0; i < offsets[section]; i += 1) { | |
auto entry = ranges[section * sectionLength + i]; | |
buffer.writePixel(entry.offset, entry.color); | |
} | |
offsets[section] = 0; | |
} | |
void flush() { | |
buffer.steps += this.steps; | |
for (int i = 0; i < this.sections; i += 1) { | |
flushSection(i); | |
} | |
} | |
} | |
extern(C) void setbuf(void* stream, char* buf); | |
void main(string[] args) { | |
int seed = 1; | |
long iters = 1000; | |
int width = 640, height = 480; | |
string output = "out.png"; | |
bool htmlProgress; | |
float varyFactor = 0; | |
for (int i = 0; i < args.length; i += 1) { | |
auto arg = args[i]; | |
if (arg == "-h" || arg == "--help") { | |
print("Usage: ./fflame | |
-e, --seed num : main rng seed. Specifies the image. | |
-i, --iters n : number of iterations before exiting | |
-s, --size wxh : target image size | |
-o, --output file.png : png file to save to | |
-x, --html : print progress as html/js progress bar | |
-v, --vary f : randomize and interpolate (for animations) | |
-h, --help : this screen"); | |
return; | |
} | |
if (arg == "-e" || arg == "--seed") { | |
seed = args[i + 1].atoi; | |
i += 1; | |
continue; | |
} | |
if (arg == "-i" || arg == "--iters") { | |
iters = args[i + 1].toStringz.atol; | |
i += 1; | |
continue; | |
} | |
if (arg == "-s" || arg == "--size") { | |
string size = args[i + 1]; | |
auto wh = size.split("x"); | |
width = wh[0].atoi; | |
height = wh[1].atoi; | |
i += 1; | |
continue; | |
} | |
if (arg == "-x" || arg == "--html") { | |
htmlProgress = true; | |
continue; | |
} | |
if (arg == "-v" || arg == "--vary") { | |
varyFactor = args[i + 1].atof; | |
i += 1; | |
continue; | |
} | |
if (arg == "-o" || arg == "--output") { | |
output = args[i + 1]; | |
i += 1; | |
continue; | |
} | |
print("Unknown argument " ~ arg); | |
assert(false); | |
} | |
int subtasks = 100; | |
if (width < 400 && height < 400) subtasks = 4; | |
auto pool = new ThreadPool(8); | |
srand(seed); | |
auto game = new ChaosGame; | |
for (int i <- 0 .. 1 + rand() % 5) | |
game.functions ~= randMetafun; | |
game.finalFunction = randMetafun; | |
game.vary(varyFactor); | |
Buffer buffer = new Buffer(width, height); | |
for (int j <- 0 .. subtasks) { | |
auto task = new RenderGame(game, buffer, iters / subtasks); | |
pool.addTask(task); | |
} | |
void printProgressBar(float progress) { | |
if (!htmlProgress) return; | |
int percent = cast(int) (progress * 100); | |
print("<script type=\"text/javascript\">$(\"#progress\").html(\"" | |
~ itoa(percent) | |
~ "%\");</script>"); | |
} | |
setbuf(cxruntime_stdout(), null); | |
pool.waitComplete(&printProgressBar); | |
auto surface = SDL_CreateRGBSurface(0, width, height, 32, 0, 0, 0, 0); | |
buffer.paint(surface); | |
void* out_; | |
if (output == "-") { | |
out_ = SDL_RWFromFP(cxruntime_stdout(), false); | |
} else { | |
out_ = SDL_RWFromFile(toStringz(output), toStringz("wb")); | |
} | |
IMG_SavePNG_RW(surface, out_); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment