Batch rendering

Batch rendering
Batch rendering
Batch rendering

Author Mitja Felicijan <mitja.felicijan@gmail.com> 2026-05-11 17:16:18 +0200
Committer Mitja Felicijan <mitja.felicijan@gmail.com> 2026-05-11 18:00:56 +0200
Commit 4893b978b51c66421cf880ae2fba4d08e9ce91fb (patch)
-rw-r--r-- main.c 231
-rw-r--r-- nonstd.h 561
2 files changed, 719 insertions, 73 deletions
diff --git a/main.c b/main.c
  
1
// Traditional OpenGL "Immediate Mode" (glBegin/glEnd) is slow for many objects 
  
2
// because every function call creates CPU overhead. Instead, we:
  
3
// 1. Calculate all vertex data (Position, Color, UV) on the CPU.
  
4
// 2. Store them in a contiguous memory buffer (Batch).
  
5
// 3. Send the entire buffer to the GPU in a single call (glDrawArrays).
  
6
  
1
#include <stdio.h>
7
#include <stdio.h>
2
#include <stdlib.h>
8
#include <stdlib.h>
3
#include <getopt.h>
9
#include <getopt.h>
  
10
#include <math.h>
  
11
  
  
12
#define NONSTD_IMPLEMENTATION
  
13
#include "nonstd.h"
4
  
14
  
5
#define GL_SILENCE_DEPRECATION
15
#define GL_SILENCE_DEPRECATION
  
16
  
  
17
#ifndef M_PI
  
18
#define M_PI 3.14159265358979323846
  
19
#endif
6
  
20
  
7
#ifdef __APPLE__
21
#ifdef __APPLE__
8
    #include <OpenGL/gl.h>
22
    #include <OpenGL/gl.h>
...
24
	float x, y, z, w;
38
	float x, y, z, w;
25
} Vec4;
39
} Vec4;
26
  
40
  
  
41
// A packed structure representing a single vertex.
  
42
// The GPU needs to know the "stride" (size) of this struct to jump between vertices.
  
43
typedef struct {
  
44
    float x, y, z;    // Position
  
45
    float r, g, b;    // Color
  
46
    float u, v;       // Texture Coordinates (UV)
  
47
} Vertex;
  
48
  
  
49
// Batching using nonstd.h dynamic arrays.
  
50
// This allows us to push an arbitrary number of vertices without manual reallocs.
  
51
typedef array(Vertex) Batch;
  
52
  
  
53
Batch textured_batch = {0}; // Grouping textured faces to minimize state changes
  
54
Batch colored_batch = {0};  // Grouping solid color faces
  
55
  
  
56
// Rodrigues' rotation formula.
  
57
// Since we are batching, we can't use glRotatef (which modifies the global matrix).
  
58
// We must rotate every vertex manually on the CPU before pushing it to the buffer.
  
59
Vec3 vec3_rotate(Vec3 v, float angle, Vec3 axis) {
  
60
    float rad = angle * M_PI / 180.0f;
  
61
    float c = cosf(rad);
  
62
    float s = sinf(rad);
  
63
    
  
64
    float len = sqrtf(axis.x*axis.x + axis.y*axis.y + axis.z*axis.z);
  
65
    if (len > 0) { axis.x /= len; axis.y /= len; axis.z /= len; }
  
66
  
  
67
    float x = axis.x, y = axis.y, z = axis.z;
  
68
    float dot = v.x*x + v.y*y + v.z*z;
  
69
    Vec3 cross = {
  
70
        y*v.z - z*v.y,
  
71
        z*v.x - x*v.z,
  
72
        x*v.y - y*v.x
  
73
    };
  
74
  
  
75
    return (Vec3){
  
76
        v.x*c + cross.x*s + x*dot*(1-c),
  
77
        v.y*c + cross.y*s + y*dot*(1-c),
  
78
        v.z*c + cross.z*s + z*dot*(1-c)
  
79
    };
  
80
}
  
81
  
27
#define COLOR_RED     (Vec3){ .x = 1.0f, .y = 0.0f, .z = 0.0f }
82
#define COLOR_RED     (Vec3){ .x = 1.0f, .y = 0.0f, .z = 0.0f }
28
#define COLOR_GREEN   (Vec3){ .x = 0.0f, .y = 1.0f, .z = 0.0f }
83
#define COLOR_GREEN   (Vec3){ .x = 0.0f, .y = 1.0f, .z = 0.0f }
29
#define COLOR_BLUE    (Vec3){ .x = 0.0f, .y = 0.0f, .z = 1.0f }
84
#define COLOR_BLUE    (Vec3){ .x = 0.0f, .y = 0.0f, .z = 1.0f }
...
86
  
141
  
87
	while (getc(fp) != '\n'); // skip single whitespace
142
	while (getc(fp) != '\n'); // skip single whitespace
88
  
143
  
89
	unsigned char* data = (unsigned char*)malloc((*width) * (*height) * 3);
144
	unsigned char* data = ALLOC(unsigned char, (*width) * (*height) * 3);
90
	if (!data) {
145
	if (!data) {
91
		fprintf(stderr, "Memory allocation failed\n");
146
		fprintf(stderr, "Memory allocation failed\n");
92
		fclose(fp);
147
		fclose(fp);
...
95
  
150
  
96
	if (fread(data, 1, (*width) * (*height) * 3, fp) != (size_t)((*width) * (*height) * 3)) {
151
	if (fread(data, 1, (*width) * (*height) * 3, fp) != (size_t)((*width) * (*height) * 3)) {
97
		fprintf(stderr, "Error reading PPM data\n");
152
		fprintf(stderr, "Error reading PPM data\n");
98
		free(data);
153
		FREE(data);
99
		fclose(fp);
154
		fclose(fp);
100
		return NULL;
155
		return NULL;
101
	}
156
	}
...
117
  
172
  
118
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
173
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
119
  
174
  
120
	free(data);
175
	FREE(data);
121
}
176
}
122
  
177
  
123
void draw_text(float x, float y, void* font, const char* string, Vec3 color) {
178
void draw_text(float x, float y, void* font, const char* string, Vec3 color) {
...
143
	glMatrixMode(GL_MODELVIEW);
198
	glMatrixMode(GL_MODELVIEW);
144
}
199
}
145
  
200
  
146
void draw_cube(void) {
201
void batch_push_vertex(Batch* batch, Vertex v) {
147
	glEnable(GL_TEXTURE_2D);
202
    array_push(*batch, v);
148
	glBindTexture(GL_TEXTURE_2D, game.texture_id);
203
}
149
	glBegin(GL_QUADS);
  
150
		// Front Face (Textured)
  
151
		glColor3f(1.0f, 1.0f, 1.0f);
  
152
  
204
  
153
		// Backface
205
// Takes object-space coordinates and transforms them into world-space.
154
		glTexCoord2f(0.0f, 0.0f); glVertex3f(-0.1f, -0.1f,  0.1f);
206
// Vertices are then sorted into either the textured or colored batch.
155
		glTexCoord2f(1.0f, 0.0f); glVertex3f( 0.1f, -0.1f,  0.1f);
207
void batch_push_cube(Vec3 pos, float size, float angle, Vec3 axis) {
156
		glTexCoord2f(1.0f, 1.0f); glVertex3f( 0.1f,  0.1f,  0.1f);
208
    float h = size / 2.0f;
157
		glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.1f,  0.1f,  0.1f);
209
    Vec3 corners[8] = {
  
210
        {-h, -h,  h}, { h, -h,  h}, { h,  h,  h}, {-h,  h,  h}, // Front
  
211
        {-h, -h, -h}, { h, -h, -h}, { h,  h, -h}, {-h,  h, -h}  // Back
  
212
    };
158
  
213
  
159
		// Top face
214
    // Transform vertices (Rotation -> Translation)
160
		glTexCoord2f(0.0f, 0.0f); glVertex3f(-0.1f,  0.1f, -0.1f);
215
    for (int i = 0; i < 8; i++) {
161
		glTexCoord2f(1.0f, 0.0f); glVertex3f(-0.1f,  0.1f,  0.1f);
216
        if (angle != 0.0f) corners[i] = vec3_rotate(corners[i], angle, axis);
162
		glTexCoord2f(1.0f, 1.0f); glVertex3f( 0.1f,  0.1f,  0.1f);
217
        corners[i].x += pos.x;
163
		glTexCoord2f(0.0f, 1.0f); glVertex3f( 0.1f,  0.1f, -0.1f);
218
        corners[i].y += pos.y;
164
	glEnd();
219
        corners[i].z += pos.z;
165
	glDisable(GL_TEXTURE_2D);
220
    }
  
221
  
  
222
    // Pushing Front and Top faces to the TEXTURED batch
  
223
    Vec3 white = {1,1,1};
  
224
    batch_push_vertex(&textured_batch, (Vertex){corners[0].x, corners[0].y, corners[0].z, white.x, white.y, white.z, 0, 0});
  
225
    batch_push_vertex(&textured_batch, (Vertex){corners[1].x, corners[1].y, corners[1].z, white.x, white.y, white.z, 1, 0});
  
226
    batch_push_vertex(&textured_batch, (Vertex){corners[2].x, corners[2].y, corners[2].z, white.x, white.y, white.z, 1, 1});
  
227
    batch_push_vertex(&textured_batch, (Vertex){corners[3].x, corners[3].y, corners[3].z, white.x, white.y, white.z, 0, 1});
166
  
228
  
167
	glBegin(GL_QUADS);
229
    batch_push_vertex(&textured_batch, (Vertex){corners[7].x, corners[7].y, corners[7].z, white.x, white.y, white.z, 0, 0});
168
		// Back Face (Yellow)
230
    batch_push_vertex(&textured_batch, (Vertex){corners[3].x, corners[3].y, corners[3].z, white.x, white.y, white.z, 1, 0});
169
		glColor3f(1.0f, 1.0f, 0.0f);
231
    batch_push_vertex(&textured_batch, (Vertex){corners[2].x, corners[2].y, corners[2].z, white.x, white.y, white.z, 1, 1});
170
		glVertex3f(-0.1f, -0.1f, -0.1f);
232
    batch_push_vertex(&textured_batch, (Vertex){corners[6].x, corners[6].y, corners[6].z, white.x, white.y, white.z, 0, 1});
171
		glVertex3f(-0.1f,  0.1f, -0.1f);
  
172
		glVertex3f( 0.1f,  0.1f, -0.1f);
  
173
		glVertex3f( 0.1f, -0.1f, -0.1f);
  
174
  
233
  
175
		// Top Face (Blue)
234
    // Pushing remaining faces to the COLORED batch
176
		glColor3f(0.0f, 0.0f, 1.0f);
235
    Vec3 yellow = {1,1,0};
177
		glVertex3f(-0.1f,  0.1f, -0.1f);
236
    batch_push_vertex(&colored_batch, (Vertex){corners[4].x, corners[4].y, corners[4].z, yellow.x, yellow.y, yellow.z, 0, 0});
178
		glVertex3f(-0.1f,  0.1f,  0.1f);
237
    batch_push_vertex(&colored_batch, (Vertex){corners[7].x, corners[7].y, corners[7].z, yellow.x, yellow.y, yellow.z, 0, 0});
179
		glVertex3f( 0.1f,  0.1f,  0.1f);
238
    batch_push_vertex(&colored_batch, (Vertex){corners[6].x, corners[6].y, corners[6].z, yellow.x, yellow.y, yellow.z, 0, 0});
180
		glVertex3f( 0.1f,  0.1f, -0.1f);
239
    batch_push_vertex(&colored_batch, (Vertex){corners[5].x, corners[5].y, corners[5].z, yellow.x, yellow.y, yellow.z, 0, 0});
181
  
240
  
182
		// Bottom Face (Red)
241
    Vec3 magenta = {1,0,1};
183
		glColor3f(1.0f, 0.0f, 1.0f);
242
    batch_push_vertex(&colored_batch, (Vertex){corners[4].x, corners[4].y, corners[4].z, magenta.x, magenta.y, magenta.z, 0, 0});
184
		glVertex3f(-0.1f, -0.1f, -0.1f);
243
    batch_push_vertex(&colored_batch, (Vertex){corners[5].x, corners[5].y, corners[5].z, magenta.x, magenta.y, magenta.z, 0, 0});
185
		glVertex3f( 0.1f, -0.1f, -0.1f);
244
    batch_push_vertex(&colored_batch, (Vertex){corners[1].x, corners[1].y, corners[1].z, magenta.x, magenta.y, magenta.z, 0, 0});
186
		glVertex3f( 0.1f, -0.1f,  0.1f);
245
    batch_push_vertex(&colored_batch, (Vertex){corners[0].x, corners[0].y, corners[0].z, magenta.x, magenta.y, magenta.z, 0, 0});
187
		glVertex3f(-0.1f, -0.1f,  0.1f);
  
188
  
246
  
189
		// Right Face (Magenta)
247
    Vec3 red = {1,0,0};
190
		glColor3f(1.0f, 0.0f, 1.0f);
248
    batch_push_vertex(&colored_batch, (Vertex){corners[1].x, corners[1].y, corners[1].z, red.x, red.y, red.z, 0, 0});
191
		glVertex3f( 0.1f, -0.1f, -0.1f);
249
    batch_push_vertex(&colored_batch, (Vertex){corners[5].x, corners[5].y, corners[5].z, red.x, red.y, red.z, 0, 0});
192
		glVertex3f( 0.1f,  0.1f, -0.1f);
250
    batch_push_vertex(&colored_batch, (Vertex){corners[6].x, corners[6].y, corners[6].z, red.x, red.y, red.z, 0, 0});
193
		glVertex3f( 0.1f,  0.1f,  0.1f);
251
    batch_push_vertex(&colored_batch, (Vertex){corners[2].x, corners[2].y, corners[2].z, red.x, red.y, red.z, 0, 0});
194
		glVertex3f( 0.1f, -0.1f,  0.1f);
  
195
  
252
  
196
		// Left Face (Cyan)
253
    Vec3 cyan = {0,1,1};
197
		glColor3f(0.0f, 1.0f, 1.0f);
254
    batch_push_vertex(&colored_batch, (Vertex){corners[4].x, corners[4].y, corners[4].z, cyan.x, cyan.y, cyan.z, 0, 0});
198
		glVertex3f(-0.1f, -0.1f, -0.1f);
255
    batch_push_vertex(&colored_batch, (Vertex){corners[0].x, corners[0].y, corners[0].z, cyan.x, cyan.y, cyan.z, 0, 0});
199
		glVertex3f(-0.1f, -0.1f,  0.1f);
256
    batch_push_vertex(&colored_batch, (Vertex){corners[3].x, corners[3].y, corners[3].z, cyan.x, cyan.y, cyan.z, 0, 0});
200
		glVertex3f(-0.1f,  0.1f,  0.1f);
257
    batch_push_vertex(&colored_batch, (Vertex){corners[7].x, corners[7].y, corners[7].z, cyan.x, cyan.y, cyan.z, 0, 0});
201
		glVertex3f(-0.1f,  0.1f, -0.1f);
  
202
	glEnd();
  
203
}
258
}
204
  
259
  
205
void draw_quad() {
260
// The main render call that talks to the GPU.
206
	glBegin(GL_POLYGON);
261
void batch_render() {
207
		glVertex3f (0.25, 0.25, 0.0);
262
    // Enable client-side arrays (tells OpenGL we are passing memory pointers)
208
		glVertex3f (0.75, 0.25, 0.0);
263
    glEnableClientState(GL_VERTEX_ARRAY);
209
		glVertex3f (0.75, 0.75, 0.0);
264
    glEnableClientState(GL_COLOR_ARRAY);
210
		glVertex3f (0.25, 0.75, 0.0);
265
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
211
	glEnd();
266
  
  
267
    // 1. Render all TEXTURED geometry
  
268
    if (textured_batch.length > 0) {
  
269
        glEnable(GL_TEXTURE_2D);
  
270
        glBindTexture(GL_TEXTURE_2D, game.texture_id);
  
271
        
  
272
        Vertex* v = textured_batch.data;
  
273
        // Pointer math: specify (size, type, stride, offset)
  
274
        glVertexPointer(3, GL_FLOAT, sizeof(Vertex), &v[0].x);
  
275
        glColorPointer(3, GL_FLOAT, sizeof(Vertex), &v[0].r);
  
276
        glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &v[0].u);
  
277
        
  
278
        glDrawArrays(GL_QUADS, 0, textured_batch.length);
  
279
        glDisable(GL_TEXTURE_2D);
  
280
    }
  
281
  
  
282
    // 2. Render all COLORED geometry
  
283
    if (colored_batch.length > 0) {
  
284
        Vertex* v = colored_batch.data;
  
285
        glVertexPointer(3, GL_FLOAT, sizeof(Vertex), &v[0].x);
  
286
        glColorPointer(3, GL_FLOAT, sizeof(Vertex), &v[0].r);
  
287
        glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &v[0].u);
  
288
        
  
289
        glDrawArrays(GL_QUADS, 0, colored_batch.length);
  
290
    }
  
291
  
  
292
    glDisableClientState(GL_VERTEX_ARRAY);
  
293
    glDisableClientState(GL_COLOR_ARRAY);
  
294
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
  
295
    
  
296
    // Clear length (re-use capacity) for the next frame
  
297
    array_clear(textured_batch);
  
298
    array_clear(colored_batch);
212
}
299
}
  
300
  
213
  
301
  
214
void render_display(void) {
302
void render_display(void) {
215
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
303
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
...
217
	glMatrixMode(GL_MODELVIEW);
305
	glMatrixMode(GL_MODELVIEW);
218
	glLoadIdentity();
306
	glLoadIdentity();
219
  
307
  
220
	// Render Moving Square
308
	// Render Batch of Cubes
221
	/* glPushMatrix(); */
309
	for (int i = 0; i < 10; i++) {
222
	/* 	glTranslatef(x_offset, y_offset, 0.0f); */
310
		for (int j = 0; j < 10; j++) {
223
	/* 	glColor3f(1.0, 1.0, 1.0); */
311
			float x = 0.1f + i * 0.1f;
224
	/* 	draw_quad(); */
312
			float y = 0.1f + j * 0.1f;
225
	/* glPopMatrix(); */
313
			float angle = game.cube_angle + (i + j) * 10.0f;
226
  
314
			batch_push_cube((Vec3){x, y, 0.0f}, 0.05f, angle, (Vec3){1.0f, 1.0f, 1.0f});
227
	// Render Spinning Cube
315
		}
228
	glPushMatrix();
316
	}
229
		glTranslatef(0.5f, 0.5f, 0.0f);
317
	batch_render();
230
		glRotatef(game.cube_angle, 1.0f, 1.0f, 1.0f);
  
231
		draw_cube();
  
232
	glPopMatrix();
  
233
  
318
  
234
	// Render UI Text
319
	// Render UI Text
235
	char fps_text[32];
320
	char fps_text[32];
...
diff --git a/nonstd.h b/nonstd.h
  
1
// nonstd.h
  
2
// A collection of useful functions and macros.
  
3
// This library is licensed under the BSD 2-Clause License.
  
4
// 
  
5
// This file provides both the interface and the implementation.
  
6
// To instantiate the implementation,
  
7
//     #define NONSTD_IMPLEMENTATION
  
8
// before including this file.
  
9
  
  
10
#ifdef NONSTD_IMPLEMENTATION
  
11
#ifndef _POSIX_C_SOURCE
  
12
#define _POSIX_C_SOURCE 200809L
  
13
#endif
  
14
#endif
  
15
  
  
16
#ifndef NONSTD_H
  
17
#define NONSTD_H
  
18
  
  
19
#include <stdarg.h>
  
20
#include <stddef.h>
  
21
#include <stdint.h>
  
22
#include <stdio.h>
  
23
#include <stdlib.h>
  
24
#include <string.h>
  
25
#include <sys/time.h>
  
26
#include <time.h>
  
27
#include <unistd.h>
  
28
  
  
29
#ifndef NONSTD_DEF
  
30
#ifdef NONSTD_STATIC
  
31
#define NONSTD_DEF static
  
32
#else
  
33
#define NONSTD_DEF extern
  
34
#endif
  
35
#endif
  
36
  
  
37
typedef int8_t i8;
  
38
typedef uint8_t u8;
  
39
typedef int16_t i16;
  
40
typedef uint16_t u16;
  
41
typedef int32_t i32;
  
42
typedef uint32_t u32;
  
43
typedef int64_t i64;
  
44
typedef uint64_t u64;
  
45
typedef unsigned int uint;
  
46
typedef unsigned long ulong;
  
47
typedef intptr_t isize;
  
48
typedef uintptr_t usize;
  
49
typedef char c8;
  
50
  
  
51
#define countof(a) (sizeof(a) / sizeof((a)[0]))
  
52
  
  
53
#define static_foreach(type, var, array)                 \
  
54
	for (size_t _i_##var = 0, _n_##var = countof(array); \
  
55
		 _i_##var < _n_##var && ((var) = (array)[_i_##var], 1); ++_i_##var)
  
56
  
  
57
#define ALLOC(type, n) ((type *)safe_malloc(sizeof(type), (n)))
  
58
#define REALLOC(ptr, type, n) ((type *)safe_realloc((ptr), sizeof(type), (n)))
  
59
#define FREE(ptr)   \
  
60
	do {            \
  
61
		free(ptr);  \
  
62
		ptr = NULL; \
  
63
	} while (0)
  
64
  
  
65
NONSTD_DEF void *safe_malloc(size_t item_size, size_t count);
  
66
NONSTD_DEF void *safe_realloc(void *ptr, size_t item_size, size_t count);
  
67
  
  
68
#ifndef MIN
  
69
#define MIN(a, b) ((a) < (b) ? (a) : (b))
  
70
#endif
  
71
#ifndef MAX
  
72
#define MAX(a, b) ((a) > (b) ? (a) : (b))
  
73
#endif
  
74
#define CLAMP(x, lo, hi) (MIN((hi), MAX((lo), (x))))
  
75
  
  
76
// From https://github.com/tsoding/nob.h/blob/e2c9a46f01d052ab740140e74453665dc3334832/nob.h#L205-L206.
  
77
#define UNUSED(value) (void)(value)
  
78
#define TODO(message)                                                      \
  
79
	do {                                                                   \
  
80
		fprintf(stderr, "%s:%d: TODO: %s\n", __FILE__, __LINE__, message); \
  
81
		abort();                                                           \
  
82
	} while (0)
  
83
#define UNREACHABLE(message)                                                      \
  
84
	do {                                                                          \
  
85
		fprintf(stderr, "%s:%d: UNREACHABLE: %s\n", __FILE__, __LINE__, message); \
  
86
		abort();                                                                  \
  
87
	} while (0)
  
88
  
  
89
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
  
90
#define STATIC_ASSERT(expr, msg) _Static_assert((expr), msg)
  
91
#else
  
92
#define STATIC_ASSERT(expr, msg) \
  
93
	typedef char static_assertion_##msg[(expr) ? 1 : -1]
  
94
#endif
  
95
  
  
96
// String view - read-only, non-owning reference to a string
  
97
typedef struct {
  
98
	const char *data;
  
99
	size_t length;
  
100
} stringv;
  
101
  
  
102
NONSTD_DEF stringv sv_from_cstr(const char *s);
  
103
NONSTD_DEF stringv sv_from_parts(const char *data, size_t length);
  
104
NONSTD_DEF stringv sv_slice(stringv sv, size_t start, size_t end);
  
105
NONSTD_DEF int sv_equals(stringv a, stringv b);
  
106
NONSTD_DEF int sv_starts_with(stringv sv, stringv prefix);
  
107
NONSTD_DEF int sv_ends_with(stringv sv, stringv suffix);
  
108
  
  
109
// String builder - owning, mutable, dynamically growing string buffer
  
110
typedef struct {
  
111
	char *data;
  
112
	size_t length;
  
113
	size_t capacity;
  
114
} stringb;
  
115
  
  
116
NONSTD_DEF void sb_init(stringb *sb, size_t initial_cap);
  
117
NONSTD_DEF void sb_free(stringb *sb);
  
118
NONSTD_DEF void sb_ensure(stringb *sb, size_t additional);
  
119
NONSTD_DEF void sb_append_cstr(stringb *sb, const char *s);
  
120
NONSTD_DEF void sb_append_sv(stringb *sb, stringv sv);
  
121
NONSTD_DEF void sb_append_char(stringb *sb, char c);
  
122
NONSTD_DEF stringv sb_as_sv(const stringb *sb);
  
123
  
  
124
// Slice - generic non-owning view into an array
  
125
// Usage: SLICE_DEF(int); slice(int) view = ...;
  
126
#define SLICE_DEF(T)   \
  
127
	typedef struct {   \
  
128
		T *data;       \
  
129
		size_t length; \
  
130
	} slice_##T
  
131
  
  
132
#define slice(T) slice_##T
  
133
  
  
134
#define make_slice(T, ptr, len) ((slice(T)){.data = (ptr), .length = (len)})
  
135
  
  
136
#define array_as_slice(T, arr) \
  
137
	((slice(T)){.data = (arr).data, .length = (arr).length})
  
138
  
  
139
// Dynamic array - generic type-safe growable array using macros
  
140
// Usage: array(int) numbers; array_init(numbers);
  
141
#define array(T)         \
  
142
	struct {             \
  
143
		T *data;         \
  
144
		size_t length;   \
  
145
		size_t capacity; \
  
146
	}
  
147
  
  
148
#define array_init(arr)     \
  
149
	do {                    \
  
150
		(arr).capacity = 0; \
  
151
		(arr).data = NULL;  \
  
152
		(arr).length = 0;   \
  
153
	} while (0)
  
154
  
  
155
#define array_init_cap(arr, initial_cap)                             \
  
156
	do {                                                             \
  
157
		(arr).capacity = (initial_cap) ? (initial_cap) : 16;         \
  
158
		(arr).data = ALLOC(__typeof__(*(arr).data), (arr).capacity); \
  
159
		(arr).length = 0;                                            \
  
160
	} while (0)
  
161
  
  
162
#define array_free(arr)     \
  
163
	do {                    \
  
164
		FREE((arr).data);   \
  
165
		(arr).length = 0;   \
  
166
		(arr).capacity = 0; \
  
167
	} while (0)
  
168
  
  
169
#define array_ensure(arr, additional)                                    \
  
170
	do {                                                                 \
  
171
		size_t _needed = (arr).length + (additional);                    \
  
172
		if (_needed > (arr).capacity) {                                  \
  
173
			size_t _new_cap = (arr).capacity ? (arr).capacity : 16;      \
  
174
			while (_new_cap < _needed) {                                 \
  
175
				if (_new_cap > SIZE_MAX / 2) {                           \
  
176
					_new_cap = SIZE_MAX;                                 \
  
177
					break;                                               \
  
178
				}                                                        \
  
179
				_new_cap *= 2;                                           \
  
180
			}                                                            \
  
181
			if (_new_cap < _needed) { /* Overflow or OOM */              \
  
182
				break;                                                   \
  
183
			}                                                            \
  
184
			void *_new_data =                                            \
  
185
				safe_realloc((arr).data, sizeof(*(arr).data), _new_cap); \
  
186
			if (_new_data) {                                             \
  
187
				(arr).data = _new_data;                                  \
  
188
				(arr).capacity = _new_cap;                               \
  
189
			}                                                            \
  
190
		}                                                                \
  
191
	} while (0)
  
192
  
  
193
#define array_push(arr, value)                    \
  
194
	do {                                          \
  
195
		array_ensure((arr), 1);                   \
  
196
		if ((arr).length < (arr).capacity) {      \
  
197
			(arr).data[(arr).length++] = (value); \
  
198
		}                                         \
  
199
	} while (0)
  
200
  
  
201
#define array_pop(arr) ((arr).length > 0 ? (arr).data[--(arr).length] : 0)
  
202
  
  
203
#define array_get(arr, index) ((arr).data[index])
  
204
  
  
205
#define array_set(arr, index, value)     \
  
206
	do {                                 \
  
207
		if ((index) < (arr).length) {    \
  
208
			(arr).data[index] = (value); \
  
209
		}                                \
  
210
	} while (0)
  
211
  
  
212
#define array_insert(arr, index, value)                              \
  
213
	do {                                                             \
  
214
		if ((index) <= (arr).length) {                               \
  
215
			array_ensure((arr), 1);                                  \
  
216
			if ((arr).length < (arr).capacity) {                     \
  
217
				for (size_t _i = (arr).length; _i > (index); --_i) { \
  
218
					(arr).data[_i] = (arr).data[_i - 1];             \
  
219
				}                                                    \
  
220
				(arr).data[index] = (value);                         \
  
221
				(arr).length++;                                      \
  
222
			}                                                        \
  
223
		}                                                            \
  
224
	} while (0)
  
225
  
  
226
#define array_remove(arr, index)                                     \
  
227
	do {                                                             \
  
228
		if ((index) < (arr).length) {                                \
  
229
			for (size_t _i = (index); _i < (arr).length - 1; ++_i) { \
  
230
				(arr).data[_i] = (arr).data[_i + 1];                 \
  
231
			}                                                        \
  
232
			(arr).length--;                                          \
  
233
		}                                                            \
  
234
	} while (0)
  
235
  
  
236
#define array_clear(arr)  \
  
237
	do {                  \
  
238
		(arr).length = 0; \
  
239
	} while (0)
  
240
  
  
241
#define array_reserve(arr, new_capacity)                                       \
  
242
	do {                                                                       \
  
243
		if ((new_capacity) > (arr).capacity) {                                 \
  
244
			void *_new_data =                                                  \
  
245
				safe_realloc((arr).data, sizeof(*(arr).data), (new_capacity)); \
  
246
			if (_new_data) {                                                   \
  
247
				(arr).data = _new_data;                                        \
  
248
				(arr).capacity = (new_capacity);                               \
  
249
			}                                                                  \
  
250
		}                                                                      \
  
251
	} while (0)
  
252
  
  
253
#define array_foreach(arr, var)                                        \
  
254
	for (size_t _i_##var = 0;                                          \
  
255
		 _i_##var < (arr).length && ((var) = (arr).data[_i_##var], 1); \
  
256
		 ++_i_##var)
  
257
  
  
258
#define array_foreach_idx(arr, var, index) \
  
259
	for (size_t index = 0;                 \
  
260
		 index < (arr).length && ((var) = (arr).data[index], 1); ++index)
  
261
  
  
262
// Arena - block-based memory allocator
  
263
typedef struct {
  
264
	char *ptr;
  
265
	char *end;
  
266
	array(char *) blocks;
  
267
} Arena;
  
268
  
  
269
#define ARENA_DEFAULT_BLOCK_SIZE (4096)
  
270
  
  
271
NONSTD_DEF Arena arena_make(void);
  
272
NONSTD_DEF void arena_grow(Arena *a, size_t min_size);
  
273
NONSTD_DEF void *arena_alloc(Arena *a, size_t size);
  
274
NONSTD_DEF void arena_free(Arena *a);
  
275
  
  
276
// File I/O helpers
  
277
NONSTD_DEF char *read_entire_file(const char *filepath, size_t *out_size);
  
278
NONSTD_DEF int write_entire_file(const char *filepath, const void *data, size_t size);
  
279
NONSTD_DEF stringb read_entire_file_sb(const char *filepath);
  
280
NONSTD_DEF int write_file_sv(const char *filepath, stringv sv);
  
281
NONSTD_DEF int write_file_sb(const char *filepath, const stringb *sb);
  
282
  
  
283
#ifdef NONSTD_IMPLEMENTATION
  
284
  
  
285
NONSTD_DEF void *safe_malloc(size_t item_size, size_t count) {
  
286
	if (count != 0 && item_size > SIZE_MAX / count) {
  
287
		return NULL;
  
288
	}
  
289
	return malloc(item_size * count);
  
290
}
  
291
  
  
292
NONSTD_DEF void *safe_realloc(void *ptr, size_t item_size, size_t count) {
  
293
	if (count != 0 && item_size > SIZE_MAX / count) {
  
294
		return NULL;
  
295
	}
  
296
	return realloc(ptr, item_size * count);
  
297
}
  
298
  
  
299
// String View Implementation
  
300
  
  
301
NONSTD_DEF stringv sv_from_cstr(const char *s) {
  
302
	return (stringv){.data = s, .length = s ? strlen(s) : 0};
  
303
}
  
304
  
  
305
NONSTD_DEF stringv sv_from_parts(const char *data, size_t length) {
  
306
	return (stringv){.data = data, .length = length};
  
307
}
  
308
  
  
309
NONSTD_DEF stringv sv_slice(stringv sv, size_t start, size_t end) {
  
310
	if (start > sv.length) {
  
311
		start = sv.length;
  
312
	}
  
313
	if (end > sv.length) {
  
314
		end = sv.length;
  
315
	}
  
316
	if (start > end) {
  
317
		start = end;
  
318
	}
  
319
	return (stringv){.data = sv.data + start, .length = end - start};
  
320
}
  
321
  
  
322
NONSTD_DEF int sv_equals(stringv a, stringv b) {
  
323
	return a.length == b.length && (a.length == 0 || memcmp(a.data, b.data, a.length) == 0);
  
324
}
  
325
  
  
326
NONSTD_DEF int sv_starts_with(stringv sv, stringv prefix) {
  
327
	return sv.length >= prefix.length && memcmp(sv.data, prefix.data, prefix.length) == 0;
  
328
}
  
329
  
  
330
NONSTD_DEF int sv_ends_with(stringv sv, stringv suffix) {
  
331
	return sv.length >= suffix.length && memcmp(sv.data + sv.length - suffix.length, suffix.data, suffix.length) == 0;
  
332
}
  
333
  
  
334
// String Builder Implementation
  
335
  
  
336
NONSTD_DEF void sb_init(stringb *sb, size_t initial_cap) {
  
337
	sb->capacity = initial_cap ? initial_cap : 16;
  
338
	sb->data = ALLOC(char, sb->capacity);
  
339
	sb->length = 0;
  
340
	if (sb->data) {
  
341
		sb->data[0] = '\0';
  
342
	}
  
343
}
  
344
  
  
345
NONSTD_DEF void sb_free(stringb *sb) {
  
346
	FREE(sb->data);
  
347
	sb->length = 0;
  
348
	sb->capacity = 0;
  
349
}
  
350
  
  
351
NONSTD_DEF void sb_ensure(stringb *sb, size_t additional) {
  
352
	size_t needed = sb->length + additional + 1;
  
353
	size_t new_cap = sb->capacity;
  
354
  
  
355
	if (needed > new_cap) {
  
356
		while (new_cap < needed) {
  
357
			if (new_cap > SIZE_MAX / 2) {
  
358
				new_cap = SIZE_MAX;
  
359
				break;
  
360
			}
  
361
			new_cap *= 2;
  
362
		}
  
363
		if (new_cap < needed)
  
364
			return; // Overflow
  
365
  
  
366
		char *new_data = safe_realloc(sb->data, sizeof(char), new_cap);
  
367
		if (new_data) {
  
368
			sb->data = new_data;
  
369
			sb->capacity = new_cap;
  
370
		}
  
371
	}
  
372
}
  
373
  
  
374
NONSTD_DEF void sb_append_cstr(stringb *sb, const char *s) {
  
375
	if (!s) {
  
376
		return;
  
377
	}
  
378
	size_t slength = strlen(s);
  
379
	sb_ensure(sb, slength);
  
380
	if (sb->length + slength + 1 <= sb->capacity) {
  
381
		memcpy(sb->data + sb->length, s, slength);
  
382
		sb->length += slength;
  
383
		sb->data[sb->length] = '\0';
  
384
	}
  
385
}
  
386
  
  
387
NONSTD_DEF void sb_append_sv(stringb *sb, stringv sv) {
  
388
	if (!sv.data || sv.length == 0) {
  
389
		return;
  
390
	}
  
391
	sb_ensure(sb, sv.length);
  
392
	if (sb->length + sv.length + 1 <= sb->capacity) {
  
393
		memcpy(sb->data + sb->length, sv.data, sv.length);
  
394
		sb->length += sv.length;
  
395
		sb->data[sb->length] = '\0';
  
396
	}
  
397
}
  
398
  
  
399
NONSTD_DEF void sb_append_char(stringb *sb, char c) {
  
400
	sb_ensure(sb, 1);
  
401
	if (sb->length + 2 <= sb->capacity) {
  
402
		sb->data[sb->length++] = c;
  
403
		sb->data[sb->length] = '\0';
  
404
	}
  
405
}
  
406
  
  
407
NONSTD_DEF stringv sb_as_sv(const stringb *sb) {
  
408
	return (stringv){.data = sb->data, .length = sb->length};
  
409
}
  
410
  
  
411
NONSTD_DEF Arena arena_make(void) {
  
412
	Arena a = {0};
  
413
	array_init(a.blocks);
  
414
	return a;
  
415
}
  
416
  
  
417
// Arena Implementation
  
418
  
  
419
NONSTD_DEF void arena_grow(Arena *a, size_t min_size) {
  
420
	size_t size = MAX(ARENA_DEFAULT_BLOCK_SIZE, min_size);
  
421
	char *block = ALLOC(char, size);
  
422
	a->ptr = block;
  
423
	a->end = block + size;
  
424
	array_push(a->blocks, block);
  
425
}
  
426
  
  
427
NONSTD_DEF void *arena_alloc(Arena *a, size_t size) {
  
428
	// Align to 8 bytes basically
  
429
	size_t align = sizeof(void *);
  
430
	uintptr_t current = (uintptr_t)a->ptr;
  
431
	uintptr_t aligned = (current + align - 1) & ~(align - 1);
  
432
	uintptr_t end = (uintptr_t)a->end;
  
433
  
  
434
	// Check for overflow (aligned wrapped around) or out of bounds (aligned >= end)
  
435
	// or not enough space ((end - aligned) < size)
  
436
	if (aligned < current || aligned >= end || (end - aligned) < size) {
  
437
		arena_grow(a, size);
  
438
		current = (uintptr_t)a->ptr;
  
439
		aligned = (current + align - 1) & ~(align - 1);
  
440
		end = (uintptr_t)a->end;
  
441
	}
  
442
  
  
443
	// Double check after grow (in case grow failed or size is just too huge)
  
444
	if (aligned < current || aligned >= end || (end - aligned) < size) {
  
445
		return NULL;
  
446
	}
  
447
  
  
448
	a->ptr = (char *)(aligned + size);
  
449
	return (void *)aligned;
  
450
}
  
451
  
  
452
NONSTD_DEF void arena_free(Arena *a) {
  
453
	char *block;
  
454
	array_foreach(a->blocks, block) { FREE(block); }
  
455
	array_free(a->blocks);
  
456
	a->ptr = NULL;
  
457
	a->end = NULL;
  
458
}
  
459
  
  
460
// File I/O Implementation
  
461
  
  
462
NONSTD_DEF char *read_entire_file(const char *filepath, size_t *out_size) {
  
463
	FILE *f = fopen(filepath, "rb");
  
464
	if (!f) {
  
465
		return NULL;
  
466
	}
  
467
  
  
468
	fseek(f, 0, SEEK_END);
  
469
	long length = ftell(f);
  
470
	fseek(f, 0, SEEK_SET);
  
471
  
  
472
	if (length < 0) {
  
473
		fclose(f);
  
474
		return NULL;
  
475
	}
  
476
  
  
477
	size_t size = (size_t)length;
  
478
	char *buffer = ALLOC(char, size + 1);
  
479
	if (!buffer) {
  
480
		fclose(f);
  
481
		return NULL;
  
482
	}
  
483
  
  
484
	size_t read = fread(buffer, 1, size, f);
  
485
	fclose(f);
  
486
  
  
487
	if (read != size) {
  
488
		FREE(buffer);
  
489
		return NULL;
  
490
	}
  
491
  
  
492
	buffer[size] = '\0';
  
493
	if (out_size) {
  
494
		*out_size = size;
  
495
	}
  
496
  
  
497
	return buffer;
  
498
}
  
499
  
  
500
NONSTD_DEF int write_entire_file(const char *filepath, const void *data, size_t size) {
  
501
	FILE *f = fopen(filepath, "wb");
  
502
	if (!f) {
  
503
		return 0;
  
504
	}
  
505
  
  
506
	size_t written = fwrite(data, 1, size, f);
  
507
	fclose(f);
  
508
  
  
509
	return written == size;
  
510
}
  
511
  
  
512
NONSTD_DEF stringb read_entire_file_sb(const char *filepath) {
  
513
	size_t size = 0;
  
514
	char *data = read_entire_file(filepath, &size);
  
515
	stringb sb = {0};
  
516
	if (data) {
  
517
		sb.data = data;
  
518
		sb.length = size;
  
519
		sb.capacity = size + 1;
  
520
	}
  
521
	return sb;
  
522
}
  
523
  
  
524
NONSTD_DEF int write_file_sv(const char *filepath, stringv sv) {
  
525
	return write_entire_file(filepath, sv.data, sv.length);
  
526
}
  
527
  
  
528
NONSTD_DEF int write_file_sb(const char *filepath, const stringb *sb) {
  
529
	return write_entire_file(filepath, sb->data, sb->length);
  
530
}
  
531
  
  
532
#endif // NONSTD_IMPLEMENTATION
  
533
  
  
534
#endif // NONSTD_H
  
535
  
  
536
/*
  
537
BSD 2-Clause License
  
538
  
  
539
Copyright (c) 2026, Mitja Felicijan <mitja.felicijan@gmail.com>
  
540
  
  
541
Redistribution and use in source and binary forms, with or without
  
542
modification, are permitted provided that the following conditions are met:
  
543
  
  
544
1. Redistributions of source code must retain the above copyright notice, this
  
545
   list of conditions and the following disclaimer.
  
546
  
  
547
2. Redistributions in binary form must reproduce the above copyright notice,
  
548
   this list of conditions and the following disclaimer in the documentation
  
549
   and/or other materials provided with the distribution.
  
550
  
  
551
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  
552
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  
553
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  
554
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
  
555
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  
556
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  
557
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  
558
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  
559
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  
560
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  
561
*/