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

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;
}