From 403730174aeaadcf7a8aad842bc5319050411ef9 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sun, 16 Nov 2025 18:23:37 -0800 Subject: Initial commit --- CMakeLists.txt | 32 +++++ include/swgfx.h | 68 ++++++++++ src/swgfx.c | 390 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/test.c | 88 +++++++++++++ 4 files changed, 578 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 include/swgfx.h create mode 100644 src/swgfx.c create mode 100644 test/test.c diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..537c00d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.30) + +project(swgfx) + +set(CMAKE_C_STANDARD 23) +set(CMAKE_C_STANDARD_REQUIRED On) +set(CMAKE_C_EXTENSIONS Off) + +# Library. + +add_library(swgfx STATIC + include/swgfx.h + src/swgfx.c) + +target_include_directories(swgfx PUBLIC + include/) + +target_link_libraries(swgfx PRIVATE + math) + +target_compile_options(swgfx PRIVATE -Wall -Wextra) + +# Tests. + +add_executable(swgfx-test + test/test.c) + +target_link_libraries(swgfx-test + swgfx) + +target_compile_options(swgfx-test PRIVATE -Wall -Wextra) + diff --git a/include/swgfx.h b/include/swgfx.h new file mode 100644 index 0000000..b6dc769 --- /dev/null +++ b/include/swgfx.h @@ -0,0 +1,68 @@ +/* +Software rendering library. + +Cooridnate 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. + The viewport axes extend down and to the right. + +Multi-threading: +- Internal resources (swgfx context) are externally synchronized. +- External resources (colour buffer) are internally synchronized. +*/ +#pragma once + +#include +#include + +typedef float R; + +typedef struct sgVec2i { int x, y; } sgVec2i; +typedef struct sgVec2 { R x, y; } sgVec2; +typedef struct sgVec3 { R x, y, z; } sgVec3; +typedef struct sgVec4 { R x, y, z, w; } sgVec4; + +typedef sgVec3 sgNormal; + +typedef struct sgQuadi { sgVec2i p0, p1; } sgQuadi; +typedef struct sgQuad { sgVec2 p0, p1; } sgQuad; +typedef struct sgTri2 { sgVec2 p0, p1, p2; } sgTri2; +typedef struct sgTri3 { sgVec3 p0, p1, p2; } sgTri3; + +// TODO: Should we use real-valued colours? +typedef struct sgPixel { uint8_t r, g, b, a; } sgPixel; + +typedef struct swgfx swgfx; + +swgfx* sgNew(); +void sgDel(swgfx**); + +// TODO: Write client app first, then implement the functions below in the C file. + +void sgColourBuffer(swgfx*, sgVec2i dimensions, sgPixel* buffer); +void sgPresent (swgfx*, sgVec2i dimensions, sgPixel* screen); + +void sgCam (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); + +void sgClear(swgfx*); +void sgPixels(swgfx*, size_t count, const sgVec2i* positions, sgPixel colour); +void sgQuads (swgfx*, size_t count, const sgQuad*); +void sgQuadsi(swgfx*, size_t count, const sgQuadi*); +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 sgCheck(swgfx*); + +// Memory +#define SG_ALIGN 64 +#define SG_ALIGN_PTR(P) ((uintptr_t)(P) & (~(SG_ALIGN-1))) +#define SG_ALIGN_ALLOC(COUNT, TYPE) (TYPE*)sgAlloc(1, COUNT * sizeof(TYPE)) +#define SG_FREE(PP) sgFree((void**)PP) +void* sgAlloc(size_t count, size_t size); +void sgFree(void**); diff --git a/src/swgfx.c b/src/swgfx.c new file mode 100644 index 0000000..772a691 --- /dev/null +++ b/src/swgfx.c @@ -0,0 +1,390 @@ +/* +Matrices: + - Column-major math convention. + - Column-major memory storage. + +Coordinate systems: + - Right-handed. + - NDC in [-1, +1]. +*/ +#include + +#include +#include // sqrt +#include +#include +#include + +static const 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; +typedef struct sgAABB2 { sgVec2 pmin, pmax; } sgAABB2; + +// Column-major math, column-major storage. +typedef struct sgMat4 { + R val[4][4]; // (col, row) +} sgMat4; + +typedef struct swgfx { + sgVec2i dims; // Colour buffer dimensions. + sgPixel* colour; // Colour buffer. + sgViewport_t viewport; + sgMat4 view; // View matrix. + sgMat4 proj; // Projection matrix. +} swgfx; + +static inline sgVec3 neg3(sgVec3 v) { return (sgVec3){-v.x, -v.y, -v.z}; } + +static inline sgVec3 sub3(sgVec3 a, sgVec3 b) { + return (sgVec3){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, + a.z * b.x - a.x * b.z, + a.x * b.y - a.y * b.x}; +} + +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 sgVec3 normalize3(sgVec3 v) { + const R n = norm3(v); + assert(n > 0); + return (sgVec3){v.x / n, v.y / n, v.z / n}; +} + +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 + R m20, R m21, R m22, R m23, // v0.z v1.z v2.z v3.z + R m30, R m31, R m32, R m33) { // v0.w v1.w v2.w v3.w + return (sgMat4) { + .val = {{m00, m10, m20, m30}, // col 0 + {m01, m11, m21, m31}, // col 1 + {m02, m12, m22, m32}, // col 2 + {m03, m13, m23, m33}}}; // col 3 +} + +static inline sgMat4 Mat4FromVec3(sgVec3 right, sgVec3 up, sgVec3 forward, sgVec3 position) { + return Mat4( + right.x, up.x, forward.x, position.x, + right.y, up.y, forward.y, position.y, + right.z, up.z, forward.z, position.z, + 0, 0, 0, 1); +} + +static inline R Mat4At(sgMat4 m, int row, int col) { return m.val[col][row]; } + +static inline sgMat4 Mat4Mul(sgMat4 A, sgMat4 B) { + R m00 = Mat4At(A, 0, 0) * Mat4At(B, 0, 0) + + Mat4At(A, 0, 1) * Mat4At(B, 1, 0) + + Mat4At(A, 0, 2) * Mat4At(B, 2, 0) + + Mat4At(A, 0, 3) * Mat4At(B, 3, 0); + R m01 = Mat4At(A, 0, 0) * Mat4At(B, 0, 1) + + Mat4At(A, 0, 1) * Mat4At(B, 1, 1) + + Mat4At(A, 0, 2) * Mat4At(B, 2, 1) + + Mat4At(A, 0, 3) * Mat4At(B, 3, 1); + R m02 = Mat4At(A, 0, 0) * Mat4At(B, 0, 2) + + Mat4At(A, 0, 1) * Mat4At(B, 1, 2) + + Mat4At(A, 0, 2) * Mat4At(B, 2, 2) + + Mat4At(A, 0, 3) * Mat4At(B, 3, 2); + R m03 = Mat4At(A, 0, 0) * Mat4At(B, 0, 3) + + Mat4At(A, 0, 1) * Mat4At(B, 1, 3) + + Mat4At(A, 0, 2) * Mat4At(B, 2, 3) + + Mat4At(A, 0, 3) * Mat4At(B, 3, 3); + + R m10 = Mat4At(A, 1, 0) * Mat4At(B, 0, 0) + + Mat4At(A, 1, 1) * Mat4At(B, 1, 0) + + Mat4At(A, 1, 2) * Mat4At(B, 2, 0) + + Mat4At(A, 1, 3) * Mat4At(B, 3, 0); + R m11 = Mat4At(A, 1, 0) * Mat4At(B, 0, 1) + + Mat4At(A, 1, 1) * Mat4At(B, 1, 1) + + Mat4At(A, 1, 2) * Mat4At(B, 2, 1) + + Mat4At(A, 1, 3) * Mat4At(B, 3, 1); + R m12 = Mat4At(A, 1, 0) * Mat4At(B, 0, 2) + + Mat4At(A, 1, 1) * Mat4At(B, 1, 2) + + Mat4At(A, 1, 2) * Mat4At(B, 2, 2) + + Mat4At(A, 1, 3) * Mat4At(B, 3, 2); + R m13 = Mat4At(A, 1, 0) * Mat4At(B, 0, 3) + + Mat4At(A, 1, 1) * Mat4At(B, 1, 3) + + Mat4At(A, 1, 2) * Mat4At(B, 2, 3) + + Mat4At(A, 1, 3) * Mat4At(B, 3, 3); + + R m20 = Mat4At(A, 2, 0) * Mat4At(B, 0, 0) + + Mat4At(A, 2, 1) * Mat4At(B, 1, 0) + + Mat4At(A, 2, 2) * Mat4At(B, 2, 0) + + Mat4At(A, 2, 3) * Mat4At(B, 3, 0); + R m21 = Mat4At(A, 2, 0) * Mat4At(B, 0, 1) + + Mat4At(A, 2, 1) * Mat4At(B, 1, 1) + + Mat4At(A, 2, 2) * Mat4At(B, 2, 1) + + Mat4At(A, 2, 3) * Mat4At(B, 3, 1); + R m22 = Mat4At(A, 2, 0) * Mat4At(B, 0, 2) + + Mat4At(A, 2, 1) * Mat4At(B, 1, 2) + + Mat4At(A, 2, 2) * Mat4At(B, 2, 2) + + Mat4At(A, 2, 3) * Mat4At(B, 3, 2); + R m23 = Mat4At(A, 2, 0) * Mat4At(B, 0, 3) + + Mat4At(A, 2, 1) * Mat4At(B, 1, 3) + + Mat4At(A, 2, 2) * Mat4At(B, 2, 3) + + Mat4At(A, 2, 3) * Mat4At(B, 3, 3); + + R m30 = Mat4At(A, 3, 0) * Mat4At(B, 0, 0) + + Mat4At(A, 3, 1) * Mat4At(B, 1, 0) + + Mat4At(A, 3, 2) * Mat4At(B, 2, 0) + + Mat4At(A, 3, 3) * Mat4At(B, 3, 0); + R m31 = Mat4At(A, 3, 0) * Mat4At(B, 0, 1) + + Mat4At(A, 3, 1) * Mat4At(B, 1, 1) + + Mat4At(A, 3, 2) * Mat4At(B, 2, 1) + + Mat4At(A, 3, 3) * Mat4At(B, 3, 1); + R m32 = Mat4At(A, 3, 0) * Mat4At(B, 0, 2) + + Mat4At(A, 3, 1) * Mat4At(B, 1, 2) + + Mat4At(A, 3, 2) * Mat4At(B, 2, 2) + + Mat4At(A, 3, 3) * Mat4At(B, 3, 2); + R m33 = Mat4At(A, 3, 0) * Mat4At(B, 0, 3) + + Mat4At(A, 3, 1) * Mat4At(B, 1, 3) + + Mat4At(A, 3, 2) * Mat4At(B, 2, 3) + + Mat4At(A, 3, 3) * Mat4At(B, 3, 3); + + return Mat4( + m00, m01, m02, m03, + m10, m11, m12, m13, + m20, m21, m22, m23, + m30, m31, m32, m33); +} + +static inline sgVec3 Mat4MulVec3(sgMat4 m, sgVec3 v, R w) { + return (sgVec3) { + .x = Mat4At(m, 0, 0) * v.x + Mat4At(m, 0, 1) * v.y + Mat4At(m, 0, 2) * v.z + Mat4At(m, 0, 3) * w, + .y = Mat4At(m, 1, 0) * v.x + Mat4At(m, 1, 1) * v.y + Mat4At(m, 1, 2) * v.z + Mat4At(m, 1, 3) * 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 sgMat4 Mat4Look(sgVec3 position, sgVec3 forward, sgVec3 up) { + const sgVec3 right = normalize3(cross3(forward, up)); + up = normalize3(cross3(right, forward)); + return Mat4FromVec3(right, up, neg3(forward), position); +} + +static inline sgMat4 Mat4Perspective(R fovy, R aspect, R near, R far) { + R f = tan(fovy / 2.0); + assert(f > 0.0); + f = 1.0 / 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); +} + +static inline sgPixel* PixelRow(sgPixel* image, int width, int y) { + return image + (y * width); +} + +static inline sgPixel* Pixel(sgPixel* image, int width, int x, int 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 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 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 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; +} + +static inline sgVec3 Barycentric(const sgTri2 tri, sgVec2 p) { + // There is no need to compute the third coordinate explicitly: a + b + c = 1. + // But this results in a worse rasterization of the triangle along one of the edges. + // It seems we can patch it with a small epsilon, though. + // --- + // Division by zero is only possible if the triangle has zero area. + /*return (sgVec3){ + f(tri.p1, tri.p2, p) / f(tri.p1, tri.p2, tri.p0), + f(tri.p2, tri.p0, p) / f(tri.p2, tri.p0, tri.p1), + 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; + return (sgVec3){a,b,c}; +} + +#define is_pow2_or_0(X) ((X & (X - 1)) == 0) + +static size_t align(size_t size) { + static_assert(is_pow2_or_0(SG_ALIGN)); + constexpr size_t mask = SG_ALIGN - 1; + return (size + mask) & (~mask); +} + +void* sgAlloc(size_t count, size_t size) { + const size_t total = align(count * size); + void* const ptr = aligned_alloc(SG_ALIGN, total); + memset(ptr, 0, total); + return ptr; +} + +void sgFree(void** pp) { + assert(pp); + if (*pp) { + free(*pp); + *pp = nullptr; + } +} + +swgfx* sgNew() { + swgfx* gfx = SG_ALIGN_ALLOC(1, swgfx); + return gfx; +} + +void sgDel(swgfx** ppSwgfx) { + assert(ppSwgfx); + if (*ppSwgfx) { + free(*ppSwgfx); + *ppSwgfx = 0; + } +} + +void sgColourBuffer(swgfx* gfx, sgVec2i dimensions, sgPixel* buffer) { + assert(gfx); + gfx->dims = dimensions; + gfx->colour = buffer; +} + +void sgPresent(swgfx* gfx, sgVec2i dimensions, sgPixel* screen) { + assert(gfx); + assert(screen); + // Integer scaling only. + assert((dimensions.x % gfx->dims.x) == 0); + assert((dimensions.y % gfx->dims.y) == 0); + + const int sx = dimensions.x / gfx->dims.x; + const int sy = dimensions.y / gfx->dims.y; + + const sgPixel* src = gfx->colour; + sgPixel* dst = screen; + + // Replicate each row 'sy' times. + for (int y = 0; y < gfx->dims.y; ++y, src += gfx->dims.x) { + for (int yy = y*sy; yy < (y+1)*sy; ++yy) { + // Replicate each column 'sx' times. + const sgPixel* src_col = src; + for (int x = 0; x < gfx->dims.x; ++x, ++src_col) { + for (int xx = x*sx; xx < (x+1)*sx; ++xx, ++dst) { + *dst = *src_col; + } + } + } + } +} + +void sgCam(swgfx* gfx, sgVec3 position, sgVec3 forward) { + assert(gfx); + gfx->view = Mat4Look(position, forward, Up3); +} + +void sgPerspective(swgfx* gfx, R fovy, R aspect, R near, R far) { + assert(gfx); + gfx->proj = Mat4Perspective(fovy, aspect, near, far); +} + +void sgViewport(swgfx* gfx, int x0, int y0, int width, int height) { + assert(gfx); + gfx->viewport = (sgViewport_t){x0, y0, width, height}; +} + +void sgClear(swgfx* gfx) { + assert(gfx); + memset(gfx->colour, 0, gfx->dims.x * gfx->dims.y * sizeof(sgPixel)); +} + +void sgPixels(swgfx* gfx, size_t count, const sgVec2i* positions, sgPixel colour) { + assert(gfx); + 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. +// TODO: Otherwise, rasterize the triangle the simple way and check whether each +// individual pixel is within bounds; do not explicitly clip the triangle. +// TODO: Actually, I think we can just clip the triangle's AABB and then walk +// over those pixels instead of checking every individual pixel in the +// non-clipped AABB. Edit: I think this doesn't work; draw it and you'll +// see. Some pixels that should be rasterized will fall out of the clipped +// AABB. + +void sgTriangles2(swgfx* gfx, size_t count, const sgTri2* tris) { + assert(gfx); + for (size_t i = 0; i < count; ++i) { + DrawTriangle2(gfx, &tris[i]); + } +} + +void sgTriangles(swgfx* gfx, size_t count, const sgTri3* tris, const sgNormal*) { + assert(gfx); + 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); + } +} + +static inline void AssertViewportWithinBuffer(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); +} + +void sgCheck(swgfx* gfx) { + assert(gfx); + AssertViewportWithinBuffer(gfx); +} diff --git a/test/test.c b/test/test.c new file mode 100644 index 0000000..668dec8 --- /dev/null +++ b/test/test.c @@ -0,0 +1,88 @@ +#include + +#include +#include +#include + +typedef struct RGB { uint8_t r, g, b; } RGB; + +static bool WritePPM(int width, int height, const RGB* image, const char* path) { + const size_t num_pixels = width * height; + + FILE* file = fopen(path, "wb"); + if (!file) { + return false; + } + + fprintf(file, "P6\n%d %d\n255\n", width, height); + if (fwrite(image, sizeof(RGB), num_pixels, file) != num_pixels) { + fclose(file); + return false; + } + + fclose(file); + return true; +} + +void ToRGB(int width, int height, const sgPixel* rgba, RGB* rgb) { + assert(rgba); + assert(rgb); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + const sgPixel p = *rgba++; + *rgb++ = (RGB){p.r, p.g, p.b}; + } + } +} + +static void Checkerboard(swgfx* gfx, int width, int height) { + assert(gfx); + const sgPixel colour = (sgPixel){255, 0, 255, 255}; + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + if (((x ^ y) & 1) == 1) { + const sgVec2i position = (sgVec2i){x, y}; + sgPixels(gfx, 1, &position, colour); + } + } + } +} + +static void TestTriangle(swgfx* gfx) { + assert(gfx); + + const int BufferWidth = 160; + const int BufferHeight = 120; + const int N = BufferWidth * BufferHeight; + const sgVec2i BufferDims = (sgVec2i){.x = BufferWidth, .y = BufferHeight}; + + sgPixel colour[N]; + RGB rgb[N]; + + sgColourBuffer(gfx, BufferDims, colour); + sgClear(gfx); + Checkerboard(gfx, BufferWidth, BufferHeight); + + const sgVec2 p0 = (sgVec2){20, 20}; + const sgVec2 p1 = (sgVec2){80, 20}; + const sgVec2 p2 = (sgVec2){50, 50}; + const sgTri2 tri = (sgTri2){p0, p1, p2}; + sgTriangles2(gfx, 1, &tri); + + ToRGB(BufferWidth, BufferHeight, colour, rgb); + WritePPM(BufferWidth, BufferHeight, rgb, "triangle.ppm"); +} + +int main() { + swgfx* gfx = 0; + if (!(gfx = sgNew())) { + fprintf(stderr, "Failed to create swgfx\n"); + return 1; + } + + TestTriangle(gfx); + + sgDel(&gfx); + return 0; +} + -- cgit v1.2.3