summaryrefslogtreecommitdiff
path: root/src/swgfx.c
diff options
context:
space:
mode:
author3gg <3gg@shellblade.net>2025-11-16 18:23:37 -0800
committer3gg <3gg@shellblade.net>2025-11-16 18:23:37 -0800
commit403730174aeaadcf7a8aad842bc5319050411ef9 (patch)
treeca19383484d51593666fc8add3da2dd00098ef42 /src/swgfx.c
Initial commitHEADmain
Diffstat (limited to 'src/swgfx.c')
-rw-r--r--src/swgfx.c390
1 files changed, 390 insertions, 0 deletions
diff --git a/src/swgfx.c b/src/swgfx.c
new file mode 100644
index 0000000..772a691
--- /dev/null
+++ b/src/swgfx.c
@@ -0,0 +1,390 @@
1/*
2Matrices:
3 - Column-major math convention.
4 - Column-major memory storage.
5
6Coordinate systems:
7 - Right-handed.
8 - NDC in [-1, +1].
9*/
10#include <swgfx.h>
11
12#include <assert.h>
13#include <math.h> // sqrt
14#include <stdint.h>
15#include <stdlib.h>
16#include <string.h>
17
18static const sgVec3 Up3 = (sgVec3){0,1,0};
19
20typedef struct sgViewport_t { int x0, y0, width, height; } sgViewport_t;
21typedef struct sgTri2 { sgVec2 p0, p1, p2; } sgTri2;
22typedef struct sgAABB2 { sgVec2 pmin, pmax; } sgAABB2;
23
24// Column-major math, column-major storage.
25typedef struct sgMat4 {
26 R val[4][4]; // (col, row)
27} sgMat4;
28
29typedef struct swgfx {
30 sgVec2i dims; // Colour buffer dimensions.
31 sgPixel* colour; // Colour buffer.
32 sgViewport_t viewport;
33 sgMat4 view; // View matrix.
34 sgMat4 proj; // Projection matrix.
35} swgfx;
36
37static inline sgVec3 neg3(sgVec3 v) { return (sgVec3){-v.x, -v.y, -v.z}; }
38
39static inline sgVec3 sub3(sgVec3 a, sgVec3 b) {
40 return (sgVec3){a.x - b.x, a.y - b.y, a.z - b.z};
41}
42
43static inline sgVec3 cross3(sgVec3 a, sgVec3 b) {
44 return (sgVec3) {
45 a.y * b.z - a.z * b.y,
46 a.z * b.x - a.x * b.z,
47 a.x * b.y - a.y * b.x};
48}
49
50static inline R normsq3(sgVec3 v) { return v.x * v.x + v.y * v.y + v.z * v.z; }
51
52static inline R norm3(sgVec3 v) { return sqrt(normsq3(v)); }
53
54static inline sgVec3 normalize3(sgVec3 v) {
55 const R n = norm3(v);
56 assert(n > 0);
57 return (sgVec3){v.x / n, v.y / n, v.z / n};
58}
59
60static inline sgMat4 Mat4(
61 R m00, R m01, R m02, R m03, // v0.x v1.x v2.x v3.x
62 R m10, R m11, R m12, R m13, // v0.y v1.y v2.y v3.y
63 R m20, R m21, R m22, R m23, // v0.z v1.z v2.z v3.z
64 R m30, R m31, R m32, R m33) { // v0.w v1.w v2.w v3.w
65 return (sgMat4) {
66 .val = {{m00, m10, m20, m30}, // col 0
67 {m01, m11, m21, m31}, // col 1
68 {m02, m12, m22, m32}, // col 2
69 {m03, m13, m23, m33}}}; // col 3
70}
71
72static inline sgMat4 Mat4FromVec3(sgVec3 right, sgVec3 up, sgVec3 forward, sgVec3 position) {
73 return Mat4(
74 right.x, up.x, forward.x, position.x,
75 right.y, up.y, forward.y, position.y,
76 right.z, up.z, forward.z, position.z,
77 0, 0, 0, 1);
78}
79
80static inline R Mat4At(sgMat4 m, int row, int col) { return m.val[col][row]; }
81
82static inline sgMat4 Mat4Mul(sgMat4 A, sgMat4 B) {
83 R m00 = Mat4At(A, 0, 0) * Mat4At(B, 0, 0) +
84 Mat4At(A, 0, 1) * Mat4At(B, 1, 0) +
85 Mat4At(A, 0, 2) * Mat4At(B, 2, 0) +
86 Mat4At(A, 0, 3) * Mat4At(B, 3, 0);
87 R m01 = Mat4At(A, 0, 0) * Mat4At(B, 0, 1) +
88 Mat4At(A, 0, 1) * Mat4At(B, 1, 1) +
89 Mat4At(A, 0, 2) * Mat4At(B, 2, 1) +
90 Mat4At(A, 0, 3) * Mat4At(B, 3, 1);
91 R m02 = Mat4At(A, 0, 0) * Mat4At(B, 0, 2) +
92 Mat4At(A, 0, 1) * Mat4At(B, 1, 2) +
93 Mat4At(A, 0, 2) * Mat4At(B, 2, 2) +
94 Mat4At(A, 0, 3) * Mat4At(B, 3, 2);
95 R m03 = Mat4At(A, 0, 0) * Mat4At(B, 0, 3) +
96 Mat4At(A, 0, 1) * Mat4At(B, 1, 3) +
97 Mat4At(A, 0, 2) * Mat4At(B, 2, 3) +
98 Mat4At(A, 0, 3) * Mat4At(B, 3, 3);
99
100 R m10 = Mat4At(A, 1, 0) * Mat4At(B, 0, 0) +
101 Mat4At(A, 1, 1) * Mat4At(B, 1, 0) +
102 Mat4At(A, 1, 2) * Mat4At(B, 2, 0) +
103 Mat4At(A, 1, 3) * Mat4At(B, 3, 0);
104 R m11 = Mat4At(A, 1, 0) * Mat4At(B, 0, 1) +
105 Mat4At(A, 1, 1) * Mat4At(B, 1, 1) +
106 Mat4At(A, 1, 2) * Mat4At(B, 2, 1) +
107 Mat4At(A, 1, 3) * Mat4At(B, 3, 1);
108 R m12 = Mat4At(A, 1, 0) * Mat4At(B, 0, 2) +
109 Mat4At(A, 1, 1) * Mat4At(B, 1, 2) +
110 Mat4At(A, 1, 2) * Mat4At(B, 2, 2) +
111 Mat4At(A, 1, 3) * Mat4At(B, 3, 2);
112 R m13 = Mat4At(A, 1, 0) * Mat4At(B, 0, 3) +
113 Mat4At(A, 1, 1) * Mat4At(B, 1, 3) +
114 Mat4At(A, 1, 2) * Mat4At(B, 2, 3) +
115 Mat4At(A, 1, 3) * Mat4At(B, 3, 3);
116
117 R m20 = Mat4At(A, 2, 0) * Mat4At(B, 0, 0) +
118 Mat4At(A, 2, 1) * Mat4At(B, 1, 0) +
119 Mat4At(A, 2, 2) * Mat4At(B, 2, 0) +
120 Mat4At(A, 2, 3) * Mat4At(B, 3, 0);
121 R m21 = Mat4At(A, 2, 0) * Mat4At(B, 0, 1) +
122 Mat4At(A, 2, 1) * Mat4At(B, 1, 1) +
123 Mat4At(A, 2, 2) * Mat4At(B, 2, 1) +
124 Mat4At(A, 2, 3) * Mat4At(B, 3, 1);
125 R m22 = Mat4At(A, 2, 0) * Mat4At(B, 0, 2) +
126 Mat4At(A, 2, 1) * Mat4At(B, 1, 2) +
127 Mat4At(A, 2, 2) * Mat4At(B, 2, 2) +
128 Mat4At(A, 2, 3) * Mat4At(B, 3, 2);
129 R m23 = Mat4At(A, 2, 0) * Mat4At(B, 0, 3) +
130 Mat4At(A, 2, 1) * Mat4At(B, 1, 3) +
131 Mat4At(A, 2, 2) * Mat4At(B, 2, 3) +
132 Mat4At(A, 2, 3) * Mat4At(B, 3, 3);
133
134 R m30 = Mat4At(A, 3, 0) * Mat4At(B, 0, 0) +
135 Mat4At(A, 3, 1) * Mat4At(B, 1, 0) +
136 Mat4At(A, 3, 2) * Mat4At(B, 2, 0) +
137 Mat4At(A, 3, 3) * Mat4At(B, 3, 0);
138 R m31 = Mat4At(A, 3, 0) * Mat4At(B, 0, 1) +
139 Mat4At(A, 3, 1) * Mat4At(B, 1, 1) +
140 Mat4At(A, 3, 2) * Mat4At(B, 2, 1) +
141 Mat4At(A, 3, 3) * Mat4At(B, 3, 1);
142 R m32 = Mat4At(A, 3, 0) * Mat4At(B, 0, 2) +
143 Mat4At(A, 3, 1) * Mat4At(B, 1, 2) +
144 Mat4At(A, 3, 2) * Mat4At(B, 2, 2) +
145 Mat4At(A, 3, 3) * Mat4At(B, 3, 2);
146 R m33 = Mat4At(A, 3, 0) * Mat4At(B, 0, 3) +
147 Mat4At(A, 3, 1) * Mat4At(B, 1, 3) +
148 Mat4At(A, 3, 2) * Mat4At(B, 2, 3) +
149 Mat4At(A, 3, 3) * Mat4At(B, 3, 3);
150
151 return Mat4(
152 m00, m01, m02, m03,
153 m10, m11, m12, m13,
154 m20, m21, m22, m23,
155 m30, m31, m32, m33);
156}
157
158static inline sgVec3 Mat4MulVec3(sgMat4 m, sgVec3 v, R w) {
159 return (sgVec3) {
160 .x = Mat4At(m, 0, 0) * v.x + Mat4At(m, 0, 1) * v.y + Mat4At(m, 0, 2) * v.z + Mat4At(m, 0, 3) * w,
161 .y = Mat4At(m, 1, 0) * v.x + Mat4At(m, 1, 1) * v.y + Mat4At(m, 1, 2) * v.z + Mat4At(m, 1, 3) * w,
162 .z = Mat4At(m, 2, 0) * v.x + Mat4At(m, 2, 1) * v.y + Mat4At(m, 2, 2) * v.z + Mat4At(m, 2, 3) * w};
163}
164
165static inline sgMat4 Mat4Look(sgVec3 position, sgVec3 forward, sgVec3 up) {
166 const sgVec3 right = normalize3(cross3(forward, up));
167 up = normalize3(cross3(right, forward));
168 return Mat4FromVec3(right, up, neg3(forward), position);
169}
170
171static inline sgMat4 Mat4Perspective(R fovy, R aspect, R near, R far) {
172 R f = tan(fovy / 2.0);
173 assert(f > 0.0);
174 f = 1.0 / f;
175 const R a = near - far;
176 return Mat4(
177 f / aspect, 0, 0, 0,
178 0, f, 0, 0,
179 0, 0, (far + near) / a, (2 * far * near / a),
180 0, 0, -1, 0);
181}
182
183static inline sgPixel* PixelRow(sgPixel* image, int width, int y) {
184 return image + (y * width);
185}
186
187static inline sgPixel* Pixel(sgPixel* image, int width, int x, int y) {
188 return image + (y * width) + x;
189}
190
191#define XY(X,Y) Pixel(gfx->colour, gfx->dims.x, X, Y)
192
193static inline R rmin(R a, R b) { return (a <= b) ? a : b; }
194static inline R rmax(R a, R b) { return (a >= b) ? a : b; }
195
196static inline sgVec2 min2(sgVec2 a, sgVec2 b) {
197 return (sgVec2){.x = rmin(a.x, b.x), .y = rmin(a.y, b.y) };
198}
199
200static inline sgVec2 max2(sgVec2 a, sgVec2 b) {
201 return (sgVec2){.x = rmax(a.x, b.x), .y = rmax(a.y, b.y) };
202}
203
204static inline sgAABB2 TriangleAabb2(const sgTri2 tri) {
205 return (sgAABB2){.pmin = min2(min2(tri.p0, tri.p1), tri.p2),
206 .pmax = max2(max2(tri.p0, tri.p1), tri.p2)};
207}
208
209static inline R f(sgVec2 a, sgVec2 b, sgVec2 p) {
210 return (a.y - b.y)*p.x + (b.x - a.x)*p.y + a.x*b.y - b.x*a.y;
211}
212
213static inline sgVec3 Barycentric(const sgTri2 tri, sgVec2 p) {
214 // There is no need to compute the third coordinate explicitly: a + b + c = 1.
215 // But this results in a worse rasterization of the triangle along one of the edges.
216 // It seems we can patch it with a small epsilon, though.
217 // ---
218 // Division by zero is only possible if the triangle has zero area.
219 /*return (sgVec3){
220 f(tri.p1, tri.p2, p) / f(tri.p1, tri.p2, tri.p0),
221 f(tri.p2, tri.p0, p) / f(tri.p2, tri.p0, tri.p1),
222 f(tri.p0, tri.p1, p) / f(tri.p0, tri.p1, tri.p2)};*/
223 const R b = f(tri.p0, tri.p2, p) / f(tri.p0, tri.p2, tri.p1);
224 const R c = f(tri.p0, tri.p1, p) / f(tri.p0, tri.p1, tri.p2);
225 const R a = /*f(tri.p1, tri.p2, p) / f(tri.p1, tri.p2, tri.p0);*/1 - b - c - 1e-7;
226 return (sgVec3){a,b,c};
227}
228
229#define is_pow2_or_0(X) ((X & (X - 1)) == 0)
230
231static size_t align(size_t size) {
232 static_assert(is_pow2_or_0(SG_ALIGN));
233 constexpr size_t mask = SG_ALIGN - 1;
234 return (size + mask) & (~mask);
235}
236
237void* sgAlloc(size_t count, size_t size) {
238 const size_t total = align(count * size);
239 void* const ptr = aligned_alloc(SG_ALIGN, total);
240 memset(ptr, 0, total);
241 return ptr;
242}
243
244void sgFree(void** pp) {
245 assert(pp);
246 if (*pp) {
247 free(*pp);
248 *pp = nullptr;
249 }
250}
251
252swgfx* sgNew() {
253 swgfx* gfx = SG_ALIGN_ALLOC(1, swgfx);
254 return gfx;
255}
256
257void sgDel(swgfx** ppSwgfx) {
258 assert(ppSwgfx);
259 if (*ppSwgfx) {
260 free(*ppSwgfx);
261 *ppSwgfx = 0;
262 }
263}
264
265void sgColourBuffer(swgfx* gfx, sgVec2i dimensions, sgPixel* buffer) {
266 assert(gfx);
267 gfx->dims = dimensions;
268 gfx->colour = buffer;
269}
270
271void sgPresent(swgfx* gfx, sgVec2i dimensions, sgPixel* screen) {
272 assert(gfx);
273 assert(screen);
274 // Integer scaling only.
275 assert((dimensions.x % gfx->dims.x) == 0);
276 assert((dimensions.y % gfx->dims.y) == 0);
277
278 const int sx = dimensions.x / gfx->dims.x;
279 const int sy = dimensions.y / gfx->dims.y;
280
281 const sgPixel* src = gfx->colour;
282 sgPixel* dst = screen;
283
284 // Replicate each row 'sy' times.
285 for (int y = 0; y < gfx->dims.y; ++y, src += gfx->dims.x) {
286 for (int yy = y*sy; yy < (y+1)*sy; ++yy) {
287 // Replicate each column 'sx' times.
288 const sgPixel* src_col = src;
289 for (int x = 0; x < gfx->dims.x; ++x, ++src_col) {
290 for (int xx = x*sx; xx < (x+1)*sx; ++xx, ++dst) {
291 *dst = *src_col;
292 }
293 }
294 }
295 }
296}
297
298void sgCam(swgfx* gfx, sgVec3 position, sgVec3 forward) {
299 assert(gfx);
300 gfx->view = Mat4Look(position, forward, Up3);
301}
302
303void sgPerspective(swgfx* gfx, R fovy, R aspect, R near, R far) {
304 assert(gfx);
305 gfx->proj = Mat4Perspective(fovy, aspect, near, far);
306}
307
308void sgViewport(swgfx* gfx, int x0, int y0, int width, int height) {
309 assert(gfx);
310 gfx->viewport = (sgViewport_t){x0, y0, width, height};
311}
312
313void sgClear(swgfx* gfx) {
314 assert(gfx);
315 memset(gfx->colour, 0, gfx->dims.x * gfx->dims.y * sizeof(sgPixel));
316}
317
318void sgPixels(swgfx* gfx, size_t count, const sgVec2i* positions, sgPixel colour) {
319 assert(gfx);
320 for (size_t i = 0; i < count; ++i) {
321 const sgVec2i p = positions[i];
322 *XY(p.x, p.y) = colour;
323 }
324}
325
326static void DrawTriangle2(swgfx* gfx, const sgTri2* tri) {
327 assert(gfx);
328 assert(tri);
329 const sgAABB2 bbox = TriangleAabb2(*tri);
330 for (int y = bbox.pmin.y; y <= bbox.pmax.y; ++y) {
331 for (int x = bbox.pmin.x; x <= bbox.pmax.x; ++x) {
332 const sgVec2 p = (sgVec2){x, y};
333 // TODO: there is an incremental optimization to computing barycentric coordinates;
334 // read more about it.
335 const sgVec3 bar = Barycentric(*tri, p);
336 // We need to check the third coordinate.
337 // a + b + c = 1
338 // So, e.g., if a > 0 and b > 0, then we have c < 1, but we could also have c < 0.
339 // In the case c < 0, then point is outside the triangle.
340 if ((bar.x > 0) && (bar.y > 0) && (bar.z > 0)) {
341 const sgVec2i pi = (sgVec2i){(int)x, (int)y};
342 sgPixels(gfx, 1, &pi, (sgPixel){255, 255, 255, 255});
343 }
344 }
345 }
346}
347
348// TODO: DrawTriangle3 with clipping. Leave DrawTriangle2 to not clip for
349// performance; assume that 2D triangles are within bounds.
350// TODO: If the triangle is out of bounds, skip entirely.
351// TODO: Otherwise, rasterize the triangle the simple way and check whether each
352// individual pixel is within bounds; do not explicitly clip the triangle.
353// TODO: Actually, I think we can just clip the triangle's AABB and then walk
354// over those pixels instead of checking every individual pixel in the
355// non-clipped AABB. Edit: I think this doesn't work; draw it and you'll
356// see. Some pixels that should be rasterized will fall out of the clipped
357// AABB.
358
359void sgTriangles2(swgfx* gfx, size_t count, const sgTri2* tris) {
360 assert(gfx);
361 for (size_t i = 0; i < count; ++i) {
362 DrawTriangle2(gfx, &tris[i]);
363 }
364}
365
366void sgTriangles(swgfx* gfx, size_t count, const sgTri3* tris, const sgNormal*) {
367 assert(gfx);
368 for (size_t i = 0; i < count; ++i) {
369 // Ignore projection matrix for now. Rasterize 2D triangles.
370 const sgTri3* tri3 = &tris[i];
371 const sgTri2 tri2 = (sgTri2) {
372 .p0 = (sgVec2){tri3->p0.x, tri3->p0.y},
373 .p1 = (sgVec2){tri3->p1.x, tri3->p1.y},
374 .p2 = (sgVec2){tri3->p2.x, tri3->p2.y},
375 };
376 DrawTriangle2(gfx, &tri2);
377 }
378}
379
380static inline void AssertViewportWithinBuffer(swgfx* gfx) {
381 assert(gfx);
382 const sgViewport_t vp = gfx->viewport;
383 assert((vp.x0 + vp.width) <= gfx->dims.x);
384 assert((vp.y0 + vp.height) <= gfx->dims.y);
385}
386
387void sgCheck(swgfx* gfx) {
388 assert(gfx);
389 AssertViewportWithinBuffer(gfx);
390}