summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt6
-rw-r--r--include/swgfx.h12
-rw-r--r--src/swgfx.c278
-rw-r--r--test/test.c29
-rw-r--r--test/test.h248
5 files changed, 501 insertions, 72 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 537c00d..ec4d307 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -23,10 +23,10 @@ target_compile_options(swgfx PRIVATE -Wall -Wextra)
23# Tests. 23# Tests.
24 24
25add_executable(swgfx-test 25add_executable(swgfx-test
26 test/test.c) 26 test/test.c
27 test/test.h)
27 28
28target_link_libraries(swgfx-test 29target_link_libraries(swgfx-test
29 swgfx) 30 swgfx)
30 31
31target_compile_options(swgfx-test PRIVATE -Wall -Wextra) 32target_compile_options(swgfx-test PRIVATE -DUNIT_TEST -DNDEBUG -Wall -Wextra)
32
diff --git a/include/swgfx.h b/include/swgfx.h
index b6dc769..94eb359 100644
--- a/include/swgfx.h
+++ b/include/swgfx.h
@@ -1,7 +1,7 @@
1/* 1/*
2Software rendering library. 2Software rendering library.
3 3
4Cooridnate systems: 4Coordinate systems:
5- Pixel coordinates (i,j) refer to the center of the pixel. 5- Pixel coordinates (i,j) refer to the center of the pixel.
6 Thus, real-valued coordinates (x,y) with no fractional part point at the pixel center. 6 Thus, real-valued coordinates (x,y) with no fractional part point at the pixel center.
7- Viewport origin is the top-left corner of the screen. 7- Viewport origin is the top-left corner of the screen.
@@ -30,6 +30,10 @@ typedef struct sgQuad { sgVec2 p0, p1; } sgQuad;
30typedef struct sgTri2 { sgVec2 p0, p1, p2; } sgTri2; 30typedef struct sgTri2 { sgVec2 p0, p1, p2; } sgTri2;
31typedef struct sgTri3 { sgVec3 p0, p1, p2; } sgTri3; 31typedef struct sgTri3 { sgVec3 p0, p1, p2; } sgTri3;
32 32
33typedef uint16_t sgIdx;
34typedef struct sgVert { sgIdx position, normal, texcoord; } sgVert;
35typedef struct sgTriIdx { sgVert v0, v1, v2; } sgTriIdx;
36
33// TODO: Should we use real-valued colours? 37// TODO: Should we use real-valued colours?
34typedef struct sgPixel { uint8_t r, g, b, a; } sgPixel; 38typedef struct sgPixel { uint8_t r, g, b, a; } sgPixel;
35 39
@@ -43,7 +47,9 @@ void sgDel(swgfx**);
43void sgColourBuffer(swgfx*, sgVec2i dimensions, sgPixel* buffer); 47void sgColourBuffer(swgfx*, sgVec2i dimensions, sgPixel* buffer);
44void sgPresent (swgfx*, sgVec2i dimensions, sgPixel* screen); 48void sgPresent (swgfx*, sgVec2i dimensions, sgPixel* screen);
45 49
46void sgCam (swgfx*, sgVec3 position, sgVec3 forward); 50void sgModelId (swgfx*);
51void sgModel (swgfx*, sgVec3 position, sgVec3 right, sgVec3 up, sgVec3 forward);
52void sgView (swgfx*, sgVec3 position, sgVec3 forward);
47void sgOrtho (swgfx*, R left, R right, R top, R bottom, R near, R far); 53void sgOrtho (swgfx*, R left, R right, R top, R bottom, R near, R far);
48void sgPerspective(swgfx*, R fovy, R aspect, R near, R far); 54void sgPerspective(swgfx*, R fovy, R aspect, R near, R far);
49void sgViewport (swgfx*, int x0, int y0, int width, int height); 55void sgViewport (swgfx*, int x0, int y0, int width, int height);
@@ -56,6 +62,8 @@ void sgTriangles2 (swgfx*, size_t count, const sgTri2*);
56void sgTriangleStrip2(swgfx*, size_t count, const sgVec2*); 62void sgTriangleStrip2(swgfx*, size_t count, const sgVec2*);
57void sgTriangles (swgfx*, size_t count, const sgTri3*, const sgNormal*); 63void sgTriangles (swgfx*, size_t count, const sgTri3*, const sgNormal*);
58void sgTriangleStrip (swgfx*, size_t count, const sgVec3*, const sgNormal*); 64void sgTriangleStrip (swgfx*, size_t count, const sgVec3*, const sgNormal*);
65void sgTrianglesIndexed(swgfx*, size_t numIndices, const sgIdx* indices, const sgVec3* positions);
66void sgTrianglesIndexedNonUniform(swgfx*, size_t numTris, const sgTriIdx* tris, const sgVec3* positions);
59 67
60void sgCheck(swgfx*); 68void sgCheck(swgfx*);
61 69
diff --git a/src/swgfx.c b/src/swgfx.c
index 772a691..3b9ce29 100644
--- a/src/swgfx.c
+++ b/src/swgfx.c
@@ -6,6 +6,11 @@ Matrices:
6Coordinate systems: 6Coordinate systems:
7 - Right-handed. 7 - Right-handed.
8 - NDC in [-1, +1]. 8 - NDC in [-1, +1].
9 - Viewport goes up and to the right.
10 - Window goes down and to the right.
11 - (x,y) is the center of a pixel.
12 - Top-left: (x - 1/2, y - 1/2)
13 - Bottom-right: (x + 1/2, y + 1/2)
9*/ 14*/
10#include <swgfx.h> 15#include <swgfx.h>
11 16
@@ -15,7 +20,7 @@ Coordinate systems:
15#include <stdlib.h> 20#include <stdlib.h>
16#include <string.h> 21#include <string.h>
17 22
18static const sgVec3 Up3 = (sgVec3){0,1,0}; 23static constexpr sgVec3 Up3 = (sgVec3){0,1,0};
19 24
20typedef struct sgViewport_t { int x0, y0, width, height; } sgViewport_t; 25typedef struct sgViewport_t { int x0, y0, width, height; } sgViewport_t;
21typedef struct sgTri2 { sgVec2 p0, p1, p2; } sgTri2; 26typedef struct sgTri2 { sgVec2 p0, p1, p2; } sgTri2;
@@ -30,8 +35,18 @@ typedef struct swgfx {
30 sgVec2i dims; // Colour buffer dimensions. 35 sgVec2i dims; // Colour buffer dimensions.
31 sgPixel* colour; // Colour buffer. 36 sgPixel* colour; // Colour buffer.
32 sgViewport_t viewport; 37 sgViewport_t viewport;
38 sgMat4 model; // Model matrix.
33 sgMat4 view; // View matrix. 39 sgMat4 view; // View matrix.
34 sgMat4 proj; // Projection matrix. 40 sgMat4 proj; // Projection matrix.
41 // Pre-multiplied matrices.
42 // The model matrix changes once per object, more frequently than view or
43 // projection. View and projection are expected to change infrequently, maybe
44 // once per frame.
45 // Make it so that changing the model matrix only requires one matrix
46 // multiplication (mvp = model * viewProj) and not two (mvp = model * view * projection)
47 // before rendering the model's triangles.
48 sgMat4 viewProj; // View-projection matrix.
49 sgMat4 mvp; // Model-view-projection matrix.
35} swgfx; 50} swgfx;
36 51
37static inline sgVec3 neg3(sgVec3 v) { return (sgVec3){-v.x, -v.y, -v.z}; } 52static inline sgVec3 neg3(sgVec3 v) { return (sgVec3){-v.x, -v.y, -v.z}; }
@@ -40,6 +55,10 @@ static inline sgVec3 sub3(sgVec3 a, sgVec3 b) {
40 return (sgVec3){a.x - b.x, a.y - b.y, a.z - b.z}; 55 return (sgVec3){a.x - b.x, a.y - b.y, a.z - b.z};
41} 56}
42 57
58static inline R dot3(sgVec3 a, sgVec3 b) {
59 return a.x * b.x + a.y * b.y + a.z * b.z;
60}
61
43static inline sgVec3 cross3(sgVec3 a, sgVec3 b) { 62static inline sgVec3 cross3(sgVec3 a, sgVec3 b) {
44 return (sgVec3) { 63 return (sgVec3) {
45 a.y * b.z - a.z * b.y, 64 a.y * b.z - a.z * b.y,
@@ -48,8 +67,7 @@ static inline sgVec3 cross3(sgVec3 a, sgVec3 b) {
48} 67}
49 68
50static inline R normsq3(sgVec3 v) { return v.x * v.x + v.y * v.y + v.z * v.z; } 69static inline R normsq3(sgVec3 v) { return v.x * v.x + v.y * v.y + v.z * v.z; }
51 70static inline R norm3 (sgVec3 v) { return (R)sqrt(normsq3(v)); }
52static inline R norm3(sgVec3 v) { return sqrt(normsq3(v)); }
53 71
54static inline sgVec3 normalize3(sgVec3 v) { 72static inline sgVec3 normalize3(sgVec3 v) {
55 const R n = norm3(v); 73 const R n = norm3(v);
@@ -57,6 +75,10 @@ static inline sgVec3 normalize3(sgVec3 v) {
57 return (sgVec3){v.x / n, v.y / n, v.z / n}; 75 return (sgVec3){v.x / n, v.y / n, v.z / n};
58} 76}
59 77
78static inline sgVec4 Vec4FromVec3(sgVec3 v, R w) {
79 return (sgVec4){v.x, v.y, v.z, w};
80}
81
60static inline sgMat4 Mat4( 82static inline sgMat4 Mat4(
61 R m00, R m01, R m02, R m03, // v0.x v1.x v2.x v3.x 83 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 84 R m10, R m11, R m12, R m13, // v0.y v1.y v2.y v3.y
@@ -78,6 +100,10 @@ static inline sgMat4 Mat4FromVec3(sgVec3 right, sgVec3 up, sgVec3 forward, sgVec
78} 100}
79 101
80static inline R Mat4At(sgMat4 m, int row, int col) { return m.val[col][row]; } 102static inline R Mat4At(sgMat4 m, int row, int col) { return m.val[col][row]; }
103static inline sgVec3 Mat4v0(sgMat4 m) { return *((sgVec3*)m.val[0]); }
104static inline sgVec3 Mat4v1(sgMat4 m) { return *((sgVec3*)m.val[1]); }
105static inline sgVec3 Mat4v2(sgMat4 m) { return *((sgVec3*)m.val[2]); }
106static inline sgVec3 Mat4v3(sgMat4 m) { return *((sgVec3*)m.val[3]); }
81 107
82static inline sgMat4 Mat4Mul(sgMat4 A, sgMat4 B) { 108static inline sgMat4 Mat4Mul(sgMat4 A, sgMat4 B) {
83 R m00 = Mat4At(A, 0, 0) * Mat4At(B, 0, 0) + 109 R m00 = Mat4At(A, 0, 0) * Mat4At(B, 0, 0) +
@@ -162,6 +188,31 @@ static inline sgVec3 Mat4MulVec3(sgMat4 m, sgVec3 v, R 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}; 188 .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} 189}
164 190
191static inline sgVec4 Mat4MulVec4(sgMat4 m, sgVec4 v) {
192 sgVec4 u;
193 u.x = Mat4At(m, 0, 0) * v.x + Mat4At(m, 0, 1) * v.y +
194 Mat4At(m, 0, 2) * v.z + Mat4At(m, 0, 3) * v.w;
195 u.y = Mat4At(m, 1, 0) * v.x + Mat4At(m, 1, 1) * v.y +
196 Mat4At(m, 1, 2) * v.z + Mat4At(m, 1, 3) * v.w;
197 u.z = Mat4At(m, 2, 0) * v.x + Mat4At(m, 2, 1) * v.y +
198 Mat4At(m, 2, 2) * v.z + Mat4At(m, 2, 3) * v.w;
199 u.w = Mat4At(m, 3, 0) * v.x + Mat4At(m, 3, 1) * v.y +
200 Mat4At(m, 3, 2) * v.z + Mat4At(m, 3, 3) * v.w;
201 return u;
202}
203
204static inline sgMat4 Mat4InverseTransform(sgMat4 m) {
205 const sgVec3 r = Mat4v0(m);
206 const sgVec3 u = Mat4v1(m);
207 const sgVec3 f = Mat4v2(m);
208 const sgVec3 t = Mat4v3(m);
209 return Mat4(
210 r.x, r.y, r.z, -dot3(r, t),
211 u.x, u.y, u.z, -dot3(u, t),
212 f.x, f.y, f.z, -dot3(f, t),
213 0.f, 0.f, 0.f, 1.f);
214}
215
165static inline sgMat4 Mat4Look(sgVec3 position, sgVec3 forward, sgVec3 up) { 216static inline sgMat4 Mat4Look(sgVec3 position, sgVec3 forward, sgVec3 up) {
166 const sgVec3 right = normalize3(cross3(forward, up)); 217 const sgVec3 right = normalize3(cross3(forward, up));
167 up = normalize3(cross3(right, forward)); 218 up = normalize3(cross3(right, forward));
@@ -169,43 +220,59 @@ static inline sgMat4 Mat4Look(sgVec3 position, sgVec3 forward, sgVec3 up) {
169} 220}
170 221
171static inline sgMat4 Mat4Perspective(R fovy, R aspect, R near, R far) { 222static inline sgMat4 Mat4Perspective(R fovy, R aspect, R near, R far) {
172 R f = tan(fovy / 2.0); 223 R f = (R)tan(fovy / 2.0);
173 assert(f > 0.0); 224 assert(f > 0.0);
174 f = 1.0 / f; 225 f = 1.f / f;
175 const R a = near - far; 226 const R a = near - far;
176 return Mat4( 227 return Mat4(
177 f / aspect, 0, 0, 0, 228 f / aspect, 0, 0, 0,
178 0, f, 0, 0, 229 0, f, 0, 0,
179 0, 0, (far + near) / a, (2 * far * near / a), 230 0, 0, (far + near) / a, (2 * far * near / a),
180 0, 0, -1, 0); 231 0, 0, -1, 0);
181} 232}
182 233
183static inline sgPixel* PixelRow(sgPixel* image, int width, int y) { 234#ifndef _NDEBUG
184 return image + (y * width); 235static bool InBounds(int width, int height, sgVec2i p) {
236 return (0 <= p.x) && (p.x < width) &&
237 (0 <= p.y) && (p.y < height);
185} 238}
239#endif // _NDEBUG
186 240
187static inline sgPixel* Pixel(sgPixel* image, int width, int x, int y) { 241static inline sgPixel* Pixel(sgPixel* image, int width, int height, int x, int y) {
242 assert(InBounds(width, height, (sgVec2i){x,y}));
188 return image + (y * width) + x; 243 return image + (y * width) + x;
189} 244}
190 245
191#define XY(X,Y) Pixel(gfx->colour, gfx->dims.x, X, Y) 246static inline R rmin(R a, R b) { return (a <= b) ? a : b; }
192 247static inline R rmax(R a, R b) { return (a >= b) ? a : b; }
193static inline R rmin(R a, R b) { return (a <= b) ? a : b; } 248static inline int imin(int a, int b) { return (a <= b) ? a : b; }
194static inline R rmax(R a, R b) { return (a >= b) ? a : b; } 249static inline int imax(int a, int b) { return (a >= b) ? a : b; }
195
196static inline sgVec2 min2(sgVec2 a, sgVec2 b) { 250static inline sgVec2 min2(sgVec2 a, sgVec2 b) {
197 return (sgVec2){.x = rmin(a.x, b.x), .y = rmin(a.y, b.y) }; 251 return (sgVec2){.x = rmin(a.x, b.x), .y = rmin(a.y, b.y) };
198} 252}
199
200static inline sgVec2 max2(sgVec2 a, sgVec2 b) { 253static inline sgVec2 max2(sgVec2 a, sgVec2 b) {
201 return (sgVec2){.x = rmax(a.x, b.x), .y = rmax(a.y, b.y) }; 254 return (sgVec2){.x = rmax(a.x, b.x), .y = rmax(a.y, b.y) };
202} 255}
256static inline sgVec2i min2i(sgVec2i a, sgVec2i b) {
257 return (sgVec2i){.x = imin(a.x, b.x), .y = imin(a.y, b.y) };
258}
259static inline sgVec2i max2i(sgVec2i a, sgVec2i b) {
260 return (sgVec2i){.x = imax(a.x, b.x), .y = imax(a.y, b.y) };
261}
203 262
204static inline sgAABB2 TriangleAabb2(const sgTri2 tri) { 263static inline sgAABB2 TriangleAabb2(const sgTri2 tri) {
205 return (sgAABB2){.pmin = min2(min2(tri.p0, tri.p1), tri.p2), 264 return (sgAABB2){.pmin = min2(min2(tri.p0, tri.p1), tri.p2),
206 .pmax = max2(max2(tri.p0, tri.p1), tri.p2)}; 265 .pmax = max2(max2(tri.p0, tri.p1), tri.p2)};
207} 266}
208 267
268static inline sgVec2i Clip(const swgfx* gfx, const sgVec2i p) {
269 assert(gfx);
270 constexpr sgVec2i lower = (sgVec2i){0,0};
271 const sgVec2i upper = (sgVec2i){gfx->viewport.width - 1,
272 gfx->viewport.height - 1};
273 return max2i(lower, min2i(upper, p));
274}
275
209static inline R f(sgVec2 a, sgVec2 b, sgVec2 p) { 276static 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; 277 return (a.y - b.y)*p.x + (b.x - a.x)*p.y + a.x*b.y - b.x*a.y;
211} 278}
@@ -222,10 +289,77 @@ static inline sgVec3 Barycentric(const sgTri2 tri, sgVec2 p) {
222 f(tri.p0, tri.p1, p) / f(tri.p0, tri.p1, tri.p2)};*/ 289 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); 290 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); 291 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; 292 const R a = /*f(tri.p1, tri.p2, p) / f(tri.p1, tri.p2, tri.p0);*/1.f - b - c - (R)1e-7;
226 return (sgVec3){a,b,c}; 293 return (sgVec3){a,b,c};
227} 294}
228 295
296static void DrawTriangle2(swgfx* gfx, const sgTri2* tri) {
297 assert(gfx);
298 assert(tri);
299 const sgAABB2 bbox = TriangleAabb2(*tri);
300 // We consider (x,y) to be the pixel center.
301 // Draw all pixels touched by the bounding box. TODO: Multi-sampling.
302 sgVec2i pmin = (sgVec2i){(int)bbox.pmin.x, (int)bbox.pmin.y};
303 sgVec2i pmax = (sgVec2i){(int)(bbox.pmax.x + 0.5f), (int)(bbox.pmax.y + 0.5f)};
304 // Clip to screen space.
305 pmin = Clip(gfx, pmin);
306 pmax = Clip(gfx, pmax);
307 // Draw.
308 for (int y = pmin.y; y <= pmax.y; ++y) {
309 for (int x = pmin.x; x <= pmax.x; ++x) {
310 const sgVec2 p = (sgVec2){(R)x, (R)y};
311 // TODO: there is an incremental optimization to computing barycentric coordinates;
312 // read more about it.
313 const sgVec3 bar = Barycentric(*tri, p);
314 // We need to check the third coordinate.
315 // a + b + c = 1
316 // So, e.g., if a >= 0 and b >= 0, then we have c <= 1, but we could also have c <= 0.
317 // In the case c <= 0, then point is outside the triangle.
318 if ((bar.x >= 0) && (bar.y >= 0) && (bar.z >= 0)) {
319 const sgVec2i pi = (sgVec2i){(int)x, (int)y};
320 sgPixels(gfx, 1, &pi, (sgPixel){255, 255, 255, 255});
321 }
322 }
323 }
324}
325
326static inline sgVec3 PerspDivide(sgVec4 v) {
327 return (sgVec3){v.x / v.w, v.y / v.w, v.z / v.w};
328}
329
330// TODO: Compute a viewport matrix in sgViewport() instead.
331static inline sgVec2 ViewportTransform(sgViewport_t vp, sgVec3 ndc) {
332 return (sgVec2){
333 .x = (ndc.x+1.f) * ((R)vp.width/2.f) + (R)vp.x0,
334 .y = (ndc.y+1.f) * ((R)vp.height/2.f) + (R)vp.y0};
335}
336
337static inline sgVec2 ViewportToWindow(sgViewport_t vp, sgVec2 p) {
338 return (sgVec2){p.x, (R)vp.height - p.y};
339}
340
341static inline sgVec2 TransformPosition(const swgfx* gfx, sgVec3 p) {
342 assert(gfx);
343 // Model to clip space.
344 const sgVec4 p_clip = Mat4MulVec4(gfx->mvp, Vec4FromVec3(p, 1));
345 // TODO: Backface culling.
346 // Perspective divide.
347 const sgVec3 p_ndc = PerspDivide(p_clip);
348 // TODO: Clip.
349 const sgVec2 p_vp = ViewportTransform(gfx->viewport, p_ndc);
350 return ViewportToWindow(gfx->viewport, p_vp);
351}
352
353static void DrawTriangle3(swgfx* gfx, const sgTri3* tri) {
354 assert(gfx);
355 assert(tri);
356 const sgVec2 p0 = TransformPosition(gfx, tri->p0);
357 const sgVec2 p1 = TransformPosition(gfx, tri->p1);
358 const sgVec2 p2 = TransformPosition(gfx, tri->p2);
359 const sgTri2 tri2 = (sgTri2){p0, p1, p2};
360 DrawTriangle2(gfx, &tri2);
361}
362
229#define is_pow2_or_0(X) ((X & (X - 1)) == 0) 363#define is_pow2_or_0(X) ((X & (X - 1)) == 0)
230 364
231static size_t align(size_t size) { 365static size_t align(size_t size) {
@@ -295,14 +429,44 @@ void sgPresent(swgfx* gfx, sgVec2i dimensions, sgPixel* screen) {
295 } 429 }
296} 430}
297 431
298void sgCam(swgfx* gfx, sgVec3 position, sgVec3 forward) { 432static void sgUpdateViewProjection(swgfx* gfx) {
433 assert(gfx);
434 gfx->viewProj = Mat4Mul(gfx->proj, gfx->view);
435}
436
437static void sgUpdateMvp(swgfx* gfx) {
438 assert(gfx);
439 gfx->mvp = Mat4Mul(gfx->viewProj, gfx->model);
440}
441
442void sgModelId(swgfx* gfx) {
299 assert(gfx); 443 assert(gfx);
300 gfx->view = Mat4Look(position, forward, Up3); 444 sgModel(gfx,
445 (sgVec3){0,0,0},
446 (sgVec3){1, 0, 0},
447 (sgVec3){0, 1, 0},
448 (sgVec3){0, 0, 1});
449}
450
451void sgModel(swgfx* gfx, sgVec3 position, sgVec3 right, sgVec3 up, sgVec3 forward) {
452 assert(gfx);
453 gfx->model = Mat4FromVec3(right, up, forward, position);
454 sgUpdateMvp(gfx);
455}
456
457void sgView(swgfx* gfx, sgVec3 position, sgVec3 forward) {
458 assert(gfx);
459 const sgMat4 camera = Mat4Look(position, forward, Up3);
460 gfx->view = Mat4InverseTransform(camera);
461 sgUpdateViewProjection(gfx);
462 sgUpdateMvp(gfx);
301} 463}
302 464
303void sgPerspective(swgfx* gfx, R fovy, R aspect, R near, R far) { 465void sgPerspective(swgfx* gfx, R fovy, R aspect, R near, R far) {
304 assert(gfx); 466 assert(gfx);
305 gfx->proj = Mat4Perspective(fovy, aspect, near, far); 467 gfx->proj = Mat4Perspective(fovy, aspect, near, far);
468 sgUpdateViewProjection(gfx);
469 sgUpdateMvp(gfx);
306} 470}
307 471
308void sgViewport(swgfx* gfx, int x0, int y0, int width, int height) { 472void sgViewport(swgfx* gfx, int x0, int y0, int width, int height) {
@@ -317,34 +481,13 @@ void sgClear(swgfx* gfx) {
317 481
318void sgPixels(swgfx* gfx, size_t count, const sgVec2i* positions, sgPixel colour) { 482void sgPixels(swgfx* gfx, size_t count, const sgVec2i* positions, sgPixel colour) {
319 assert(gfx); 483 assert(gfx);
484#define XY(X,Y) Pixel(gfx->colour, gfx->dims.x, gfx->dims.y, X, Y)
320 for (size_t i = 0; i < count; ++i) { 485 for (size_t i = 0; i < count; ++i) {
321 const sgVec2i p = positions[i]; 486 const sgVec2i p = positions[i];
322 *XY(p.x, p.y) = colour; 487 *XY(p.x, p.y) = colour;
323 } 488 }
324} 489}
325 490
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 491// TODO: DrawTriangle3 with clipping. Leave DrawTriangle2 to not clip for
349// performance; assume that 2D triangles are within bounds. 492// performance; assume that 2D triangles are within bounds.
350// TODO: If the triangle is out of bounds, skip entirely. 493// TODO: If the triangle is out of bounds, skip entirely.
@@ -365,26 +508,51 @@ void sgTriangles2(swgfx* gfx, size_t count, const sgTri2* tris) {
365 508
366void sgTriangles(swgfx* gfx, size_t count, const sgTri3* tris, const sgNormal*) { 509void sgTriangles(swgfx* gfx, size_t count, const sgTri3* tris, const sgNormal*) {
367 assert(gfx); 510 assert(gfx);
511 assert(tris);
368 for (size_t i = 0; i < count; ++i) { 512 for (size_t i = 0; i < count; ++i) {
369 // Ignore projection matrix for now. Rasterize 2D triangles. 513 const sgTri3* tri = &tris[i];
370 const sgTri3* tri3 = &tris[i]; 514 DrawTriangle3(gfx, tri);
371 const sgTri2 tri2 = (sgTri2) { 515 }
372 .p0 = (sgVec2){tri3->p0.x, tri3->p0.y}, 516}
373 .p1 = (sgVec2){tri3->p1.x, tri3->p1.y}, 517
374 .p2 = (sgVec2){tri3->p2.x, tri3->p2.y}, 518void sgTrianglesIndexed(swgfx* gfx, size_t numIndices, const sgIdx* indices, const sgVec3* positions) {
375 }; 519 assert(gfx);
376 DrawTriangle2(gfx, &tri2); 520 assert(indices);
521 assert(positions);
522 for (size_t i = 0; i < numIndices; i+=3) {
523 const sgIdx i0 = indices[i];
524 const sgIdx i1 = indices[i+1];
525 const sgIdx i2 = indices[i+2];
526 const sgVec3 p0 = positions[i0];
527 const sgVec3 p1 = positions[i1];
528 const sgVec3 p2 = positions[i2];
529 const sgTri3 tri = (sgTri3){p0, p1, p2};
530 DrawTriangle3(gfx, &tri);
531 }
532}
533
534void sgTrianglesIndexedNonUniform(swgfx* gfx, size_t numTris, const sgTriIdx* tris, const sgVec3* positions) {
535 assert(gfx);
536 assert(tris);
537 assert(positions);
538 for (size_t t = 0; t < numTris; ++t) {
539 const sgTriIdx* triIdx = &tris[t];
540 const sgTri3 tri = (sgTri3){
541 positions[triIdx->v0.position],
542 positions[triIdx->v1.position],
543 positions[triIdx->v2.position]};
544 DrawTriangle3(gfx, &tri);
377 } 545 }
378} 546}
379 547
380static inline void AssertViewportWithinBuffer(swgfx* gfx) { 548static bool ViewportWithinBuffer(swgfx* gfx) {
381 assert(gfx); 549 assert(gfx);
382 const sgViewport_t vp = gfx->viewport; 550 const sgViewport_t vp = gfx->viewport;
383 assert((vp.x0 + vp.width) <= gfx->dims.x); 551 return ((vp.x0 + vp.width) <= gfx->dims.x) &&
384 assert((vp.y0 + vp.height) <= gfx->dims.y); 552 ((vp.y0 + vp.height) <= gfx->dims.y);
385} 553}
386 554
387void sgCheck(swgfx* gfx) { 555void sgCheck(swgfx* gfx) {
388 assert(gfx); 556 assert(gfx);
389 AssertViewportWithinBuffer(gfx); 557 assert(ViewportWithinBuffer(gfx));
390} 558}
diff --git a/test/test.c b/test/test.c
index 668dec8..be3be93 100644
--- a/test/test.c
+++ b/test/test.c
@@ -1,3 +1,5 @@
1#include "test.h"
2
1#include <swgfx.h> 3#include <swgfx.h>
2 4
3#include <assert.h> 5#include <assert.h>
@@ -24,7 +26,7 @@ static bool WritePPM(int width, int height, const RGB* image, const char* path)
24 return true; 26 return true;
25} 27}
26 28
27void ToRGB(int width, int height, const sgPixel* rgba, RGB* rgb) { 29static void ToRGB(int width, int height, const sgPixel* rgba, RGB* rgb) {
28 assert(rgba); 30 assert(rgba);
29 assert(rgb); 31 assert(rgb);
30 for (int y = 0; y < height; ++y) { 32 for (int y = 0; y < height; ++y) {
@@ -71,18 +73,21 @@ static void TestTriangle(swgfx* gfx) {
71 73
72 ToRGB(BufferWidth, BufferHeight, colour, rgb); 74 ToRGB(BufferWidth, BufferHeight, colour, rgb);
73 WritePPM(BufferWidth, BufferHeight, rgb, "triangle.ppm"); 75 WritePPM(BufferWidth, BufferHeight, rgb, "triangle.ppm");
76 // TODO: Assert the contents. Turn this file into a unit test executable.
77 // Write a helper function that first writes the image to a file and then
78 // asserts its contents.
74} 79}
75 80
76int main() { 81#define GFX_TEST(NAME, FUNC) \
77 swgfx* gfx = 0; 82 TEST_CASE(NAME) {\
78 if (!(gfx = sgNew())) { 83 swgfx* gfx = nullptr;\
79 fprintf(stderr, "Failed to create swgfx\n"); 84 if (!(gfx = sgNew())) {\
80 return 1; 85 SET_FAILURE("Failed to create swgfx\n", false);\
86 }\
87 FUNC(gfx);\
88 sgDel(&gfx);\
81 } 89 }
82
83 TestTriangle(gfx);
84
85 sgDel(&gfx);
86 return 0;
87}
88 90
91GFX_TEST(triangle, TestTriangle)
92
93int main() { return 0; }
diff --git a/test/test.h b/test/test.h
new file mode 100644
index 0000000..cdd2f05
--- /dev/null
+++ b/test/test.h
@@ -0,0 +1,248 @@
1// SPDX-License-Identifier: MIT
2#pragma once
3
4#ifdef UNIT_TEST
5
6#include <stdbool.h>
7#include <stdio.h>
8#include <stdlib.h>
9#include <string.h>
10
11#if defined(__DragonFly__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || \
12 defined(__NetBSD__) || defined(__OpenBSD__)
13#define USE_SYSCTL_FOR_ARGS 1
14// clang-format off
15#include <sys/types.h>
16#include <sys/sysctl.h>
17// clang-format on
18#include <unistd.h> // getpid
19#endif
20
21struct test_file_metadata;
22
23struct test_failure {
24 bool present;
25 const char *message;
26 const char *file;
27 int line;
28 bool owned;
29};
30
31struct test_case_metadata {
32 void (*fn)(struct test_case_metadata *, struct test_file_metadata *);
33 struct test_failure failure;
34 const char *name;
35 struct test_case_metadata *next;
36};
37
38struct test_file_metadata {
39 bool registered;
40 const char *name;
41 struct test_file_metadata *next;
42 struct test_case_metadata *tests;
43};
44
45struct test_file_metadata __attribute__((weak)) * test_file_head;
46
47#define SET_FAILURE(_message, _owned) \
48 metadata->failure = (struct test_failure) { \
49 .present = true, \
50 .message = _message, \
51 .file = __FILE__, \
52 .line = __LINE__, \
53 .owned = _owned, \
54 }
55
56#define TEST_EQUAL(a, b) \
57 do { \
58 if ((a) != (b)) { \
59 SET_FAILURE(#a " != " #b, false); \
60 return; \
61 } \
62 } while (0)
63
64#define TEST_NOTEQUAL(a, b) \
65 do { \
66 if ((a) == (b)) { \
67 SET_FAILURE(#a " == " #b, false); \
68 return; \
69 } \
70 } while (0)
71
72#define TEST_TRUE(a) \
73 do { \
74 if (!(a)) { \
75 SET_FAILURE(#a " is not true", false); \
76 return; \
77 } \
78 } while (0)
79
80#define TEST_STREQUAL(a, b) \
81 do { \
82 if (strcmp(a, b) != 0) { \
83 const char *test_strequal__part2 = " != " #b; \
84 size_t test_strequal__len = \
85 strlen(a) + strlen(test_strequal__part2) + 3; \
86 char *test_strequal__buf = malloc(test_strequal__len); \
87 snprintf(test_strequal__buf, test_strequal__len, "\"%s\"%s", a, \
88 test_strequal__part2); \
89 SET_FAILURE(test_strequal__buf, true); \
90 return; \
91 } \
92 } while (0)
93
94#define TEST_STRNEQUAL(a, b, len) \
95 do { \
96 if (strncmp(a, b, len) != 0) { \
97 const char *test_strnequal__part2 = " != " #b; \
98 size_t test_strnequal__len2 = \
99 len + strlen(test_strnequal__part2) + 3; \
100 char *test_strnequal__buf = malloc(test_strnequal__len2); \
101 snprintf(test_strnequal__buf, test_strnequal__len2, \
102 "\"%.*s\"%s", (int)len, a, test_strnequal__part2); \
103 SET_FAILURE(test_strnequal__buf, true); \
104 return; \
105 } \
106 } while (0)
107
108#define TEST_STREQUAL3(str, expected, len) \
109 do { \
110 if (len != strlen(expected) || strncmp(str, expected, len) != 0) { \
111 const char *test_strequal3__part2 = " != " #expected; \
112 size_t test_strequal3__len2 = \
113 len + strlen(test_strequal3__part2) + 3; \
114 char *test_strequal3__buf = malloc(test_strequal3__len2); \
115 snprintf(test_strequal3__buf, test_strequal3__len2, \
116 "\"%.*s\"%s", (int)len, str, test_strequal3__part2); \
117 SET_FAILURE(test_strequal3__buf, true); \
118 return; \
119 } \
120 } while (0)
121
122#define TEST_CASE(_name) \
123 static void __test_h_##_name(struct test_case_metadata *, \
124 struct test_file_metadata *); \
125 static struct test_file_metadata __test_h_file##_name; \
126 static struct test_case_metadata __test_h_meta_##_name = { \
127 .fn = __test_h_##_name, \
128 .failure = {}, \
129 .name = #_name, \
130 .next = 0, \
131 }; \
132 static void __attribute__((constructor(101))) __test_h_##_name##_register(void) { \
133 __test_h_meta_##_name.next = __test_h_file##_name.tests; \
134 __test_h_file##_name.tests = &__test_h_meta_##_name; \
135 if (!__test_h_file##_name.registered) { \
136 __test_h_file##_name.name = __FILE__; \
137 __test_h_file##_name.next = test_file_head; \
138 test_file_head = &__test_h_file##_name; \
139 __test_h_file##_name.registered = true; \
140 } \
141 } \
142 static void __test_h_##_name( \
143 struct test_case_metadata *metadata __attribute__((unused)), \
144 struct test_file_metadata *file_metadata __attribute__((unused)))
145
146extern void __attribute__((weak)) (*test_h_unittest_setup)(void);
147/// Run defined tests, return true if all tests succeeds
148/// @param[out] tests_run if not NULL, set to whether tests were run
149static inline void __attribute__((constructor(102))) run_tests(void) {
150 bool should_run = false;
151#ifdef USE_SYSCTL_FOR_ARGS
152 int mib[] = {
153 CTL_KERN,
154#if defined(__NetBSD__) || defined(__OpenBSD__)
155 KERN_PROC_ARGS,
156 getpid(),
157 KERN_PROC_ARGV,
158#else
159 KERN_PROC,
160 KERN_PROC_ARGS,
161 getpid(),
162#endif
163 };
164 char *arg = NULL;
165 size_t arglen;
166 sysctl(mib, sizeof(mib) / sizeof(mib[0]), NULL, &arglen, NULL, 0);
167 arg = malloc(arglen);
168 sysctl(mib, sizeof(mib) / sizeof(mib[0]), arg, &arglen, NULL, 0);
169#else
170 FILE *cmdlinef = fopen("/proc/self/cmdline", "r");
171 char *arg = NULL;
172 int arglen;
173 fscanf(cmdlinef, "%ms%n", &arg, &arglen);
174 fclose(cmdlinef);
175#endif
176 for (char *pos = arg; pos < arg + arglen; pos += strlen(pos) + 1) {
177 if (strcmp(pos, "--unittest") == 0) {
178 should_run = true;
179 break;
180 }
181 }
182 free(arg);
183
184 if (!should_run) {
185 return;
186 }
187
188 if (&test_h_unittest_setup) {
189 test_h_unittest_setup();
190 }
191
192 struct test_file_metadata *i = test_file_head;
193 int failed = 0, success = 0;
194 while (i) {
195 fprintf(stderr, "Running tests from %s:\n", i->name);
196 struct test_case_metadata *j = i->tests;
197 while (j) {
198 fprintf(stderr, "\t%s ... ", j->name);
199 j->failure.present = false;
200 j->fn(j, i);
201 if (j->failure.present) {
202 fprintf(stderr, "failed (%s at %s:%d)\n", j->failure.message,
203 j->failure.file, j->failure.line);
204 if (j->failure.owned) {
205 free((char *)j->failure.message);
206 j->failure.message = NULL;
207 }
208 failed++;
209 } else {
210 fprintf(stderr, "passed\n");
211 success++;
212 }
213 j = j->next;
214 }
215 fprintf(stderr, "\n");
216 i = i->next;
217 }
218 int total = failed + success;
219 fprintf(stderr, "Test results: passed %d/%d, failed %d/%d\n", success, total,
220 failed, total);
221 exit(failed == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
222}
223
224#else
225
226#include <stdbool.h>
227
228#define TEST_CASE(name) static void __attribute__((unused)) __test_h_##name(void)
229
230#define TEST_EQUAL(a, b) \
231 (void)(a); \
232 (void)(b)
233#define TEST_NOTEQUAL(a, b) \
234 (void)(a); \
235 (void)(b)
236#define TEST_TRUE(a) (void)(a)
237#define TEST_STREQUAL(a, b) \
238 (void)(a); \
239 (void)(b)
240#define TEST_STRNEQUAL(a, b, len) \
241 (void)(a); \
242 (void)(b); \
243 (void)(len)
244#define TEST_STREQUAL3(str, expected, len) \
245 (void)(str); \
246 (void)(expected); \
247 (void)(len)
248#endif