From f9e38bf24e1874d773083d06ca443cce9bd92d56 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 9 Mar 2024 08:55:46 -0800 Subject: Rename gfx_app -> app. --- app/CMakeLists.txt | 14 ++++ app/README.md | 3 + app/include/gfx/app.h | 95 +++++++++++++++++++++++ app/src/app.c | 205 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 317 insertions(+) create mode 100644 app/CMakeLists.txt create mode 100644 app/README.md create mode 100644 app/include/gfx/app.h create mode 100644 app/src/app.c (limited to 'app') diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt new file mode 100644 index 0000000..7e60351 --- /dev/null +++ b/app/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.0) + +project(gfx-app) + +add_library(gfx-app + src/app.c) + +target_include_directories(gfx-app PUBLIC + include/) + +target_link_libraries(gfx-app PUBLIC + glfw + log + timer) diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..b21d1aa --- /dev/null +++ b/app/README.md @@ -0,0 +1,3 @@ +# Gfx App + +A small library to more conveniently create a window and run a game loop. diff --git a/app/include/gfx/app.h b/app/include/gfx/app.h new file mode 100644 index 0000000..ffff4bc --- /dev/null +++ b/app/include/gfx/app.h @@ -0,0 +1,95 @@ +#pragma once + +#include + +typedef struct GfxAppState GfxAppState; + +/// Application settings. +typedef struct GfxAppDesc { + int argc; // Number of application arguments. + const char** argv; // Application arguments. + int width; // Window width. + int height; // Window height. + int max_fps; // Desired maximum display framerate. 0 to disable. + double update_delta_time; // Desired delta time between frame updates. + const char* title; // Window title. + GfxAppState* app_state; +} GfxAppDesc; + +typedef bool (*GfxAppInit)(GfxAppState*, int argc, const char** argv); +typedef void (*GfxAppShutdown)(GfxAppState*); +typedef void (*GfxAppUpdate)(GfxAppState*, double t, double dt); +typedef void (*GfxAppRender)(GfxAppState*); +typedef void (*GfxAppResize)(GfxAppState*, int width, int height); + +/// Application callback functions. +typedef struct GfxAppCallbacks { + GfxAppInit init; + GfxAppShutdown shutdown; + GfxAppUpdate update; + GfxAppRender render; + GfxAppResize resize; +} GfxAppCallbacks; + +typedef enum Key { + KeyA = 'a', + KeyB, + KeyC, + KeyD, + KeyE, + KeyF, + KeyG, + KeyH, + KeyI, + KeyJ, + KeyK, + KeyL, + KeyM, + KeyN, + KeyO, + KeyP, + KeyQ, + KeyR, + KeyS, + KeyT, + KeyU, + KeyV, + KeyW, + KeyX, + KeyY, + KeyZ, +} Key; + +/// Create a window with an OpenGL context and run the main loop. +bool gfx_app_run(const GfxAppDesc*, const GfxAppCallbacks*); + +/// Get the mouse coordinates relative to the app's window. +void gfx_app_get_mouse_position(double* x, double* y); + +/// Return true if the given key is pressed. +bool gfx_is_key_pressed(Key); + +/// Define a main function that initializes and puts the application in a loop. +/// See also: gfx_app_run(). +#define GFX_APP_MAIN(WIDTH, HEIGHT, MAX_FPS, TITLE) \ + int main(int argc, const char** argv) { \ + GfxAppState app_state = {0}; \ + gfx_app_run( \ + &(GfxAppDesc){ \ + .argc = argc, \ + .argv = argv, \ + .width = WIDTH, \ + .height = HEIGHT, \ + .max_fps = MAX_FPS, \ + .update_delta_time = MAX_FPS > 0 ? 1.0 / (double)MAX_FPS : 0.0, \ + .title = TITLE, \ + .app_state = &app_state, \ + }, \ + &(GfxAppCallbacks){ \ + .init = (GfxAppInit)app_init, \ + .update = (GfxAppUpdate)app_update, \ + .render = (GfxAppRender)app_render, \ + .resize = (GfxAppResize)app_resize, \ + .shutdown = (GfxAppShutdown)app_end}); \ + return 0; \ + } diff --git a/app/src/app.c b/app/src/app.c new file mode 100644 index 0000000..b6d10ca --- /dev/null +++ b/app/src/app.c @@ -0,0 +1,205 @@ +#include + +#include +#include +#include + +#include +#include + +/// Application state. +typedef struct GfxApp { + GfxAppState* app_state; + GfxAppCallbacks callbacks; + int max_fps; + double update_delta_time; + GLFWwindow* window; +} GfxApp; + +/// Storing the application state in a global variable so that we can call the +/// application's callbacks from GLFW callbacks. +static GfxApp g_gfx_app; + +/// Called by GLFW when the window is resized. +static void on_resize(GLFWwindow* window, int width, int height) { + (*g_gfx_app.callbacks.resize)(g_gfx_app.app_state, width, height); +} + +/// Run the application's main loop. +static void loop(GfxApp* app) { + assert(app); + assert(app->window); + + const time_delta min_frame_time = + app->max_fps > 0 ? sec_to_time_delta(1.0 / (double)(app->max_fps)) : 0; + const time_delta update_dt = sec_to_time_delta(app->update_delta_time); + time_delta time = 0; + time_delta time_budget = 0; + Timer timer = timer_make(); + + // Warm up the update to initialize the application's state. + (*app->callbacks.update)( + app->app_state, time_delta_to_sec(time), time_delta_to_sec(update_dt)); + + // Warm up the rendering before entering the main loop. A renderer can + // compile shaders and do other initialization the first time it renders a + // scene. + (*app->callbacks.render)(app->app_state); + glfwSwapBuffers(app->window); + + timer_start(&timer); + while (!glfwWindowShouldClose(app->window)) { + timer_tick(&timer); + time_budget += timer.delta_time; + + while (time_budget >= update_dt) { + (*app->callbacks.update)( + app->app_state, time_delta_to_sec(time), + time_delta_to_sec(update_dt)); + + time += update_dt; + time_budget -= update_dt; + } + + (*app->callbacks.render)(app->app_state); + glfwSwapBuffers(app->window); + glfwPollEvents(); + + const time_point frame_end = time_now(); + const time_delta frame_time = time_diff(timer.last_tick, frame_end); + if ((min_frame_time > 0) && (frame_time < min_frame_time)) { + time_sleep(min_frame_time - frame_time); + } + } +} + +bool gfx_app_run(const GfxAppDesc* desc, const GfxAppCallbacks* callbacks) { + assert(desc); + assert(callbacks); + + bool success = false; + + g_gfx_app.app_state = desc->app_state; + g_gfx_app.callbacks = *callbacks; + g_gfx_app.max_fps = desc->max_fps; + g_gfx_app.update_delta_time = desc->update_delta_time; + g_gfx_app.window = 0; + + if (!glfwInit()) { + LOGE("glfwInit() failed"); + return false; + } + + const int major = 4; + const int minor = 4; + glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API); + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, major); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, minor); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE); + // TODO: Test antialiasing later on. + // glfwWindowHint(GLFW_SAMPLES, 4); + + const char* title = desc->title ? desc->title : "Gfx Application"; + + g_gfx_app.window = + glfwCreateWindow(desc->width, desc->height, title, NULL, NULL); + if (!g_gfx_app.window) { + LOGE("glfwCreateWindow() failed"); + goto cleanup; + } + glfwMakeContextCurrent(g_gfx_app.window); + + // Initialize the application's state before setting any callbacks. + if (!(*g_gfx_app.callbacks.init)( + g_gfx_app.app_state, desc->argc, desc->argv)) { + LOGE("Failed to initialize application"); + goto cleanup; + } + + // Trigger an initial resize for convenience. + (*g_gfx_app.callbacks.resize)(g_gfx_app.app_state, desc->width, desc->height); + + // Set GLFW callbacks now that the application has been initialized. + glfwSetWindowSizeCallback(g_gfx_app.window, on_resize); + + loop(&g_gfx_app); + + (*g_gfx_app.callbacks.shutdown)(g_gfx_app.app_state); + + success = true; + +cleanup: + if (g_gfx_app.window) { + glfwDestroyWindow(g_gfx_app.window); + } + glfwTerminate(); + return success; +} + +void gfx_app_get_mouse_position(double* x, double* y) { + glfwGetCursorPos(g_gfx_app.window, x, y); +} + +static int to_glfw_key(Key key); + +bool gfx_is_key_pressed(Key key) { + return glfwGetKey(g_gfx_app.window, to_glfw_key(key)) == GLFW_PRESS; +} + +static int to_glfw_key(Key key) { + switch (key) { + case KeyA: + return GLFW_KEY_A; + case KeyB: + return GLFW_KEY_B; + case KeyC: + return GLFW_KEY_C; + case KeyD: + return GLFW_KEY_D; + case KeyE: + return GLFW_KEY_E; + case KeyF: + return GLFW_KEY_F; + case KeyG: + return GLFW_KEY_G; + case KeyH: + return GLFW_KEY_H; + case KeyI: + return GLFW_KEY_I; + case KeyJ: + return GLFW_KEY_J; + case KeyK: + return GLFW_KEY_K; + case KeyL: + return GLFW_KEY_L; + case KeyM: + return GLFW_KEY_M; + case KeyN: + return GLFW_KEY_N; + case KeyO: + return GLFW_KEY_O; + case KeyP: + return GLFW_KEY_P; + case KeyQ: + return GLFW_KEY_Q; + case KeyR: + return GLFW_KEY_R; + case KeyS: + return GLFW_KEY_S; + case KeyT: + return GLFW_KEY_T; + case KeyU: + return GLFW_KEY_U; + case KeyV: + return GLFW_KEY_V; + case KeyW: + return GLFW_KEY_W; + case KeyX: + return GLFW_KEY_X; + case KeyY: + return GLFW_KEY_Y; + case KeyZ: + return GLFW_KEY_Z; + } +} -- cgit v1.2.3