partially done work on total source tree rework, separation of engine context and game context, generalization of renderer for different backends as well as web platform target

This commit is contained in:
2024-09-16 09:07:01 +03:00
parent ca0305feab
commit 551d60ef85
59 changed files with 2892 additions and 890 deletions

View File

@ -0,0 +1,142 @@
#include "townengine/twn_game_object.h"
#include "townengine/context.h"
#include <x-watcher.h>
#include <SDL2/SDL.h>
#include <dlfcn.h>
#define GAME_OBJECT_PATH "./libgame.so"
#define MODIFIED_TICKS_MERGED 10
static void (*game_tick_callback)(void);
static void (*game_end_callback)(void);
static x_watcher *watcher;
static void *handle = NULL;
static uint64_t last_tick_modified;
static bool loaded_after_modification = true;
static SDL_mutex *lock;
static void load_game_object(void) {
/* needs to be closed otherwise symbols aren't resolved again */
if (handle) {
dlclose(handle);
handle = NULL;
game_tick_callback = NULL;
game_end_callback = NULL;
}
void *new_handle = dlopen(GAME_OBJECT_PATH, RTLD_LAZY);
if (!new_handle) {
log_critical("Hot Reload Error: Cannot open game code shared object (%s)", dlerror());
goto ERR_OPENING_SO;
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic"
game_tick_callback = (void (*)(void))dlsym(new_handle, "game_tick");
if (!game_tick_callback) {
CRY("Hot Reload Error", "game_tick_callback() symbol wasn't found");
goto ERR_GETTING_PROC;
}
game_end_callback = (void (*)(void))dlsym(new_handle, "game_end");
if (!game_end_callback) {
CRY("Hot Reload Error", "game_end_callback() symbol wasn't found");
goto ERR_GETTING_PROC;
}
#pragma GCC diagnostic pop
handle = new_handle;
if (ctx.tick_count != 0)
log_info("Game object was reloaded\n");
return;
ERR_GETTING_PROC:
dlclose(new_handle);
game_tick_callback = NULL;
game_end_callback = NULL;
ERR_OPENING_SO:
SDL_UnlockMutex(lock);
}
static void watcher_callback(XWATCHER_FILE_EVENT event,
const char *path,
int context,
void *data)
{
(void)context;
(void)path;
(void)data;
switch(event) {
case XWATCHER_FILE_MODIFIED:
SDL_LockMutex(lock);
last_tick_modified = ctx.tick_count;
loaded_after_modification = false;
SDL_UnlockMutex(lock);
break;
default:
break;
}
}
void game_object_load(void) {
watcher = xWatcher_create();
xWatcher_reference dir;
dir.path = GAME_OBJECT_PATH;
dir.callback_func = watcher_callback;
xWatcher_appendFile(watcher, &dir);
xWatcher_start(watcher);
lock = SDL_CreateMutex();
load_game_object();
}
void game_object_unload(void) {
game_end_callback();
xWatcher_destroy(watcher);
watcher = NULL;
dlclose(handle);
handle = NULL;
game_tick_callback = NULL;
game_end_callback = NULL;
SDL_DestroyMutex(lock);
}
bool game_object_try_reloading(void) {
bool result = false;
/* only load the modified library after some time, as compilers make a lot of modifications */
SDL_LockMutex(lock);
if (ctx.tick_count - last_tick_modified > MODIFIED_TICKS_MERGED &&
!loaded_after_modification) {
load_game_object();
loaded_after_modification = true;
result = true;
} SDL_UnlockMutex(lock);
return result;
}
void game_object_tick(void) {
game_tick_callback();
}

View File

@ -0,0 +1,21 @@
#include "townengine/twn_game_object.h"
#include "townengine/game_api.h"
void game_object_load(void) {
}
void game_object_unload(void) {
game_end();
}
bool game_object_try_reloading(void) {
return false;
}
void game_object_tick(void) {
game_tick();
}

View File

@ -0,0 +1,88 @@
#include "townengine/twn_game_object.h"
#include "townengine/context.h"
#include <errhandlingapi.h>
#include <libloaderapi.h>
#define GAME_OBJECT_PATH "libgame.dll"
static void (*game_tick_callback)(void);
static void (*game_end_callback)(void);
static void *handle = NULL;
static void load_game_object(void) {
/* needs to be closed otherwise symbols aren't resolved again */
if (handle) {
FreeLibrary(handle);
handle = NULL;
game_tick_callback = NULL;
game_end_callback = NULL;
}
void *new_handle = LoadLibraryA(GAME_OBJECT_PATH);
if (!new_handle) {
log_critical("Hot Reload Error: Cannot open game code shared object (%s)", GetLastError());
goto ERR_OPENING_SO;
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic"
game_tick_callback = (void (*)(void))GetProcAddress(new_handle, "game_tick");
if (!game_tick_callback) {
CRY("Hot Reload Error", "game_tick_callback() symbol wasn't found");
goto ERR_GETTING_PROC;
}
game_end_callback = (void (*)(void))GetProcAddress(new_handle, "game_end");
if (!game_end_callback) {
CRY("Hot Reload Error", "game_end_callback() symbol wasn't found");
goto ERR_GETTING_PROC;
}
#pragma GCC diagnostic pop
handle = new_handle;
if (ctx.tick_count != 0)
log_info("Game object was reloaded\n");
return;
ERR_GETTING_PROC:
FreeLibrary(new_handle);
game_tick_callback = NULL;
game_end_callback = NULL;
ERR_OPENING_SO:
die_abruptly();
}
void game_object_load(void) {
load_game_object();
}
void game_object_unload(void) {
game_end_callback();
FreeLibrary(handle);
handle = NULL;
game_tick_callback = NULL;
game_end_callback = NULL;
}
/* doesn't support reloading because of problems with file watching */
bool game_object_try_reloading(void) {
return false;
}
void game_object_tick(void) {
game_tick_callback();
}

View File

@ -0,0 +1,87 @@
#include "townengine/util.h"
#include "townengine/context.h"
#include "twn_rendering_c.h"
#include <SDL2/SDL.h>
#include <stb_ds.h>
void push_circle(t_fvec2 position, float radius, t_color color) {
struct circle_primitive circle = {
.radius = radius,
.color = color,
.position = position,
};
struct primitive_2d primitive = {
.type = PRIMITIVE_2D_CIRCLE,
.circle = circle,
};
arrput(ctx.render_queue_2d, primitive);
}
/* TODO: caching and reuse scheme */
/* vertices_out and indices_out MUST BE FREED */
void create_circle_geometry(t_fvec2 position,
t_color color,
float radius,
size_t num_vertices,
SDL_Vertex **vertices_out,
int **indices_out)
{
SDL_Vertex *vertices = cmalloc(sizeof *vertices * (num_vertices + 1));
int *indices = cmalloc(sizeof *indices * (num_vertices * 3));
/* the angle (in radians) to rotate by on each iteration */
float seg_rotation_angle = (360.0f / (float)num_vertices) * ((float)M_PI / 180);
vertices[0].position.x = (float)position.x;
vertices[0].position.y = (float)position.y;
vertices[0].color.r = color.r;
vertices[0].color.g = color.g;
vertices[0].color.b = color.b;
vertices[0].color.a = color.a;
vertices[0].tex_coord = (SDL_FPoint){ 0, 0 };
/* this point will rotate around the center */
float start_x = 0.0f - radius;
float start_y = 0.0f;
for (size_t i = 1; i < num_vertices + 1; ++i) {
float final_seg_rotation_angle = (float)i * seg_rotation_angle;
vertices[i].position.x =
cosf(final_seg_rotation_angle) * start_x -
sinf(final_seg_rotation_angle) * start_y;
vertices[i].position.y =
cosf(final_seg_rotation_angle) * start_y +
sinf(final_seg_rotation_angle) * start_x;
vertices[i].position.x += position.x;
vertices[i].position.y += position.y;
vertices[i].color.r = color.r;
vertices[i].color.g = color.g;
vertices[i].color.b = color.b;
vertices[i].color.a = color.a;
vertices[i].tex_coord = (SDL_FPoint){ 0, 0 };
size_t triangle_offset = 3 * (i - 1);
/* center point index */
indices[triangle_offset] = 0;
/* generated point index */
indices[triangle_offset + 1] = (int)i;
size_t index = (i + 1) % num_vertices;
if (index == 0)
index = num_vertices;
indices[triangle_offset + 2] = (int)index;
}
*vertices_out = vertices;
*indices_out = indices;
}

View File

@ -0,0 +1,28 @@
static gpu_texture new_gl_texture(void) {
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
#if !defined(EMSCRIPTEN)
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
#endif
glBindTexture(GL_TEXTURE_2D, 0);
return create_gpu_texture(TEXTURE_FILTER_NEAREST, true);
}
glTexImage2D(GL_TEXTURE_2D,
0,
GL_RGBA8,
surface->w,
surface->h,
0,
GL_RGBA,
GL_UNSIGNED_BYTE,
surface->pixels);

View File

@ -0,0 +1,429 @@
#include "twn_gl_15_rendering_c.h"
#include "twn_rendering_c.h"
#include "townengine/util.h"
#include "townengine/config.h"
#include "townengine/context.h"
#include "twn_text_c.h"
#include <glad/glad.h>
/* interleaved vertex array data */
/* TODO: use int16_t for uvs */
/* TODO: use packed types? */
/* TODO: int16_t could be used for positioning, but we would need to have more CPU calcs */
struct element_indexed_quad {
/* upper-left */
t_fvec2 v0;
t_fvec2 uv0;
t_color c0;
/* bottom-left */
t_fvec2 v1;
t_fvec2 uv1;
t_color c1;
/* bottom-right */
t_fvec2 v2;
t_fvec2 uv2;
t_color c2;
/* upper-right */
t_fvec2 v3;
t_fvec2 uv3;
t_color c3;
};
struct element_indexed_quad_without_color {
/* upper-left */
t_fvec2 v0;
t_fvec2 uv0;
/* bottom-left */
t_fvec2 v1;
t_fvec2 uv1;
/* bottom-right */
t_fvec2 v2;
t_fvec2 uv2;
/* upper-right */
t_fvec2 v3;
t_fvec2 uv3;
};
typedef enum {
PIPELINE_NO,
PIPELINE_SPACE,
PIPELINE_2D,
} pipeline;
static pipeline pipeline_last_used = PIPELINE_NO;
void use_space_pipeline(void) {
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
glShadeModel(GL_SMOOTH);
if (GLAD_GL_ARB_depth_clamp)
glDisable(GL_DEPTH_CLAMP);
glMatrixMode(GL_PROJECTION);
glLoadMatrixf(&camera_projection_matrix.row[0].x);
glMatrixMode(GL_MODELVIEW);
glLoadMatrixf(&camera_look_at_matrix.row[0].x);
glEnable(GL_CULL_FACE);
glEnable(GL_DEPTH_TEST);
glDepthMask(GL_TRUE);
glDepthFunc(GL_LESS);
glDepthRange(0, 1);
glDisable(GL_BLEND);
glEnable(GL_ALPHA_TEST); /* TODO: infer its usage? */
glAlphaFunc(GL_EQUAL, 1.0f);
glEnable(GL_TEXTURE_2D);
glActiveTexture(GL_TEXTURE0);
/* solid white, no modulation */
glColor4ub(255, 255, 255, 255);
pipeline_last_used = PIPELINE_SPACE;
}
void use_2d_pipeline(void) {
if (pipeline_last_used == PIPELINE_SPACE) {
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glFlush();
}
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST);
glShadeModel(GL_FLAT);
/* removes near/far plane comparison and discard */
if (GLAD_GL_ARB_depth_clamp)
glDisable(GL_DEPTH_CLAMP);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0, RENDER_BASE_WIDTH, RENDER_BASE_HEIGHT, 0, -1, 1);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glEnable(GL_TEXTURE_2D);
glActiveTexture(GL_TEXTURE0);
glDisable(GL_CULL_FACE);
glEnable(GL_DEPTH_TEST);
pipeline_last_used = PIPELINE_2D;
}
void upload_quad_vertices(t_frect rect) {
/* client memory needs to be reachable on glDraw*, so */
static float vertices[6 * 2];
vertices[0] = rect.x; vertices[1] = rect.y;
vertices[2] = rect.x; vertices[3] = rect.y + rect.h;
vertices[4] = rect.x + rect.w; vertices[5] = rect.y + rect.h;
vertices[6] = rect.x + rect.w; vertices[7] = rect.y + rect.h;
vertices[8] = rect.x + rect.w; vertices[9] = rect.y;
vertices[10] = rect.x; vertices[11] = rect.y;
glVertexPointer(2, GL_FLOAT, 0, (void *)&vertices);
}
void render_rectangle(const struct rect_primitive *rectangle) {
glColor4ub(rectangle->color.r, rectangle->color.g,
rectangle->color.b, rectangle->color.a);
glEnableClientState(GL_VERTEX_ARRAY);
upload_quad_vertices(rectangle->rect);
glDrawArrays(GL_TRIANGLES, 0, 6);
glDisableClientState(GL_VERTEX_ARRAY);
}
void render_circle(const struct circle_primitive *circle) {
SDL_Vertex *vertices = NULL;
int *indices = NULL;
int num_vertices = (int)circle->radius;
create_circle_geometry(circle->position,
circle->color,
circle->radius,
num_vertices,
&vertices,
&indices);
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(2,
GL_FLOAT,
sizeof (SDL_Vertex),
&vertices->position);
glEnableClientState(GL_COLOR_ARRAY);
glColorPointer(4,
GL_UNSIGNED_BYTE,
sizeof (SDL_Vertex),
&vertices->color);
glDrawElements(GL_TRIANGLES,
num_vertices * 3,
GL_UNSIGNED_INT,
indices);
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);
free(vertices);
free(indices);
}
void use_texture_mode(enum texture_mode mode) {
if (mode == TEXTURE_MODE_GHOSTLY) {
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glDepthFunc(GL_ALWAYS);
glDepthMask(GL_FALSE);
glDisable(GL_ALPHA_TEST);
} else if (mode == TEXTURE_MODE_SEETHROUGH) {
glDisable(GL_BLEND);
glDepthFunc(GL_LEQUAL);
glDepthMask(GL_TRUE);
glEnable(GL_ALPHA_TEST);
glAlphaFunc(GL_EQUAL, 1.0f);
} else {
glDisable(GL_BLEND);
glDepthFunc(GL_LESS);
glDepthMask(GL_TRUE);
glDisable(GL_ALPHA_TEST);
}
}
vertex_buffer_builder build_vertex_buffer(vertex_buffer buffer, size_t bytes) {
glBindBuffer(GL_ARRAY_BUFFER, buffer);
void *mapping = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
if (!mapping)
CRY("build_vertex_buffer", "Error mapping a vertex array buffer");
return (vertex_buffer_builder) {
.mapping = mapping,
.bytes_left = bytes,
};
}
bool push_to_vertex_buffer_builder(vertex_buffer_builder *builder,
void *bytes, size_t size) {
if (builder->bytes_left == 0)
return false;
memcpy(builder->mapping, bytes, size);
builder->bytes_left -= size;
/* trigger data send */
if (builder->bytes_left == 0) {
glUnmapBuffer(GL_ARRAY_BUFFER);
return false;
}
return true;
}
void finally_render_sprites(const struct primitive_2d primitives[],
const struct sprite_batch batch,
const vertex_buffer vertex_buffer)
{
/* TODO: maybe do, dunno */
// glBindBuffer(GL_VERTEX_ARRAY, vertex_buffer);
(void)vertex_buffer;
GLsizei off;
GLsizei voff;
GLsizei uvoff;
if (!batch.constant_colored) {
off = offsetof(struct element_indexed_quad, v1);
voff = offsetof(struct element_indexed_quad, v0);
uvoff = offsetof(struct element_indexed_quad, uv0);
} else {
off = offsetof(struct element_indexed_quad_without_color, v1);
voff = offsetof(struct element_indexed_quad_without_color, v0);
uvoff = offsetof(struct element_indexed_quad_without_color, uv0);
}
/* vertex specification */
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(2,
GL_FLOAT,
off,
(void *)(size_t)voff);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glClientActiveTexture(GL_TEXTURE0);
glTexCoordPointer(2,
GL_FLOAT,
off,
(void *)(size_t)uvoff);
if (!batch.constant_colored) {
glEnableClientState(GL_COLOR_ARRAY);
glColorPointer(4,
GL_UNSIGNED_BYTE,
off,
(void *)offsetof(struct element_indexed_quad, c0));
} else
glColor4ub(primitives[0].sprite.color.r,
primitives[0].sprite.color.g,
primitives[0].sprite.color.b,
primitives[0].sprite.color.a);
if (!batch.repeat)
textures_bind(&ctx.texture_cache, primitives->sprite.texture_key);
else
textures_bind_repeating(&ctx.texture_cache, primitives->sprite.texture_key);
bind_quad_element_buffer();
glDrawElements(GL_TRIANGLES, 6 * (GLsizei)batch.size, GL_UNSIGNED_SHORT, NULL);
/* clear the state */
glBufferData(GL_ARRAY_BUFFER, 0, NULL, GL_STREAM_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_COLOR_ARRAY);
glBindTexture(GL_TEXTURE_2D, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
size_t get_sprite_payload_size(struct sprite_batch batch) {
if (batch.constant_colored)
return sizeof (struct element_indexed_quad_without_color);
else
return sizeof (struct element_indexed_quad);
}
bool push_sprite_payload_to_vertex_buffer_builder(struct sprite_batch batch,
vertex_buffer_builder *builder,
t_fvec2 v0, t_fvec2 v1, t_fvec2 v2, t_fvec2 v3,
t_fvec2 uv0, t_fvec2 uv1, t_fvec2 uv2, t_fvec2 uv3,
t_color color)
{
if (!batch.constant_colored) {
struct element_indexed_quad buffer_element = {
.v0 = v0,
.v1 = v1,
.v2 = v2,
.v3 = v3,
.uv0 = uv0,
.uv1 = uv1,
.uv2 = uv2,
.uv3 = uv3,
/* equal for all (flat shaded) */
.c0 = color,
.c1 = color,
.c2 = color,
.c3 = color,
};
return push_to_vertex_buffer_builder(builder, &buffer_element, sizeof buffer_element);
} else {
struct element_indexed_quad_without_color buffer_element = {
.v0 = v0,
.v1 = v1,
.v2 = v2,
.v3 = v3,
.uv0 = uv0,
.uv1 = uv1,
.uv2 = uv2,
.uv3 = uv3,
};
return push_to_vertex_buffer_builder(builder, &buffer_element, sizeof buffer_element);
}
}
void finally_draw_uncolored_space_traingle_batch(const struct mesh_batch *batch,
const t_texture_key texture_key,
const vertex_buffer vertex_buffer)
{
const size_t primitives_len = arrlenu(batch->primitives);
textures_bind(&ctx.texture_cache, texture_key);
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
/* vertex specification*/
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3,
GL_FLOAT,
offsetof(struct uncolored_space_triangle_payload, v1),
(void *)offsetof(struct uncolored_space_triangle_payload, v0));
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glClientActiveTexture(GL_TEXTURE0);
glTexCoordPointer(2,
GL_FLOAT,
offsetof(struct uncolored_space_triangle_payload, v1),
(void *)offsetof(struct uncolored_space_triangle_payload, uv0));
/* commit for drawing */
glDrawArrays(GL_TRIANGLES, 0, 3 * (GLint)primitives_len);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);
/* invalidate the buffer immediately */
glBufferData(GL_ARRAY_BUFFER, 0, NULL, GL_STREAM_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
bool push_text_payload_to_vertex_buffer_builder(struct font_data const *font_data,
vertex_buffer_builder *builder,
stbtt_aligned_quad quad)
{
(void)font_data;
glTexCoord2f(quad.s0, quad.t0);
glVertex2f(quad.x0, quad.y0);
glTexCoord2f(quad.s1, quad.t0);
glVertex2f(quad.x1, quad.y0);
glTexCoord2f(quad.s1, quad.t1);
glVertex2f(quad.x1, quad.y1);
glTexCoord2f(quad.s0, quad.t1);
glVertex2f(quad.x0, quad.y1);
}
void finally_draw_text(struct font_data const *font_data,
size_t len,
t_color color,
vertex_buffer buffer)
{
use_texture_mode(TEXTURE_MODE_GHOSTLY);
glBindTexture(GL_TEXTURE_2D, font_data->texture);
glColor4ub(color.r, color.g, color.b, color.a);
}
size_t get_text_payload_size(void) {
return sizeof (struct element_indexed_quad_without_color);
}

View File

@ -0,0 +1,65 @@
#ifndef TWN_GL_15_RENDERING_H
#define TWN_GL_15_RENDERING_H
/*
* OpenGL 1.5 and any 2.0+ compatibility version render implementation.
*/
#include "twn_rendering_c.h"
#include "twn_gl_any_rendering_c.h"
#ifdef EMSCRIPTEN
#include <GLES2/gl2.h>
#else
#include <glad/glad.h>
#endif
#include <stb_truetype.h>
void render_circle(const struct circle_primitive *circle);
void render_rectangle(const struct rect_primitive *rectangle);
void use_space_pipeline(void);
void use_2d_pipeline(void);
void use_texture_mode(enum texture_mode mode);
/* uses present in 1.5 buffer mapping feature */
vertex_buffer_builder build_vertex_buffer(vertex_buffer buffer, size_t bytes);
/* collects bytes for sending to the gpu until all is pushed, which is when false is returned */
bool push_to_vertex_buffer_builder(vertex_buffer_builder *builder,
void *bytes,
size_t size);
void finally_render_sprites(struct primitive_2d const primitives[],
struct sprite_batch batch,
vertex_buffer buffer);
size_t get_sprite_payload_size(struct sprite_batch batch);
bool push_sprite_payload_to_vertex_buffer_builder(struct sprite_batch batch,
vertex_buffer_builder *builder,
t_fvec2 v0, t_fvec2 v1, t_fvec2 v2, t_fvec2 v3,
t_fvec2 uv0, t_fvec2 uv1, t_fvec2 uv2, t_fvec2 uv3,
t_color color);
void finally_draw_uncolored_space_traingle_batch(struct mesh_batch const *batch,
t_texture_key texture_key,
vertex_buffer buffer);
size_t get_text_payload_size(void);
bool push_text_payload_to_vertex_buffer_builder(struct font_data const *font_data,
vertex_buffer_builder *builder,
stbtt_aligned_quad quad);
void finally_draw_text(struct font_data const *font_data,
size_t len,
t_color color,
vertex_buffer buffer);
#endif

View File

@ -0,0 +1,89 @@
#include "twn_gl_any_rendering_c.h"
#include "townengine/context.h"
#include "townengine/util.h"
#ifdef EMSCRIPTEN
#include <GLES2/gl2.h>
#else
#include <glad/glad.h>
#endif
void setup_viewport(int x, int y, int width, int height) {
glViewport(x, y, width, height);
}
//////// VERTEX BUFFER ////////
vertex_buffer create_vertex_buffer(void) {
GLuint result;
glGenBuffers(1, &result);
return result;
}
void delete_vertex_buffer(vertex_buffer buffer) {
glDeleteBuffers(1, &buffer);
}
void specify_vertex_buffer(vertex_buffer buffer, void *data, size_t bytes) {
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, bytes, data, GL_STREAM_DRAW);
}
//////// END OF VERTEX BUFFER ////////
void bind_quad_element_buffer(void) {
static GLuint buffer = 0;
/* it's only generated once at runtime */
if (buffer == 0) {
glGenBuffers(1, &buffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
QUAD_ELEMENT_BUFFER_LENGTH * 6 * sizeof(uint16_t),
NULL,
GL_STATIC_DRAW);
uint16_t *const indices = glMapBuffer(GL_ELEMENT_ARRAY_BUFFER,
GL_WRITE_ONLY);
if (!indices)
CRY("Quad indices generation", "glMapBuffer() failed");
for (uint16_t i = 0; i < QUAD_ELEMENT_BUFFER_LENGTH; ++i) {
indices[i * 6 + 0] = (uint16_t)(i * 4 + 0);
indices[i * 6 + 1] = (uint16_t)(i * 4 + 1);
indices[i * 6 + 2] = (uint16_t)(i * 4 + 2);
indices[i * 6 + 3] = (uint16_t)(i * 4 + 2);
indices[i * 6 + 4] = (uint16_t)(i * 4 + 3);
indices[i * 6 + 5] = (uint16_t)(i * 4 + 0);
}
glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER);
} else
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer);
}
void clear_draw_buffer(void) {
glClearColor((1.0f / 255) * 230,
(1.0f / 255) * 230,
(1.0f / 255) * 230, 1);
glClear(GL_COLOR_BUFFER_BIT |
GL_DEPTH_BUFFER_BIT |
GL_STENCIL_BUFFER_BIT);
}
void swap_buffers(void) {
SDL_GL_SwapWindow(ctx.window);
}
void set_depth_range(double low, double high) {
glDepthRange(low, high);
}

View File

@ -0,0 +1,43 @@
#ifndef TWN_GL_ANY_RENDERING_H
#define TWN_GL_ANY_RENDERING_H
/*
* Any OpenGL version base render methods.
*/
#ifdef EMSCRIPTEN
#include <GLES2/gl2.h>
#else
#include <glad/glad.h>
#endif
#include <stdbool.h>
#define QUAD_ELEMENT_BUFFER_LENGTH (65536 / 6)
typedef GLuint vertex_buffer;
typedef struct vertex_buffer_builder {
size_t bytes_left;
void *mapping;
} vertex_buffer_builder;
vertex_buffer create_vertex_buffer(void);
void delete_vertex_buffer(vertex_buffer buffer);
void specify_vertex_buffer(vertex_buffer buffer, void *data, size_t bytes);
void setup_viewport(int x, int y, int width, int height);
void bind_quad_element_buffer(void);
void clear_draw_buffer(void);
void swap_buffers(void);
void set_depth_range(double low, double high);
#endif

View File

@ -0,0 +1,29 @@
#ifndef TWN_GPU_TEXTURE_H
#define TWN_GPU_TEXTURE_H
#ifdef EMSCRIPTEN
#include <GLES2/gl2.h>
#else
#include <glad/glad.h>
#endif
#include <stdbool.h>
typedef GLuint gpu_texture;
enum texture_filter {
TEXTURE_FILTER_NEAREAST,
TEXTURE_FILTER_LINEAR,
};
gpu_texture create_gpu_texture(enum texture_filter filter, bool generate_mipmaps);
void delete_gpu_texture(gpu_texture texture);
void specify_gpu_texture(gpu_texture texture, void *pixels, int channels, int width, int height);
void bind_gpu_texture(gpu_texture texture);
#endif

View File

@ -0,0 +1,150 @@
#include "twn_rendering_c.h"
#include "townengine/twn_rendering.h"
#include "townengine/textures/internal_api.h"
#include "townengine/context.h"
#include "townengine/camera.h"
#include "twn_rendering_platform.h"
#include <SDL2/SDL.h>
#include <stb_ds.h>
#ifdef EMSCRIPTEN
#include <GLES2/gl2.h>
#else
#include <glad/glad.h>
#endif
#include <stddef.h>
#include <tgmath.h>
/* TODO: have a default initialized one */
t_matrix4 camera_projection_matrix;
t_matrix4 camera_look_at_matrix;
void render_queue_clear(void) {
/* this doesn't even _deserve_ a TODO */
/* if you're gonna remove it, this is also being done in main.c */
for (size_t i = 0; i < arrlenu(ctx.render_queue_2d); ++i) {
if (ctx.render_queue_2d[i].type == PRIMITIVE_2D_TEXT) {
free(ctx.render_queue_2d[i].text.text);
}
}
/* since i don't intend to free the queues, */
/* it's faster and simpler to just "start over" */
/* and start overwriting the existing data */
arrsetlen(ctx.render_queue_2d, 0);
for (size_t i = 0; i < hmlenu(ctx.uncolored_mesh_batches); ++i)
arrsetlen(ctx.uncolored_mesh_batches[i].value.primitives, 0);
}
/* rectangle */
void push_rectangle(t_frect rect, t_color color) {
struct rect_primitive rectangle = {
.rect = rect,
.color = color,
};
struct primitive_2d primitive = {
.type = PRIMITIVE_2D_RECT,
.rect = rectangle,
};
arrput(ctx.render_queue_2d, primitive);
}
static void render_2d(void) {
use_2d_pipeline();
const size_t render_queue_len = arrlenu(ctx.render_queue_2d);
size_t batch_count = 0;
for (size_t i = 0; i < render_queue_len; ++i) {
const struct primitive_2d *current = &ctx.render_queue_2d[i];
switch (current->type) {
case PRIMITIVE_2D_SPRITE: {
const struct sprite_batch batch =
collect_sprite_batch(current, render_queue_len - i);
/* TODO: what's even the point? just use OR_EQUAL comparison */
set_depth_range((double)batch_count / UINT16_MAX, 1.0);
render_sprites(current, batch);
i += batch.size - 1; ++batch_count;
break;
}
case PRIMITIVE_2D_RECT:
render_rectangle(&current->rect);
break;
case PRIMITIVE_2D_CIRCLE:
render_circle(&current->circle);
break;
case PRIMITIVE_2D_TEXT:
render_text(&current->text);
break;
}
}
}
static void render_space(void) {
/* nothing to do, abort */
/* as space pipeline isn't used we can have fewer changes and initialization costs */
if (hmlenu(ctx.uncolored_mesh_batches) == 0)
return;
use_space_pipeline();
for (size_t i = 0; i < hmlenu(ctx.uncolored_mesh_batches); ++i) {
draw_uncolored_space_traingle_batch(&ctx.uncolored_mesh_batches[i].value,
ctx.uncolored_mesh_batches[i].key);
}
}
void render(void) {
textures_update_atlas(&ctx.texture_cache);
/* fit rendering context onto the resizable screen */
if (ctx.window_size_has_changed) {
if ((float)ctx.window_w / (float)ctx.window_h > RENDER_BASE_RATIO) {
float ratio = (float)ctx.window_h / (float)RENDER_BASE_HEIGHT;
int w = (int)((float)RENDER_BASE_WIDTH * ratio);
setup_viewport(
ctx.window_w / 2 - w / 2,
0,
w,
ctx.window_h
);
} else {
float ratio = (float)ctx.window_w / (float)RENDER_BASE_WIDTH;
int h = (int)((float)RENDER_BASE_HEIGHT * ratio);
setup_viewport(
0,
ctx.window_h / 2 - h / 2,
ctx.window_w,
h
);
}
}
clear_draw_buffer();
render_space();
render_2d();
swap_buffers();
}
void set_camera(const t_camera *const camera) {
/* TODO: skip recaulculating if it's the same? */
camera_projection_matrix = camera_perspective(camera);
camera_look_at_matrix = camera_look_at(camera);
}

View File

@ -0,0 +1,145 @@
#ifndef RENDERING_INTERNAL_API_H
#define RENDERING_INTERNAL_API_H
#include "townengine/textures/internal_api.h"
#include "townengine/util.h"
#include "townengine/macros/option.h"
#include <SDL2/SDL.h>
#include <stdbool.h>
extern t_matrix4 camera_projection_matrix;
extern t_matrix4 camera_look_at_matrix;
struct sprite_primitive {
t_frect rect;
t_color color;
float rotation;
t_texture_key texture_key;
bool flip_x;
bool flip_y;
bool repeat;
m_option_list(
t_fvec2, texture_origin )
};
struct rect_primitive {
t_frect rect;
t_color color;
};
struct circle_primitive {
float radius;
t_color color;
t_fvec2 position;
};
struct text_primitive {
t_color color;
t_fvec2 position;
char *text;
const char *font;
int height_px;
};
enum primitive_2d_type {
PRIMITIVE_2D_SPRITE,
PRIMITIVE_2D_RECT,
PRIMITIVE_2D_CIRCLE,
PRIMITIVE_2D_TEXT,
};
struct primitive_2d {
enum primitive_2d_type type;
union {
struct sprite_primitive sprite;
struct rect_primitive rect;
struct circle_primitive circle;
struct text_primitive text;
};
};
/* union for in-place recalculation of texture coordinates */
union uncolored_space_triangle {
/* pending for sending, uvs are not final as texture atlases could update */
struct uncolored_space_triangle_primitive {
t_fvec3 v0;
t_fvec2 uv0; /* in pixels */
t_fvec3 v1;
t_fvec2 uv1; /* in pixels */
t_fvec3 v2;
t_fvec2 uv2; /* in pixels */
} primitive;
/* TODO: have it packed? */
/* structure that is passed in opengl vertex array */
struct uncolored_space_triangle_payload {
t_fvec3 v0;
t_fvec2 uv0;
t_fvec3 v1;
t_fvec2 uv1;
t_fvec3 v2;
t_fvec2 uv2;
} payload;
};
/* batch of primitives with overlapping properties */
struct mesh_batch {
uint8_t *primitives;
};
struct mesh_batch_item {
t_texture_key key;
struct mesh_batch value;
};
struct text_cache {
struct font_data **data;
};
/* renders the background, then the primitives in all render queues */
void render(void);
/* clears all render queues */
void render_queue_clear(void);
void push_circle(t_fvec2 position, float radius, t_color color);
void unfurl_triangle(const char *path,
t_fvec3 v0,
t_fvec3 v1,
t_fvec3 v2,
t_shvec2 uv0,
t_shvec2 uv1,
t_shvec2 uv2);
void create_circle_geometry(t_fvec2 position,
t_color color,
float radius,
size_t num_vertices,
SDL_Vertex **vertices_out,
int **indices_out);
struct sprite_batch {
size_t size; /* how many primitives are in current batch */
enum texture_mode mode;
bool constant_colored; /* whether colored batch is uniformly colored */
bool repeat; /* whether repeat is needed */
} collect_sprite_batch(const struct primitive_2d primitives[], size_t len);
void render_sprites(const struct primitive_2d primitives[],
const struct sprite_batch batch);
void draw_uncolored_space_traingle_batch(struct mesh_batch *batch,
t_texture_key texture_key);
void render_text(const struct text_primitive *text);
void text_cache_init(struct text_cache *cache);
void text_cache_deinit(struct text_cache *cache);
#endif

View File

@ -0,0 +1,10 @@
#ifndef TWN_RENDERING_PLATFORM_H
#define TWN_RENDERING_PLATFORM_H
#ifdef EMSCRIPTEN
#include "twn_gl_es2_rendering_c.h"
#else
#include "twn_gl_15_rendering_c.h"
#endif
#endif

205
src/rendering/twn_sprites.c Normal file
View File

@ -0,0 +1,205 @@
#include "townengine/twn_rendering.h"
#include "twn_rendering_c.h"
#include "townengine/context.h"
#include "townengine/util.h"
#include "townengine/textures/internal_api.h"
#include "twn_rendering_platform.h"
#include <stb_ds.h>
#include <stdbool.h>
#include <stddef.h>
/*
* an implementation note:
* try to avoid doing expensive work in the push functions,
* because they will be called multiple times in the main loop
* before anything is really rendered
*/
/* TODO: it might make sense to infer alpha channel presence / meaningfulness for textures in atlas */
/* so that they are rendered with no blend / batched in a way to reduce overdraw automatically */
void push_sprite(const t_push_sprite_args args) {
struct sprite_primitive sprite = {
.rect = args.rect,
.color = m_or(args, color, ((t_color) { 255, 255, 255, 255 })),
.rotation = m_or(args, rotation, 0.0f),
.texture_key = textures_get_key(&ctx.texture_cache, args.path),
.flip_x = m_or(args, flip_x, false),
.flip_y = m_or(args, flip_y, false),
.repeat = !m_or(args, stretch, true),
m_opt_from(texture_origin, args, texture_origin)
};
struct primitive_2d primitive = {
.type = PRIMITIVE_2D_SPRITE,
.sprite = sprite,
};
arrput(ctx.render_queue_2d, primitive);
}
struct sprite_batch collect_sprite_batch(const struct primitive_2d primitives[], size_t len) {
/* assumes that first primitive is already a sprite */
const uint16_t texture_key_id = primitives[0].sprite.texture_key.id;
const int atlas_id = textures_get_atlas_id(&ctx.texture_cache, primitives[0].sprite.texture_key);
struct sprite_batch batch = {
.mode = textures_get_mode(&ctx.texture_cache, primitives[0].sprite.texture_key),
.constant_colored = true,
.repeat = primitives[0].sprite.repeat,
};
const uint32_t uniform_color = *(const uint32_t *)&primitives[0].sprite.color;
/* batch size is clamped so that reallocated short indices could be used */
if (len >= QUAD_ELEMENT_BUFFER_LENGTH)
len = QUAD_ELEMENT_BUFFER_LENGTH;
for (size_t i = 0; i < len; ++i) {
const struct primitive_2d *const current = &primitives[i];
/* don't touch things other than sprites */
if (current->type != PRIMITIVE_2D_SPRITE)
break;
/* only collect the same blend modes */
const enum texture_mode mode = textures_get_mode(&ctx.texture_cache, current->sprite.texture_key);
if (mode != batch.mode)
break;
/* only collect the same texture atlases */
if (textures_get_atlas_id(&ctx.texture_cache, current->sprite.texture_key) != atlas_id)
break;
/* repeated textures require separate handling */
if (batch.repeat) {
/* all must be repeated */
if (!current->sprite.repeat)
break;
/* all must be of same texture id, not just atlas id */
if (current->sprite.texture_key.id != texture_key_id)
break;
}
/* if all are modulated the same we can skip sending the color data */
if (*(const uint32_t *)&current->sprite.color != uniform_color)
batch.constant_colored = false;
++batch.size;
}
return batch;
}
/* assumes that orthogonal matrix setup is done already */
void render_sprites(const struct primitive_2d primitives[],
const struct sprite_batch batch)
{
/* single vertex array is used for every batch with NULL glBufferData() trick at the end */
static vertex_buffer vertex_array = 0;
if (vertex_array == 0)
vertex_array = create_vertex_buffer();
use_sprite_blendmode(batch.mode);
const t_frect dims =
textures_get_dims(&ctx.texture_cache, primitives->sprite.texture_key);
/* vertex population over a vertex buffer builder interface */
{
vertex_buffer_builder payload = build_vertex_buffer(vertex_array, get_sprite_payload_size(batch) * batch.size);
for (size_t i = 0; i < batch.size; ++i) {
/* render opaques front to back */
const size_t cur = batch.mode == TEXTURE_MODE_GHOSTLY ? i : batch.size - i - 1;
const struct sprite_primitive sprite = primitives[cur].sprite;
const t_frect srcrect =
textures_get_srcrect(&ctx.texture_cache, primitives[cur].sprite.texture_key);
t_fvec2 uv0, uv1, uv2, uv3;
if (!sprite.repeat) {
const float wr = srcrect.w / dims.w;
const float hr = srcrect.h / dims.h;
const float xr = srcrect.x / dims.w;
const float yr = srcrect.y / dims.h;
uv0 = (t_fvec2){ xr + wr * sprite.flip_x, yr + hr * sprite.flip_y };
uv1 = (t_fvec2){ xr + wr * sprite.flip_x, yr + hr * !sprite.flip_y };
uv2 = (t_fvec2){ xr + wr * !sprite.flip_x, yr + hr * !sprite.flip_y };
uv3 = (t_fvec2){ xr + wr * !sprite.flip_x, yr + hr * sprite.flip_y };
/* TODO: texture_origin support */
} else {
/* try fitting texture into supplied destination rectangle */
const float rx = sprite.rect.w / srcrect.w;
const float ry = sprite.rect.h / srcrect.h;
uv0 = (t_fvec2){ rx * sprite.flip_x, ry * sprite.flip_y };
uv1 = (t_fvec2){ rx * sprite.flip_x, ry * !sprite.flip_y };
uv2 = (t_fvec2){ rx * !sprite.flip_x, ry * !sprite.flip_y };
uv3 = (t_fvec2){ rx * !sprite.flip_x, ry * sprite.flip_y };
if (m_is_set(sprite, texture_origin)) {
/* displace origin */
const float ax = sprite.texture_origin_opt.x / srcrect.w;
const float ay = sprite.texture_origin_opt.y / srcrect.h;
uv0.x += ax; uv1.x += ax; uv2.x += ax; uv3.x += ax;
uv0.y += ay; uv1.y += ay; uv2.y += ay; uv3.y += ay;
}
}
t_fvec2 v0, v1, v2, v3;
/* todo: fast PI/2 degree divisible rotations? */
if (sprite.rotation == 0.0f) {
/* non-rotated case */
v0 = (t_fvec2){ sprite.rect.x, sprite.rect.y };
v1 = (t_fvec2){ sprite.rect.x, sprite.rect.y + sprite.rect.h };
v2 = (t_fvec2){ sprite.rect.x + sprite.rect.w, sprite.rect.y + sprite.rect.h };
v3 = (t_fvec2){ sprite.rect.x + sprite.rect.w, sprite.rect.y };
} else if (sprite.rect.w == sprite.rect.h) {
/* rotated square case */
const t_fvec2 c = frect_center(sprite.rect);
const t_fvec2 t = fast_cossine(sprite.rotation + (float)M_PI_4);
const t_fvec2 d = {
.x = t.x * sprite.rect.w * (float)M_SQRT1_2,
.y = t.y * sprite.rect.h * (float)M_SQRT1_2,
};
v0 = (t_fvec2){ c.x - d.x, c.y - d.y };
v1 = (t_fvec2){ c.x - d.y, c.y + d.x };
v2 = (t_fvec2){ c.x + d.x, c.y + d.y };
v3 = (t_fvec2){ c.x + d.y, c.y - d.x };
} else {
/* rotated non-square case*/
const t_fvec2 c = frect_center(sprite.rect);
const t_fvec2 t = fast_cossine(sprite.rotation);
const t_fvec2 h = { sprite.rect.w / 2, sprite.rect.h / 2 };
v0 = (t_fvec2){ c.x + t.x * -h.x - t.y * -h.y, c.y + t.y * -h.x + t.x * -h.y };
v1 = (t_fvec2){ c.x + t.x * -h.x - t.y * +h.y, c.y + t.y * -h.x + t.x * +h.y };
v2 = (t_fvec2){ c.x + t.x * +h.x - t.y * +h.y, c.y + t.y * +h.x + t.x * +h.y };
v3 = (t_fvec2){ c.x + t.x * +h.x - t.y * -h.y, c.y + t.y * +h.x + t.x * -h.y };
}
push_sprite_payload_to_vertex_buffer_builder(batch, &payload, v0, v1, v2, v3, uv0, uv1, uv2, uv3, sprite.color);
}
}
finally_render_sprites(primitives, batch, vertex_array);
}

221
src/rendering/twn_text.c Normal file
View File

@ -0,0 +1,221 @@
#include "twn_rendering_c.h"
#include "townengine/util.h"
#include "townengine/config.h"
#include "townengine/context.h"
#include "townengine/twn_rendering.h"
#include "twn_rendering_platform.h"
#include <stb_truetype.h>
#define ASCII_START 32
#define ASCII_END 128
#define NUM_DISPLAY_ASCII ((ASCII_END - ASCII_START) + 1)
struct font_data {
stbtt_packedchar char_data[NUM_DISPLAY_ASCII];
stbtt_fontinfo info;
const char *file_path;
unsigned char *file_bytes;
size_t file_bytes_len;
gpu_texture texture;
int height_px;
float scale_factor;
int ascent;
int descent;
int line_gap;
};
static struct font_data *text_load_font_data(const char *path, int height_px) {
struct font_data *font_data = ccalloc(1, sizeof *font_data);
font_data->file_path = path;
font_data->height_px = height_px;
unsigned char* bitmap = ccalloc(TEXT_FONT_TEXTURE_SIZE * TEXT_FONT_TEXTURE_SIZE, 1);
{
unsigned char *buf = NULL;
int64_t buf_len = file_to_bytes(path, &buf);
stbtt_InitFont(&font_data->info, buf, stbtt_GetFontOffsetForIndex(buf, 0));
/* might as well get these now, for later */
font_data->file_bytes = buf;
font_data->file_bytes_len = buf_len;
font_data->scale_factor = stbtt_ScaleForPixelHeight(&font_data->info, (float)height_px);
stbtt_GetFontVMetrics(
&font_data->info,
&font_data->ascent,
&font_data->descent,
&font_data->line_gap
);
font_data->ascent = (int)((float)font_data->ascent * font_data->scale_factor);
font_data->descent = (int)((float)font_data->descent * font_data->scale_factor);
font_data->line_gap = (int)((float)font_data->line_gap * font_data->scale_factor);
stbtt_pack_context pctx;
stbtt_PackBegin(&pctx, bitmap, TEXT_FONT_TEXTURE_SIZE, TEXT_FONT_TEXTURE_SIZE, 0, 1, NULL);
stbtt_PackSetOversampling(&pctx, TEXT_FONT_OVERSAMPLING, TEXT_FONT_OVERSAMPLING);
stbtt_PackFontRange(&pctx, buf, 0, (float)height_px, ASCII_START, NUM_DISPLAY_ASCII, font_data->char_data);
stbtt_PackEnd(&pctx);
}
font_data->texture = create_gpu_texture(TEXT_FONT_FILTERING, true);
specify_gpu_texture(
font_data->texture,
bitmap,
1,
TEXT_FONT_TEXTURE_SIZE,
TEXT_FONT_TEXTURE_SIZE
);
free(bitmap);
return font_data;
}
static void text_destroy_font_data(struct font_data *font_data) {
free(font_data->file_bytes);
delete_gpu_texture(font_data->texture);
free(font_data);
}
static void text_draw_with(struct font_data* font_data, char* text, t_fvec2 position, t_color color) {
static vertex_buffer vertex_array = 0;
if (vertex_array == 0)
vertex_array = create_vertex_buffer();
const size_t len = SDL_strlen(text);
vertex_buffer_builder payload = build_vertex_buffer(vertex_array, get_text_payload_size() * len);
for (size_t i = 0; i < len; ++i) {
const char c = text[i];
/* outside the range of what we want to display */
//if (c < ASCII_START || c > ASCII_END)
if (c < ASCII_START)
continue;
/* stb_truetype.h conveniently provides everything we need to draw here! */
stbtt_aligned_quad quad;
stbtt_GetPackedQuad(
font_data->char_data,
TEXT_FONT_TEXTURE_SIZE,
TEXT_FONT_TEXTURE_SIZE,
c - ASCII_START,
&position.x,
&position.y,
&quad,
true
);
/* have to do this so the "origin" is at the top left */
/* maybe there's a better way, or maybe this isn't a good idea... */
/* who knows */
quad.y0 += (float)font_data->ascent;
quad.y1 += (float)font_data->ascent;
push_text_payload_to_vertex_buffer_builder(font_data, &payload, quad);
}
finally_draw_text(font_data, len, color, vertex_array);
}
static void ensure_font_cache(const char *font_path, int height_px) {
/* HACK: stupid, bad, don't do this */
bool is_cached = false;
for (size_t i = 0; i < arrlenu(ctx.text_cache.data); ++i) {
struct font_data *font_data = ctx.text_cache.data[i];
if ((strcmp(font_path, font_data->file_path) == 0) && height_px == font_data->height_px) {
is_cached = true;
break;
}
}
if (!is_cached) {
struct font_data *new_font_data = text_load_font_data(font_path, height_px);
arrput(ctx.text_cache.data, new_font_data);
}
}
static struct font_data *get_font_data(const char *font_path, int height_px) {
struct font_data *font_data = NULL;
for (size_t i = 0; i < arrlenu(ctx.text_cache.data); ++i) {
font_data = ctx.text_cache.data[i];
if ((strcmp(font_path, font_data->file_path) == 0) && height_px == font_data->height_px) {
break;
}
}
return font_data;
}
void render_text(const struct text_primitive *text) {
struct font_data *font_data = get_font_data(text->font, text->height_px);
text_draw_with(font_data, text->text, text->position, text->color);
}
void text_cache_init(struct text_cache *cache) {
arrsetlen(cache->data, 0);
}
void text_cache_deinit(struct text_cache *cache) {
for (size_t i = 0; i < arrlenu(ctx.text_cache.data); ++i) {
text_destroy_font_data(ctx.text_cache.data[i]);
}
arrfree(cache->data);
}
void push_text(char *string, t_fvec2 position, int height_px, t_color color, const char *font_path) {
ensure_font_cache(font_path, height_px);
/* the string might not be around by the time it's used, so copy it */
/* TODO: arena */
/* NOTE: can we trust strlen? */
char *dup_string = cmalloc(strlen(string) + 1);
strcpy(dup_string, string);
struct text_primitive text = {
.color = color,
.position = position,
.text = dup_string,
.font = font_path,
.height_px = height_px,
};
struct primitive_2d primitive = {
.type = PRIMITIVE_2D_TEXT,
.text = text,
};
arrput(ctx.render_queue_2d, primitive);
}
int get_text_width(char *string, int height_px, const char *font_path) {
ensure_font_cache(font_path, height_px);
struct font_data *font_data = get_font_data(font_path, height_px);
int length = 0;
for (const char *p = string; *p != '\0'; ++p) {
const char c = *p;
int advance_width = 0;
int left_side_bearing = 0;
stbtt_GetCodepointHMetrics(&font_data->info, (int)c, &advance_width, &left_side_bearing);
length += advance_width;
}
return (int)((float)length * font_data->scale_factor);
}

View File

@ -0,0 +1,26 @@
#include "twn_rendering_platform.h"
#include <stb_truetype.h>
#define ASCII_START 32
#define ASCII_END 128
#define NUM_DISPLAY_ASCII ((ASCII_END - ASCII_START) + 1)
struct font_data {
stbtt_packedchar char_data[NUM_DISPLAY_ASCII];
stbtt_fontinfo info;
const char *file_path;
unsigned char *file_bytes;
size_t file_bytes_len;
gpu_texture texture;
int height_px;
float scale_factor;
int ascent;
int descent;
int line_gap;
};

View File

@ -0,0 +1,81 @@
#include "twn_rendering_c.h"
#include "twn_context.h"
#include "twn_textures_c.h"
#include "twn_rendering_platform.h"
#include <stb_ds.h>
/* TODO: automatic handling of repeating textures */
/* for that we could allocate a loner texture */
void unfurl_triangle(const char *path,
t_fvec3 v0,
t_fvec3 v1,
t_fvec3 v2,
t_shvec2 uv0,
t_shvec2 uv1,
t_shvec2 uv2)
{
const t_texture_key texture_key = textures_get_key(&ctx.texture_cache, path);
struct mesh_batch_item *batch_p = hmgetp_null(ctx.uncolored_mesh_batches, texture_key);
if (!batch_p) {
struct mesh_batch item = {0};
hmput(ctx.uncolored_mesh_batches, texture_key, item);
batch_p = &ctx.uncolored_mesh_batches[hmlenu(ctx.uncolored_mesh_batches) - 1]; /* TODO: can last index be used? */
}
union uncolored_space_triangle triangle = { .primitive = {
.v0 = v0,
.v1 = v1,
.v2 = v2,
.uv1 = m_to_fvec2(uv1),
.uv0 = m_to_fvec2(uv0),
.uv2 = m_to_fvec2(uv2),
}};
union uncolored_space_triangle *triangles = (union uncolored_space_triangle *)batch_p->value.primitives;
arrpush(triangles, triangle);
batch_p->value.primitives = (uint8_t *)triangles;
}
void draw_uncolored_space_traingle_batch(struct mesh_batch *batch,
t_texture_key texture_key)
{
static vertex_buffer vertex_array = 0;
if (vertex_array == 0)
vertex_array = create_vertex_buffer();
const size_t primitives_len = arrlenu(batch->primitives);
/* nothing to do */
if (primitives_len == 0)
return;
const t_frect srcrect = textures_get_srcrect(&ctx.texture_cache, texture_key);
const t_frect dims = textures_get_dims(&ctx.texture_cache, texture_key);
const float wr = srcrect.w / dims.w;
const float hr = srcrect.h / dims.h;
const float xr = srcrect.x / dims.w;
const float yr = srcrect.y / dims.h;
/* update pixel-based uvs to correspond with texture atlases */
for (size_t i = 0; i < primitives_len; ++i) {
struct uncolored_space_triangle_payload *payload =
&((union uncolored_space_triangle *)batch->primitives)[i].payload;
payload->uv0.x = xr + ((float)payload->uv0.x / srcrect.w) * wr;
payload->uv0.y = yr + ((float)payload->uv0.y / srcrect.h) * hr;
payload->uv1.x = xr + ((float)payload->uv1.x / srcrect.w) * wr;
payload->uv1.y = yr + ((float)payload->uv1.y / srcrect.h) * hr;
payload->uv2.x = xr + ((float)payload->uv2.x / srcrect.w) * wr;
payload->uv2.y = yr + ((float)payload->uv2.y / srcrect.h) * hr;
}
specify_vertex_buffer(vertex_array, batch->primitives, primitives_len * sizeof (struct uncolored_space_triangle_payload));
finally_draw_uncolored_space_traingle_batch(batch, texture_key, vertex_array);
}

View File

@ -0,0 +1,68 @@
#include "elf.h"
#include <fcntl.h>
#include <unistd.h>
#include <sys/auxv.h>
#include <elf.h>
#include <linux/limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
bool infer_elf_section_bounds(const char *const restrict name,
const char **restrict vm_start,
const char **restrict vm_end)
{
bool result = false;
char buf[PATH_MAX];
ssize_t l = readlink("/proc/self/exe", buf, PATH_MAX);
if (l == -1)
goto ERR_CANT_READLINK;
buf[l] = 0; /* readlink() doesn't write a terminator */
int elf = open(buf, O_RDONLY);
if (elf == -1)
goto ERR_CANT_OPEN_SELF;
/* elf header */
Elf64_Ehdr ehdr;
read(elf, &ehdr, sizeof ehdr);
if (ehdr.e_ident[EI_MAG0] != ELFMAG0 ||
ehdr.e_ident[EI_MAG1] != ELFMAG1 ||
ehdr.e_ident[EI_MAG2] != ELFMAG2 ||
ehdr.e_ident[EI_MAG3] != ELFMAG3)
goto ERR_NOT_ELF;
/* section header string table */
Elf64_Shdr shstrdr;
lseek(elf, ehdr.e_shoff + ehdr.e_shstrndx * sizeof (Elf64_Shdr), SEEK_SET);
read(elf, &shstrdr, sizeof shstrdr);
char *sh = malloc(shstrdr.sh_size);
lseek(elf, shstrdr.sh_offset, SEEK_SET);
read(elf, sh, shstrdr.sh_size);
/* walk sections searching for needed name */
lseek(elf, ehdr.e_shoff, SEEK_SET);
for (size_t s = 0; s < ehdr.e_shnum; ++s) {
Elf64_Shdr shdr;
read(elf, &shdr, sizeof shdr);
if (strcmp(&sh[shdr.sh_name], name) == 0) {
result = true;
*vm_start = getauxval(AT_ENTRY) - ehdr.e_entry + (char *)shdr.sh_addr;
*vm_end = getauxval(AT_ENTRY) - ehdr.e_entry + (char *)shdr.sh_addr + shdr.sh_size;
break;
}
}
free(sh);
ERR_NOT_ELF:
close(elf);
ERR_CANT_OPEN_SELF:
ERR_CANT_READLINK:
return result;
}

View File

@ -0,0 +1,10 @@
#ifndef TWN_ELF_H
#define TWN_ELF_H
#include <stdbool.h>
bool infer_elf_section_bounds(const char *restrict name,
const char **restrict vm_start,
const char **restrict vm_end);
#endif

345
src/twn_audio.c Normal file
View File

@ -0,0 +1,345 @@
#include "townengine/audio/internal_api.h"
#include "townengine/config.h"
#include "townengine/context.h"
#include "townengine/util.h"
#include <SDL2/SDL.h>
#include <stb_ds.h>
#include <physfs.h>
#define STB_VORBIS_HEADER_ONLY
#include <stb_vorbis.c>
#include <stdint.h>
#include <string.h>
/* TODO: default to float sampling format? */
static const char *audio_exts[audio_file_type_count] = {
".ogg", /* audio_file_type_ogg */
".xm", /* audio_file_type_xm */
};
/* TODO: count frames without use, free the memory when threshold is met */
/* TODO: count repeated usages for sound effect cases with rendering to ram? */
/* stores path to data hash, useful for sound effects */
static struct audio_file_cache {
char *key;
struct audio_file_cache_value {
unsigned char *data;
size_t len;
} value;
} *audio_file_cache;
static int64_t get_audio_data(const char *path, unsigned char **data) {
const struct audio_file_cache *cache = shgetp_null(audio_file_cache, path);
if (!cache) {
unsigned char *file;
int64_t len = file_to_bytes(path, &file);
if (len == -1) {
CRY("Audio error", "Error reading file");
return -1;
}
const struct audio_file_cache_value value = { file, (size_t)len };
shput(audio_file_cache, path, value);
*data = file;
return len;
}
*data = cache->value.data;
return (int64_t)cache->value.len;
}
void play_audio(const char *path, const char *channel) {
const struct audio_channel_item *pair = shgetp_null(ctx.audio_channels, channel);
if (!pair)
play_audio_ex(path, channel, get_default_audio_args());
else
play_audio_ex(path, channel, pair->value.args);
}
static t_audio_file_type infer_audio_file_type(const char *path) {
size_t path_len = strlen(path);
for (int i = 0; i < audio_file_type_count; ++i) {
size_t ext_length = strlen(audio_exts[i]);
if (path_len <= ext_length)
continue;
if (strcmp(&path[path_len - ext_length], audio_exts[i]) == 0)
return (t_audio_file_type)i;
}
return audio_file_type_unknown;
}
/* TODO: error propagation and clearing of resources on partial success? */
/* or should we expect things to simply fail? */
static union audio_context init_audio_context(const char *path, t_audio_file_type type) {
switch (type) {
case audio_file_type_ogg: {
unsigned char *data;
int64_t len = get_audio_data(path, &data);
if (len == -1) {
CRY("Audio error", "Error reading file");
break;
}
int error = 0;
stb_vorbis* handle = stb_vorbis_open_memory(data, (int)len, &error, NULL);
if (error != 0) {
CRY("Audio error", "Error reading .ogg file");
break;
}
stb_vorbis_info info = stb_vorbis_get_info(handle);
return (union audio_context) {
.vorbis = {
.data = data,
.handle = handle,
.frequency = info.sample_rate,
.channel_count = (uint8_t)info.channels,
}
};
}
case audio_file_type_xm: {
unsigned char *data;
int64_t len = get_audio_data(path, &data);
if (len == -1) {
CRY("Audio error", "Error reading file");
break;
}
xm_context_t *handle;
int response = xm_create_context_safe(&handle,
(const char *)data,
(size_t)len,
AUDIO_FREQUENCY);
if (response != 0) {
CRY("Audio error", "Error loading xm module");
break;
}
xm_set_max_loop_count(handle, 1);
return (union audio_context) {
.xm = { .handle = handle }
};
}
default:
CRY("Audio error", "Unhandled audio format (in init)");
return (union audio_context){0};
}
return (union audio_context){0};
}
static void repeat_audio(struct audio_channel *channel) {
switch (channel->file_type) {
case audio_file_type_ogg: {
stb_vorbis_seek_start(channel->context.vorbis.handle);
break;
}
case audio_file_type_xm: {
xm_restart(channel->context.xm.handle);
break;
}
default:
CRY("Audio error", "Unhandled audio format (in repeat)");
break;
}
}
void play_audio_ex(const char *path, const char *channel, t_play_audio_args args) {
struct audio_channel_item *pair = shgetp_null(ctx.audio_channels, channel);
/* create a channel if it doesn't exist */
if (!pair) {
t_audio_file_type file_type = infer_audio_file_type(path);
struct audio_channel new_channel = {
.args = args,
.file_type = file_type,
.context = init_audio_context(path, file_type),
.path = path,
.name = channel,
};
shput(ctx.audio_channels, channel, new_channel);
pair = shgetp_null(ctx.audio_channels, channel);
}
/* TODO: destroy and create new context when channel is reused for different file */
/* works for both restarts and new audio */
if (strcmp(pair->value.path, path) == 0)
repeat_audio(&pair->value);
}
t_play_audio_args *get_audio_args(const char *channel) {
struct audio_channel_item *pair = shgetp_null(ctx.audio_channels, channel);
if (!pair)
return NULL;
return &pair->value.args;
}
t_play_audio_args get_default_audio_args(void) {
return (t_play_audio_args){
.repeat = false,
.crossfade = false,
.volume = 1.0f,
.panning = 0.0f,
};
}
/* this assumes int16_t based streams */
static void audio_mixin_streams(const struct audio_channel *channel,
uint8_t *restrict a,
uint8_t *restrict b,
size_t frames)
{
int16_t *const sa = (int16_t *)a;
int16_t *const sb = (int16_t *)b;
const float left_panning = fminf(fabsf(channel->args.panning - 1.0f), 1.0f);
const float right_panning = fminf(fabsf(channel->args.panning + 1.0f), 1.0f);
#if AUDIO_N_CHANNELS == 2
for (size_t s = 0; s < frames; s += 2) {
/* left channel */
sa[s] += (int16_t)(sb[s] * channel->args.volume * left_panning);
/* right channel */
sa[s + 1] += (int16_t)(sb[s + 1] * channel->args.volume * right_panning);
}
#else
#error "Unimplemented channel count"
#endif
}
/* remember: sample is data for all channels where frame is a part of it */
static void audio_sample_and_mixin_channel(const struct audio_channel *channel,
uint8_t *stream,
int len)
{
static uint8_t buffer[16384];
const int int16_buffer_frames = sizeof (buffer) / sizeof (int16_t);
const int float_buffer_frames = sizeof (buffer) / sizeof (float);
const int stream_frames = len / (int)(sizeof (int16_t));
switch (channel->file_type) {
case audio_file_type_ogg: {
/* feed stream for needed conversions */
for (int i = 0; i < stream_frames; ) {
const int n_frames = (stream_frames - i) > int16_buffer_frames ?
int16_buffer_frames : stream_frames - i;
const int samples_per_channel = stb_vorbis_get_samples_short_interleaved(
channel->context.vorbis.handle,
channel->context.vorbis.channel_count,
(int16_t *)buffer,
n_frames);
/* handle end of file */
if (samples_per_channel == 0) {
if (channel->args.repeat) {
/* seek to start and try sampling some more */
stb_vorbis_seek_start(channel->context.vorbis.handle);
continue;
} else
/* leave silence */
break;
}
/* panning and mixing */
audio_mixin_streams(channel,
&stream[i * sizeof(int16_t)], buffer,
samples_per_channel * 2);
i += samples_per_channel * 2;
}
break;
}
case audio_file_type_xm: {
for (int i = 0; i < stream_frames; ) {
const int n_frames = (stream_frames - i) > float_buffer_frames ?
float_buffer_frames : stream_frames - i;
const int samples_per_channel = xm_generate_samples(channel->context.xm.handle,
(float *)buffer,
n_frames / 2);
/* handle end of file */
if (samples_per_channel == 0) {
if (channel->args.repeat) {
/* seek to start and try sampling some more */
xm_restart(channel->context.xm.handle);
continue;
} else
/* leave silence */
break;
}
/* convert floats to int16_t */
for (int p = 0; p < samples_per_channel * 2; ++p)
((int16_t *)buffer)[p] = (int16_t)(((float *)buffer)[p] * (float)INT16_MAX);
/* panning and mixing */
audio_mixin_streams(channel,
&stream[i * sizeof(int16_t)],
buffer,
samples_per_channel * 2);
i += samples_per_channel * 2;
}
break;
}
default:
CRY("Audio error", "Unhandled audio format (in sampling)");
break;
}
}
static void sanity_check_channel(const struct audio_channel *channel) {
if (channel->args.volume < 0.0f || channel->args.volume > 1.0f)
log_warn("Volume argument is out of range for channel (%s)", channel->name);
if (channel->args.panning < -1.0f || channel->args.panning > 1.0f)
log_warn("panning argument is out of range for channel (%s)", channel->name);
}
void audio_callback(void *userdata, uint8_t *stream, int len) {
(void)userdata;
/* prepare for mixing */
SDL_memset(stream, 0, len);
for (int i = 0; i < shlen(ctx.audio_channels); ++i) {
sanity_check_channel(&ctx.audio_channels[i].value);
audio_sample_and_mixin_channel(&ctx.audio_channels[i].value, stream, len);
}
}

55
src/twn_audio_c.h Normal file
View File

@ -0,0 +1,55 @@
#ifndef TWN_AUDIO_C_H
#define TWN_AUDIO_C_H
#include "twn_audio.h"
#include <SDL2/SDL_audio.h>
#define STB_VORBIS_HEADER_ONLY
#include <stb_vorbis.c>
#include <xm.h>
#include <stdbool.h>
#include <stdint.h>
typedef enum audio_file_type {
audio_file_type_ogg,
audio_file_type_xm,
audio_file_type_count,
audio_file_type_unknown,
} t_audio_file_type;
union audio_context {
struct {
stb_vorbis *handle;
unsigned char *data;
int frequency;
uint8_t channel_count;
} vorbis;
struct {
xm_context_t *handle;
} xm;
};
struct audio_channel {
t_play_audio_args args;
enum audio_file_type file_type;
union audio_context context; /* interpreted by `file_type` value */
const char *path;
const char *name;
};
struct audio_channel_item {
char *key;
struct audio_channel value;
};
void audio_callback(void *userdata, uint8_t *stream, int len);
#endif

52
src/twn_camera.c Normal file
View File

@ -0,0 +1,52 @@
#include "twn_camera.h"
#include "twn_context.h"
#include <math.h>
#define CAMERA_NEAR_Z 0.1f
#define CAMERA_FAR_Z 100.0f
t_matrix4 camera_look_at(const t_camera *const camera) {
/* from cglm */
const t_fvec3 r = m_vec_norm(m_vec_cross(camera->target, camera->up));
const t_fvec3 u = m_vec_cross(r, camera->target);
t_matrix4 result;
result.row[0].x = r.x;
result.row[0].y = u.x;
result.row[0].z = -camera->target.x;
result.row[1].x = r.y;
result.row[1].y = u.y;
result.row[1].z = -camera->target.y;
result.row[2].x = r.z;
result.row[2].y = u.z;
result.row[2].z = -camera->target.z;
result.row[3].x = -m_vec_dot(r, camera->pos);
result.row[3].y = -m_vec_dot(u, camera->pos);
result.row[3].z = m_vec_dot(camera->target, camera->pos);
result.row[0].w = result.row[1].w = result.row[2].w = 0.0f;
result.row[3].w = 1.0f;
return result;
}
t_matrix4 camera_perspective(const t_camera *const camera) {
/* from cglm */
t_matrix4 result = {0};
const float aspect = RENDER_BASE_RATIO;
const float f = 1.0f / tanf(camera->fov * 0.5f);
const float fn = 1.0f / (CAMERA_NEAR_Z - CAMERA_FAR_Z);
result.row[0].x = f / aspect;
result.row[1].y = f;
result.row[2].z = (CAMERA_NEAR_Z + CAMERA_FAR_Z) * fn;
result.row[2].w = -1.0f;
result.row[3].z = 2.0f * CAMERA_NEAR_Z * CAMERA_FAR_Z * fn;
return result;
}

8
src/twn_concatenate_c.h Normal file
View File

@ -0,0 +1,8 @@
#ifndef TWN_CONCATENATE_H
#define TWN_CONCATENATE_H
#define m_concatenate(p_a, p_b) m_concatenate_(p_a, p_b)
#define m_concatenate_(p_a, p_b) m_concatenate__(p_a, p_b)
#define m_concatenate__(p_a, p_b) p_a##p_b
#endif

3
src/twn_context.c Normal file
View File

@ -0,0 +1,3 @@
#include "twn_engine_context_c.h"
t_engine_ctx ctx = {0};

View File

@ -0,0 +1,50 @@
#ifndef TWN_ENGINE_CONTEXT_H
#define TWN_ENGINE_CONTEXT_H
#include "twn_context.h"
#include "textures/internal_api.h"
#include "twn_input.h"
#include <SDL2/SDL.h>
#include <stdbool.h>
#include <stdint.h>
typedef struct engine_context {
t_ctx game_context;
/* the program's actual argc and argv */
int argc;
char **argv;
struct primitive_2d *render_queue_2d;
struct mesh_batch_item *uncolored_mesh_batches;
struct text_cache text_cache;
struct texture_cache texture_cache;
struct audio_channel_item *audio_channels;
SDL_AudioDeviceID audio_device;
int audio_stream_frequency;
SDL_AudioFormat audio_stream_format;
uint8_t audio_stream_channel_count;
/* main loop machinery */
int64_t clocks_per_second;
int64_t prev_frame_time;
int64_t desired_frametime; /* how long one tick should be */
int64_t frame_accumulator;
int64_t delta_averager_residual;
int64_t time_averager[4];
SDL_GLContext *gl_context;
SDL_Window *window;
uint32_t window_id;
bool resync_flag;
bool was_successful;
} t_engine_ctx;
extern t_engine_ctx ctx = ;
#endif

12
src/twn_game_object.c Normal file
View File

@ -0,0 +1,12 @@
#if defined(TWN_FEATURE_DYNLIB_GAME)
#if defined(__linux__)
#include "game_object/twn_linux_game_object_c.h"
#elif defined(_WIN32)
#include "game_object/twn_win32_game_object_c.h"
#else
#warning "TWN_FEATURE_DYNLIB_GAME is set, but not supported"
#include "game_object/twn_static_game_object_c.h"
#endif
#else
#include "game_object/twn_static_game_object_c.h"
#endif

22
src/twn_game_object_c.h Normal file
View File

@ -0,0 +1,22 @@
#ifndef TWN_GAME_OBJECT_H
#define TWN_GAME_OBJECT_H
#include <stdbool.h>
/*
* game object provides an interface for bridging between game code and engine.
* for example, it might implement dynamic load libraries with hot reloading.
*/
void game_object_load(void);
void game_object_unload(void);
/* returns true if reload happened, otherwise false */
bool game_object_try_reloading(void);
void game_object_tick(void);
#endif

320
src/twn_input.c Normal file
View File

@ -0,0 +1,320 @@
#include "townengine/input/internal_api.h"
#include "townengine/context.h"
#include "townengine/util.h"
#include <SDL2/SDL.h>
#include <stb_ds.h>
#include <stdbool.h>
#include <stdlib.h>
static void update_action_pressed_state(struct input_state *input, struct action *action) {
for (size_t i = 0; i < SDL_arraysize(action->bindings); ++i) {
switch (action->bindings[i].source) {
case BUTTON_SOURCE_NOT_SET:
break;
case BUTTON_SOURCE_KEYBOARD_PHYSICAL:
/* not pressed */
if (input->keyboard_state[action->bindings[i].code.scancode] == 0) {
action->just_changed = action->is_pressed;
action->is_pressed = false;
}
/* pressed */
else {
action->just_changed = !action->is_pressed;
action->is_pressed = true;
return;
}
break;
case BUTTON_SOURCE_MOUSE:
/* not pressed */
if ((input->mouse_state & action->bindings[i].code.mouse_button) == 0) {
action->just_changed = action->is_pressed;
action->is_pressed = false;
}
/* pressed */
else {
action->just_changed = !action->is_pressed;
action->is_pressed = true;
action->position.x = (float)input->mouse_window_position.x;
action->position.x = (float)input->mouse_window_position.x;
/* TODO: */
/*
* SDL_RenderWindowToLogical will turn window mouse
* coords into a position inside the logical render
* area. this has to be done to get an accurate point
* that can actually be used in game logic
*/
// SDL_RenderWindowToLogical(input->renderer,
// input->mouse_window_position.x,
// input->mouse_window_position.y,
// &action->position.x,
// &action->position.y);
return;
}
break;
default:
break;
}
}
}
static void input_bind_code_to_action(struct input_state *input,
char *action_name,
enum button_source source,
union button_code code)
{
struct action_hash_item *action_item = shgetp_null(input->action_hash, action_name);
if (action_item == NULL) {
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
return;
}
struct action *action = &action_item->value;
/* check every binding to make sure this code isn't already bound */
for (size_t i = 0; i < SDL_arraysize(action->bindings); ++i) {
struct button *binding = &action->bindings[i];
if (binding->source != source)
break;
bool is_already_bound = false;
switch (binding->source) {
case BUTTON_SOURCE_NOT_SET:
break;
case BUTTON_SOURCE_KEYBOARD_PHYSICAL:
is_already_bound = binding->code.scancode == code.scancode;
break;
case BUTTON_SOURCE_KEYBOARD_CHARACTER:
is_already_bound = binding->code.keycode == code.keycode;
break;
case BUTTON_SOURCE_GAMEPAD:
is_already_bound = binding->code.gamepad_button == code.gamepad_button;
break;
case BUTTON_SOURCE_MOUSE:
is_already_bound = binding->code.mouse_button == code.mouse_button;
break;
}
if (is_already_bound) {
log_warn("(%s) Code already bound to action \"%s\".", __func__, action_name);
return;
}
}
/* if we're at max bindings, forget the first element and shift the rest */
if (action->num_bindings == SDL_arraysize(action->bindings)) {
--action->num_bindings;
size_t shifted_size = (sizeof action->bindings) - (sizeof action->bindings[0]);
memmove(action->bindings, action->bindings + 1, shifted_size);
}
action->bindings[action->num_bindings++] = (struct button) {
.source = source,
.code = code,
};
}
static void input_unbind_code_from_action(struct input_state *input,
char *action_name,
enum button_source source,
union button_code code)
{
struct action_hash_item *action_item = shgetp_null(input->action_hash, action_name);
if (action_item == NULL) {
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
return;
}
struct action *action = &action_item->value;
/* check every binding to make sure this code is bound */
size_t index = 0;
bool is_bound = false;
for (index = 0; index < SDL_arraysize(action->bindings); ++index) {
struct button *binding = &action->bindings[index];
if (binding->source != source)
continue;
switch (binding->source) {
case BUTTON_SOURCE_NOT_SET:
break;
case BUTTON_SOURCE_KEYBOARD_PHYSICAL:
is_bound = binding->code.scancode == code.scancode;
break;
case BUTTON_SOURCE_KEYBOARD_CHARACTER:
is_bound = binding->code.keycode == code.keycode;
break;
case BUTTON_SOURCE_GAMEPAD:
is_bound = binding->code.gamepad_button == code.gamepad_button;
break;
case BUTTON_SOURCE_MOUSE:
is_bound = binding->code.mouse_button == code.mouse_button;
break;
}
if (is_bound)
break;
}
if (!is_bound) {
log_warn("(%s) Code is not bound to action \"%s\".", __func__, action_name);
return;
}
/* remove the element to unbind and shift the rest so there isn't a gap */
if (action->num_bindings == SDL_arraysize(action->bindings)) {
size_t elements_after_index = action->num_bindings - index;
size_t shifted_size = elements_after_index * (sizeof action->bindings[0]);
memmove(action->bindings + index, action->bindings + index + 1, shifted_size);
--action->num_bindings;
}
}
void input_state_init(struct input_state *input) {
sh_new_strdup(input->action_hash);
}
void input_state_deinit(struct input_state *input) {
shfree(input->action_hash);
}
void input_state_update(struct input_state *input) {
input->keyboard_state = SDL_GetKeyboardState(NULL);
input->mouse_state = SDL_GetMouseState(&input->mouse_window_position.x,
&input->mouse_window_position.y);
SDL_GetRelativeMouseState(&input->mouse_relative_position.x,
&input->mouse_relative_position.y);
for (size_t i = 0; i < shlenu(input->action_hash); ++i) {
struct action *action = &input->action_hash[i].value;
update_action_pressed_state(input, action);
}
}
void input_bind_action_scancode(struct input_state *input,
char *action_name,
SDL_Scancode scancode)
{
input_bind_code_to_action(input,
action_name,
BUTTON_SOURCE_KEYBOARD_PHYSICAL,
(union button_code) { .scancode = scancode });
}
void input_unbind_action_scancode(struct input_state *input,
char *action_name,
SDL_Scancode scancode)
{
input_unbind_code_from_action(input,
action_name,
BUTTON_SOURCE_KEYBOARD_PHYSICAL,
(union button_code) { .scancode = scancode });
}
void input_bind_action_mouse(struct input_state *input,
char *action_name,
uint8_t mouse_button)
{
input_bind_code_to_action(input,
action_name,
BUTTON_SOURCE_MOUSE,
(union button_code) { .mouse_button = mouse_button});
}
void input_unbind_action_mouse(struct input_state *input,
char *action_name,
uint8_t mouse_button)
{
input_unbind_code_from_action(input,
action_name,
BUTTON_SOURCE_MOUSE,
(union button_code) { .mouse_button = mouse_button});
}
void input_add_action(struct input_state *input, char *action_name) {
if (shgeti(input->action_hash, action_name) >= 0) {
log_warn("(%s) Action \"%s\" is already registered.", __func__, action_name);
return;
}
shput(input->action_hash, action_name, (struct action) { 0 });
}
void input_delete_action(struct input_state *input, char *action_name) {
if (shdel(input->action_hash, action_name) == 0)
log_warn("(%s) Action \"%s\" is not registered.", __func__, action_name);
}
bool input_is_action_pressed(struct input_state *input, char *action_name) {
struct action_hash_item *action = shgetp_null(input->action_hash, action_name);
if (action == NULL) {
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
return false;
}
return action->value.is_pressed;
}
bool input_is_action_just_pressed(struct input_state *input, char *action_name) {
struct action_hash_item *action = shgetp_null(input->action_hash, action_name);
if (action == NULL) {
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
return false;
}
return action->value.is_pressed && action->value.just_changed;
}
bool input_is_action_just_released(struct input_state *input, char *action_name) {
struct action_hash_item *action = shgetp_null(input->action_hash, action_name);
if (action == NULL) {
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
return false;
}
return !action->value.is_pressed && action->value.just_changed;
}
t_fvec2 input_get_action_position(struct input_state *input, char *action_name) {
struct action_hash_item *action = shgetp_null(input->action_hash, action_name);
if (action == NULL) {
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
return (t_fvec2) { 0 };
}
return action->value.position;
}
void input_set_mouse_captured(struct input_state *input, bool enabled) {
(void)input;
/* TODO: returns -1 if not supported, but like... do we care? */
SDL_SetRelativeMouseMode(enabled);
}
bool input_is_mouse_captured(struct input_state *input) {
(void)input;
return SDL_GetRelativeMouseMode();
}
void input_reset_state(struct input_state *input) {
stbds_shfree(input->action_hash);
}

8
src/twn_input_c.h Normal file
View File

@ -0,0 +1,8 @@
#ifndef INPUT_INTERNAL_API_H
#define INPUT_INTERNAL_API_H
#include "townengine/input.h"
void input_reset_state(struct input_state *input);
#endif

439
src/twn_loop.c Normal file
View File

@ -0,0 +1,439 @@
#include "twn_loop.h"
#include "townengine/context.h"
#include "townengine/rendering.h"
#include "townengine/input/internal_api.h"
#include "townengine/util.h"
#include "townengine/twn_game_object.h"
#include "townengine/audio/internal_api.h"
#include "townengine/textures/internal_api.h"
#include "townengine/rendering/internal_api.h"
#include <SDL2/SDL.h>
#include <physfs.h>
#include <stb_ds.h>
#ifdef EMSCRIPTEN
#include <GLES2/gl2.h>
#else
#include <glad/glad.h>
#endif
#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>
#include <tgmath.h>
#include <limits.h>
static void poll_events(void) {
SDL_Event e;
ctx.window_size_has_changed = false;
while (SDL_PollEvent(&e)) {
switch (e.type) {
case SDL_QUIT:
ctx.is_running = false;
break;
case SDL_WINDOWEVENT:
if (e.window.windowID != ctx.window_id)
break;
switch (e.window.event) {
case SDL_WINDOWEVENT_SIZE_CHANGED:
ctx.window_size_has_changed = true;
break;
default:
break;
}
break;
default:
break;
}
}
}
#ifndef EMSCRIPTEN
static void APIENTRY opengl_log(GLenum source,
GLenum type,
GLuint id,
GLenum severity,
GLsizei length,
const GLchar* message,
const void* userParam)
{
(void)source;
(void)type;
(void)id;
(void)severity;
(void)userParam;
log_info("OpenGL: %.*s\n", length, message);
}
#endif
static void main_loop(void) {
/*
if (!ctx.is_running) {
end(ctx);
clean_up(ctx);
emscripten_cancel_main_loop();
}
*/
/* frame timer */
int64_t current_frame_time = SDL_GetPerformanceCounter();
int64_t delta_time = current_frame_time - ctx.prev_frame_time;
ctx.prev_frame_time = current_frame_time;
ctx.delta_time = delta_time;
/* handle unexpected timer anomalies (overflow, extra slow frames, etc) */
if (delta_time > ctx.desired_frametime * 8) { /* ignore extra-slow frames */
delta_time = ctx.desired_frametime;
}
delta_time = MAX(0, delta_time);
/* vsync time snapping */
/* get the refresh rate of the current display (it might not always be the same one) */
int display_framerate = 60; /* a reasonable guess */
SDL_DisplayMode current_display_mode;
int current_display_index = SDL_GetWindowDisplayIndex(ctx.window);
if (SDL_GetCurrentDisplayMode(current_display_index, &current_display_mode) == 0)
display_framerate = current_display_mode.refresh_rate;
int64_t snap_hz = display_framerate;
if (snap_hz <= 0)
snap_hz = 60;
/* these are to snap delta time to vsync values if it's close enough */
int64_t vsync_maxerror = (int64_t)((double)ctx.clocks_per_second * 0.0002);
size_t snap_frequency_count = 8;
for (size_t i = 0; i < snap_frequency_count; ++i) {
int64_t frequency = (ctx.clocks_per_second / snap_hz) * (i+1);
if (llabs(delta_time - frequency) < vsync_maxerror) {
delta_time = frequency;
break;
}
}
/* delta time averaging */
size_t time_averager_count = SDL_arraysize(ctx.time_averager);
for (size_t i = 0; i < time_averager_count - 1; ++i) {
ctx.time_averager[i] = ctx.time_averager[i+1];
}
ctx.time_averager[time_averager_count - 1] = delta_time;
int64_t averager_sum = 0;
for (size_t i = 0; i < time_averager_count; ++i) {
averager_sum += ctx.time_averager[i];
}
delta_time = averager_sum / time_averager_count;
ctx.delta_averager_residual += averager_sum % time_averager_count;
delta_time += ctx.delta_averager_residual / time_averager_count;
ctx.delta_averager_residual %= time_averager_count;
/* add to the accumulator */
ctx.frame_accumulator += delta_time;
/* spiral of death protection */
if (ctx.frame_accumulator > ctx.desired_frametime * 8) {
ctx.resync_flag = true;
}
/* timer resync if requested */
if (ctx.resync_flag) {
ctx.frame_accumulator = 0;
delta_time = ctx.desired_frametime;
ctx.resync_flag = false;
}
/* finally, let's get to work */
int frames = 0;
while (ctx.frame_accumulator >= ctx.desired_frametime * ctx.update_multiplicity) {
frames += 1;
for (size_t i = 0; i < ctx.update_multiplicity; ++i) {
/* TODO: disable rendering pushes on not-last ? */
render_queue_clear();
poll_events();
if (ctx.window_size_has_changed) {
t_vec2 size;
SDL_GetWindowSize(ctx.window, &size.x, &size.y);
ctx.window_w = size.x;
ctx.window_h = size.y;
}
input_state_update(&ctx.input);
game_object_tick();
ctx.frame_accumulator -= ctx.desired_frametime;
ctx.tick_count = (ctx.tick_count % ULLONG_MAX) + 1;
ctx.initialization_needed = false;
}
}
/* TODO: in some cases machine might want to assume frames will be fed as much as possible */
/* which for now is broken as glBufferData with NULL is used all over right after render */
if (frames != 0)
render();
}
static bool initialize(void) {
if (SDL_Init(SDL_INIT_EVERYTHING & ~SDL_INIT_HAPTIC) == -1) {
CRY_SDL("SDL initialization failed.");
return false;
}
/* TODO: recognize cli parameter to turn it on on release */
/* debug mode _defaults_ to being enabled on debug builds. */
/* you should be able to enable it at runtime on any build */
#ifndef NDEBUG
ctx.debug = true;
#else
ctx.debug = false;
#endif
#ifdef EMSCRIPTEN
/* emscripten interpretes those as GL ES version against WebGL */
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
#else
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 1);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 5);
if (ctx.debug)
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG);
else
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_NO_ERROR);
#endif
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6);
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 0);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_ACCELERATED_VISUAL, 1);
/* init got far enough to create a window */
ctx.window = SDL_CreateWindow("townengine",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
RENDER_BASE_WIDTH,
RENDER_BASE_HEIGHT,
// SDL_WINDOW_ALLOW_HIGHDPI |
SDL_WINDOW_RESIZABLE |
SDL_WINDOW_OPENGL);
if (ctx.window == NULL) {
CRY_SDL("Window creation failed.");
goto fail;
}
ctx.gl_context = SDL_GL_CreateContext(ctx.window);
if (!ctx.gl_context) {
CRY_SDL("GL context creation failed.");
goto fail;
}
if (SDL_GL_MakeCurrent(ctx.window, ctx.gl_context)) {
CRY_SDL("GL context binding failed.");
goto fail;
}
if (SDL_GL_SetSwapInterval(-1))
SDL_GL_SetSwapInterval(1);
#ifndef EMSCRIPTEN
if (gladLoadGL() == 0) {
CRY("Init", "GLAD failed");
goto fail;
}
#endif
log_info("OpenGL context: %s\n", glGetString(GL_VERSION));
#ifndef EMSCRIPTEN
glHint(GL_TEXTURE_COMPRESSION_HINT, GL_NICEST);
glHint(GL_GENERATE_MIPMAP_HINT, GL_NICEST);
glHint(GL_FOG_HINT, GL_FASTEST);
#endif
/* might need this to have multiple windows */
ctx.window_id = SDL_GetWindowID(ctx.window);
glViewport(0, 0, RENDER_BASE_WIDTH, RENDER_BASE_HEIGHT);
/* TODO: */
// SDL_GetRendererOutputSize(ctx.renderer, &ctx.window_w, &ctx.window_h);
ctx.window_w = RENDER_BASE_WIDTH;
ctx.window_h = RENDER_BASE_HEIGHT;
/* audio initialization */
{
SDL_AudioSpec request, got;
SDL_zero(request);
request.freq = AUDIO_FREQUENCY;
request.format = AUDIO_S16;
request.channels = AUDIO_N_CHANNELS;
request.callback = audio_callback;
/* TODO: check for errors */
ctx.audio_device = SDL_OpenAudioDevice(NULL, 0, &request, &got, 0);
ctx.audio_stream_format = got.format;
ctx.audio_stream_frequency = got.freq;
ctx.audio_stream_channel_count = got.channels;
SDL_PauseAudioDevice(ctx.audio_device, 0);
}
/* filesystem time */
/* TODO: ANDROID: see the warning in physicsfs PHYSFS_init docs/header */
if (!PHYSFS_init(ctx.argv[0]) ||
!PHYSFS_setSaneConfig(ORGANIZATION_NAME, APP_NAME, PACKAGE_EXTENSION, false, true))
{
CRY_PHYSFS("Filesystem initialization failed.");
goto fail;
}
/* you could change this at runtime if you wanted */
ctx.update_multiplicity = 1;
#ifndef EMSCRIPTEN
/* hook up opengl debugging callback */
if (ctx.debug) {
glEnable(GL_DEBUG_OUTPUT);
glDebugMessageCallback(opengl_log, NULL);
}
#endif
/* random seeding */
/* SDL_GetPerformanceCounter returns some platform-dependent number. */
/* it should vary between game instances. i checked! random enough for me. */
ctx.random_seed = SDL_GetPerformanceCounter();
srand((unsigned int)ctx.random_seed);
stbds_rand_seed(ctx.random_seed);
/* main loop machinery */
ctx.is_running = true;
ctx.resync_flag = true;
ctx.clocks_per_second = SDL_GetPerformanceFrequency();
ctx.prev_frame_time = SDL_GetPerformanceCounter();
ctx.desired_frametime = ctx.clocks_per_second / TICKS_PER_SECOND;
ctx.frame_accumulator = 0;
ctx.tick_count = 0;
/* delta time averaging */
ctx.delta_averager_residual = 0;
for (size_t i = 0; i < SDL_arraysize(ctx.time_averager); ++i) {
ctx.time_averager[i] = ctx.desired_frametime;
}
/* rendering */
/* these are dynamic arrays and will be allocated lazily by stb_ds */
ctx.render_queue_2d = NULL;
ctx.uncolored_mesh_batches = NULL;
textures_cache_init(&ctx.texture_cache, ctx.window);
text_cache_init(&ctx.text_cache);
/* input */
input_state_init(&ctx.input);
/* scripting */
/*
if (!scripting_init(ctx)) {
goto fail;
}
*/
return true;
fail:
SDL_Quit();
return false;
}
/* will not be called on an abnormal exit */
static void clean_up(void) {
/*
scripting_deinit(ctx);
*/
input_state_deinit(&ctx.input);
/* if you're gonna remove this, it's also being done in rendering.c */
for (size_t i = 0; i < arrlenu(ctx.render_queue_2d); ++i) {
if (ctx.render_queue_2d[i].type == PRIMITIVE_2D_TEXT) {
free(ctx.render_queue_2d[i].text.text);
}
}
arrfree(ctx.render_queue_2d);
text_cache_deinit(&ctx.text_cache);
textures_cache_deinit(&ctx.texture_cache);
PHYSFS_deinit();
SDL_Quit();
}
static void reset_state(void) {
input_reset_state(&ctx.input);
textures_reset_state();
}
int enter_loop(int argc, char **argv) {
ctx.argc = argc;
ctx.argv = argv;
if (!initialize())
return EXIT_FAILURE;
for (int i = 1; i < (argc - 1); ++i) {
if (strcmp(argv[i], "--data-dir") == 0) {
if (!PHYSFS_mount(argv[i+1], NULL, true)) {
CRY_PHYSFS("Data dir mount override failed.");
return EXIT_FAILURE;
}
}
}
game_object_load();
ctx.was_successful = true;
ctx.initialization_needed = true;
while (ctx.is_running) {
if (game_object_try_reloading()) {
ctx.initialization_needed = true;
reset_state();
}
main_loop();
}
game_object_unload();
clean_up();
return ctx.was_successful ? EXIT_SUCCESS : EXIT_FAILURE;
}

11
src/twn_main.c Normal file
View File

@ -0,0 +1,11 @@
#include "twn_loop.h"
#define SDL_MAIN_HANDLED
#include <SDL2/SDL.h>
int main(int argc, char **argv) {
SDL_SetMainReady();
return enter_loop(argc, argv);
}

97
src/twn_option_c.h Normal file
View File

@ -0,0 +1,97 @@
#ifndef TWN_OPTION_H
#define TWN_OPTION_H
#include "twn_concatenate_c.h"
#include "twn_varargcount_c.h"
#include <stdbool.h>
/* usage example:
*
* struct result {
* float f;
* m_option_list(
* int, v
* )
* }
*
* struct result = {
* m_set(f, 1.0), -- non options could be set with this
* m_opt(v, 5), -- options are supposed to be initialized like that
* }
*
*/
#define m_set(p_member, p_value) .p_member = (p_value)
#define m_opt(p_member, p_value) .p_member##_opt = (p_value), .p_member##_opt_set = 1
#define m_is_set(p_value, p_member) ((p_value).p_member##_opt_set)
/* warn: beware of double evaluation! */
#define m_or(p_value, p_member, p_default) ((p_value).p_member##_opt_set ? (p_value).p_member##_opt : (p_default))
/* warn: beware of double evaluation! */
#define m_opt_from(p_member, p_value, p_member_from) .p_member##_opt = (p_value).p_member_from##_opt, \
.p_member##_opt_set = (p_value).p_member_from##_opt_set
#define m_option_list_2(t0, m0) \
t0 m0##_opt; \
bool m0##_opt_set : 1; \
#define m_option_list_4(t0, m0, t1, m1) \
t0 m0##_opt; \
t1 m1##_opt; \
bool m0##_opt_set : 1; \
bool m1##_opt_set : 1; \
#define m_option_list_6(t0, m0, t1, m1, t2, m2) \
t0 m0##_opt; \
t1 m1##_opt; \
t2 m1##_opt; \
bool m0##_opt_set : 1; \
bool m1##_opt_set : 1; \
bool m2##_opt_set : 1; \
#define m_option_list_8(t0, m0, t1, m1, t2, m2, t3, m3) \
t0 m0##_opt; \
t1 m1##_opt; \
t2 m2##_opt; \
t3 m3##_opt; \
bool m0##_opt_set : 1; \
bool m1##_opt_set : 1; \
bool m2##_opt_set : 1; \
bool m3##_opt_set : 1; \
#define m_option_list_10(t0, m0, t1, m1, t2, m2, \
t3, m3, t4, m4) \
t0 m0##_opt; \
t1 m1##_opt; \
t2 m2##_opt; \
t3 m3##_opt; \
t4 m4##_opt; \
bool m0##_opt_set : 1; \
bool m1##_opt_set : 1; \
bool m2##_opt_set : 1; \
bool m3##_opt_set : 1; \
bool m4##_opt_set : 1; \
#define m_option_list_12(t0, m0, t1, m1, t2, m2, \
t3, m3, t4, m4, t5, m5) \
t0 m0##_opt; \
t1 m1##_opt; \
t2 m2##_opt; \
t3 m3##_opt; \
t4 m4##_opt; \
t5 m5##_opt; \
bool m0##_opt_set : 1; \
bool m1##_opt_set : 1; \
bool m2##_opt_set : 1; \
bool m3##_opt_set : 1; \
bool m4##_opt_set : 1; \
bool m5##_opt_set : 1; \
#define m_option_list_(p_n, ...) m_concatenate(m_option_list_, p_n)(__VA_ARGS__)
#define m_option_list(...) m_option_list_(m_narg(__VA_ARGS__), __VA_ARGS__)
#endif

11
src/twn_texture_modes_c.h Normal file
View File

@ -0,0 +1,11 @@
#ifndef TWN_TEXTURES_MODES_H
#define TWN_TEXTURES_MODES_H
/* alpha channel information */
enum texture_mode {
TEXTURE_MODE_OPAQUE, /* all pixels are solid */
TEXTURE_MODE_SEETHROUGH, /* some pixels are alpha zero */
TEXTURE_MODE_GHOSTLY, /* arbitrary alpha values */
};
#endif

572
src/twn_textures.c Normal file
View File

@ -0,0 +1,572 @@
#include "townengine/textures/internal_api.h"
#include "townengine/config.h"
#include "townengine/util.h"
#include "townengine/context.h"
#include <SDL2/SDL.h>
#include <physfs.h>
#include <physfsrwops.h>
#include <stb_ds.h>
#include <stb_rect_pack.h>
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
static SDL_Surface *image_to_surface(const char *path) {
SDL_RWops *handle = PHYSFSRWOPS_openRead(path);
if (handle == NULL)
goto ERR_CANNOT_OPEN_FILE;
/* TODO: try using callbacks so that less memory is used */
Sint64 file_size = handle->size(handle);
SDL_assert_always(file_size != -1);
void *file_mem = malloc(file_size);
size_t read = handle->read(handle, file_mem, 1, file_size);
SDL_assert_always(read == (size_t)file_size);
SDL_FreeRW(handle);
if (!file_mem)
goto ERR_CANNOT_ALLOCATE_MEM;
int width, height, channels;
void *image_mem = stbi_load_from_memory(file_mem, (int)file_size, &width, &height, &channels, 4);
if (!image_mem)
goto ERR_CANNOT_READ_IMAGE;
free(file_mem);
Uint32 rmask, gmask, bmask, amask;
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
rmask = 0xff000000;
gmask = 0x00ff0000;
bmask = 0x0000ff00;
amask = 0x000000ff;
#else
rmask = 0x000000ff;
gmask = 0x0000ff00;
bmask = 0x00ff0000;
amask = 0xff000000;
#endif
SDL_Surface* surface = SDL_CreateRGBSurfaceFrom(image_mem, width, height,
channels * 8,
width * channels,
rmask, gmask, bmask, amask);
if (surface == NULL)
goto ERR_CANNOT_CREATE_SURFACE;
SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_NONE);
SDL_SetSurfaceRLE(surface, true);
return surface;
ERR_CANNOT_CREATE_SURFACE:
stbi_image_free(image_mem);
ERR_CANNOT_READ_IMAGE:
free(file_mem);
ERR_CANNOT_ALLOCATE_MEM:
SDL_FreeRW(handle);
ERR_CANNOT_OPEN_FILE:
CRY(path, "Failed to load image. Aborting...");
die_abruptly();
}
static SDL_Surface *create_surface(int width, int height) {
Uint32 rmask, gmask, bmask, amask;
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
rmask = 0xff000000;
gmask = 0x00ff0000;
bmask = 0x0000ff00;
amask = 0x000000ff;
#else
rmask = 0x000000ff;
gmask = 0x0000ff00;
bmask = 0x00ff0000;
amask = 0xff000000;
#endif
SDL_Surface *surface = SDL_CreateRGBSurface(0,
width,
height,
TEXTURE_ATLAS_BIT_DEPTH,
rmask,
gmask,
bmask,
amask);
SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_NONE);
SDL_SetSurfaceRLE(surface, true);
return surface;
}
/* adds a new, blank atlas surface to the cache */
static void add_new_atlas(struct texture_cache *cache) {
SDL_Surface *new_atlas = create_surface(TEXTURE_ATLAS_SIZE, TEXTURE_ATLAS_SIZE);
arrput(cache->atlas_surfaces, new_atlas);
arrput(cache->atlas_textures, create_gpu_texture(TEXTURE_FILTER_NEAREAST, true));
}
static void upload_texture_from_surface(gpu_texture texture, SDL_Surface *surface) {
SDL_LockSurface(surface);
specify_gpu_texture(texture, surface->pixels, surface->w, surface->h);
SDL_UnlockSurface(surface);
}
static void recreate_current_atlas_texture(struct texture_cache *cache) {
/* TODO: figure out if SDL_UpdateTexture alone is faster than blitting */
/* TODO: should surfaces be freed after they cannot be referenced in atlas builing? */
SDL_Surface *atlas_surface = cache->atlas_surfaces[cache->atlas_index];
/* clear */
SDL_FillRect(atlas_surface, NULL, 0);
/* blit the texture surfaces onto the atlas */
for (size_t i = 0; i < shlenu(cache->hash); ++i) {
/* skip all that aren't part of currently built one */
if (cache->hash[i].value.atlas_index != cache->atlas_index)
continue;
/* skip loners */
if (cache->hash[i].value.loner_texture != 0)
continue;
SDL_BlitSurface(cache->hash[i].value.data,
NULL,
atlas_surface,
&(SDL_Rect){
.x = (int)cache->hash[i].value.srcrect.x,
.y = (int)cache->hash[i].value.srcrect.y,
.w = (int)cache->hash[i].value.srcrect.w,
.h = (int)cache->hash[i].value.srcrect.h,
});
}
/* texturize it! */
upload_texture_from_surface(cache->atlas_textures[cache->atlas_index], atlas_surface);
}
/* uses the textures currently in the cache to create an array of stbrp_rects */
static stbrp_rect *create_rects_from_cache(struct texture_cache *cache) {
stbrp_rect *rects = NULL;
for (size_t i = 0; i < shlenu(cache->hash); ++i) {
if (cache->hash[i].value.loner_texture != 0)
continue;
const SDL_Surface *surface_data = cache->hash[i].value.data;
stbrp_rect new_rect = {
.w = surface_data->w,
.h = surface_data->h,
};
arrput(rects, new_rect);
}
return rects;
}
/* returns an array which contains a _copy_ of every unpacked rect in rects. */
/* each of these copies will have their original index in rects saved in */
/* their `id` field, which is an int. */
static stbrp_rect *filter_unpacked_rects(stbrp_rect *rects) {
stbrp_rect *unpacked_rects = NULL;
for (size_t i = 0; i < arrlenu(rects); ++i) {
/* already packed */
if (rects[i].was_packed)
continue;
arrput(unpacked_rects, rects[i]);
/* stb_rect_pack mercifully gives you a free userdata int */
/* the index is saved there so the original array can be updated later */
unpacked_rects[arrlenu(unpacked_rects)-1].id = (int)i;
}
return unpacked_rects;
}
/* updates the original rects array with the data from packed_rects */
/* returns true if all rects were packed successfully */
static bool update_rects(struct texture_cache *cache, stbrp_rect *rects, stbrp_rect *packed_rects) {
/* !!! do not grow either of the arrays !!! */
/* the reallocation will try to reassign the array pointer, to no effect. */
/* see stb_ds.h */
bool packed_all = true;
for (size_t i = 0; i < arrlenu(packed_rects); ++i) {
/* we can check if any rects failed to be packed right here */
/* it's not ideal, but it avoids another iteration */
if (!packed_rects[i].was_packed) {
packed_all = false;
continue;
}
rects[packed_rects[i].id] = packed_rects[i];
/* while the order of the elements in the hash map is unknown to us, */
/* their equivalents in `rects` are in that same (unknown) order, which means */
/* we can use the index we had saved to find the original texture struct */
cache->hash[packed_rects[i].id].value.atlas_index = cache->atlas_index;
}
return packed_all;
}
/* updates the atlas location of every rect in the cache */
static void update_texture_rects_in_atlas(struct texture_cache *cache, stbrp_rect *rects) {
for (size_t i = 0; i < arrlenu(rects); ++i) {
cache->hash[i].value.srcrect = (t_frect) {
.x = (float)rects[i].x,
.y = (float)rects[i].y,
.w = (float)rects[i].w,
.h = (float)rects[i].h,
};
}
}
void textures_cache_init(struct texture_cache *cache, SDL_Window *window) {
cache->window = window;
sh_new_arena(cache->hash);
cache->node_buffer = cmalloc(sizeof *cache->node_buffer * TEXTURE_ATLAS_SIZE);
add_new_atlas(cache);
recreate_current_atlas_texture(cache);
}
void textures_cache_deinit(struct texture_cache *cache) {
/* free atlas textures */
for (size_t i = 0; i < arrlenu(cache->atlas_textures); ++i) {
delete_gpu_texture(cache->atlas_textures[i]);
}
arrfree(cache->atlas_textures);
/* free atlas surfaces */
for (size_t i = 0; i < arrlenu(cache->atlas_surfaces); ++i) {
SDL_FreeSurface(cache->atlas_surfaces[i]);
}
arrfree(cache->atlas_surfaces);
/* free cache hashes */
for (size_t i = 0; i < shlenu(cache->hash); ++i) {
stbi_image_free(cache->hash[i].value.data->pixels);
SDL_FreeSurface(cache->hash[i].value.data);
}
shfree(cache->hash);
free(cache->node_buffer);
}
void textures_dump_atlases(struct texture_cache *cache) {
PHYSFS_mkdir("/dump");
const char string_template[] = "/dump/atlas%zd.png";
char buf[2048]; /* larger than will ever be necessary */
size_t i = 0;
for (; i < arrlenu(cache->atlas_surfaces); ++i) {
snprintf(buf, sizeof buf, string_template, i);
SDL_RWops *handle = PHYSFSRWOPS_openWrite(buf);
if (handle == NULL) {
CRY("Texture atlas dump failed.", "File could not be opened");
return;
}
/* TODO: */
// IMG_SavePNG_RW(cache->atlas_surfaces[i], handle, true);
CRY("Unimplemented", "textures_dump_atlases dumping is not there, sorry");
log_info("Dumped atlas %s", buf);
}
}
static enum texture_mode infer_texture_mode(SDL_Surface *surface) {
const uint32_t amask = surface->format->Amask;
if (amask == 0)
return TEXTURE_MODE_OPAQUE;
enum texture_mode result = TEXTURE_MODE_OPAQUE;
SDL_LockSurface(surface);
for (int i = 0; i < surface->w * surface->h; ++i) {
/* TODO: don't assume 32 bit depth ? */
t_color color;
SDL_GetRGBA(((uint32_t *)surface->pixels)[i], surface->format, &color.r, &color.g, &color.b, &color.a);
if (color.a == 0)
result = TEXTURE_MODE_SEETHROUGH;
else if (color.a != 255) {
result = TEXTURE_MODE_GHOSTLY;
break;
}
}
SDL_UnlockSurface(surface);
return result;
}
static t_texture_key textures_load(struct texture_cache *cache, const char *path) {
/* no need to do anything if it was loaded already */
const ptrdiff_t i = shgeti(cache->hash, path);
if (i >= 0)
return (t_texture_key){ (uint16_t)i };
SDL_Surface *surface = image_to_surface(path);
struct texture new_texture = {
.data = surface,
.mode = infer_texture_mode(surface),
};
/* it's a "loner texture," it doesn't fit in an atlas so it's not in one */
if (surface->w >= TEXTURE_ATLAS_SIZE || surface->h >= TEXTURE_ATLAS_SIZE) {
new_texture.loner_texture = create_gpu_texture(TEXTURE_FILTER_NEAREAST, true);
upload_texture_from_surface(new_texture.loner_texture, surface);
new_texture.srcrect = (t_frect) { .w = (float)surface->w, .h = (float)surface->h };
shput(cache->hash, path, new_texture);
return (t_texture_key){ (uint16_t)shgeti(cache->hash, path) };
} else {
new_texture.atlas_index = cache->atlas_index;
shput(cache->hash, path, new_texture);
cache->is_dirty = true;
return (t_texture_key){ (uint16_t)shgeti(cache->hash, path) };
}
}
void textures_update_atlas(struct texture_cache *cache) {
if (!cache->is_dirty)
return;
/* this function makes a lot more sense if you read stb_rect_pack.h */
stbrp_context pack_ctx; /* target info */
stbrp_init_target(&pack_ctx,
TEXTURE_ATLAS_SIZE,
TEXTURE_ATLAS_SIZE,
cache->node_buffer,
TEXTURE_ATLAS_SIZE);
stbrp_rect *rects = create_rects_from_cache(cache);
/* we have to keep packing, and creating atlases if necessary, */
/* until all rects have been packed. */
/* ideally, this will not iterate more than once. */
bool textures_remaining = true;
while (textures_remaining) {
stbrp_rect *rects_to_pack = filter_unpacked_rects(rects);
stbrp_pack_rects(&pack_ctx, rects_to_pack, (int)arrlen(rects_to_pack));
textures_remaining = !update_rects(cache, rects, rects_to_pack);
arrfree(rects_to_pack); /* got what we needed */
/* some textures couldn't be packed */
if (textures_remaining) {
update_texture_rects_in_atlas(cache, rects);
recreate_current_atlas_texture(cache);
/* need a new atlas for next time */
add_new_atlas(cache);
++cache->atlas_index;
}
};
update_texture_rects_in_atlas(cache, rects);
recreate_current_atlas_texture(cache);
cache->is_dirty = false;
arrfree(rects);
}
/* EXPERIMANTAL: LIKELY TO BE REMOVED! */
#if defined(__linux__) && !defined(HOT_RELOAD_SUPPORT) /* use rodata elf section for fast lookups of repeating textures */
#include "townengine/system/linux/elf.h"
static const char *rodata_start;
static const char *rodata_stop;
static const char *last_path = NULL;
static t_texture_key last_texture;
static struct ptr_to_texture {
const void *key;
t_texture_key value;
} *ptr_to_texture;
/* TODO: separate and reuse */
t_texture_key textures_get_key(struct texture_cache *cache, const char *path) {
if (rodata_stop == NULL)
if (!infer_elf_section_bounds(".rodata", &rodata_start, &rodata_stop))
CRY("Section inference", ".rodata section lookup failed");
/* the fastest path */
if (path == last_path)
return last_texture;
else {
/* moderately fast path, by pointer hashing */
const ptrdiff_t texture = hmgeti(ptr_to_texture, path);
if (texture != -1) {
if (path >= rodata_start && path < rodata_stop)
last_path = path;
last_texture = ptr_to_texture[texture].value;
return last_texture;
}
}
/* try loading */
last_texture = textures_load(cache, path);
hmput(ptr_to_texture, path, last_texture);
if (path >= rodata_start && path < rodata_stop)
last_path = path;
return last_texture;
}
#else
t_texture_key textures_get_key(struct texture_cache *cache, const char *path) {
/* hash tables are assumed to be stable, so we just return indices */
const ptrdiff_t texture = shgeti(cache->hash, path);
/* load it if it isn't */
if (texture == -1) {
return textures_load(cache, path);
} else
return (t_texture_key){ (uint16_t)texture };
}
#endif /* generic implementation of textures_get_key() */
int32_t textures_get_atlas_id(const struct texture_cache *cache, t_texture_key key) {
if (m_texture_key_is_valid(key)) {
if (cache->hash[key.id].value.loner_texture != 0)
return -cache->hash[key.id].value.loner_texture;
else
return cache->hash[key.id].value.atlas_index;
} else {
CRY("Texture lookup failed.",
"Tried to get atlas id that isn't loaded.");
return 0;
}
}
t_frect textures_get_srcrect(const struct texture_cache *cache, t_texture_key key) {
if (m_texture_key_is_valid(key)) {
return cache->hash[key.id].value.srcrect;
} else {
CRY("Texture lookup failed.",
"Tried to get texture that isn't loaded.");
return (t_frect){ 0, 0, 0, 0 };
}
}
t_frect textures_get_dims(const struct texture_cache *cache, t_texture_key key) {
if (m_texture_key_is_valid(key)) {
if (cache->hash[key.id].value.loner_texture != 0)
return cache->hash[key.id].value.srcrect;
else
return (t_frect){ .w = TEXTURE_ATLAS_SIZE, .h = TEXTURE_ATLAS_SIZE };
} else {
CRY("Texture lookup failed.",
"Tried to get texture that isn't loaded.");
return (t_frect){ 0, 0, 0, 0 };
}
}
void textures_bind(const struct texture_cache *cache, t_texture_key key) {
if (m_texture_key_is_valid(key)) {
if (cache->hash[key.id].value.loner_texture == 0)
bind_gpu_texture(cache->atlas_textures[cache->hash[key.id].value.atlas_index]);
else
bind_gpu_texture(cache->hash[key.id].value.loner_texture);
} else if (key.id == 0) {
CRY("Texture binding failed.",
"Tried to get texture that isn't loaded.");
}
}
/* TODO: alternative schemes, such as: array texture, fragment shader and geometry division */
void textures_bind_repeating(const struct texture_cache *cache, t_texture_key key) {
if (m_texture_key_is_valid(key)) {
if (cache->hash[key.id].value.loner_texture == 0) {
/* already allocated */
if (cache->hash[key.id].value.repeating_texture != 0) {
bind_gpu_texture(cache->hash[key.id].value.repeating_texture);
return;
}
const struct texture texture = cache->hash[key.id].value;
const gpu_texture repeating_texture = create_gpu_texture(TEXTURE_FILTER_NEAREAST, false);
SDL_LockSurface(texture.data);
specify_gpu_texture(repeating_texture,
texture.data->pixels,
texture.data->w,
texture.data->h);
SDL_UnlockSurface(texture.data);
cache->hash[key.id].value.repeating_texture = repeating_texture;
} else
bind_gpu_texture(cache->hash[key.id].value.loner_texture);
} else if (key.id == 0) {
CRY("Texture binding failed.",
"Tried to get texture that isn't loaded.");
}
}
enum texture_mode textures_get_mode(const struct texture_cache *cache, t_texture_key key) {
if (m_texture_key_is_valid(key)) {
return cache->hash[key.id].value.mode;
} else {
CRY("Texture binding failed.",
"Tried to get texture that isn't loaded.");
return TEXTURE_MODE_GHOSTLY;
}
}
size_t textures_get_num_atlases(const struct texture_cache *cache) {
return cache->atlas_index + 1;
}
void textures_reset_state(void) {
#if defined(__linux__) && !defined(HOT_RELOAD_SUPPORT)
last_path = NULL;
last_texture = (t_texture_key){0};
shfree(ptr_to_texture);
#endif
}

92
src/twn_textures_c.h Normal file
View File

@ -0,0 +1,92 @@
#ifndef TWN_TEXTURES_H
#define TWN_TEXTURES_H
#include "twn_util.h"
#include "twn_texture_modes.h"
#include "twn_engine_api.h"
#include "twn_gpu_texture.h"
#include <SDL2/SDL.h>
#include <stb_rect_pack.h>
#include <stdbool.h>
struct texture {
t_frect srcrect; /* position in atlas */
SDL_Surface *data; /* original image data */
int atlas_index;
gpu_texture loner_texture; /* stored directly for loners, == 0 means atlas_index should be used */
gpu_texture repeating_texture; /* separately allocated texture, for loners == loner_texture */
enum texture_mode mode;
};
struct texture_cache_item {
char *key;
struct texture value;
};
struct texture_cache {
SDL_Window *window; /* from context */
struct texture_cache_item *hash;
stbrp_node *node_buffer; /* used internally by stb_rect_pack */
SDL_Surface **atlas_surfaces;
gpu_texture *atlas_textures; /* shared by atlas textures */
int atlas_index; /* atlas that is currently being built */
bool is_dirty; /* current atlas needs to be recreated */
};
/* type safe structure for persistent texture handles */
typedef struct { uint16_t id; } t_texture_key;
/* tests whether given key structure corresponds to any texture */
#define m_texture_key_is_valid(p_key) ((p_key).id != (uint16_t)-1)
void textures_cache_init(struct texture_cache *cache, SDL_Window *window);
void textures_cache_deinit(struct texture_cache *cache);
/* for debugging */
void textures_dump_atlases(struct texture_cache *cache);
/* loads an image if it isn't in the cache, otherwise a no-op. */
/* can be called from anywhere at any time after init, useful if you want to */
/* preload textures you know will definitely be used */
// void textures_load(struct texture_cache *cache, const char *path);
/* repacks the current texture atlas based on the texture cache if needed */
/* any previously returned srcrect results are invalidated after that */
/* call it every time before rendering */
void textures_update_atlas(struct texture_cache *cache);
/* returns a persistent handle to some texture in cache, loading it if needed */
/* check the result with m_texture_key_is_valid() */
t_texture_key textures_get_key(struct texture_cache *cache, const char *path);
/* returns a rect in a texture cache of the given key */
t_frect textures_get_srcrect(const struct texture_cache *cache, t_texture_key key);
/* returns a rect of dimensions of the whole texture (whole atlas) */
t_frect textures_get_dims(const struct texture_cache *cache, t_texture_key key);
/* returns an identifier that is equal for all textures placed in the same atlas */
int32_t textures_get_atlas_id(const struct texture_cache *cache, t_texture_key key);
void textures_bind(const struct texture_cache *cache, t_texture_key key);
void textures_bind_repeating(const struct texture_cache *cache, t_texture_key key);
/* returns helpful information about contents of alpha channel in given texture */
enum texture_mode textures_get_mode(const struct texture_cache *cache, t_texture_key key);
/* returns the number of atlases in the cache */
size_t textures_get_num_atlases(const struct texture_cache *cache);
/* TODO: should recieve texture_cache, get_key optimization cache should be cleared some other way */
void textures_reset_state(void);
#endif

231
src/twn_util.c Normal file
View File

@ -0,0 +1,231 @@
#include "twn_util.h"
#include "twn_context.h"
#include <SDL2/SDL.h>
#include <physfsrwops.h>
#define STB_DS_IMPLEMENTATION
#define STBDS_ASSERT SDL_assert
#include <stb_ds.h>
#define STB_RECT_PACK_IMPLEMENTATION
#define STBRP_ASSERT SDL_assert
#include <stb_rect_pack.h>
#define STB_TRUETYPE_IMPLEMENTATION
#include <stb_truetype.h>
#include <stdlib.h>
#include <stdarg.h>
#include <stdnoreturn.h>
#include <string.h>
void cry_impl(const char *file, const int line, const char *title, const char *text) {
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
"TEARS AT %s:%d: %s ... %s", file, line, title, text);
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, text, NULL);
}
static void log_impl(const char *restrict format, va_list args, SDL_LogPriority priority) {
SDL_LogMessageV(SDL_LOG_CATEGORY_APPLICATION,
priority,
format,
args);
}
void log_info(const char *restrict format, ...) {
va_list args;
va_start(args, format);
log_impl(format, args, SDL_LOG_PRIORITY_INFO);
va_end(args);
}
void log_critical(const char *restrict format, ...) {
va_list args;
va_start(args, format);
log_impl(format, args, SDL_LOG_PRIORITY_CRITICAL);
va_end(args);
}
void log_warn(const char *restrict format, ...) {
va_list args;
va_start(args, format);
log_impl(format, args, SDL_LOG_PRIORITY_WARN);
va_end(args);
}
noreturn static void alloc_failure_death(void) {
log_critical("Allocation failure. Aborting NOW.");
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR,
"MEMORY ALLOCATION FAILURE.",
"FAILED TO ALLOCATE MEMORY. "
"YOU MIGHT BE UNLUCKY. "
"THE GAME WILL EXIT NOW.",
NULL);
die_abruptly();
}
noreturn void die_abruptly(void) {
/* a zombie window will linger if we don't at least try to quit SDL */
SDL_Quit();
abort();
}
void *cmalloc(size_t size) {
void *ptr = malloc(size);
if (ptr == NULL)
alloc_failure_death();
return ptr;
}
void *crealloc(void *ptr, size_t size) {
void *out = realloc(ptr, size);
if (out == NULL)
alloc_failure_death();
return out;
}
void *ccalloc(size_t num, size_t size) {
void *ptr = calloc(num, size);
if (ptr == NULL)
alloc_failure_death();
return ptr;
}
double clamp(double d, double min, double max) {
const double t = d < min ? min : d;
return t > max ? max : t;
}
float clampf(float f, float min, float max) {
const float t = f < min ? min : f;
return t > max ? max : t;
}
int clampi(int i, int min, int max) {
const int t = i < min ? min : i;
return t > max ? max : t;
}
int64_t file_to_bytes(const char *path, unsigned char **buf_out) {
SDL_RWops *handle = PHYSFSRWOPS_openRead(path);
if (handle == NULL) {
return -1;
}
int64_t data_size = SDL_RWseek(handle, 0, RW_SEEK_END);
SDL_RWseek(handle, 0, RW_SEEK_SET); /* reset offset into data */
*buf_out = cmalloc(data_size);
SDL_RWread(handle, *buf_out, sizeof **buf_out, data_size / sizeof **buf_out);
SDL_RWclose(handle); /* we got all we needed from the stream */
return data_size;
}
char *file_to_str(const char *path) {
SDL_RWops *handle = PHYSFSRWOPS_openRead(path);
if (handle == NULL) {
return NULL;
}
int64_t data_size = SDL_RWseek(handle, 0, RW_SEEK_END);
SDL_RWseek(handle, 0, RW_SEEK_SET); /* reset offset into data */
char *str_out = cmalloc(data_size + 1); /* data plus final null */
size_t len = data_size / sizeof *str_out;
SDL_RWread(handle, str_out, sizeof *str_out, len);
SDL_RWclose(handle); /* we got all we needed from the stream */
str_out[len] = '\0';
return str_out;
}
bool strends(const char *str, const char *suffix) {
size_t str_length = strlen(str);
size_t suffix_length = strlen(suffix);
if (suffix_length > str_length)
return false;
return memcmp((str + str_length) - suffix_length, suffix, suffix_length) == 0;
}
bool intersect_rect(const t_rect *a, const t_rect *b, t_rect *result) {
SDL_Rect a_sdl = { a->x, a->y, a->w, a->h };
SDL_Rect b_sdl = { b->x, b->y, b->w, b->h };
SDL_Rect result_sdl = { 0 };
bool intersection = SDL_IntersectRect(&a_sdl, &b_sdl, &result_sdl);
*result = (t_rect){ result_sdl.x, result_sdl.y, result_sdl.w, result_sdl.h };
return intersection;
}
bool intersect_frect(const t_frect *a, const t_frect *b, t_frect *result) {
SDL_FRect a_sdl = { a->x, a->y, a->w, a->h };
SDL_FRect b_sdl = { b->x, b->y, b->w, b->h };
SDL_FRect result_sdl = { 0 };
bool intersection = SDL_IntersectFRect(&a_sdl, &b_sdl, &result_sdl);
*result = (t_frect){ result_sdl.x, result_sdl.y, result_sdl.w, result_sdl.h };
return intersection;
}
t_frect to_frect(t_rect rect) {
return (t_frect) {
.h = (float)rect.h,
.w = (float)rect.w,
.x = (float)rect.x,
.y = (float)rect.y,
};
}
t_fvec2 frect_center(t_frect rect) {
return (t_fvec2){
.x = rect.x + rect.w / 2,
.y = rect.y + rect.h / 2,
};
}
void tick_timer(int *value) {
*value = MAX(*value - 1, 0);
}
void tick_ftimer(float *value) {
*value = MAX(*value - ((float)ctx.delta_time / (float)ctx.clocks_per_second), 0.0f);
}
bool repeat_ftimer(float *value, float at) {
*value -= (float)ctx.delta_time / (float)ctx.clocks_per_second;
if (*value < 0.0f) {
*value += at;
return true;
}
return false;
}

9
src/twn_varargcount_c.h Normal file
View File

@ -0,0 +1,9 @@
#ifndef TWN_VARARGCOUNT_H
#define TWN_VARARGCOUNT_H
#define m_narg(...) m_narg_(__VA_ARGS__, m_rseq_n_())
#define m_narg_(...) m_arg_n_(__VA_ARGS__)
#define m_arg_n_(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, N, ...) N
#define m_rseq_n_() 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
#endif