diff options
| author | 3gg <3gg@shellblade.net> | 2025-12-27 12:00:21 -0800 |
|---|---|---|
| committer | 3gg <3gg@shellblade.net> | 2025-12-27 12:00:21 -0800 |
| commit | 07fb91b9571fc0add797cbcd0adcc8711401a2be (patch) | |
| tree | 2f7c2e7536e92c1fa86c902939ac47bf931da877 | |
| parent | 403730174aeaadcf7a8aad842bc5319050411ef9 (diff) | |
Enough stuff to draw a cube
| -rw-r--r-- | CMakeLists.txt | 6 | ||||
| -rw-r--r-- | include/swgfx.h | 12 | ||||
| -rw-r--r-- | src/swgfx.c | 278 | ||||
| -rw-r--r-- | test/test.c | 29 | ||||
| -rw-r--r-- | test/test.h | 248 |
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 | ||
| 25 | add_executable(swgfx-test | 25 | add_executable(swgfx-test |
| 26 | test/test.c) | 26 | test/test.c |
| 27 | test/test.h) | ||
| 27 | 28 | ||
| 28 | target_link_libraries(swgfx-test | 29 | target_link_libraries(swgfx-test |
| 29 | swgfx) | 30 | swgfx) |
| 30 | 31 | ||
| 31 | target_compile_options(swgfx-test PRIVATE -Wall -Wextra) | 32 | target_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 | /* |
| 2 | Software rendering library. | 2 | Software rendering library. |
| 3 | 3 | ||
| 4 | Cooridnate systems: | 4 | Coordinate 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; | |||
| 30 | typedef struct sgTri2 { sgVec2 p0, p1, p2; } sgTri2; | 30 | typedef struct sgTri2 { sgVec2 p0, p1, p2; } sgTri2; |
| 31 | typedef struct sgTri3 { sgVec3 p0, p1, p2; } sgTri3; | 31 | typedef struct sgTri3 { sgVec3 p0, p1, p2; } sgTri3; |
| 32 | 32 | ||
| 33 | typedef uint16_t sgIdx; | ||
| 34 | typedef struct sgVert { sgIdx position, normal, texcoord; } sgVert; | ||
| 35 | typedef struct sgTriIdx { sgVert v0, v1, v2; } sgTriIdx; | ||
| 36 | |||
| 33 | // TODO: Should we use real-valued colours? | 37 | // TODO: Should we use real-valued colours? |
| 34 | typedef struct sgPixel { uint8_t r, g, b, a; } sgPixel; | 38 | typedef struct sgPixel { uint8_t r, g, b, a; } sgPixel; |
| 35 | 39 | ||
| @@ -43,7 +47,9 @@ void sgDel(swgfx**); | |||
| 43 | void sgColourBuffer(swgfx*, sgVec2i dimensions, sgPixel* buffer); | 47 | void sgColourBuffer(swgfx*, sgVec2i dimensions, sgPixel* buffer); |
| 44 | void sgPresent (swgfx*, sgVec2i dimensions, sgPixel* screen); | 48 | void sgPresent (swgfx*, sgVec2i dimensions, sgPixel* screen); |
| 45 | 49 | ||
| 46 | void sgCam (swgfx*, sgVec3 position, sgVec3 forward); | 50 | void sgModelId (swgfx*); |
| 51 | void sgModel (swgfx*, sgVec3 position, sgVec3 right, sgVec3 up, sgVec3 forward); | ||
| 52 | void sgView (swgfx*, sgVec3 position, sgVec3 forward); | ||
| 47 | void sgOrtho (swgfx*, R left, R right, R top, R bottom, R near, R far); | 53 | void sgOrtho (swgfx*, R left, R right, R top, R bottom, R near, R far); |
| 48 | void sgPerspective(swgfx*, R fovy, R aspect, R near, R far); | 54 | void sgPerspective(swgfx*, R fovy, R aspect, R near, R far); |
| 49 | void sgViewport (swgfx*, int x0, int y0, int width, int height); | 55 | void sgViewport (swgfx*, int x0, int y0, int width, int height); |
| @@ -56,6 +62,8 @@ void sgTriangles2 (swgfx*, size_t count, const sgTri2*); | |||
| 56 | void sgTriangleStrip2(swgfx*, size_t count, const sgVec2*); | 62 | void sgTriangleStrip2(swgfx*, size_t count, const sgVec2*); |
| 57 | void sgTriangles (swgfx*, size_t count, const sgTri3*, const sgNormal*); | 63 | void sgTriangles (swgfx*, size_t count, const sgTri3*, const sgNormal*); |
| 58 | void sgTriangleStrip (swgfx*, size_t count, const sgVec3*, const sgNormal*); | 64 | void sgTriangleStrip (swgfx*, size_t count, const sgVec3*, const sgNormal*); |
| 65 | void sgTrianglesIndexed(swgfx*, size_t numIndices, const sgIdx* indices, const sgVec3* positions); | ||
| 66 | void sgTrianglesIndexedNonUniform(swgfx*, size_t numTris, const sgTriIdx* tris, const sgVec3* positions); | ||
| 59 | 67 | ||
| 60 | void sgCheck(swgfx*); | 68 | void 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: | |||
| 6 | Coordinate systems: | 6 | Coordinate 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 | ||
| 18 | static const sgVec3 Up3 = (sgVec3){0,1,0}; | 23 | static constexpr sgVec3 Up3 = (sgVec3){0,1,0}; |
| 19 | 24 | ||
| 20 | typedef struct sgViewport_t { int x0, y0, width, height; } sgViewport_t; | 25 | typedef struct sgViewport_t { int x0, y0, width, height; } sgViewport_t; |
| 21 | typedef struct sgTri2 { sgVec2 p0, p1, p2; } sgTri2; | 26 | typedef 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 | ||
| 37 | static inline sgVec3 neg3(sgVec3 v) { return (sgVec3){-v.x, -v.y, -v.z}; } | 52 | static 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 | ||
| 58 | static inline R dot3(sgVec3 a, sgVec3 b) { | ||
| 59 | return a.x * b.x + a.y * b.y + a.z * b.z; | ||
| 60 | } | ||
| 61 | |||
| 43 | static inline sgVec3 cross3(sgVec3 a, sgVec3 b) { | 62 | static 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 | ||
| 50 | static inline R normsq3(sgVec3 v) { return v.x * v.x + v.y * v.y + v.z * v.z; } | 69 | static inline R normsq3(sgVec3 v) { return v.x * v.x + v.y * v.y + v.z * v.z; } |
| 51 | 70 | static inline R norm3 (sgVec3 v) { return (R)sqrt(normsq3(v)); } | |
| 52 | static inline R norm3(sgVec3 v) { return sqrt(normsq3(v)); } | ||
| 53 | 71 | ||
| 54 | static inline sgVec3 normalize3(sgVec3 v) { | 72 | static 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 | ||
| 78 | static inline sgVec4 Vec4FromVec3(sgVec3 v, R w) { | ||
| 79 | return (sgVec4){v.x, v.y, v.z, w}; | ||
| 80 | } | ||
| 81 | |||
| 60 | static inline sgMat4 Mat4( | 82 | static 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 | ||
| 80 | static inline R Mat4At(sgMat4 m, int row, int col) { return m.val[col][row]; } | 102 | static inline R Mat4At(sgMat4 m, int row, int col) { return m.val[col][row]; } |
| 103 | static inline sgVec3 Mat4v0(sgMat4 m) { return *((sgVec3*)m.val[0]); } | ||
| 104 | static inline sgVec3 Mat4v1(sgMat4 m) { return *((sgVec3*)m.val[1]); } | ||
| 105 | static inline sgVec3 Mat4v2(sgMat4 m) { return *((sgVec3*)m.val[2]); } | ||
| 106 | static inline sgVec3 Mat4v3(sgMat4 m) { return *((sgVec3*)m.val[3]); } | ||
| 81 | 107 | ||
| 82 | static inline sgMat4 Mat4Mul(sgMat4 A, sgMat4 B) { | 108 | static 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 | ||
| 191 | static 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 | |||
| 204 | static 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 | |||
| 165 | static inline sgMat4 Mat4Look(sgVec3 position, sgVec3 forward, sgVec3 up) { | 216 | static 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 | ||
| 171 | static inline sgMat4 Mat4Perspective(R fovy, R aspect, R near, R far) { | 222 | static 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 | ||
| 183 | static inline sgPixel* PixelRow(sgPixel* image, int width, int y) { | 234 | #ifndef _NDEBUG |
| 184 | return image + (y * width); | 235 | static 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 | ||
| 187 | static inline sgPixel* Pixel(sgPixel* image, int width, int x, int y) { | 241 | static 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) | 246 | static inline R rmin(R a, R b) { return (a <= b) ? a : b; } |
| 192 | 247 | static inline R rmax(R a, R b) { return (a >= b) ? a : b; } | |
| 193 | static inline R rmin(R a, R b) { return (a <= b) ? a : b; } | 248 | static inline int imin(int a, int b) { return (a <= b) ? a : b; } |
| 194 | static inline R rmax(R a, R b) { return (a >= b) ? a : b; } | 249 | static inline int imax(int a, int b) { return (a >= b) ? a : b; } |
| 195 | |||
| 196 | static inline sgVec2 min2(sgVec2 a, sgVec2 b) { | 250 | static 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 | |||
| 200 | static inline sgVec2 max2(sgVec2 a, sgVec2 b) { | 253 | static 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 | } |
| 256 | static inline sgVec2i min2i(sgVec2i a, sgVec2i b) { | ||
| 257 | return (sgVec2i){.x = imin(a.x, b.x), .y = imin(a.y, b.y) }; | ||
| 258 | } | ||
| 259 | static 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 | ||
| 204 | static inline sgAABB2 TriangleAabb2(const sgTri2 tri) { | 263 | static 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 | ||
| 268 | static 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 | |||
| 209 | static inline R f(sgVec2 a, sgVec2 b, sgVec2 p) { | 276 | static 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 | ||
| 296 | static 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 | |||
| 326 | static 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. | ||
| 331 | static 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 | |||
| 337 | static inline sgVec2 ViewportToWindow(sgViewport_t vp, sgVec2 p) { | ||
| 338 | return (sgVec2){p.x, (R)vp.height - p.y}; | ||
| 339 | } | ||
| 340 | |||
| 341 | static 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 | |||
| 353 | static 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 | ||
| 231 | static size_t align(size_t size) { | 365 | static size_t align(size_t size) { |
| @@ -295,14 +429,44 @@ void sgPresent(swgfx* gfx, sgVec2i dimensions, sgPixel* screen) { | |||
| 295 | } | 429 | } |
| 296 | } | 430 | } |
| 297 | 431 | ||
| 298 | void sgCam(swgfx* gfx, sgVec3 position, sgVec3 forward) { | 432 | static void sgUpdateViewProjection(swgfx* gfx) { |
| 433 | assert(gfx); | ||
| 434 | gfx->viewProj = Mat4Mul(gfx->proj, gfx->view); | ||
| 435 | } | ||
| 436 | |||
| 437 | static void sgUpdateMvp(swgfx* gfx) { | ||
| 438 | assert(gfx); | ||
| 439 | gfx->mvp = Mat4Mul(gfx->viewProj, gfx->model); | ||
| 440 | } | ||
| 441 | |||
| 442 | void 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 | |||
| 451 | void 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 | |||
| 457 | void 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 | ||
| 303 | void sgPerspective(swgfx* gfx, R fovy, R aspect, R near, R far) { | 465 | void 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 | ||
| 308 | void sgViewport(swgfx* gfx, int x0, int y0, int width, int height) { | 472 | void sgViewport(swgfx* gfx, int x0, int y0, int width, int height) { |
| @@ -317,34 +481,13 @@ void sgClear(swgfx* gfx) { | |||
| 317 | 481 | ||
| 318 | void sgPixels(swgfx* gfx, size_t count, const sgVec2i* positions, sgPixel colour) { | 482 | void 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 | ||
| 326 | static 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 | ||
| 366 | void sgTriangles(swgfx* gfx, size_t count, const sgTri3* tris, const sgNormal*) { | 509 | void 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}, | 518 | void 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 | |||
| 534 | void 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 | ||
| 380 | static inline void AssertViewportWithinBuffer(swgfx* gfx) { | 548 | static 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 | ||
| 387 | void sgCheck(swgfx* gfx) { | 555 | void 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 | ||
| 27 | void ToRGB(int width, int height, const sgPixel* rgba, RGB* rgb) { | 29 | static 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 | ||
| 76 | int 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 | ||
| 91 | GFX_TEST(triangle, TestTriangle) | ||
| 92 | |||
| 93 | int 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 | |||
| 21 | struct test_file_metadata; | ||
| 22 | |||
| 23 | struct test_failure { | ||
| 24 | bool present; | ||
| 25 | const char *message; | ||
| 26 | const char *file; | ||
| 27 | int line; | ||
| 28 | bool owned; | ||
| 29 | }; | ||
| 30 | |||
| 31 | struct 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 | |||
| 38 | struct test_file_metadata { | ||
| 39 | bool registered; | ||
| 40 | const char *name; | ||
| 41 | struct test_file_metadata *next; | ||
| 42 | struct test_case_metadata *tests; | ||
| 43 | }; | ||
| 44 | |||
| 45 | struct 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 | |||
| 146 | extern 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 | ||
| 149 | static 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 | ||
