diff --git a/main.c b/main.c index b5424592ff6eaec3cbcbf5e0ce612af499d9c24a..cd3fbf1ef4fde8d54c2d8ad1020168b1b4074b0a 100644 --- a/main.c +++ b/main.c @@ -1,8 +1,22 @@ +// Traditional OpenGL "Immediate Mode" (glBegin/glEnd) is slow for many objects +// because every function call creates CPU overhead. Instead, we: +// 1. Calculate all vertex data (Position, Color, UV) on the CPU. +// 2. Store them in a contiguous memory buffer (Batch). +// 3. Send the entire buffer to the GPU in a single call (glDrawArrays). + #include #include #include +#include + +#define NONSTD_IMPLEMENTATION +#include "nonstd.h" #define GL_SILENCE_DEPRECATION + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif #ifdef __APPLE__ #include @@ -24,6 +38,47 @@ typedef struct { float x, y, z, w; } Vec4; +// A packed structure representing a single vertex. +// The GPU needs to know the "stride" (size) of this struct to jump between vertices. +typedef struct { + float x, y, z; // Position + float r, g, b; // Color + float u, v; // Texture Coordinates (UV) +} Vertex; + +// Batching using nonstd.h dynamic arrays. +// This allows us to push an arbitrary number of vertices without manual reallocs. +typedef array(Vertex) Batch; + +Batch textured_batch = {0}; // Grouping textured faces to minimize state changes +Batch colored_batch = {0}; // Grouping solid color faces + +// Rodrigues' rotation formula. +// Since we are batching, we can't use glRotatef (which modifies the global matrix). +// We must rotate every vertex manually on the CPU before pushing it to the buffer. +Vec3 vec3_rotate(Vec3 v, float angle, Vec3 axis) { + float rad = angle * M_PI / 180.0f; + float c = cosf(rad); + float s = sinf(rad); + + float len = sqrtf(axis.x*axis.x + axis.y*axis.y + axis.z*axis.z); + if (len > 0) { axis.x /= len; axis.y /= len; axis.z /= len; } + + float x = axis.x, y = axis.y, z = axis.z; + float dot = v.x*x + v.y*y + v.z*z; + Vec3 cross = { + y*v.z - z*v.y, + z*v.x - x*v.z, + x*v.y - y*v.x + }; + + return (Vec3){ + v.x*c + cross.x*s + x*dot*(1-c), + v.y*c + cross.y*s + y*dot*(1-c), + v.z*c + cross.z*s + z*dot*(1-c) + }; +} + #define COLOR_RED (Vec3){ .x = 1.0f, .y = 0.0f, .z = 0.0f } #define COLOR_GREEN (Vec3){ .x = 0.0f, .y = 1.0f, .z = 0.0f } #define COLOR_BLUE (Vec3){ .x = 0.0f, .y = 0.0f, .z = 1.0f } @@ -86,7 +141,7 @@ } while (getc(fp) != '\n'); // skip single whitespace - unsigned char* data = (unsigned char*)malloc((*width) * (*height) * 3); + unsigned char* data = ALLOC(unsigned char, (*width) * (*height) * 3); if (!data) { fprintf(stderr, "Memory allocation failed\n"); fclose(fp); @@ -95,7 +150,7 @@ } if (fread(data, 1, (*width) * (*height) * 3, fp) != (size_t)((*width) * (*height) * 3)) { fprintf(stderr, "Error reading PPM data\n"); - free(data); + FREE(data); fclose(fp); return NULL; } @@ -117,7 +172,7 @@ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); - free(data); + FREE(data); } void draw_text(float x, float y, void* font, const char* string, Vec3 color) { @@ -143,73 +198,106 @@ glPopMatrix(); glMatrixMode(GL_MODELVIEW); } -void draw_cube(void) { - glEnable(GL_TEXTURE_2D); - glBindTexture(GL_TEXTURE_2D, game.texture_id); - glBegin(GL_QUADS); - // Front Face (Textured) - glColor3f(1.0f, 1.0f, 1.0f); +void batch_push_vertex(Batch* batch, Vertex v) { + array_push(*batch, v); +} - // Backface - glTexCoord2f(0.0f, 0.0f); glVertex3f(-0.1f, -0.1f, 0.1f); - glTexCoord2f(1.0f, 0.0f); glVertex3f( 0.1f, -0.1f, 0.1f); - glTexCoord2f(1.0f, 1.0f); glVertex3f( 0.1f, 0.1f, 0.1f); - glTexCoord2f(0.0f, 1.0f); glVertex3f(-0.1f, 0.1f, 0.1f); +// Takes object-space coordinates and transforms them into world-space. +// Vertices are then sorted into either the textured or colored batch. +void batch_push_cube(Vec3 pos, float size, float angle, Vec3 axis) { + float h = size / 2.0f; + Vec3 corners[8] = { + {-h, -h, h}, { h, -h, h}, { h, h, h}, {-h, h, h}, // Front + {-h, -h, -h}, { h, -h, -h}, { h, h, -h}, {-h, h, -h} // Back + }; - // Top face - glTexCoord2f(0.0f, 0.0f); glVertex3f(-0.1f, 0.1f, -0.1f); - glTexCoord2f(1.0f, 0.0f); glVertex3f(-0.1f, 0.1f, 0.1f); - glTexCoord2f(1.0f, 1.0f); glVertex3f( 0.1f, 0.1f, 0.1f); - glTexCoord2f(0.0f, 1.0f); glVertex3f( 0.1f, 0.1f, -0.1f); - glEnd(); - glDisable(GL_TEXTURE_2D); + // Transform vertices (Rotation -> Translation) + for (int i = 0; i < 8; i++) { + if (angle != 0.0f) corners[i] = vec3_rotate(corners[i], angle, axis); + corners[i].x += pos.x; + corners[i].y += pos.y; + corners[i].z += pos.z; + } + + // Pushing Front and Top faces to the TEXTURED batch + Vec3 white = {1,1,1}; + batch_push_vertex(&textured_batch, (Vertex){corners[0].x, corners[0].y, corners[0].z, white.x, white.y, white.z, 0, 0}); + batch_push_vertex(&textured_batch, (Vertex){corners[1].x, corners[1].y, corners[1].z, white.x, white.y, white.z, 1, 0}); + batch_push_vertex(&textured_batch, (Vertex){corners[2].x, corners[2].y, corners[2].z, white.x, white.y, white.z, 1, 1}); + batch_push_vertex(&textured_batch, (Vertex){corners[3].x, corners[3].y, corners[3].z, white.x, white.y, white.z, 0, 1}); - glBegin(GL_QUADS); - // Back Face (Yellow) - glColor3f(1.0f, 1.0f, 0.0f); - glVertex3f(-0.1f, -0.1f, -0.1f); - glVertex3f(-0.1f, 0.1f, -0.1f); - glVertex3f( 0.1f, 0.1f, -0.1f); - glVertex3f( 0.1f, -0.1f, -0.1f); + batch_push_vertex(&textured_batch, (Vertex){corners[7].x, corners[7].y, corners[7].z, white.x, white.y, white.z, 0, 0}); + batch_push_vertex(&textured_batch, (Vertex){corners[3].x, corners[3].y, corners[3].z, white.x, white.y, white.z, 1, 0}); + batch_push_vertex(&textured_batch, (Vertex){corners[2].x, corners[2].y, corners[2].z, white.x, white.y, white.z, 1, 1}); + batch_push_vertex(&textured_batch, (Vertex){corners[6].x, corners[6].y, corners[6].z, white.x, white.y, white.z, 0, 1}); - // Top Face (Blue) - glColor3f(0.0f, 0.0f, 1.0f); - glVertex3f(-0.1f, 0.1f, -0.1f); - glVertex3f(-0.1f, 0.1f, 0.1f); - glVertex3f( 0.1f, 0.1f, 0.1f); - glVertex3f( 0.1f, 0.1f, -0.1f); + // Pushing remaining faces to the COLORED batch + Vec3 yellow = {1,1,0}; + batch_push_vertex(&colored_batch, (Vertex){corners[4].x, corners[4].y, corners[4].z, yellow.x, yellow.y, yellow.z, 0, 0}); + batch_push_vertex(&colored_batch, (Vertex){corners[7].x, corners[7].y, corners[7].z, yellow.x, yellow.y, yellow.z, 0, 0}); + batch_push_vertex(&colored_batch, (Vertex){corners[6].x, corners[6].y, corners[6].z, yellow.x, yellow.y, yellow.z, 0, 0}); + batch_push_vertex(&colored_batch, (Vertex){corners[5].x, corners[5].y, corners[5].z, yellow.x, yellow.y, yellow.z, 0, 0}); - // Bottom Face (Red) - glColor3f(1.0f, 0.0f, 1.0f); - glVertex3f(-0.1f, -0.1f, -0.1f); - glVertex3f( 0.1f, -0.1f, -0.1f); - glVertex3f( 0.1f, -0.1f, 0.1f); - glVertex3f(-0.1f, -0.1f, 0.1f); + Vec3 magenta = {1,0,1}; + batch_push_vertex(&colored_batch, (Vertex){corners[4].x, corners[4].y, corners[4].z, magenta.x, magenta.y, magenta.z, 0, 0}); + batch_push_vertex(&colored_batch, (Vertex){corners[5].x, corners[5].y, corners[5].z, magenta.x, magenta.y, magenta.z, 0, 0}); + batch_push_vertex(&colored_batch, (Vertex){corners[1].x, corners[1].y, corners[1].z, magenta.x, magenta.y, magenta.z, 0, 0}); + batch_push_vertex(&colored_batch, (Vertex){corners[0].x, corners[0].y, corners[0].z, magenta.x, magenta.y, magenta.z, 0, 0}); - // Right Face (Magenta) - glColor3f(1.0f, 0.0f, 1.0f); - glVertex3f( 0.1f, -0.1f, -0.1f); - glVertex3f( 0.1f, 0.1f, -0.1f); - glVertex3f( 0.1f, 0.1f, 0.1f); - glVertex3f( 0.1f, -0.1f, 0.1f); + Vec3 red = {1,0,0}; + batch_push_vertex(&colored_batch, (Vertex){corners[1].x, corners[1].y, corners[1].z, red.x, red.y, red.z, 0, 0}); + batch_push_vertex(&colored_batch, (Vertex){corners[5].x, corners[5].y, corners[5].z, red.x, red.y, red.z, 0, 0}); + batch_push_vertex(&colored_batch, (Vertex){corners[6].x, corners[6].y, corners[6].z, red.x, red.y, red.z, 0, 0}); + batch_push_vertex(&colored_batch, (Vertex){corners[2].x, corners[2].y, corners[2].z, red.x, red.y, red.z, 0, 0}); - // Left Face (Cyan) - glColor3f(0.0f, 1.0f, 1.0f); - glVertex3f(-0.1f, -0.1f, -0.1f); - glVertex3f(-0.1f, -0.1f, 0.1f); - glVertex3f(-0.1f, 0.1f, 0.1f); - glVertex3f(-0.1f, 0.1f, -0.1f); - glEnd(); + Vec3 cyan = {0,1,1}; + batch_push_vertex(&colored_batch, (Vertex){corners[4].x, corners[4].y, corners[4].z, cyan.x, cyan.y, cyan.z, 0, 0}); + batch_push_vertex(&colored_batch, (Vertex){corners[0].x, corners[0].y, corners[0].z, cyan.x, cyan.y, cyan.z, 0, 0}); + batch_push_vertex(&colored_batch, (Vertex){corners[3].x, corners[3].y, corners[3].z, cyan.x, cyan.y, cyan.z, 0, 0}); + batch_push_vertex(&colored_batch, (Vertex){corners[7].x, corners[7].y, corners[7].z, cyan.x, cyan.y, cyan.z, 0, 0}); } -void draw_quad() { - glBegin(GL_POLYGON); - glVertex3f (0.25, 0.25, 0.0); - glVertex3f (0.75, 0.25, 0.0); - glVertex3f (0.75, 0.75, 0.0); - glVertex3f (0.25, 0.75, 0.0); - glEnd(); +// The main render call that talks to the GPU. +void batch_render() { + // Enable client-side arrays (tells OpenGL we are passing memory pointers) + glEnableClientState(GL_VERTEX_ARRAY); + glEnableClientState(GL_COLOR_ARRAY); + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + + // 1. Render all TEXTURED geometry + if (textured_batch.length > 0) { + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, game.texture_id); + + Vertex* v = textured_batch.data; + // Pointer math: specify (size, type, stride, offset) + glVertexPointer(3, GL_FLOAT, sizeof(Vertex), &v[0].x); + glColorPointer(3, GL_FLOAT, sizeof(Vertex), &v[0].r); + glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &v[0].u); + + glDrawArrays(GL_QUADS, 0, textured_batch.length); + glDisable(GL_TEXTURE_2D); + } + + // 2. Render all COLORED geometry + if (colored_batch.length > 0) { + Vertex* v = colored_batch.data; + glVertexPointer(3, GL_FLOAT, sizeof(Vertex), &v[0].x); + glColorPointer(3, GL_FLOAT, sizeof(Vertex), &v[0].r); + glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &v[0].u); + + glDrawArrays(GL_QUADS, 0, colored_batch.length); + } + + glDisableClientState(GL_VERTEX_ARRAY); + glDisableClientState(GL_COLOR_ARRAY); + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + + // Clear length (re-use capacity) for the next frame + array_clear(textured_batch); + array_clear(colored_batch); } + void render_display(void) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); @@ -217,19 +305,16 @@ glMatrixMode(GL_MODELVIEW); glLoadIdentity(); - // Render Moving Square - /* glPushMatrix(); */ - /* glTranslatef(x_offset, y_offset, 0.0f); */ - /* glColor3f(1.0, 1.0, 1.0); */ - /* draw_quad(); */ - /* glPopMatrix(); */ - - // Render Spinning Cube - glPushMatrix(); - glTranslatef(0.5f, 0.5f, 0.0f); - glRotatef(game.cube_angle, 1.0f, 1.0f, 1.0f); - draw_cube(); - glPopMatrix(); + // Render Batch of Cubes + for (int i = 0; i < 10; i++) { + for (int j = 0; j < 10; j++) { + float x = 0.1f + i * 0.1f; + float y = 0.1f + j * 0.1f; + float angle = game.cube_angle + (i + j) * 10.0f; + batch_push_cube((Vec3){x, y, 0.0f}, 0.05f, angle, (Vec3){1.0f, 1.0f, 1.0f}); + } + } + batch_render(); // Render UI Text char fps_text[32]; diff --git a/nonstd.h b/nonstd.h new file mode 100644 index 0000000000000000000000000000000000000000..5e819e0d82d5f4a92f4b8bd4159977a7f634ca85 --- /dev/null +++ b/nonstd.h @@ -0,0 +1,561 @@ +// nonstd.h +// A collection of useful functions and macros. +// This library is licensed under the BSD 2-Clause License. +// +// This file provides both the interface and the implementation. +// To instantiate the implementation, +// #define NONSTD_IMPLEMENTATION +// before including this file. + +#ifdef NONSTD_IMPLEMENTATION +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif +#endif + +#ifndef NONSTD_H +#define NONSTD_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef NONSTD_DEF +#ifdef NONSTD_STATIC +#define NONSTD_DEF static +#else +#define NONSTD_DEF extern +#endif +#endif + +typedef int8_t i8; +typedef uint8_t u8; +typedef int16_t i16; +typedef uint16_t u16; +typedef int32_t i32; +typedef uint32_t u32; +typedef int64_t i64; +typedef uint64_t u64; +typedef unsigned int uint; +typedef unsigned long ulong; +typedef intptr_t isize; +typedef uintptr_t usize; +typedef char c8; + +#define countof(a) (sizeof(a) / sizeof((a)[0])) + +#define static_foreach(type, var, array) \ + for (size_t _i_##var = 0, _n_##var = countof(array); \ + _i_##var < _n_##var && ((var) = (array)[_i_##var], 1); ++_i_##var) + +#define ALLOC(type, n) ((type *)safe_malloc(sizeof(type), (n))) +#define REALLOC(ptr, type, n) ((type *)safe_realloc((ptr), sizeof(type), (n))) +#define FREE(ptr) \ + do { \ + free(ptr); \ + ptr = NULL; \ + } while (0) + +NONSTD_DEF void *safe_malloc(size_t item_size, size_t count); +NONSTD_DEF void *safe_realloc(void *ptr, size_t item_size, size_t count); + +#ifndef MIN +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#endif +#ifndef MAX +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#endif +#define CLAMP(x, lo, hi) (MIN((hi), MAX((lo), (x)))) + +// From https://github.com/tsoding/nob.h/blob/e2c9a46f01d052ab740140e74453665dc3334832/nob.h#L205-L206. +#define UNUSED(value) (void)(value) +#define TODO(message) \ + do { \ + fprintf(stderr, "%s:%d: TODO: %s\n", __FILE__, __LINE__, message); \ + abort(); \ + } while (0) +#define UNREACHABLE(message) \ + do { \ + fprintf(stderr, "%s:%d: UNREACHABLE: %s\n", __FILE__, __LINE__, message); \ + abort(); \ + } while (0) + +#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L +#define STATIC_ASSERT(expr, msg) _Static_assert((expr), msg) +#else +#define STATIC_ASSERT(expr, msg) \ + typedef char static_assertion_##msg[(expr) ? 1 : -1] +#endif + +// String view - read-only, non-owning reference to a string +typedef struct { + const char *data; + size_t length; +} stringv; + +NONSTD_DEF stringv sv_from_cstr(const char *s); +NONSTD_DEF stringv sv_from_parts(const char *data, size_t length); +NONSTD_DEF stringv sv_slice(stringv sv, size_t start, size_t end); +NONSTD_DEF int sv_equals(stringv a, stringv b); +NONSTD_DEF int sv_starts_with(stringv sv, stringv prefix); +NONSTD_DEF int sv_ends_with(stringv sv, stringv suffix); + +// String builder - owning, mutable, dynamically growing string buffer +typedef struct { + char *data; + size_t length; + size_t capacity; +} stringb; + +NONSTD_DEF void sb_init(stringb *sb, size_t initial_cap); +NONSTD_DEF void sb_free(stringb *sb); +NONSTD_DEF void sb_ensure(stringb *sb, size_t additional); +NONSTD_DEF void sb_append_cstr(stringb *sb, const char *s); +NONSTD_DEF void sb_append_sv(stringb *sb, stringv sv); +NONSTD_DEF void sb_append_char(stringb *sb, char c); +NONSTD_DEF stringv sb_as_sv(const stringb *sb); + +// Slice - generic non-owning view into an array +// Usage: SLICE_DEF(int); slice(int) view = ...; +#define SLICE_DEF(T) \ + typedef struct { \ + T *data; \ + size_t length; \ + } slice_##T + +#define slice(T) slice_##T + +#define make_slice(T, ptr, len) ((slice(T)){.data = (ptr), .length = (len)}) + +#define array_as_slice(T, arr) \ + ((slice(T)){.data = (arr).data, .length = (arr).length}) + +// Dynamic array - generic type-safe growable array using macros +// Usage: array(int) numbers; array_init(numbers); +#define array(T) \ + struct { \ + T *data; \ + size_t length; \ + size_t capacity; \ + } + +#define array_init(arr) \ + do { \ + (arr).capacity = 0; \ + (arr).data = NULL; \ + (arr).length = 0; \ + } while (0) + +#define array_init_cap(arr, initial_cap) \ + do { \ + (arr).capacity = (initial_cap) ? (initial_cap) : 16; \ + (arr).data = ALLOC(__typeof__(*(arr).data), (arr).capacity); \ + (arr).length = 0; \ + } while (0) + +#define array_free(arr) \ + do { \ + FREE((arr).data); \ + (arr).length = 0; \ + (arr).capacity = 0; \ + } while (0) + +#define array_ensure(arr, additional) \ + do { \ + size_t _needed = (arr).length + (additional); \ + if (_needed > (arr).capacity) { \ + size_t _new_cap = (arr).capacity ? (arr).capacity : 16; \ + while (_new_cap < _needed) { \ + if (_new_cap > SIZE_MAX / 2) { \ + _new_cap = SIZE_MAX; \ + break; \ + } \ + _new_cap *= 2; \ + } \ + if (_new_cap < _needed) { /* Overflow or OOM */ \ + break; \ + } \ + void *_new_data = \ + safe_realloc((arr).data, sizeof(*(arr).data), _new_cap); \ + if (_new_data) { \ + (arr).data = _new_data; \ + (arr).capacity = _new_cap; \ + } \ + } \ + } while (0) + +#define array_push(arr, value) \ + do { \ + array_ensure((arr), 1); \ + if ((arr).length < (arr).capacity) { \ + (arr).data[(arr).length++] = (value); \ + } \ + } while (0) + +#define array_pop(arr) ((arr).length > 0 ? (arr).data[--(arr).length] : 0) + +#define array_get(arr, index) ((arr).data[index]) + +#define array_set(arr, index, value) \ + do { \ + if ((index) < (arr).length) { \ + (arr).data[index] = (value); \ + } \ + } while (0) + +#define array_insert(arr, index, value) \ + do { \ + if ((index) <= (arr).length) { \ + array_ensure((arr), 1); \ + if ((arr).length < (arr).capacity) { \ + for (size_t _i = (arr).length; _i > (index); --_i) { \ + (arr).data[_i] = (arr).data[_i - 1]; \ + } \ + (arr).data[index] = (value); \ + (arr).length++; \ + } \ + } \ + } while (0) + +#define array_remove(arr, index) \ + do { \ + if ((index) < (arr).length) { \ + for (size_t _i = (index); _i < (arr).length - 1; ++_i) { \ + (arr).data[_i] = (arr).data[_i + 1]; \ + } \ + (arr).length--; \ + } \ + } while (0) + +#define array_clear(arr) \ + do { \ + (arr).length = 0; \ + } while (0) + +#define array_reserve(arr, new_capacity) \ + do { \ + if ((new_capacity) > (arr).capacity) { \ + void *_new_data = \ + safe_realloc((arr).data, sizeof(*(arr).data), (new_capacity)); \ + if (_new_data) { \ + (arr).data = _new_data; \ + (arr).capacity = (new_capacity); \ + } \ + } \ + } while (0) + +#define array_foreach(arr, var) \ + for (size_t _i_##var = 0; \ + _i_##var < (arr).length && ((var) = (arr).data[_i_##var], 1); \ + ++_i_##var) + +#define array_foreach_idx(arr, var, index) \ + for (size_t index = 0; \ + index < (arr).length && ((var) = (arr).data[index], 1); ++index) + +// Arena - block-based memory allocator +typedef struct { + char *ptr; + char *end; + array(char *) blocks; +} Arena; + +#define ARENA_DEFAULT_BLOCK_SIZE (4096) + +NONSTD_DEF Arena arena_make(void); +NONSTD_DEF void arena_grow(Arena *a, size_t min_size); +NONSTD_DEF void *arena_alloc(Arena *a, size_t size); +NONSTD_DEF void arena_free(Arena *a); + +// File I/O helpers +NONSTD_DEF char *read_entire_file(const char *filepath, size_t *out_size); +NONSTD_DEF int write_entire_file(const char *filepath, const void *data, size_t size); +NONSTD_DEF stringb read_entire_file_sb(const char *filepath); +NONSTD_DEF int write_file_sv(const char *filepath, stringv sv); +NONSTD_DEF int write_file_sb(const char *filepath, const stringb *sb); + +#ifdef NONSTD_IMPLEMENTATION + +NONSTD_DEF void *safe_malloc(size_t item_size, size_t count) { + if (count != 0 && item_size > SIZE_MAX / count) { + return NULL; + } + return malloc(item_size * count); +} + +NONSTD_DEF void *safe_realloc(void *ptr, size_t item_size, size_t count) { + if (count != 0 && item_size > SIZE_MAX / count) { + return NULL; + } + return realloc(ptr, item_size * count); +} + +// String View Implementation + +NONSTD_DEF stringv sv_from_cstr(const char *s) { + return (stringv){.data = s, .length = s ? strlen(s) : 0}; +} + +NONSTD_DEF stringv sv_from_parts(const char *data, size_t length) { + return (stringv){.data = data, .length = length}; +} + +NONSTD_DEF stringv sv_slice(stringv sv, size_t start, size_t end) { + if (start > sv.length) { + start = sv.length; + } + if (end > sv.length) { + end = sv.length; + } + if (start > end) { + start = end; + } + return (stringv){.data = sv.data + start, .length = end - start}; +} + +NONSTD_DEF int sv_equals(stringv a, stringv b) { + return a.length == b.length && (a.length == 0 || memcmp(a.data, b.data, a.length) == 0); +} + +NONSTD_DEF int sv_starts_with(stringv sv, stringv prefix) { + return sv.length >= prefix.length && memcmp(sv.data, prefix.data, prefix.length) == 0; +} + +NONSTD_DEF int sv_ends_with(stringv sv, stringv suffix) { + return sv.length >= suffix.length && memcmp(sv.data + sv.length - suffix.length, suffix.data, suffix.length) == 0; +} + +// String Builder Implementation + +NONSTD_DEF void sb_init(stringb *sb, size_t initial_cap) { + sb->capacity = initial_cap ? initial_cap : 16; + sb->data = ALLOC(char, sb->capacity); + sb->length = 0; + if (sb->data) { + sb->data[0] = '\0'; + } +} + +NONSTD_DEF void sb_free(stringb *sb) { + FREE(sb->data); + sb->length = 0; + sb->capacity = 0; +} + +NONSTD_DEF void sb_ensure(stringb *sb, size_t additional) { + size_t needed = sb->length + additional + 1; + size_t new_cap = sb->capacity; + + if (needed > new_cap) { + while (new_cap < needed) { + if (new_cap > SIZE_MAX / 2) { + new_cap = SIZE_MAX; + break; + } + new_cap *= 2; + } + if (new_cap < needed) + return; // Overflow + + char *new_data = safe_realloc(sb->data, sizeof(char), new_cap); + if (new_data) { + sb->data = new_data; + sb->capacity = new_cap; + } + } +} + +NONSTD_DEF void sb_append_cstr(stringb *sb, const char *s) { + if (!s) { + return; + } + size_t slength = strlen(s); + sb_ensure(sb, slength); + if (sb->length + slength + 1 <= sb->capacity) { + memcpy(sb->data + sb->length, s, slength); + sb->length += slength; + sb->data[sb->length] = '\0'; + } +} + +NONSTD_DEF void sb_append_sv(stringb *sb, stringv sv) { + if (!sv.data || sv.length == 0) { + return; + } + sb_ensure(sb, sv.length); + if (sb->length + sv.length + 1 <= sb->capacity) { + memcpy(sb->data + sb->length, sv.data, sv.length); + sb->length += sv.length; + sb->data[sb->length] = '\0'; + } +} + +NONSTD_DEF void sb_append_char(stringb *sb, char c) { + sb_ensure(sb, 1); + if (sb->length + 2 <= sb->capacity) { + sb->data[sb->length++] = c; + sb->data[sb->length] = '\0'; + } +} + +NONSTD_DEF stringv sb_as_sv(const stringb *sb) { + return (stringv){.data = sb->data, .length = sb->length}; +} + +NONSTD_DEF Arena arena_make(void) { + Arena a = {0}; + array_init(a.blocks); + return a; +} + +// Arena Implementation + +NONSTD_DEF void arena_grow(Arena *a, size_t min_size) { + size_t size = MAX(ARENA_DEFAULT_BLOCK_SIZE, min_size); + char *block = ALLOC(char, size); + a->ptr = block; + a->end = block + size; + array_push(a->blocks, block); +} + +NONSTD_DEF void *arena_alloc(Arena *a, size_t size) { + // Align to 8 bytes basically + size_t align = sizeof(void *); + uintptr_t current = (uintptr_t)a->ptr; + uintptr_t aligned = (current + align - 1) & ~(align - 1); + uintptr_t end = (uintptr_t)a->end; + + // Check for overflow (aligned wrapped around) or out of bounds (aligned >= end) + // or not enough space ((end - aligned) < size) + if (aligned < current || aligned >= end || (end - aligned) < size) { + arena_grow(a, size); + current = (uintptr_t)a->ptr; + aligned = (current + align - 1) & ~(align - 1); + end = (uintptr_t)a->end; + } + + // Double check after grow (in case grow failed or size is just too huge) + if (aligned < current || aligned >= end || (end - aligned) < size) { + return NULL; + } + + a->ptr = (char *)(aligned + size); + return (void *)aligned; +} + +NONSTD_DEF void arena_free(Arena *a) { + char *block; + array_foreach(a->blocks, block) { FREE(block); } + array_free(a->blocks); + a->ptr = NULL; + a->end = NULL; +} + +// File I/O Implementation + +NONSTD_DEF char *read_entire_file(const char *filepath, size_t *out_size) { + FILE *f = fopen(filepath, "rb"); + if (!f) { + return NULL; + } + + fseek(f, 0, SEEK_END); + long length = ftell(f); + fseek(f, 0, SEEK_SET); + + if (length < 0) { + fclose(f); + return NULL; + } + + size_t size = (size_t)length; + char *buffer = ALLOC(char, size + 1); + if (!buffer) { + fclose(f); + return NULL; + } + + size_t read = fread(buffer, 1, size, f); + fclose(f); + + if (read != size) { + FREE(buffer); + return NULL; + } + + buffer[size] = '\0'; + if (out_size) { + *out_size = size; + } + + return buffer; +} + +NONSTD_DEF int write_entire_file(const char *filepath, const void *data, size_t size) { + FILE *f = fopen(filepath, "wb"); + if (!f) { + return 0; + } + + size_t written = fwrite(data, 1, size, f); + fclose(f); + + return written == size; +} + +NONSTD_DEF stringb read_entire_file_sb(const char *filepath) { + size_t size = 0; + char *data = read_entire_file(filepath, &size); + stringb sb = {0}; + if (data) { + sb.data = data; + sb.length = size; + sb.capacity = size + 1; + } + return sb; +} + +NONSTD_DEF int write_file_sv(const char *filepath, stringv sv) { + return write_entire_file(filepath, sv.data, sv.length); +} + +NONSTD_DEF int write_file_sb(const char *filepath, const stringb *sb) { + return write_entire_file(filepath, sb->data, sb->length); +} + +#endif // NONSTD_IMPLEMENTATION + +#endif // NONSTD_H + +/* +BSD 2-Clause License + +Copyright (c) 2026, Mitja Felicijan + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/