After reading quite a bit about fixed-point arithmetic I think I can say I've understood the basics, unfortunately I don't know yet how to convert routines that use sin/cos/sqrt or any other fp function.
Consider this simple mcve:
#include <math.h>
#include <stdio.h>
#include <ctime>
#include <fstream>
#include <iostream>
typedef char S8;
typedef short S16;
typedef int S32;
typedef unsigned char U8;
typedef unsigned short U16;
typedef unsigned int U32;
typedef float F32;
typedef double F64;
// -------- Fixed point helpers QM.N(32bits) --------
typedef S32 FP32;
#define LUT_SIZE_BITS 9 // 0xffffffff>>(32-9)=511; 32-23=9; 2^9=512
#define LUT_SIZE 512
#define FRACT_BITS 28 // Number fractional bits
#define M (1 << FRACT_BITS) // Scaling factor
inline F32 Q2F(FP32 X) { return ((F32)X / (F32)(M)); }
inline FP32 F2Q(F32 X) { return (FP32)(X * (M)); }
const F32 PI = 3.141592653589793f;
const F32 pi = 3.141592653589793f;
const U32 WIDTH = 256;
const U32 HEIGHT = 256;
FP32 cos_table[LUT_SIZE];
FP32 sin_table[LUT_SIZE];
void init_luts() {
const F32 deg_to_rad = PI / 180.f;
const F32 sample_to_deg = 1 / LUT_SIZE * 360.f;
for (S32 i = 0; i < LUT_SIZE; i++) {
F32 rad = ((F32)i * sample_to_deg) * deg_to_rad;
F32 c = cos(rad);
F32 s = sin(rad);
cos_table[i] = F2Q(c);
sin_table[i] = F2Q(s);
}
}
// -------- Image processing --------
U8 clamp(F32 valor) { return valor > 255 ? 255 : (valor < 0 ? 0 : (U8)valor); }
struct Pbits {
U32 width;
U32 height;
U8 *data;
Pbits(U32 width, U32 height, U8 *data) {
this->width = width;
this->height = height;
this->data = data;
}
Pbits(Pbits *src) {
this->width = src->width;
this->height = src->height;
this->data = new U8[src->width * src->height * 3];
memcpy(this->data, src->data, width * height * 3);
}
~Pbits() { delete this->data; }
void to_bgr() {
U8 r, g, b;
for (S32 y = 0; y < height; y++) {
for (S32 x = 0; x < width; x++) {
get_pixel(y, x, r, g, b);
set_pixel(y, x, b, g, r);
}
}
}
void get_pixel(U32 y, U32 x, U8 &r, U8 &g, U8 &b) {
U32 offset = (y * height * 3) + (x * 3);
r = this->data[offset + 0];
g = this->data[offset + 1];
b = this->data[offset + 2];
}
void set_pixel(U32 y, U32 x, U8 c1, U8 c2, U8 c3) {
U32 offset = (y * height * 3) + (x * 3);
data[offset] = c1;
data[offset + 1] = c2;
data[offset + 2] = c3;
}
};
void fx1_plasma(Pbits *dst, F32 t, F32 k1, F32 k2, F32 k3, F32 k4, F32 k5, F32 k6) {
U32 height = dst->height;
U32 width = dst->width;
for (U32 y = 0; y < height; y++) {
F32 uv_y = (F32)y / height;
for (U32 x = 0; x < width; x++) {
F32 uv_x = (F32)x / width;
F32 v1 = sin(uv_x * k1 + t);
F32 v2 = sin(k1 * (uv_x * sin(t) + uv_y * cos(t / k2)) + t);
F32 cx = uv_x + sin(t / k1) * k1;
F32 cy = uv_y + sin(t / k2) * k1;
F32 v3 = sin(sqrt(k3 * (cx * cx + cy * cy)) + t);
F32 vf = v1 + v2 + v3;
U8 r = (U8)clamp(255 * cos(vf * pi));
U8 g = (U8)clamp(255 * sin(vf * pi + k4 * pi / k2));
U8 b = (U8)clamp(255 * cos(vf * pi + k5 * pi / k2));
dst->set_pixel(y, x, r, g, b);
}
}
}
// -------- Image helpers --------
inline void _write_s32(U8 *dst, S32 offset, S32 v) {
dst[offset] = (U8)(v);
dst[offset + 1] = (U8)(v >> 8);
dst[offset + 2] = (U8)(v >> 16);
dst[offset + 3] = (U8)(v >> 24);
}
void write_bmp(Pbits *src, S8 *filename) {
Pbits *dst = new Pbits(src);
dst->to_bgr();
S32 w = dst->width;
S32 h = dst->height;
U8 *img = dst->data;
S32 filesize = 54 + 3 * w * h;
U8 bmpfileheader[14] = {'B', 'M', 0, 0, 0, 0, 0, 0, 0, 0, 54, 0, 0, 0};
U8 bmpinfoheader[40] = {40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 24, 0};
U8 bmppad[3] = {0, 0, 0};
_write_s32(bmpfileheader, 2, filesize);
_write_s32(bmpinfoheader, 4, w);
_write_s32(bmpinfoheader, 8, h);
FILE *f = fopen(filename, "wb");
fwrite(bmpfileheader, 1, 14, f);
fwrite(bmpinfoheader, 1, 40, f);
for (S32 i = 0; i < h; i++) {
fwrite(img + (w * (h - i - 1) * 3), 3, w, f);
fwrite(bmppad, 1, (4 - (w * 3) % 4) % 4, f);
}
delete dst;
}
void write_ppm(Pbits *dst, S8 *filename) {
std::ofstream file(filename, std::ofstream::trunc);
if (!file.is_open()) {
std::cout << "yep! file is not open" << std::endl;
}
file << "P3\n" << dst->width << " " << dst->height << "\n255\n";
U8 r, g, b, a;
for (U32 y = 0; y < dst->height; y++) {
for (U32 x = 0; x < dst->width; x++) {
dst->get_pixel(y, x, r, g, b);
file << (S32)r << " " << (S32)g << " " << (S32)b << "\n";
}
}
}
S32 main() {
Pbits *dst = new Pbits(WIDTH, HEIGHT, new U8[WIDTH * HEIGHT * 3]);
init_luts();
clock_t begin = clock();
fx1_plasma(dst, 0, 8, 36, 54, 51, 48, 4);
clock_t end = clock();
double elapsed_secs = double(end - begin) / CLOCKS_PER_SEC;
std::cout << "Generated plasma in " << elapsed_secs << "s" << std::endl;
write_ppm(dst, "plasma.ppm");
write_bmp(dst, "plasma.bmp");
delete dst;
}
This code will generate this image:
QUESTION: How would you convert this floating point algorithm into a fast fixed-point one? Right now the basics of the floating point arithmetic is +/- clear to me, as in:
fa,fb=floating point values; a,b=fixed_point ones; M=scaling factor
fa = a*M
fb = b*M
fa+fb = (a+b)*M
fa-fb = (a-b)*M
fa*fb = (a*b)*M^2
fa/fb = (a/b)
but how to use sin/cos/sqrt et al in fixed-point is still eluding me. I've found this related thread but I still don't understand how to use the trigonometric luts with random fp values.