From 07fb91b9571fc0add797cbcd0adcc8711401a2be Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 27 Dec 2025 12:00:21 -0800 Subject: Enough stuff to draw a cube --- CMakeLists.txt | 6 +- include/swgfx.h | 12 ++- src/swgfx.c | 278 +++++++++++++++++++++++++++++++++++++++++++++----------- test/test.c | 29 +++--- test/test.h | 248 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 501 insertions(+), 72 deletions(-) create mode 100644 test/test.h 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) # Tests. add_executable(swgfx-test - test/test.c) + test/test.c + test/test.h) target_link_libraries(swgfx-test swgfx) -target_compile_options(swgfx-test PRIVATE -Wall -Wextra) - +target_compile_options(swgfx-test PRIVATE -DUNIT_TEST -DNDEBUG -Wall -Wextra) 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 @@ /* Software rendering library. -Cooridnate systems: +Coordinate systems: - Pixel coordinates (i,j) refer to the center of the pixel. Thus, real-valued coordinates (x,y) with no fractional part point at the pixel center. - Viewport origin is the top-left corner of the screen. @@ -30,6 +30,10 @@ typedef struct sgQuad { sgVec2 p0, p1; } sgQuad; typedef struct sgTri2 { sgVec2 p0, p1, p2; } sgTri2; typedef struct sgTri3 { sgVec3 p0, p1, p2; } sgTri3; +typedef uint16_t sgIdx; +typedef struct sgVert { sgIdx position, normal, texcoord; } sgVert; +typedef struct sgTriIdx { sgVert v0, v1, v2; } sgTriIdx; + // TODO: Should we use real-valued colours? typedef struct sgPixel { uint8_t r, g, b, a; } sgPixel; @@ -43,7 +47,9 @@ void sgDel(swgfx**); void sgColourBuffer(swgfx*, sgVec2i dimensions, sgPixel* buffer); void sgPresent (swgfx*, sgVec2i dimensions, sgPixel* screen); -void sgCam (swgfx*, sgVec3 position, sgVec3 forward); +void sgModelId (swgfx*); +void sgModel (swgfx*, sgVec3 position, sgVec3 right, sgVec3 up, sgVec3 forward); +void sgView (swgfx*, sgVec3 position, sgVec3 forward); void sgOrtho (swgfx*, R left, R right, R top, R bottom, R near, R far); void sgPerspective(swgfx*, R fovy, R aspect, R near, R far); void sgViewport (swgfx*, int x0, int y0, int width, int height); @@ -56,6 +62,8 @@ void sgTriangles2 (swgfx*, size_t count, const sgTri2*); void sgTriangleStrip2(swgfx*, size_t count, const sgVec2*); void sgTriangles (swgfx*, size_t count, const sgTri3*, const sgNormal*); void sgTriangleStrip (swgfx*, size_t count, const sgVec3*, const sgNormal*); +void sgTrianglesIndexed(swgfx*, size_t numIndices, const sgIdx* indices, const sgVec3* positions); +void sgTrianglesIndexedNonUniform(swgfx*, size_t numTris, const sgTriIdx* tris, const sgVec3* positions); void sgCheck(swgfx*); 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: Coordinate systems: - Right-handed. - NDC in [-1, +1]. + - Viewport goes up and to the right. + - Window goes down and to the right. + - (x,y) is the center of a pixel. + - Top-left: (x - 1/2, y - 1/2) + - Bottom-right: (x + 1/2, y + 1/2) */ #include @@ -15,7 +20,7 @@ Coordinate systems: #include #include -static const sgVec3 Up3 = (sgVec3){0,1,0}; +static constexpr sgVec3 Up3 = (sgVec3){0,1,0}; typedef struct sgViewport_t { int x0, y0, width, height; } sgViewport_t; typedef struct sgTri2 { sgVec2 p0, p1, p2; } sgTri2; @@ -30,8 +35,18 @@ typedef struct swgfx { sgVec2i dims; // Colour buffer dimensions. sgPixel* colour; // Colour buffer. sgViewport_t viewport; + sgMat4 model; // Model matrix. sgMat4 view; // View matrix. sgMat4 proj; // Projection matrix. + // Pre-multiplied matrices. + // The model matrix changes once per object, more frequently than view or + // projection. View and projection are expected to change infrequently, maybe + // once per frame. + // Make it so that changing the model matrix only requires one matrix + // multiplication (mvp = model * viewProj) and not two (mvp = model * view * projection) + // before rendering the model's triangles. + sgMat4 viewProj; // View-projection matrix. + sgMat4 mvp; // Model-view-projection matrix. } swgfx; 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) { return (sgVec3){a.x - b.x, a.y - b.y, a.z - b.z}; } +static inline R dot3(sgVec3 a, sgVec3 b) { + return a.x * b.x + a.y * b.y + a.z * b.z; +} + static inline sgVec3 cross3(sgVec3 a, sgVec3 b) { return (sgVec3) { a.y * b.z - a.z * b.y, @@ -48,8 +67,7 @@ static inline sgVec3 cross3(sgVec3 a, sgVec3 b) { } static inline R normsq3(sgVec3 v) { return v.x * v.x + v.y * v.y + v.z * v.z; } - -static inline R norm3(sgVec3 v) { return sqrt(normsq3(v)); } +static inline R norm3 (sgVec3 v) { return (R)sqrt(normsq3(v)); } static inline sgVec3 normalize3(sgVec3 v) { const R n = norm3(v); @@ -57,6 +75,10 @@ static inline sgVec3 normalize3(sgVec3 v) { return (sgVec3){v.x / n, v.y / n, v.z / n}; } +static inline sgVec4 Vec4FromVec3(sgVec3 v, R w) { + return (sgVec4){v.x, v.y, v.z, w}; +} + static inline sgMat4 Mat4( R m00, R m01, R m02, R m03, // v0.x v1.x v2.x v3.x 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 } static inline R Mat4At(sgMat4 m, int row, int col) { return m.val[col][row]; } +static inline sgVec3 Mat4v0(sgMat4 m) { return *((sgVec3*)m.val[0]); } +static inline sgVec3 Mat4v1(sgMat4 m) { return *((sgVec3*)m.val[1]); } +static inline sgVec3 Mat4v2(sgMat4 m) { return *((sgVec3*)m.val[2]); } +static inline sgVec3 Mat4v3(sgMat4 m) { return *((sgVec3*)m.val[3]); } static inline sgMat4 Mat4Mul(sgMat4 A, sgMat4 B) { R m00 = Mat4At(A, 0, 0) * Mat4At(B, 0, 0) + @@ -162,6 +188,31 @@ static inline sgVec3 Mat4MulVec3(sgMat4 m, sgVec3 v, R w) { .z = Mat4At(m, 2, 0) * v.x + Mat4At(m, 2, 1) * v.y + Mat4At(m, 2, 2) * v.z + Mat4At(m, 2, 3) * w}; } +static inline sgVec4 Mat4MulVec4(sgMat4 m, sgVec4 v) { + sgVec4 u; + u.x = Mat4At(m, 0, 0) * v.x + Mat4At(m, 0, 1) * v.y + + Mat4At(m, 0, 2) * v.z + Mat4At(m, 0, 3) * v.w; + u.y = Mat4At(m, 1, 0) * v.x + Mat4At(m, 1, 1) * v.y + + Mat4At(m, 1, 2) * v.z + Mat4At(m, 1, 3) * v.w; + u.z = Mat4At(m, 2, 0) * v.x + Mat4At(m, 2, 1) * v.y + + Mat4At(m, 2, 2) * v.z + Mat4At(m, 2, 3) * v.w; + u.w = Mat4At(m, 3, 0) * v.x + Mat4At(m, 3, 1) * v.y + + Mat4At(m, 3, 2) * v.z + Mat4At(m, 3, 3) * v.w; + return u; +} + +static inline sgMat4 Mat4InverseTransform(sgMat4 m) { + const sgVec3 r = Mat4v0(m); + const sgVec3 u = Mat4v1(m); + const sgVec3 f = Mat4v2(m); + const sgVec3 t = Mat4v3(m); + return Mat4( + r.x, r.y, r.z, -dot3(r, t), + u.x, u.y, u.z, -dot3(u, t), + f.x, f.y, f.z, -dot3(f, t), + 0.f, 0.f, 0.f, 1.f); +} + static inline sgMat4 Mat4Look(sgVec3 position, sgVec3 forward, sgVec3 up) { const sgVec3 right = normalize3(cross3(forward, up)); up = normalize3(cross3(right, forward)); @@ -169,43 +220,59 @@ static inline sgMat4 Mat4Look(sgVec3 position, sgVec3 forward, sgVec3 up) { } static inline sgMat4 Mat4Perspective(R fovy, R aspect, R near, R far) { - R f = tan(fovy / 2.0); + R f = (R)tan(fovy / 2.0); assert(f > 0.0); - f = 1.0 / f; + f = 1.f / f; const R a = near - far; return Mat4( - f / aspect, 0, 0, 0, - 0, f, 0, 0, - 0, 0, (far + near) / a, (2 * far * near / a), - 0, 0, -1, 0); + f / aspect, 0, 0, 0, + 0, f, 0, 0, + 0, 0, (far + near) / a, (2 * far * near / a), + 0, 0, -1, 0); } -static inline sgPixel* PixelRow(sgPixel* image, int width, int y) { - return image + (y * width); +#ifndef _NDEBUG +static bool InBounds(int width, int height, sgVec2i p) { + return (0 <= p.x) && (p.x < width) && + (0 <= p.y) && (p.y < height); } +#endif // _NDEBUG -static inline sgPixel* Pixel(sgPixel* image, int width, int x, int y) { +static inline sgPixel* Pixel(sgPixel* image, int width, int height, int x, int y) { + assert(InBounds(width, height, (sgVec2i){x,y})); return image + (y * width) + x; } -#define XY(X,Y) Pixel(gfx->colour, gfx->dims.x, X, Y) - -static inline R rmin(R a, R b) { return (a <= b) ? a : b; } -static inline R rmax(R a, R b) { return (a >= b) ? a : b; } - +static inline R rmin(R a, R b) { return (a <= b) ? a : b; } +static inline R rmax(R a, R b) { return (a >= b) ? a : b; } +static inline int imin(int a, int b) { return (a <= b) ? a : b; } +static inline int imax(int a, int b) { return (a >= b) ? a : b; } static inline sgVec2 min2(sgVec2 a, sgVec2 b) { return (sgVec2){.x = rmin(a.x, b.x), .y = rmin(a.y, b.y) }; } - static inline sgVec2 max2(sgVec2 a, sgVec2 b) { return (sgVec2){.x = rmax(a.x, b.x), .y = rmax(a.y, b.y) }; } +static inline sgVec2i min2i(sgVec2i a, sgVec2i b) { + return (sgVec2i){.x = imin(a.x, b.x), .y = imin(a.y, b.y) }; +} +static inline sgVec2i max2i(sgVec2i a, sgVec2i b) { + return (sgVec2i){.x = imax(a.x, b.x), .y = imax(a.y, b.y) }; +} static inline sgAABB2 TriangleAabb2(const sgTri2 tri) { return (sgAABB2){.pmin = min2(min2(tri.p0, tri.p1), tri.p2), .pmax = max2(max2(tri.p0, tri.p1), tri.p2)}; } +static inline sgVec2i Clip(const swgfx* gfx, const sgVec2i p) { + assert(gfx); + constexpr sgVec2i lower = (sgVec2i){0,0}; + const sgVec2i upper = (sgVec2i){gfx->viewport.width - 1, + gfx->viewport.height - 1}; + return max2i(lower, min2i(upper, p)); +} + static inline R f(sgVec2 a, sgVec2 b, sgVec2 p) { return (a.y - b.y)*p.x + (b.x - a.x)*p.y + a.x*b.y - b.x*a.y; } @@ -222,10 +289,77 @@ static inline sgVec3 Barycentric(const sgTri2 tri, sgVec2 p) { f(tri.p0, tri.p1, p) / f(tri.p0, tri.p1, tri.p2)};*/ const R b = f(tri.p0, tri.p2, p) / f(tri.p0, tri.p2, tri.p1); const R c = f(tri.p0, tri.p1, p) / f(tri.p0, tri.p1, tri.p2); - const R a = /*f(tri.p1, tri.p2, p) / f(tri.p1, tri.p2, tri.p0);*/1 - b - c - 1e-7; + const R a = /*f(tri.p1, tri.p2, p) / f(tri.p1, tri.p2, tri.p0);*/1.f - b - c - (R)1e-7; return (sgVec3){a,b,c}; } +static void DrawTriangle2(swgfx* gfx, const sgTri2* tri) { + assert(gfx); + assert(tri); + const sgAABB2 bbox = TriangleAabb2(*tri); + // We consider (x,y) to be the pixel center. + // Draw all pixels touched by the bounding box. TODO: Multi-sampling. + sgVec2i pmin = (sgVec2i){(int)bbox.pmin.x, (int)bbox.pmin.y}; + sgVec2i pmax = (sgVec2i){(int)(bbox.pmax.x + 0.5f), (int)(bbox.pmax.y + 0.5f)}; + // Clip to screen space. + pmin = Clip(gfx, pmin); + pmax = Clip(gfx, pmax); + // Draw. + for (int y = pmin.y; y <= pmax.y; ++y) { + for (int x = pmin.x; x <= pmax.x; ++x) { + const sgVec2 p = (sgVec2){(R)x, (R)y}; + // TODO: there is an incremental optimization to computing barycentric coordinates; + // read more about it. + const sgVec3 bar = Barycentric(*tri, p); + // We need to check the third coordinate. + // a + b + c = 1 + // So, e.g., if a >= 0 and b >= 0, then we have c <= 1, but we could also have c <= 0. + // In the case c <= 0, then point is outside the triangle. + if ((bar.x >= 0) && (bar.y >= 0) && (bar.z >= 0)) { + const sgVec2i pi = (sgVec2i){(int)x, (int)y}; + sgPixels(gfx, 1, &pi, (sgPixel){255, 255, 255, 255}); + } + } + } +} + +static inline sgVec3 PerspDivide(sgVec4 v) { + return (sgVec3){v.x / v.w, v.y / v.w, v.z / v.w}; +} + +// TODO: Compute a viewport matrix in sgViewport() instead. +static inline sgVec2 ViewportTransform(sgViewport_t vp, sgVec3 ndc) { + return (sgVec2){ + .x = (ndc.x+1.f) * ((R)vp.width/2.f) + (R)vp.x0, + .y = (ndc.y+1.f) * ((R)vp.height/2.f) + (R)vp.y0}; +} + +static inline sgVec2 ViewportToWindow(sgViewport_t vp, sgVec2 p) { + return (sgVec2){p.x, (R)vp.height - p.y}; +} + +static inline sgVec2 TransformPosition(const swgfx* gfx, sgVec3 p) { + assert(gfx); + // Model to clip space. + const sgVec4 p_clip = Mat4MulVec4(gfx->mvp, Vec4FromVec3(p, 1)); + // TODO: Backface culling. + // Perspective divide. + const sgVec3 p_ndc = PerspDivide(p_clip); + // TODO: Clip. + const sgVec2 p_vp = ViewportTransform(gfx->viewport, p_ndc); + return ViewportToWindow(gfx->viewport, p_vp); +} + +static void DrawTriangle3(swgfx* gfx, const sgTri3* tri) { + assert(gfx); + assert(tri); + const sgVec2 p0 = TransformPosition(gfx, tri->p0); + const sgVec2 p1 = TransformPosition(gfx, tri->p1); + const sgVec2 p2 = TransformPosition(gfx, tri->p2); + const sgTri2 tri2 = (sgTri2){p0, p1, p2}; + DrawTriangle2(gfx, &tri2); +} + #define is_pow2_or_0(X) ((X & (X - 1)) == 0) static size_t align(size_t size) { @@ -295,14 +429,44 @@ void sgPresent(swgfx* gfx, sgVec2i dimensions, sgPixel* screen) { } } -void sgCam(swgfx* gfx, sgVec3 position, sgVec3 forward) { +static void sgUpdateViewProjection(swgfx* gfx) { + assert(gfx); + gfx->viewProj = Mat4Mul(gfx->proj, gfx->view); +} + +static void sgUpdateMvp(swgfx* gfx) { + assert(gfx); + gfx->mvp = Mat4Mul(gfx->viewProj, gfx->model); +} + +void sgModelId(swgfx* gfx) { assert(gfx); - gfx->view = Mat4Look(position, forward, Up3); + sgModel(gfx, + (sgVec3){0,0,0}, + (sgVec3){1, 0, 0}, + (sgVec3){0, 1, 0}, + (sgVec3){0, 0, 1}); +} + +void sgModel(swgfx* gfx, sgVec3 position, sgVec3 right, sgVec3 up, sgVec3 forward) { + assert(gfx); + gfx->model = Mat4FromVec3(right, up, forward, position); + sgUpdateMvp(gfx); +} + +void sgView(swgfx* gfx, sgVec3 position, sgVec3 forward) { + assert(gfx); + const sgMat4 camera = Mat4Look(position, forward, Up3); + gfx->view = Mat4InverseTransform(camera); + sgUpdateViewProjection(gfx); + sgUpdateMvp(gfx); } void sgPerspective(swgfx* gfx, R fovy, R aspect, R near, R far) { assert(gfx); gfx->proj = Mat4Perspective(fovy, aspect, near, far); + sgUpdateViewProjection(gfx); + sgUpdateMvp(gfx); } void sgViewport(swgfx* gfx, int x0, int y0, int width, int height) { @@ -317,34 +481,13 @@ void sgClear(swgfx* gfx) { void sgPixels(swgfx* gfx, size_t count, const sgVec2i* positions, sgPixel colour) { assert(gfx); +#define XY(X,Y) Pixel(gfx->colour, gfx->dims.x, gfx->dims.y, X, Y) for (size_t i = 0; i < count; ++i) { const sgVec2i p = positions[i]; *XY(p.x, p.y) = colour; } } -static void DrawTriangle2(swgfx* gfx, const sgTri2* tri) { - assert(gfx); - assert(tri); - const sgAABB2 bbox = TriangleAabb2(*tri); - for (int y = bbox.pmin.y; y <= bbox.pmax.y; ++y) { - for (int x = bbox.pmin.x; x <= bbox.pmax.x; ++x) { - const sgVec2 p = (sgVec2){x, y}; - // TODO: there is an incremental optimization to computing barycentric coordinates; - // read more about it. - const sgVec3 bar = Barycentric(*tri, p); - // We need to check the third coordinate. - // a + b + c = 1 - // So, e.g., if a > 0 and b > 0, then we have c < 1, but we could also have c < 0. - // In the case c < 0, then point is outside the triangle. - if ((bar.x > 0) && (bar.y > 0) && (bar.z > 0)) { - const sgVec2i pi = (sgVec2i){(int)x, (int)y}; - sgPixels(gfx, 1, &pi, (sgPixel){255, 255, 255, 255}); - } - } - } -} - // TODO: DrawTriangle3 with clipping. Leave DrawTriangle2 to not clip for // performance; assume that 2D triangles are within bounds. // TODO: If the triangle is out of bounds, skip entirely. @@ -365,26 +508,51 @@ void sgTriangles2(swgfx* gfx, size_t count, const sgTri2* tris) { void sgTriangles(swgfx* gfx, size_t count, const sgTri3* tris, const sgNormal*) { assert(gfx); + assert(tris); for (size_t i = 0; i < count; ++i) { - // Ignore projection matrix for now. Rasterize 2D triangles. - const sgTri3* tri3 = &tris[i]; - const sgTri2 tri2 = (sgTri2) { - .p0 = (sgVec2){tri3->p0.x, tri3->p0.y}, - .p1 = (sgVec2){tri3->p1.x, tri3->p1.y}, - .p2 = (sgVec2){tri3->p2.x, tri3->p2.y}, - }; - DrawTriangle2(gfx, &tri2); + const sgTri3* tri = &tris[i]; + DrawTriangle3(gfx, tri); + } +} + +void sgTrianglesIndexed(swgfx* gfx, size_t numIndices, const sgIdx* indices, const sgVec3* positions) { + assert(gfx); + assert(indices); + assert(positions); + for (size_t i = 0; i < numIndices; i+=3) { + const sgIdx i0 = indices[i]; + const sgIdx i1 = indices[i+1]; + const sgIdx i2 = indices[i+2]; + const sgVec3 p0 = positions[i0]; + const sgVec3 p1 = positions[i1]; + const sgVec3 p2 = positions[i2]; + const sgTri3 tri = (sgTri3){p0, p1, p2}; + DrawTriangle3(gfx, &tri); + } +} + +void sgTrianglesIndexedNonUniform(swgfx* gfx, size_t numTris, const sgTriIdx* tris, const sgVec3* positions) { + assert(gfx); + assert(tris); + assert(positions); + for (size_t t = 0; t < numTris; ++t) { + const sgTriIdx* triIdx = &tris[t]; + const sgTri3 tri = (sgTri3){ + positions[triIdx->v0.position], + positions[triIdx->v1.position], + positions[triIdx->v2.position]}; + DrawTriangle3(gfx, &tri); } } -static inline void AssertViewportWithinBuffer(swgfx* gfx) { +static bool ViewportWithinBuffer(swgfx* gfx) { assert(gfx); const sgViewport_t vp = gfx->viewport; - assert((vp.x0 + vp.width) <= gfx->dims.x); - assert((vp.y0 + vp.height) <= gfx->dims.y); + return ((vp.x0 + vp.width) <= gfx->dims.x) && + ((vp.y0 + vp.height) <= gfx->dims.y); } void sgCheck(swgfx* gfx) { assert(gfx); - AssertViewportWithinBuffer(gfx); + assert(ViewportWithinBuffer(gfx)); } 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 @@ +#include "test.h" + #include #include @@ -24,7 +26,7 @@ static bool WritePPM(int width, int height, const RGB* image, const char* path) return true; } -void ToRGB(int width, int height, const sgPixel* rgba, RGB* rgb) { +static void ToRGB(int width, int height, const sgPixel* rgba, RGB* rgb) { assert(rgba); assert(rgb); for (int y = 0; y < height; ++y) { @@ -71,18 +73,21 @@ static void TestTriangle(swgfx* gfx) { ToRGB(BufferWidth, BufferHeight, colour, rgb); WritePPM(BufferWidth, BufferHeight, rgb, "triangle.ppm"); + // TODO: Assert the contents. Turn this file into a unit test executable. + // Write a helper function that first writes the image to a file and then + // asserts its contents. } -int main() { - swgfx* gfx = 0; - if (!(gfx = sgNew())) { - fprintf(stderr, "Failed to create swgfx\n"); - return 1; +#define GFX_TEST(NAME, FUNC) \ + TEST_CASE(NAME) {\ + swgfx* gfx = nullptr;\ + if (!(gfx = sgNew())) {\ + SET_FAILURE("Failed to create swgfx\n", false);\ + }\ + FUNC(gfx);\ + sgDel(&gfx);\ } - - TestTriangle(gfx); - - sgDel(&gfx); - return 0; -} +GFX_TEST(triangle, TestTriangle) + +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 @@ +// SPDX-License-Identifier: MIT +#pragma once + +#ifdef UNIT_TEST + +#include +#include +#include +#include + +#if defined(__DragonFly__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || \ + defined(__NetBSD__) || defined(__OpenBSD__) +#define USE_SYSCTL_FOR_ARGS 1 +// clang-format off +#include +#include +// clang-format on +#include // getpid +#endif + +struct test_file_metadata; + +struct test_failure { + bool present; + const char *message; + const char *file; + int line; + bool owned; +}; + +struct test_case_metadata { + void (*fn)(struct test_case_metadata *, struct test_file_metadata *); + struct test_failure failure; + const char *name; + struct test_case_metadata *next; +}; + +struct test_file_metadata { + bool registered; + const char *name; + struct test_file_metadata *next; + struct test_case_metadata *tests; +}; + +struct test_file_metadata __attribute__((weak)) * test_file_head; + +#define SET_FAILURE(_message, _owned) \ + metadata->failure = (struct test_failure) { \ + .present = true, \ + .message = _message, \ + .file = __FILE__, \ + .line = __LINE__, \ + .owned = _owned, \ + } + +#define TEST_EQUAL(a, b) \ + do { \ + if ((a) != (b)) { \ + SET_FAILURE(#a " != " #b, false); \ + return; \ + } \ + } while (0) + +#define TEST_NOTEQUAL(a, b) \ + do { \ + if ((a) == (b)) { \ + SET_FAILURE(#a " == " #b, false); \ + return; \ + } \ + } while (0) + +#define TEST_TRUE(a) \ + do { \ + if (!(a)) { \ + SET_FAILURE(#a " is not true", false); \ + return; \ + } \ + } while (0) + +#define TEST_STREQUAL(a, b) \ + do { \ + if (strcmp(a, b) != 0) { \ + const char *test_strequal__part2 = " != " #b; \ + size_t test_strequal__len = \ + strlen(a) + strlen(test_strequal__part2) + 3; \ + char *test_strequal__buf = malloc(test_strequal__len); \ + snprintf(test_strequal__buf, test_strequal__len, "\"%s\"%s", a, \ + test_strequal__part2); \ + SET_FAILURE(test_strequal__buf, true); \ + return; \ + } \ + } while (0) + +#define TEST_STRNEQUAL(a, b, len) \ + do { \ + if (strncmp(a, b, len) != 0) { \ + const char *test_strnequal__part2 = " != " #b; \ + size_t test_strnequal__len2 = \ + len + strlen(test_strnequal__part2) + 3; \ + char *test_strnequal__buf = malloc(test_strnequal__len2); \ + snprintf(test_strnequal__buf, test_strnequal__len2, \ + "\"%.*s\"%s", (int)len, a, test_strnequal__part2); \ + SET_FAILURE(test_strnequal__buf, true); \ + return; \ + } \ + } while (0) + +#define TEST_STREQUAL3(str, expected, len) \ + do { \ + if (len != strlen(expected) || strncmp(str, expected, len) != 0) { \ + const char *test_strequal3__part2 = " != " #expected; \ + size_t test_strequal3__len2 = \ + len + strlen(test_strequal3__part2) + 3; \ + char *test_strequal3__buf = malloc(test_strequal3__len2); \ + snprintf(test_strequal3__buf, test_strequal3__len2, \ + "\"%.*s\"%s", (int)len, str, test_strequal3__part2); \ + SET_FAILURE(test_strequal3__buf, true); \ + return; \ + } \ + } while (0) + +#define TEST_CASE(_name) \ + static void __test_h_##_name(struct test_case_metadata *, \ + struct test_file_metadata *); \ + static struct test_file_metadata __test_h_file##_name; \ + static struct test_case_metadata __test_h_meta_##_name = { \ + .fn = __test_h_##_name, \ + .failure = {}, \ + .name = #_name, \ + .next = 0, \ + }; \ + static void __attribute__((constructor(101))) __test_h_##_name##_register(void) { \ + __test_h_meta_##_name.next = __test_h_file##_name.tests; \ + __test_h_file##_name.tests = &__test_h_meta_##_name; \ + if (!__test_h_file##_name.registered) { \ + __test_h_file##_name.name = __FILE__; \ + __test_h_file##_name.next = test_file_head; \ + test_file_head = &__test_h_file##_name; \ + __test_h_file##_name.registered = true; \ + } \ + } \ + static void __test_h_##_name( \ + struct test_case_metadata *metadata __attribute__((unused)), \ + struct test_file_metadata *file_metadata __attribute__((unused))) + +extern void __attribute__((weak)) (*test_h_unittest_setup)(void); +/// Run defined tests, return true if all tests succeeds +/// @param[out] tests_run if not NULL, set to whether tests were run +static inline void __attribute__((constructor(102))) run_tests(void) { + bool should_run = false; +#ifdef USE_SYSCTL_FOR_ARGS + int mib[] = { + CTL_KERN, +#if defined(__NetBSD__) || defined(__OpenBSD__) + KERN_PROC_ARGS, + getpid(), + KERN_PROC_ARGV, +#else + KERN_PROC, + KERN_PROC_ARGS, + getpid(), +#endif + }; + char *arg = NULL; + size_t arglen; + sysctl(mib, sizeof(mib) / sizeof(mib[0]), NULL, &arglen, NULL, 0); + arg = malloc(arglen); + sysctl(mib, sizeof(mib) / sizeof(mib[0]), arg, &arglen, NULL, 0); +#else + FILE *cmdlinef = fopen("/proc/self/cmdline", "r"); + char *arg = NULL; + int arglen; + fscanf(cmdlinef, "%ms%n", &arg, &arglen); + fclose(cmdlinef); +#endif + for (char *pos = arg; pos < arg + arglen; pos += strlen(pos) + 1) { + if (strcmp(pos, "--unittest") == 0) { + should_run = true; + break; + } + } + free(arg); + + if (!should_run) { + return; + } + + if (&test_h_unittest_setup) { + test_h_unittest_setup(); + } + + struct test_file_metadata *i = test_file_head; + int failed = 0, success = 0; + while (i) { + fprintf(stderr, "Running tests from %s:\n", i->name); + struct test_case_metadata *j = i->tests; + while (j) { + fprintf(stderr, "\t%s ... ", j->name); + j->failure.present = false; + j->fn(j, i); + if (j->failure.present) { + fprintf(stderr, "failed (%s at %s:%d)\n", j->failure.message, + j->failure.file, j->failure.line); + if (j->failure.owned) { + free((char *)j->failure.message); + j->failure.message = NULL; + } + failed++; + } else { + fprintf(stderr, "passed\n"); + success++; + } + j = j->next; + } + fprintf(stderr, "\n"); + i = i->next; + } + int total = failed + success; + fprintf(stderr, "Test results: passed %d/%d, failed %d/%d\n", success, total, + failed, total); + exit(failed == 0 ? EXIT_SUCCESS : EXIT_FAILURE); +} + +#else + +#include + +#define TEST_CASE(name) static void __attribute__((unused)) __test_h_##name(void) + +#define TEST_EQUAL(a, b) \ + (void)(a); \ + (void)(b) +#define TEST_NOTEQUAL(a, b) \ + (void)(a); \ + (void)(b) +#define TEST_TRUE(a) (void)(a) +#define TEST_STREQUAL(a, b) \ + (void)(a); \ + (void)(b) +#define TEST_STRNEQUAL(a, b, len) \ + (void)(a); \ + (void)(b); \ + (void)(len) +#define TEST_STREQUAL3(str, expected, len) \ + (void)(str); \ + (void)(expected); \ + (void)(len) +#endif -- cgit v1.2.3