# HG changeset patch # User Mike Becker # Date 1706042052 -3600 # Node ID 1d001eb694dcbc329c4c755aac21d30c2f10fa83 # Parent 8acde7d27904a015aa9f748839e7997a1f9e4982 bring first scene graph to live diff -r 8acde7d27904 -r 1d001eb694dc src/Makefile --- a/src/Makefile Sun Jan 21 14:01:27 2024 +0100 +++ b/src/Makefile Tue Jan 23 21:34:12 2024 +0100 @@ -43,14 +43,14 @@ $(BUILD_DIR)/context.o: context.c ascension/context.h \ ascension/datatypes.h ascension/window.h ascension/primitives.h \ - ascension/mesh.h ascension/font.h ascension/error.h ascension/utils.h \ - ascension/shader.h + ascension/mesh.h ascension/scene.h ascension/font.h ascension/error.h \ + ascension/utils.h ascension/shader.h @echo "Compiling $<" $(CC) -o $@ $(CFLAGS) -c $< $(BUILD_DIR)/error.o: error.c ascension/context.h ascension/datatypes.h \ ascension/window.h ascension/primitives.h ascension/mesh.h \ - ascension/font.h ascension/error.h ascension/utils.h + ascension/scene.h ascension/font.h ascension/error.h ascension/utils.h @echo "Compiling $<" $(CC) -o $@ $(CFLAGS) -c $< @@ -60,14 +60,14 @@ $(BUILD_DIR)/font.o: font.c ascension/font.h ascension/context.h \ ascension/datatypes.h ascension/window.h ascension/primitives.h \ - ascension/mesh.h ascension/font.h ascension/error.h + ascension/mesh.h ascension/scene.h ascension/font.h ascension/error.h @echo "Compiling $<" $(CC) -o $@ $(CFLAGS) -c $< $(BUILD_DIR)/primitives.o: primitives.c ascension/primitives.h \ ascension/mesh.h ascension/error.h ascension/context.h \ ascension/datatypes.h ascension/window.h ascension/primitives.h \ - ascension/font.h + ascension/scene.h ascension/font.h @echo "Compiling $<" $(CC) -o $@ $(CFLAGS) -c $< @@ -81,15 +81,16 @@ $(CC) -o $@ $(CFLAGS) -c $< $(BUILD_DIR)/text.o: text.c ascension/text.h ascension/font.h \ - ascension/datatypes.h ascension/context.h ascension/window.h \ - ascension/primitives.h ascension/mesh.h ascension/error.h \ - ascension/shader.h + ascension/datatypes.h ascension/scene.h ascension/context.h \ + ascension/window.h ascension/primitives.h ascension/mesh.h \ + ascension/error.h ascension/shader.h @echo "Compiling $<" $(CC) -o $@ $(CFLAGS) -c $< $(BUILD_DIR)/window.o: window.c ascension/window.h ascension/datatypes.h \ - ascension/primitives.h ascension/mesh.h ascension/context.h \ - ascension/window.h ascension/font.h ascension/error.h ascension/utils.h + ascension/primitives.h ascension/mesh.h ascension/scene.h \ + ascension/context.h ascension/window.h ascension/font.h \ + ascension/error.h ascension/utils.h @echo "Compiling $<" $(CC) -o $@ $(CFLAGS) -c $< diff -r 8acde7d27904 -r 1d001eb694dc src/ascension/ascension.h --- a/src/ascension/ascension.h Sun Jan 21 14:01:27 2024 +0100 +++ b/src/ascension/ascension.h Tue Jan 23 21:34:12 2024 +0100 @@ -32,6 +32,7 @@ #include "context.h" #include "shader.h" #include "text.h" +#include "scene.h" #endif /* ASCENSION_H */ diff -r 8acde7d27904 -r 1d001eb694dc src/ascension/scene.h --- a/src/ascension/scene.h Sun Jan 21 14:01:27 2024 +0100 +++ b/src/ascension/scene.h Tue Jan 23 21:34:12 2024 +0100 @@ -28,29 +28,60 @@ #ifndef ASCENSION_SCENE_H #define ASCENSION_SCENE_H -typedef struct AscSceneNode { - struct AscSceneNode *parent; - struct AscSceneNode *prev; - struct AscSceneNode *next; - struct AscSceneNode *children; - // TODO: add node contents -} AscSceneNode; +typedef struct AscSceneNode AscSceneNode; + +typedef void(*asc_scene_free_func)(AscSceneNode*); +typedef void(*asc_scene_draw_func)(AscSceneNode const*); + +struct AscSceneNode { + AscSceneNode *parent; + AscSceneNode *prev; + AscSceneNode *next; + AscSceneNode *children; + asc_scene_free_func free_func; + asc_scene_draw_func draw_func; + // TODO: add more node contents +}; + +/** + * Place this as first member of a structure that shall be usable as a scene node. + */ +#define extend_asc_scene_node AscSceneNode node + +#define asc_node(obj) (&((obj)->node)) typedef struct AscScene { AscSceneNode *root; // TODO: add render groups for batching } AscScene; +__attribute__((__nonnull__)) void asc_scene_init(AscScene *scene); +__attribute__((__nonnull__)) void asc_scene_destroy(AscScene *scene); -AscSceneNode *asc_scene_node_create(AscSceneNode *parent); +__attribute__((__nonnull__)) +void asc_scene_draw(AscScene const *scene); + +__attribute__((__nonnull__)) +void asc_scene_add(AscScene *scene, AscSceneNode *node); + +/** + * Creates an empty node that may serve as a container for other nodes. + * + * The free_func of this node will be a simple free(). + * + * @return the new node + */ +AscSceneNode *asc_scene_node_empty(void); void asc_scene_node_free(AscSceneNode *node); -void asc_scene_node_link(AscSceneNode *node, AscSceneNode *parent); +__attribute__((__nonnull__)) +void asc_scene_node_link(AscSceneNode * restrict parent, AscSceneNode * restrict node); +__attribute__((__nonnull__)) void asc_scene_node_unlink(AscSceneNode *node); #endif // ASCENSION_SCENE_H diff -r 8acde7d27904 -r 1d001eb694dc src/ascension/text.h --- a/src/ascension/text.h Sun Jan 21 14:01:27 2024 +0100 +++ b/src/ascension/text.h Tue Jan 23 21:34:12 2024 +0100 @@ -30,9 +30,10 @@ #include "font.h" #include "datatypes.h" -#include +#include "scene.h" typedef struct AscText { + extend_asc_scene_node; char *text; AscFont const *font; asc_vec2i position; @@ -74,13 +75,6 @@ void asc_text_update(AscText *node); /** - * Draws the text node in the current frame. - * - * @param node the text node - */ -void asc_text_draw(AscText const *node); - -/** * Releases all the memory of this node. * * @param node the text node diff -r 8acde7d27904 -r 1d001eb694dc src/ascension/window.h --- a/src/ascension/window.h Sun Jan 21 14:01:27 2024 +0100 +++ b/src/ascension/window.h Tue Jan 23 21:34:12 2024 +0100 @@ -32,6 +32,7 @@ #include "datatypes.h" #include "primitives.h" +#include "scene.h" #ifndef ASC_MAX_WINDOWS /** The maximum number of windows that can exist simultaneously. */ @@ -55,6 +56,7 @@ AscPrimitives primitives; asc_vec2i dimensions; asc_mat4f projection; + AscScene ui; } AscWindow; /** diff -r 8acde7d27904 -r 1d001eb694dc src/scene.c --- a/src/scene.c Sun Jan 21 14:01:27 2024 +0100 +++ b/src/scene.c Tue Jan 23 21:34:12 2024 +0100 @@ -28,31 +28,63 @@ #include "ascension/scene.h" #include "ascension/error.h" +#include + #include +#define asc_scene_node_layout \ + offsetof(AscSceneNode, parent), offsetof(AscSceneNode, children), \ + offsetof(AscSceneNode, prev), offsetof(AscSceneNode, next) + void asc_scene_init(AscScene *scene) { if (scene->root != NULL) { asc_error("Scene is already initialized."); return; } - scene->root = asc_scene_node_create(NULL); + scene->root = asc_scene_node_empty(); } void asc_scene_destroy(AscScene *scene) { asc_scene_node_free(scene->root); } -AscSceneNode *asc_scene_node_create(AscSceneNode *parent) { +void asc_scene_add(AscScene *scene, AscSceneNode *node) { + asc_scene_node_link(scene->root, node); +} + +static void asc_scene_draw_node(AscSceneNode *node) { + if (node->draw_func != NULL) { + node->draw_func(node); + } + if (node->children != NULL) { + asc_scene_draw_node(node->children); + } + if (node->next != NULL) { + asc_scene_draw_node(node->next); + } +} + +void asc_scene_draw(AscScene const *scene) { + // TODO: replace with UCX tree visitor + // TODO: don't visit the tree, visit the render groups + // TODO: avoid recursion + asc_scene_draw_node(scene->root); +} + +AscSceneNode *asc_scene_node_empty(void) { // TODO: check if this can remain a calloc or if it's too expensive AscSceneNode *node = calloc(1, sizeof(AscSceneNode)); assert(node != NULL); - asc_scene_node_link(node, parent); + node->free_func = (asc_scene_free_func) free; return node; } void asc_scene_node_free(AscSceneNode *node) { if (node == NULL) return; + // TODO: replace with UCX tree visitor + // TODO: avoid recursion + // free the children recursively while (node->children != NULL) { asc_scene_node_free(node->children); @@ -62,36 +94,15 @@ asc_scene_node_unlink(node); // free the node - free(node); -} - -void asc_scene_node_link( - AscSceneNode *node, - AscSceneNode *parent -) { - if (node->parent == parent) return; - if (node->parent != NULL || parent == NULL) asc_scene_node_unlink(node); - if (parent != NULL) { - if (parent->children == NULL) { - parent->children = node; - } else { - parent->children->prev = node; - node->next = parent->children; - } - node->parent = parent; + if (node->free_func != NULL) { + node->free_func(node); } } +void asc_scene_node_link(AscSceneNode * restrict parent, AscSceneNode * restrict node) { + cx_tree_link(parent, node, asc_scene_node_layout); +} + void asc_scene_node_unlink(AscSceneNode *node) { - if (node->parent == NULL) return; - AscSceneNode *left = node->prev; - AscSceneNode *right = node->next; - assert(left == NULL || node->parent->children != node); - if (left != NULL) { - left->next = right; - } else { - node->parent->children = right; - } - if (right != NULL) right->prev = left; - node->parent = node->prev = node->next = NULL; + cx_tree_unlink(node, asc_scene_node_layout); } \ No newline at end of file diff -r 8acde7d27904 -r 1d001eb694dc src/text.c --- a/src/text.c Sun Jan 21 14:01:27 2024 +0100 +++ b/src/text.c Tue Jan 23 21:34:12 2024 +0100 @@ -32,6 +32,36 @@ #include +static void asc_text_draw(AscText const *node) { + if (node->color.alpha == 0 || node->hidden || node->internal.tex_id == 0) { + return; + } + + glUseProgram(ASC_SHADER_FONT.base.id); + + // Upload projection + // TODO: when we group UI draw calls, we don't need this + glUniformMatrix4fv(ASC_SHADER_FONT.base.projection, 1, + GL_FALSE, asc_context.active_window->projection); + + // Upload model matrix + asc_mat4f model = {0}; + model[asc_mat4_index(0, 0)] = node->internal.dimension.width; + model[asc_mat4_index(1, 1)] = node->internal.dimension.height; + model[asc_mat4_index(3, 0)] = node->position.x; + model[asc_mat4_index(3, 1)] = node->position.y; + model[asc_mat4_index(3, 3)] = 1; + glUniformMatrix4fv(ASC_SHADER_FONT.base.model, 1, GL_FALSE, model); + + // Upload surface + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_RECTANGLE, node->internal.tex_id); + glUniform1i(ASC_SHADER_FONT.surface, 0); + + // Draw mesh + asc_primitives_draw_plane(); +} + AscText *asc_text(int x, int y, char const *text) { AscText *node = calloc(1, sizeof(AscText)); if (node == NULL) { @@ -39,6 +69,9 @@ return NULL; } + node->node.free_func = (asc_scene_free_func) asc_text_free; + node->node.draw_func = (asc_scene_draw_func) asc_text_draw; + node->position = (asc_vec2i) {x, y}; node->font = asc_context.active_font; node->color = asc_context.ink; @@ -93,41 +126,6 @@ SDL_FreeSurface(surface); } -void asc_text_draw(AscText const *node) { - if (node->color.alpha == 0 || node->hidden) { - return; - } - - if (node->internal.tex_id == 0) { - asc_error("Tried to redraw text node after destruction"); - return; - } - - glUseProgram(ASC_SHADER_FONT.base.id); - - // Upload projection - // TODO: when we group UI draw calls, we don't need this - glUniformMatrix4fv(ASC_SHADER_FONT.base.projection, 1, - GL_FALSE, asc_context.active_window->projection); - - // Upload model matrix - asc_mat4f model = {0}; - model[asc_mat4_index(0, 0)] = node->internal.dimension.width; - model[asc_mat4_index(1, 1)] = node->internal.dimension.height; - model[asc_mat4_index(3, 0)] = node->position.x; - model[asc_mat4_index(3, 1)] = node->position.y; - model[asc_mat4_index(3, 3)] = 1; - glUniformMatrix4fv(ASC_SHADER_FONT.base.model, 1, GL_FALSE, model); - - // Upload surface - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_RECTANGLE, node->internal.tex_id); - glUniform1i(ASC_SHADER_FONT.surface, 0); - - // Draw mesh - asc_primitives_draw_plane(); -} - void asc_text_free(AscText *node) { asc_dprintf("Release text node texture: %u", node->internal.tex_id); glDeleteTextures(1, &node->internal.tex_id); diff -r 8acde7d27904 -r 1d001eb694dc src/window.c --- a/src/window.c Sun Jan 21 14:01:27 2024 +0100 +++ b/src/window.c Tue Jan 23 21:34:12 2024 +0100 @@ -61,6 +61,10 @@ settings->title = "Ascended Window"; } +static void asc_window_init_scenes(AscWindow *window) { + asc_scene_init(&window->ui); +} + AscWindow *asc_window_initialize(unsigned int index, AscWindowSettings const *settings) { if (index >= ASC_MAX_WINDOWS) { asc_error("Maximum number of windows exceeded."); @@ -123,6 +127,7 @@ asc_dprintf("Window %u initialized", window->id); if (asc_primitives_init(&window->primitives)) { + asc_window_init_scenes(window); asc_context.active_window = window; return window; } else { @@ -152,6 +157,9 @@ asc_context.active_window = NULL; } + // destroy all scenes + asc_scene_destroy(&window->ui); + // release context related data (we have to make the GL context current for this) SDL_GL_MakeCurrent(window->window, window->glctx); asc_primitives_destroy(&window->primitives); @@ -180,10 +188,18 @@ if (window != active_window) { asc_window_activate(window); } - SDL_GL_SwapWindow(window->window); + + // Clear viewport for new frame glViewport(0, 0, window->dimensions.width, window->dimensions.height); glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // Draw the UI + asc_scene_draw(&window->ui); + + // Swap Buffers + SDL_GL_SwapWindow(window->window); + if (window != active_window) { asc_window_activate(active_window); } diff -r 8acde7d27904 -r 1d001eb694dc test/Makefile --- a/test/Makefile Sun Jan 21 14:01:27 2024 +0100 +++ b/test/Makefile Tue Jan 23 21:34:12 2024 +0100 @@ -45,8 +45,8 @@ ../src/ascension/error.h ../src/ascension/context.h \ ../src/ascension/datatypes.h ../src/ascension/window.h \ ../src/ascension/primitives.h ../src/ascension/mesh.h \ - ../src/ascension/font.h ../src/ascension/shader.h \ - ../src/ascension/text.h + ../src/ascension/scene.h ../src/ascension/font.h \ + ../src/ascension/shader.h ../src/ascension/text.h @echo "Compiling $<" $(CC) -o $@ $(CFLAGS) -c $< diff -r 8acde7d27904 -r 1d001eb694dc test/sandbox.c --- a/test/sandbox.c Sun Jan 21 14:01:27 2024 +0100 +++ b/test/sandbox.c Tue Jan 23 21:34:12 2024 +0100 @@ -52,17 +52,18 @@ AscWindow *window = asc_window_initialize(0, &settings); asc_shader_initialize_predefined(); + // create fps counter and add it to the UI asc_set_font(asc_font(ASC_FONT_REGULAR, 24)); asc_ink_rgb(255, 0, 0); - AscText *fps_counter = asc_text(50, 50, "9999 FPS"); + AscText *fps_counter = asc_text(50, 50, "XXXXX FPS"); unsigned last_fps = 0; + asc_scene_add(&window->ui, asc_node(fps_counter)); - while (asc_loop_next()) { + do { // quit application on any error if (show_message_box_on_error(window->window)) break; - - // fps counter + // update fps counter if (asc_context.elapsed_millis > 0) { unsigned fps = 1000u / asc_context.elapsed_millis; if (fps != last_fps) { @@ -71,11 +72,7 @@ asc_text_update(fps_counter); } } - asc_text_draw(fps_counter); - } - - // TODO: maybe nodes should also be "garbage collected" - asc_text_free(fps_counter); + } while (asc_loop_next()); asc_context_destroy(); return 0;