summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRoberto Esteves <contact@robertoesteves.dev>2026-01-05 14:11:50 +0000
committerRoberto Esteves <contact@robertoesteves.dev>2026-01-05 14:11:50 +0000
commit52c6a7635056ee78d282ba2a55eb26f2663bb577 (patch)
tree9d37d851e63e96e53ef3836321f03d4f2839edef
parentda12440cabe270584ff650703e90db540d2ec4c9 (diff)
refactor some stuff:HEADmaster
- add meson build system - refactor executable to be a test - remove stdlib from base library
-rw-r--r--.clang-format7
-rw-r--r--.gitignore1
-rw-r--r--include/rgl.h148
-rw-r--r--meson.build23
-rw-r--r--robsonGL.h323
-rw-r--r--src/command.c41
-rw-r--r--src/memory.c20
-rw-r--r--src/renderer_software.c100
-rw-r--r--test.c26
-rw-r--r--tests/common_linux.h70
-rw-r--r--tests/meson.build8
-rw-r--r--tests/test_stroke.c33
12 files changed, 451 insertions, 349 deletions
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..1071109
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,7 @@
+BasedOnStyle: LLVM # ou Google, Mozilla, etc.
+AllowShortFunctionsOnASingleLine: None # nunca coloca funções em uma linha
+AllowShortIfStatementsOnASingleLine: false
+AllowShortLoopsOnASingleLine: false
+AllowShortBlocksOnASingleLine: false
+BreakBeforeBraces: Attach # abre a chave em linha separada
+ColumnLimit: 80 # ajuste conforme sua preferência
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ceddaa3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.cache/
diff --git a/include/rgl.h b/include/rgl.h
new file mode 100644
index 0000000..ac69ed0
--- /dev/null
+++ b/include/rgl.h
@@ -0,0 +1,148 @@
+#ifndef RGL_H
+#define RGL_H
+
+#ifdef RGL_DEBUG
+
+static char *ERROR_MESSAGE;
+
+#define RGL_CRASH(text) \
+ do { \
+ ERROR_MESSAGE = text; \
+ __builtin_trap(); \
+ } while (0)
+
+#define RGL_ASSERT(cond, text) \
+ if (!(cond)) { \
+ RGL_CRASH(text); \
+ }
+
+#else
+
+#define RGL_CRASH(text)
+#define RGL_ASSERT(cond, text)
+
+#endif
+
+static int RGL_ABS(int n) {
+ return n < 0 ? -n : n;
+}
+
+/* Memory */
+
+typedef struct {
+ unsigned int size;
+ unsigned int capacity;
+
+ unsigned char *data;
+} RGL_Arena;
+
+void *RGL_ArenaAlloc(RGL_Arena *arena, unsigned int size);
+
+unsigned int RGL_ArenaCheckpoint(RGL_Arena *arena);
+
+void RGL_ArenaRollback(RGL_Arena *arena, unsigned int checkpoint);
+
+void RGL_ArenaReset(RGL_Arena *arena);
+
+/* Math */
+
+typedef unsigned int RGL_Fixed;
+
+#define RGL_SUBPIXEL_SHIFT 8
+#define RGL_SUBPIXEL_SCALE 256
+#define RGL_SUBPIXEL_MASK 255
+
+static RGL_Fixed RGL_GetFixedI(int x) {
+ return (RGL_Fixed)(x << RGL_SUBPIXEL_SHIFT);
+}
+
+static RGL_Fixed RGL_GetFixedF(float x) {
+ return (RGL_Fixed)(x * RGL_SUBPIXEL_SCALE);
+}
+
+static RGL_Fixed RGL_GetFixedD(double x) {
+ return (RGL_Fixed)(x * RGL_SUBPIXEL_SCALE);
+}
+
+static int RGL_FixedToInt(RGL_Fixed x) {
+ return x >> RGL_SUBPIXEL_SHIFT;
+}
+
+static RGL_Fixed RGL_FixedMul(RGL_Fixed a, RGL_Fixed b) {
+ return (RGL_Fixed)(((long int)a * b) >> RGL_SUBPIXEL_SHIFT);
+}
+
+/* Graphics */
+
+typedef unsigned int RGL_Color;
+
+#define RGL_COLOR(a, r, g, b) \
+ ((RGL_Color)((a << 24) | (r << 16) | (g << 8) | b))
+
+typedef struct {
+ unsigned int width, height;
+ unsigned int *data;
+} RGL_Buffer;
+
+typedef struct {
+ RGL_Color color;
+ unsigned int x, y, width, height;
+} RGL_Rectangle;
+
+typedef struct {
+ RGL_Fixed x, y;
+} RGL_Point;
+
+typedef enum { PathStroke, PathFill } RGL_PathVerb;
+
+typedef struct {
+ RGL_PathVerb verb;
+ unsigned int size;
+ RGL_Color color;
+} RGL_PathHeader;
+
+typedef enum {
+ Rect,
+ Path,
+} RGL_CommandType;
+
+typedef struct {
+ RGL_CommandType type;
+ union {
+ RGL_Rectangle rect;
+ RGL_PathHeader path;
+ } data;
+} RGL_Command;
+
+typedef struct {
+ RGL_Arena arena;
+ unsigned int size;
+
+ RGL_Command *commands;
+} RGL_CommandQueue;
+
+typedef struct {
+ RGL_Color color;
+ RGL_CommandQueue queue;
+
+ RGL_Command *current;
+} RGL_Context;
+
+void RGL_Rect(RGL_Context *ctx, unsigned int x, unsigned int y,
+ unsigned int width, unsigned int height);
+
+void RGL_BeginPath(RGL_Context *ctx, RGL_PathVerb verb);
+
+void RGL_PathMoveTo(RGL_Context *ctx, int x, int y);
+
+void RGL_EndPath(RGL_Context *ctx);
+
+void RGL_DrawRect(RGL_Buffer *buf, RGL_Command *cmd);
+
+void RGL_DrawStroke(RGL_Buffer *buf, RGL_PathHeader *path, RGL_Point *points);
+
+void RGL_DrawPath(RGL_Buffer *buf, RGL_PathHeader *path, RGL_Point *points);
+
+void RGL_Draw(RGL_Context *ctx, RGL_Buffer *buf);
+
+#endif
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..d5f2a26
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,23 @@
+project(
+ 'robsonGL',
+ 'c',
+ default_options: [
+ 'c_std=c89',
+ 'warning_level=everything',
+ 'prefer_static=true',
+ 'debug=true',
+ ],
+)
+
+add_project_arguments('-DRGL_DEBUG', language: 'c')
+
+src = files(
+ 'src/command.c',
+ 'src/memory.c',
+ 'src/renderer_software.c',
+)
+incdir = include_directories('include')
+
+lib_rgl = library('RGL', src, include_directories: incdir)
+
+subdir('tests')
diff --git a/robsonGL.h b/robsonGL.h
deleted file mode 100644
index 59e0d6a..0000000
--- a/robsonGL.h
+++ /dev/null
@@ -1,323 +0,0 @@
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-typedef int32_t rglFixed;
-
-#define RGL_SUBPIXEL_SHIFT 8
-#define RGL_SUBPIXEL_SCALE 256
-#define RGL_SUBPIXEL_MASK 255
-
-#define crash(text) \
- do { \
- printf(text); \
- *((int *)0) = -1; \
- } while (0)
-
-static inline rglFixed rglGetFixedI(int x) {
- return (rglFixed)(x << RGL_SUBPIXEL_SHIFT);
-}
-
-static inline rglFixed rglGetFixedF(float x) {
- return (rglFixed)(x * RGL_SUBPIXEL_SCALE);
-}
-
-static inline rglFixed rglGetFixedD(double x) {
- return (rglFixed)(x * RGL_SUBPIXEL_SCALE);
-}
-
-static inline int rglFixedToInt(rglFixed x) { return x >> RGL_SUBPIXEL_SHIFT; }
-
-static inline rglFixed rglFixedMul(rglFixed a, rglFixed b) {
- return (rglFixed)(((int64_t)a * b) >> RGL_SUBPIXEL_SHIFT);
-}
-
-typedef struct {
- uint32_t width, height;
- uint32_t *data;
-} rglBuffer;
-
-typedef struct {
- uint32_t x, y, width, height;
-} rglRectangle;
-
-typedef uint32_t rglColor;
-
-typedef struct {
- rglFixed x, y;
-} rglPoint;
-
-typedef enum { PathStroke, PathFill } rglPathVerb;
-
-typedef struct {
- uint32_t listSize;
- uint32_t listCapacity;
- rglPathVerb verb;
-
- rglFixed *xList;
- rglFixed *yList;
-} rglPath;
-
-typedef enum { Rect, Path } rglCommandType;
-
-typedef struct {
- rglCommandType type;
- rglColor color;
- union {
- rglRectangle *rect;
- rglPath *path;
- };
-} rglCommand;
-
-typedef struct {
- uint32_t size;
- rglCommand *commands;
-} rglCommandQueue;
-
-typedef struct {
- rglColor color;
- rglBuffer buf;
- rglCommandQueue queue;
-
- // TODO: make this use rglCommand in the future
- void *current;
-} rglContext;
-
-void rglQueueCommand(rglCommandQueue *queue, rglCommand *cmd) {
- // FIXME: append correctly
- queue->size += 1;
- queue->commands = cmd;
-}
-
-// TODO: Use memory arena implementation from ruim
-void rglBeginPath(rglContext *ctx, rglPathVerb verb) {
- rglPath *path = malloc(sizeof(rglPath));
-
- path->listSize = 0;
- path->listCapacity = 16;
- path->verb = verb;
- path->xList = malloc(path->listCapacity * sizeof(uint32_t));
- path->yList = malloc(path->listCapacity * sizeof(uint32_t));
-
- ctx->current = (void *)path;
-}
-
-void rglPathMoveTo(rglContext *ctx, int32_t x, int32_t y) {
- rglPath *path = (rglPath *)ctx->current;
-
- uint32_t p = path->listSize;
- path->xList[p] = rglGetFixedI(x);
- path->yList[p] = rglGetFixedI(y);
-
- path->listSize += 1;
-}
-
-void rglEndPath(rglContext *ctx) {
- rglCommand *command = malloc(sizeof(rglCommand));
-
- command->type = Path;
- command->path = (rglPath *)ctx->current;
- command->color = ctx->color;
-
- rglQueueCommand(&ctx->queue, command);
- ctx->current = 0;
-}
-
-#define COLOR(a, r, g, b) ((a << 24) | (r << 16) | (g << 8) | b)
-
-// TODO: implement shared memory mechanism
-int rglBufferAlloc(rglBuffer *buf) {
- uint32_t size = buf->width * buf->height * 4;
- buf->data = malloc(size); // ARGB pixel format
-
- if (buf->data == NULL) {
- return -1;
- }
-
- memset(buf->data, 0, size);
-
- return 0;
-}
-
-void rglDrawRect(rglBuffer *buf, rglCommand *cmd) {
- if (cmd->type != Rect)
- crash("expected rectangle type");
-
- for (int y = 0; y < buf->height; ++y) {
- for (int x = 0; x < buf->width; ++x) {
- int insideX = x >= cmd->rect->x && x <= (cmd->rect->x + cmd->rect->width);
- int insideY =
- y >= cmd->rect->y && y <= (cmd->rect->y + cmd->rect->height);
-
- if (insideX && insideY) {
- buf->data[x + (y * buf->width)] = cmd->color;
- }
- }
- }
-}
-
-void rglDrawStroke(rglBuffer *buf, rglPath *path, rglColor color) {
- for (uint32_t i = 0; i < path->listSize; ++i) {
- rglPoint p1, p2;
-
- p1.x = path->xList[i];
- p1.y = path->yList[i];
-
- p2.x = path->xList[(i + 1) % path->listSize];
- p2.y = path->yList[(i + 1) % path->listSize];
-
- int x0 = rglFixedToInt(p1.x), y0 = rglFixedToInt(p1.y);
- int x1 = rglFixedToInt(p2.x), y1 = rglFixedToInt(p2.y);
-
- int dx = abs(x1 - x0);
- int dy = -abs(y1 - y0);
-
- int sx = x0 < x1 ? 1 : -1;
- int sy = y0 < y1 ? 1 : -1;
-
- int err = dx + dy, e2;
- for (;;) {
- int idx = x0 + (y0 * buf->width);
- if (idx < buf->width * buf->height) {
- buf->data[idx] = color;
- }
-
- if (x0 == x1 && y0 == y1)
- break;
- e2 = err * 2;
- if (e2 >= dy) {
- err += dy;
- x0 += sx;
- }
-
- if (e2 <= dx) {
- err += dx;
- y0 += sy;
- }
- }
- }
-}
-
-// TODO: maybe construct edge table on command creation
-void rglDrawFill(rglBuffer *buf, rglPath *path, rglColor col) {
- struct edge_node {
- int active;
- rglFixed yMax;
- rglFixed x;
- // FIXME: make this fixed point later
- float slope;
- struct edge_node *next;
- };
-
- unsigned int x0, y0, x1, y1, x, y, xMin, yMin, lineTest;
- unsigned int i, yMinIdx, yMaxIdx;
- int j;
- struct edge_node *table;
- struct edge_node *node;
-
- table = malloc(sizeof(struct edge_node) * buf->height);
- memset(table, 0, sizeof(struct edge_node) * buf->height);
-
- for (i = 0; i < path->listSize; ++i) {
- for (j = i; j < path->listSize; ++j) {
- if (path->yList[j] < path->yList[yMinIdx]) {
- yMinIdx = j;
- }
- }
-
- x0 = rglFixedToInt(path->xList[yMinIdx]);
- y0 = rglFixedToInt(path->yList[yMinIdx]);
-
- node = &(table[y0]);
- for (j = -1; j < 2; j += 2) {
- yMaxIdx = (i + j * 1) % path->listSize;
- if (y0 == path->yList[yMaxIdx]) {
- continue;
- } else if (y0 <= path->yList[yMaxIdx]) {
- yMaxIdx = yMinIdx;
- }
-
- x1 = rglFixedToInt(path->xList[yMaxIdx]);
- y1 = rglFixedToInt(path->yList[yMaxIdx]);
-
- node->active = 1;
- node->x = x1;
- node->yMax = y1;
- node->slope = (float)(x1 - x0) / (float)(y1 - y0);
-
- if (j != -1) {
- node->next = malloc(sizeof(struct edge_node));
- node = node->next;
- }
- }
- }
-
- for (y = 0; y < buf->height; ++y) {
- yMin = buf->height - 1;
- while (table[yMin].active) {
- yMin--;
- }
-
- // Draw polygon
- node = &table[yMin];
- xMin = node->x;
-
- for (x = 0; x < buf->width; ++x) {
- i = x + (y * buf->width);
- lineTest = (x * node->slope) -
- }
- }
-}
-
-void rglDrawPath(rglBuffer *buf, rglPath *path, rglColor col) {
- switch (path->verb) {
- case PathStroke: {
- rglDrawStroke(buf, path, col);
- } break;
- case PathFill: {
- rglDrawFill(buf, path, col);
- } break;
- }
-}
-
-void rglSetColor(rglContext *ctx, int a, int r, int g, int b) {
- ctx->color = COLOR(a, r, g, b);
-}
-
-void rglDraw(rglContext *ctx) {
- for (int i = 0; i < ctx->queue.size; ++i) {
- rglCommand cmd = ctx->queue.commands[i];
- switch (cmd.type) {
- case Rect: {
- rglDrawRect(&ctx->buf, &cmd);
- } break;
- case Path: {
- rglDrawPath(&ctx->buf, cmd.path, cmd.color);
- } break;
- }
- }
-}
-
-int rglWritePPM(rglContext *ctx, const char *filename) {
- FILE *f = fopen(filename, "wb");
- if (!f)
- return -1;
-
- fprintf(f, "P6\n%d %d\n255\n", ctx->buf.width, ctx->buf.height);
-
- for (int i = 0; i < ctx->buf.width * ctx->buf.height; ++i) {
- uint32_t pixel = ctx->buf.data[i];
-
- uint8_t r = (pixel >> 16) & 0xFF;
- uint8_t g = (pixel >> 8) & 0xFF;
- uint8_t b = pixel & 0xFF;
-
- fputc(r, f);
- fputc(g, f);
- fputc(b, f);
- }
-
- fclose(f);
- return 0;
-}
diff --git a/src/command.c b/src/command.c
new file mode 100644
index 0000000..735036e
--- /dev/null
+++ b/src/command.c
@@ -0,0 +1,41 @@
+#include "rgl.h"
+
+void RGL_Rect(RGL_Context *ctx, unsigned int x, unsigned int y,
+ unsigned int width, unsigned int height) {
+ RGL_Command *rectCommand =
+ (RGL_Command *)RGL_ArenaAlloc(&ctx->queue.arena, sizeof(RGL_Command));
+
+ rectCommand->type = Rect;
+ rectCommand->data.rect.x = x;
+ rectCommand->data.rect.y = y;
+ rectCommand->data.rect.width = width;
+ rectCommand->data.rect.height = height;
+ rectCommand->data.rect.color = ctx->color;
+
+ ctx->queue.size += 1;
+}
+
+void RGL_BeginPath(RGL_Context *ctx, RGL_PathVerb verb) {
+ ctx->current =
+ (RGL_Command *)RGL_ArenaAlloc(&ctx->queue.arena, sizeof(RGL_Command));
+
+ ctx->current->type = Path;
+ ctx->current->data.path.color = ctx->color;
+ ctx->current->data.path.size = 0;
+ ctx->current->data.path.verb = verb;
+}
+
+void RGL_PathMoveTo(RGL_Context *ctx, int x, int y) {
+ RGL_Point *p =
+ (RGL_Point *)RGL_ArenaAlloc(&ctx->queue.arena, sizeof(RGL_Point));
+ p->x = RGL_GetFixedI(x);
+ p->y = RGL_GetFixedI(y);
+
+ ctx->current->data.path.size += 1;
+}
+
+void RGL_EndPath(RGL_Context *ctx) {
+ ctx->current->data.path.color = ctx->color;
+ ctx->current = 0;
+ ctx->queue.size += 1;
+}
diff --git a/src/memory.c b/src/memory.c
new file mode 100644
index 0000000..48a7cca
--- /dev/null
+++ b/src/memory.c
@@ -0,0 +1,20 @@
+#include "rgl.h"
+
+void *RGL_ArenaAlloc(RGL_Arena *arena, unsigned int size) {
+ void *data = (void *)&(arena->data[arena->size]);
+ arena->size += size;
+
+ return data;
+}
+
+unsigned int RGL_ArenaCheckpoint(RGL_Arena *arena) {
+ return arena->size;
+}
+
+void RGL_ArenaRollback(RGL_Arena *arena, unsigned int checkpoint) {
+ arena->size = checkpoint;
+}
+
+void RGL_ArenaReset(RGL_Arena *arena) {
+ arena->size = 0;
+}
diff --git a/src/renderer_software.c b/src/renderer_software.c
new file mode 100644
index 0000000..3cdd614
--- /dev/null
+++ b/src/renderer_software.c
@@ -0,0 +1,100 @@
+#include "rgl.h"
+
+void RGL_DrawRect(RGL_Buffer *buf, RGL_Command *cmd) {
+ unsigned int x, y, insideX, insideY;
+
+ RGL_ASSERT(cmd->type != Rect, "expected rectangle type");
+ for (y = 0; y < buf->height; ++y) {
+ for (x = 0; x < buf->width; ++x) {
+ insideX = x >= cmd->data.rect.x &&
+ x <= (cmd->data.rect.x + cmd->data.rect.width);
+ insideY = y >= cmd->data.rect.y &&
+ y <= (cmd->data.rect.y + cmd->data.rect.height);
+
+ if (insideX && insideY) {
+ buf->data[x + (y * buf->width)] = cmd->data.rect.color;
+ }
+ }
+ }
+}
+
+void RGL_DrawStroke(RGL_Buffer *buf, RGL_PathHeader *path, RGL_Point *points) {
+ RGL_Point p1, p2;
+ unsigned int i;
+ int x0, x1, y0, y1, dx, dy, sx, sy, idx, err, e2;
+
+ for (i = 0; i < path->size; ++i) {
+ p1 = points[i], p2 = points[(i + 1) % path->size];
+
+ x0 = RGL_FixedToInt(p1.x), y0 = RGL_FixedToInt(p1.y);
+ x1 = RGL_FixedToInt(p2.x), y1 = RGL_FixedToInt(p2.y);
+
+ dx = RGL_ABS(x1 - x0);
+ sx = x0 < x1 ? 1 : -1;
+
+ dy = -RGL_ABS(y1 - y0);
+ sy = y0 < y1 ? 1 : -1;
+
+ err = dx + dy;
+ while (1) {
+ idx = x0 + (y0 * buf->width);
+ if (idx < buf->width * buf->height) {
+ buf->data[idx] = path->color;
+ }
+
+ if (x0 == x1 && y0 == y1)
+ break;
+
+ e2 = err * 2;
+ if (e2 >= dy) {
+ err += dy;
+ x0 += sx;
+ }
+
+ if (e2 <= dx) {
+ err += dx;
+ y0 += sy;
+ }
+ }
+ }
+}
+
+void RGL_DrawPath(RGL_Buffer *buf, RGL_PathHeader *path, RGL_Point *points) {
+ switch (path->verb) {
+ case PathStroke: {
+ RGL_DrawStroke(buf, path, points);
+ } break;
+ case PathFill: {
+ /* RGL_DrawFill(buf, path, points, col); */
+ RGL_CRASH("`RGL_DrawFill` not implemented");
+ } break;
+ default: {
+ RGL_CRASH("tried to handle unknown `path->verb`");
+ } break;
+ }
+}
+
+void RGL_Draw(RGL_Context *ctx, RGL_Buffer *buf) {
+ RGL_Command cmd;
+ int i;
+ for (i = 0; i < ctx->queue.size; ++i) {
+ cmd = ctx->queue.commands[i];
+ switch (cmd.type) {
+ case Rect: {
+ RGL_DrawRect(buf, &cmd);
+ } break;
+ case Path: {
+ RGL_Point *points = (RGL_Point *)&(ctx->queue.commands[i + 1]);
+ RGL_DrawPath(buf, &cmd.data.path, points);
+
+ /* Skip point list, so it points to a valid command */
+ ctx->queue.commands =
+ (RGL_Command *)((char *)ctx->queue.commands +
+ cmd.data.path.size * sizeof(RGL_Point));
+ } break;
+ default: {
+ RGL_CRASH("unknown `cmd.type`");
+ } break;
+ }
+ }
+}
diff --git a/test.c b/test.c
deleted file mode 100644
index b371629..0000000
--- a/test.c
+++ /dev/null
@@ -1,26 +0,0 @@
-#include "robsonGL.h"
-
-int main(int argc, char *argv[]) {
- rglContext ctx;
- ctx.buf.width = 800;
- ctx.buf.height = 600;
-
- if (rglBufferAlloc(&ctx.buf))
- return 1;
-
- rglSetColor(&ctx, 255, 255, 255, 255);
-
- rglBeginPath(&ctx, PathFill);
- rglPathMoveTo(&ctx, 0, 0);
- rglPathMoveTo(&ctx, 799, 599);
- rglPathMoveTo(&ctx, 799, 0);
- rglPathMoveTo(&ctx, 0, 599);
- rglEndPath(&ctx);
-
- rglDraw(&ctx);
-
- if (rglWritePPM(&ctx, "output.ppm"))
- return 1;
-
- return 0;
-}
diff --git a/tests/common_linux.h b/tests/common_linux.h
new file mode 100644
index 0000000..167b483
--- /dev/null
+++ b/tests/common_linux.h
@@ -0,0 +1,70 @@
+#include <rgl.h>
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+
+#define crashErrno() RGL_CRASH(strerror(errno))
+#define assertErrno(cond) RGL_ASSERT(cond, strerror(errno))
+
+void BufferAlloc(RGL_Buffer *buf) {
+ unsigned int size = buf->width * buf->height * 4;
+
+ buf->data = mmap(NULL, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
+ assertErrno(buf->data != MAP_FAILED);
+}
+
+void BufferDestroy(RGL_Buffer *buf) {
+ unsigned int size = buf->width * buf->height * 4;
+ if (munmap(buf->data, size) < 0) {
+ crashErrno();
+ }
+}
+
+void ArenaInit(RGL_Arena *arena, unsigned int capacity) {
+ if (capacity) {
+ arena->capacity = capacity;
+ } else {
+ arena->capacity = 4096;
+ }
+
+ arena->data = mmap(0, arena->capacity, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
+ if (arena->data == MAP_FAILED) {
+ crashErrno();
+ }
+
+ arena->size = 0;
+}
+
+void ArenaDestroy(RGL_Arena *arena) {
+ if (munmap(arena->data, arena->capacity) < 0) {
+ crashErrno();
+ }
+}
+
+int WritePPM(RGL_Buffer buf, const char *filename) {
+ int i;
+ FILE *f = fopen(filename, "wb");
+ RGL_ASSERT(f, "failed to open file");
+
+ fprintf(f, "P6\n%d %d\n255\n", buf.width, buf.height);
+
+ for (i = 0; i < buf.width * buf.height; ++i) {
+ unsigned int pixel = buf.data[i];
+
+ unsigned char r = (pixel >> 16) & 0xFF;
+ unsigned char g = (pixel >> 8) & 0xFF;
+ unsigned char b = pixel & 0xFF;
+
+ fputc(r, f);
+ fputc(g, f);
+ fputc(b, f);
+ }
+
+ fclose(f);
+ return 0;
+}
diff --git a/tests/meson.build b/tests/meson.build
new file mode 100644
index 0000000..f4f46e7
--- /dev/null
+++ b/tests/meson.build
@@ -0,0 +1,8 @@
+exe_stroke = executable(
+ 'testStroke',
+ 'test_stroke.c',
+ link_with: [lib_rgl],
+ include_directories: incdir,
+)
+
+test('stroke', exe_stroke)
diff --git a/tests/test_stroke.c b/tests/test_stroke.c
new file mode 100644
index 0000000..1bcafd1
--- /dev/null
+++ b/tests/test_stroke.c
@@ -0,0 +1,33 @@
+#include "common_linux.h"
+
+int main(int argc, char *argv[]) {
+ RGL_Context ctx;
+ RGL_Buffer buf;
+
+ buf.width = 800;
+ buf.height = 600;
+
+ ArenaInit(&ctx.queue.arena, 0);
+ ctx.queue.commands = (RGL_Command *)ctx.queue.arena.data;
+
+ BufferAlloc(&buf);
+
+ ctx.color = RGL_COLOR(255, 255, 255, 255);
+
+ RGL_BeginPath(&ctx, PathStroke);
+ RGL_PathMoveTo(&ctx, 0, 0);
+ RGL_PathMoveTo(&ctx, 799, 599);
+ RGL_PathMoveTo(&ctx, 799, 0);
+ RGL_PathMoveTo(&ctx, 0, 599);
+ RGL_EndPath(&ctx);
+
+ RGL_Draw(&ctx, &buf);
+
+ if (WritePPM(buf, "output.ppm"))
+ return 1;
+
+ ArenaDestroy(&ctx.queue.arena);
+ BufferDestroy(&buf);
+
+ return 0;
+}