add text rendering and demo FPS counter

Wed, 15 Nov 2023 22:51:40 +0100

author
Mike Becker <universe@uap-core.de>
date
Wed, 15 Nov 2023 22:51:40 +0100
changeset 16
c5dde81b6fb2
parent 15
362b7659dc76
child 17
25013a35e07d

add text rendering and demo FPS counter

shader/font_frag.glsl file | annotate | diff | comparison | revisions
shader/font_vtx.glsl file | annotate | diff | comparison | revisions
src/Makefile file | annotate | diff | comparison | revisions
src/ascension/ascension.h file | annotate | diff | comparison | revisions
src/ascension/context.h file | annotate | diff | comparison | revisions
src/ascension/datatypes.h file | annotate | diff | comparison | revisions
src/ascension/error.h file | annotate | diff | comparison | revisions
src/ascension/mesh.h file | annotate | diff | comparison | revisions
src/ascension/primitives.h file | annotate | diff | comparison | revisions
src/ascension/shader.h file | annotate | diff | comparison | revisions
src/ascension/text.h file | annotate | diff | comparison | revisions
src/ascension/window.h file | annotate | diff | comparison | revisions
src/context.c file | annotate | diff | comparison | revisions
src/error.c file | annotate | diff | comparison | revisions
src/primitives.c file | annotate | diff | comparison | revisions
src/shader.c file | annotate | diff | comparison | revisions
src/text.c file | annotate | diff | comparison | revisions
src/window.c file | annotate | diff | comparison | revisions
test/Makefile file | annotate | diff | comparison | revisions
test/sandbox.c file | annotate | diff | comparison | revisions
--- a/shader/font_frag.glsl	Wed Nov 08 23:17:07 2023 +0100
+++ b/shader/font_frag.glsl	Wed Nov 15 22:51:40 2023 +0100
@@ -1,10 +1,10 @@
 #version 400 core
 
+layout(location = 0) out vec4 diffuse;
 in vec2 texcoord;
-out vec4 fragment;
 
 uniform sampler2DRect surface;
 
 void main(void) {
-    fragment = texture(surface, texcoord);
+    diffuse = texture(surface, texcoord);
 }
--- a/shader/font_vtx.glsl	Wed Nov 08 23:17:07 2023 +0100
+++ b/shader/font_vtx.glsl	Wed Nov 15 22:51:40 2023 +0100
@@ -1,6 +1,6 @@
 #version 400 core
 
-in vec2 position;
+layout(location = 0) in vec2 position;
 out vec2 texcoord;
 
 uniform mat4 projection;
@@ -8,5 +8,5 @@
 
 void main(void) {
     gl_Position = projection*model*vec4(position, 0.0, 1.0);
-    texcoord = position;
+    texcoord = vec2(model[0].x, model[1].y)*position;
 }
--- a/src/Makefile	Wed Nov 08 23:17:07 2023 +0100
+++ b/src/Makefile	Wed Nov 15 22:51:40 2023 +0100
@@ -27,7 +27,7 @@
 
 BUILD_DIR=../build/lib
 
-SRC  = context.c error.c window.c font.c files.c shader.c
+SRC  = context.c error.c window.c files.c shader.c font.c text.c primitives.c
 
 OBJ = $(SRC:%.c=$(BUILD_DIR)/%.o)
 
@@ -40,15 +40,16 @@
 
 FORCE:
 
-$(BUILD_DIR)/context.o: context.c ascension/context.h ascension/window.h \
- ascension/datatypes.h ascension/font.h ascension/error.h \
- ascension/utils.h ascension/shader.h
+$(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
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
-$(BUILD_DIR)/error.o: error.c ascension/context.h ascension/window.h \
- ascension/datatypes.h ascension/font.h ascension/error.h \
- ascension/utils.h
+$(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
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
@@ -57,8 +58,15 @@
 	$(CC) -o $@ $(CFLAGS) -c $<
 
 $(BUILD_DIR)/font.o: font.c ascension/font.h ascension/context.h \
- ascension/window.h ascension/datatypes.h ascension/font.h \
- ascension/error.h
+ ascension/datatypes.h ascension/window.h ascension/primitives.h \
+ ascension/mesh.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
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
@@ -67,9 +75,16 @@
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
-$(BUILD_DIR)/window.o: window.c ascension/window.h ascension/datatypes.h \
- ascension/context.h ascension/window.h ascension/font.h \
- ascension/error.h ascension/utils.h
+$(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
 	@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
+	@echo "Compiling $<"
+	$(CC) -o $@ $(CFLAGS) -c $<
+
--- a/src/ascension/ascension.h	Wed Nov 08 23:17:07 2023 +0100
+++ b/src/ascension/ascension.h	Wed Nov 15 22:51:40 2023 +0100
@@ -31,6 +31,7 @@
 #include "error.h"
 #include "context.h"
 #include "shader.h"
+#include "text.h"
 
 #endif /* ASCENSION_H */
 
--- a/src/ascension/context.h	Wed Nov 08 23:17:07 2023 +0100
+++ b/src/ascension/context.h	Wed Nov 15 22:51:40 2023 +0100
@@ -28,6 +28,7 @@
 #ifndef ASCENSION_CONTEXT_H
 #define ASCENSION_CONTEXT_H
 
+#include "datatypes.h"
 #include "window.h"
 #include "font.h"
 
@@ -50,8 +51,13 @@
     unsigned int flags;
     CxBuffer error_buffer;
     AscWindow windows[ASC_MAX_WINDOWS];
+    AscWindow const *active_window;
     AscFont fonts[ASC_MAX_FONTS];
     unsigned int fonts_loaded;
+    AscFont const *active_font;
+    asc_col4i ink;
+    unsigned int elapsed_millis;
+    float elapsed;
 } AscContext;
 
 /** Global ascension context. */
@@ -60,5 +66,25 @@
 void asc_context_initialize(void);
 void asc_context_destroy(void);
 
+
+/**
+ * Dispatches events and synchronizes all initialized windows.
+ *
+ * @return false, if the application wants to quit, true otherwise
+ */
+bool asc_loop_next(void);
+
+/**
+ * Sets the active drawing color.
+ */
+#define asc_ink(color) asc_context.ink = (color)
+#define asc_ink_rgba(r,g,b,a) asc_context.ink = (asc_col4i){(r),(g),(b),(a)}
+#define asc_ink_rgb(r,g,b) asc_context.ink = (asc_col4i){(r),(g),(b),255u}
+
+/**
+ * Sets the active drawing font.
+ */
+#define asc_set_font(font) asc_context.active_font = (font)
+
 #endif /* ASCENSION_CONTEXT_H */
 
--- a/src/ascension/datatypes.h	Wed Nov 08 23:17:07 2023 +0100
+++ b/src/ascension/datatypes.h	Wed Nov 15 22:51:40 2023 +0100
@@ -33,7 +33,7 @@
 #endif
 
 #include <stdbool.h>
-#include <SDL2/SDL_endian.h>
+#include <string.h>
 
 // --------------------------------------------------------------------------
 //    Datatype Definitions
@@ -48,25 +48,15 @@
     struct { int width, height; };
 } asc_vec2i;
 
-typedef union asc_col4i {
-    asc_ubyte data[4];
-#if SDL_BYTEORDER == SDL_BIG_ENDIAN
-    struct { asc_ubyte red, green, blue, alpha; };
-#else
-    struct { asc_ubyte alpha, blue, green, red; };
-#endif
+typedef struct asc_col4i {
+    asc_ubyte red, green, blue, alpha;
 } asc_col4i;
 
-typedef union asc_col4f {
-    float data[4];
-#if SDL_BYTEORDER == SDL_BIG_ENDIAN
-    struct { float red, green, blue, alpha; };
-#else
-    struct { float alpha, blue, green, red; };
-#endif
+typedef struct asc_col4f {
+    float red, green, blue, alpha;
 } asc_col4f;
 
-typedef float asc_mat4f[4][4];
+typedef float asc_mat4f[16];
 
 // --------------------------------------------------------------------------
 //    General Utility Functions
@@ -104,6 +94,21 @@
 //   Matrix Functions
 // --------------------------------------------------------------------------
 
+/**
+ * Computes a matrix index in column-major order.
+ * @param col the column
+ * @param row the row
+ * @param rows the number of rows
+ */
+#define asc_mat_index(col, row, rows) ((row) + (col) * (rows))
+
+/**
+ * Computes a 4x4 matrix index in column-major order.
+ * @param col the column
+ * @param row the row
+ */
+#define asc_mat4_index(col, row) asc_mat_index(col, row, 4)
+
 static inline void asc_mat4f_ortho(
         asc_mat4f mat,
         float left,
@@ -112,11 +117,12 @@
         float top
 ) {
     memset(mat, 0, sizeof(float) * 16);
-    mat[0][0] = 2.f / (right - left);
-    mat[1][1] = 2.f / (top - bottom);
-    mat[2][2] = -1;
-    mat[3][0] = -(right + left) / (right - left);
-    mat[3][1] = -(top + bottom) / (top - bottom);
+    mat[asc_mat4_index(0,0)] = 2.f / (right - left);
+    mat[asc_mat4_index(1,1)] = 2.f / (top - bottom);
+    mat[asc_mat4_index(2,2)] = -1;
+    mat[asc_mat4_index(3,0)] = -(right + left) / (right - left);
+    mat[asc_mat4_index(3,1)] = -(top + bottom) / (top - bottom);
+    mat[asc_mat4_index(3,3)] = 1;
 }
 
 #endif //ASCENSION_DATATYPES_H
--- a/src/ascension/error.h	Wed Nov 08 23:17:07 2023 +0100
+++ b/src/ascension/error.h	Wed Nov 15 22:51:40 2023 +0100
@@ -42,6 +42,8 @@
     unsigned char*: asc_error_cuchar,        \
     cxstring: asc_error_cxstr)(text)
 
+void asc_error_gl(unsigned code, cxstring message);
+
 bool asc_has_error(void);
 char const* asc_get_error(void);
 void asc_clear_error(void);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ascension/mesh.h	Wed Nov 15 22:51:40 2023 +0100
@@ -0,0 +1,37 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ * Copyright 2023 Mike Becker. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef ASCENSION_MESH_H
+#define ASCENSION_MESH_H
+
+typedef struct AscMesh {
+    unsigned vbo;
+    unsigned vao;
+    unsigned vertices;
+} AscMesh;
+
+#endif //ASCENSION_MESH_H
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ascension/primitives.h	Wed Nov 15 22:51:40 2023 +0100
@@ -0,0 +1,57 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ * Copyright 2023 Mike Becker. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef ASCENSION_PRIMITIVES_H
+#define ASCENSION_PRIMITIVES_H
+
+#include <stdbool.h>
+
+#include "mesh.h"
+
+typedef struct AscPrimitives {
+    AscMesh plane;
+} AscPrimitives;
+
+
+void asc_primitives_draw_plane(void);
+
+/**
+ * Automatically called after initializing the OpenGL context.
+ *
+ * @param primitives the data structure to initialize
+ * @return true on success, false otherwise
+ */
+bool asc_primitives_init(AscPrimitives *primitives);
+
+/**
+ * Automatically called when the OpenGL context is destroyed.
+ *
+ * @param primitives containing information about the OpenGL objects
+ */
+void asc_primitives_destroy(AscPrimitives *primitives);
+
+#endif //ASCENSION_PRIMITIVES_H
--- a/src/ascension/shader.h	Wed Nov 08 23:17:07 2023 +0100
+++ b/src/ascension/shader.h	Wed Nov 15 22:51:40 2023 +0100
@@ -34,12 +34,18 @@
 
 typedef struct AscShaderProgram {
     unsigned int id;
-    AscShader vertex;
-    AscShader fragment;
+    int model;
+    int view;
+    int projection;
 } AscShaderProgram;
 
+typedef struct AscShaderFont {
+    AscShaderProgram base;
+    int surface;
+} AscShaderFont;
 
-extern AscShaderProgram ASC_SHADER_FONT;
+
+extern AscShaderFont ASC_SHADER_FONT;
 
 
 /**
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ascension/text.h	Wed Nov 15 22:51:40 2023 +0100
@@ -0,0 +1,102 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ * Copyright 2023 Mike Becker. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef ASCENSION_TEXT_H
+#define ASCENSION_TEXT_H
+
+#include "font.h"
+#include "datatypes.h"
+#include <cx/string.h>
+
+typedef struct AscTextNode {
+    asc_vec2i position;
+    asc_vec2i dimension;
+    unsigned tex_id;
+} AscTextNode;
+
+
+/**
+ * Draws text on screen.
+ *
+ * The current context ink and font will be used.
+ *
+ * The text must be zero-terminated (use cx_strdup() e.g. to achieve that) and can
+ * be freed after the call.
+ *
+ * @param node a previously created node or
+ * @param position the position where to draw the text
+ * @param max_width the maximum width before breaking the text into new lines
+ * @param text the text to draw (must be zero-terminated!)
+ * @see asc_ink()
+ * @see asc_font()
+ */
+void asc_text_draw_lb(
+        AscTextNode *node,
+        asc_vec2i position,
+        unsigned max_width,
+        cxmutstr text);
+
+/**
+ * Draws text on screen.
+ *
+ * The current context ink and font will be used.
+ *
+ * The text must be zero-terminated (use cx_strdup() e.g. to achieve that) and can
+ * be freed after the call.
+ *
+ * @param node a previously created node or
+ * @param position the position where to draw the text
+ * @param text the text to draw (must be zero-terminated!)
+ * @see asc_ink()
+ * @see asc_font()
+ */
+void asc_text_draw(
+        AscTextNode *node,
+        asc_vec2i position,
+        cxmutstr text);
+
+/**
+ * Just redraw the text node in the current frame.
+ *
+ * Invoke this method, when the text did not change and the texture does not need
+ * to be re-generated. You can change the position of the node. When you change the dimension,
+ * the text will be scaled.
+ *
+ * @param node the text node
+ */
+void asc_text_redraw(AscTextNode *node);
+
+/**
+ * Releases the graphics memory for this node.
+ *
+ * The structure can be reused anytime for other draw calls.
+ *
+ * @param node the text node
+ */
+void asc_text_destroy(AscTextNode *node);
+
+#endif //ASCENSION_TEXT_H
--- a/src/ascension/window.h	Wed Nov 08 23:17:07 2023 +0100
+++ b/src/ascension/window.h	Wed Nov 15 22:51:40 2023 +0100
@@ -31,6 +31,7 @@
 #include <SDL2/SDL.h>
 
 #include "datatypes.h"
+#include "primitives.h"
 
 #ifndef ASC_MAX_WINDOWS
 /** The maximum number of windows that can exist simultaneously. */
@@ -48,21 +49,14 @@
 } AscWindowSettings;
 
 typedef struct AscWindow {
+    Uint32 id;
     SDL_Window* window;
     SDL_GLContext glctx;
-    Uint32 id;
+    AscPrimitives primitives;
     asc_vec2i dimensions;
     asc_mat4f projection;
 } AscWindow;
 
-
-/**
- * Dispatches events and synchronizes all initialized windows.
- *
- * @return false, if the application wants to quit, true otherwise
- */
-bool asc_loop_next(void);
-
 /**
  * Initializes the settings structure with default values.
  *
@@ -73,6 +67,8 @@
 /**
  * Creates and initializes a new window and a corresponding OpenGL context.
  *
+ * The new window will also be automatically activated (see asc_window_activate()).
+ *
  * The index specified must not be in use by another window already.
  * The maximum number of windows is defined by #ASC_MAX_WINDOWS.
  *
@@ -85,6 +81,8 @@
 /**
  * Destroys the window and its OpenGL context.
  *
+ * When this window is currently active, there will be \em no active window afterwards.
+ *
  * Still alive windows will also be destroyed by asc_context_destroy()
  * automatically.
  *
@@ -102,5 +100,15 @@
  */
 void asc_window_sync(AscWindow const *window);
 
+/**
+ * Switches the active window.
+ *
+ * In particular that makes the corresponding OpenGL context "current".
+ * When you only want to draw into one window, you'll never need this.
+ *
+ * @param the window to activate
+ */
+void asc_window_activate(AscWindow const *window);
+
 #endif /* ASCENSION_WINDOW_H */
 
--- a/src/context.c	Wed Nov 08 23:17:07 2023 +0100
+++ b/src/context.c	Wed Nov 15 22:51:40 2023 +0100
@@ -81,3 +81,59 @@
     asc_context.flags = 0;
     asc_dprintf("Ascension context destroyed.");
 }
+
+static void asc_event_window_resized(Uint32 id, Sint32 width, Sint32 height) {
+    for (unsigned int i = 0 ; i < ASC_MAX_WINDOWS ; i++) {
+        if (asc_context.windows[i].id == id) {
+            asc_context.windows[i].dimensions.width = width;
+            asc_context.windows[i].dimensions.height = height;
+            asc_mat4f_ortho(asc_context.windows[i].projection, 0, (float) width, (float) height, 0);
+            return;
+        }
+    }
+}
+
+bool asc_loop_next(void) {
+    // dispatch SDL events
+    SDL_Event event;
+    while (SDL_PollEvent(&event)) {
+        switch (event.type) {
+            case SDL_QUIT:
+                asc_set_flag(&asc_context.flags, ASC_FLAG_QUIT);
+                break;
+            case SDL_WINDOWEVENT: {
+                if (event.window.event == SDL_WINDOWEVENT_RESIZED)
+                    asc_event_window_resized(
+                            event.window.windowID,
+                            event.window.data1,
+                            event.window.data2
+                    );
+                break;
+            }
+            case SDL_KEYDOWN:
+                // TODO: remove this code and implement key press map instead
+                if (event.key.keysym.sym == SDLK_ESCAPE)
+                    return false;
+                break;
+            case SDL_KEYUP:
+                // TODO: implement key press map
+                break;
+        }
+    }
+
+    // sync the windows
+    for (unsigned int i = 0 ; i < ASC_MAX_WINDOWS ; i++) {
+        if (asc_context.windows[i].id > 0) {
+            asc_window_sync(&asc_context.windows[i]);
+        }
+    }
+
+    // compute frame time
+    static Uint32 ticks;
+    Uint32 ticks_elapsed = SDL_GetTicks() - ticks;
+    ticks = SDL_GetTicks();
+    asc_context.elapsed_millis = ticks_elapsed;
+    asc_context.elapsed = (float) ticks_elapsed / 1000.0f;
+
+    return !asc_test_flag(asc_context.flags, ASC_FLAG_QUIT);
+}
--- a/src/error.c	Wed Nov 08 23:17:07 2023 +0100
+++ b/src/error.c	Wed Nov 15 22:51:40 2023 +0100
@@ -30,6 +30,7 @@
 #include "ascension/utils.h"
 
 #include <cx/buffer.h>
+#include <GL/gl.h>
 
 void asc_error_cchar(char const* text) {
     asc_error_cxstr(cx_str(text));
@@ -69,3 +70,37 @@
     cxBufferClear(&asc_context.error_buffer);
     asc_clear_flag(&asc_context.flags, ASC_FLAG_HAS_ERROR);
 }
+
+void asc_error_gl(unsigned code, cxstring message) {
+    const char *glerr;
+    switch(code) {
+        case GL_NO_ERROR:
+            return;
+        case GL_INVALID_ENUM:
+            glerr = "invalid enum";
+            break;
+        case GL_INVALID_VALUE:
+            glerr = "invalid value";
+            break;
+        case GL_INVALID_OPERATION:
+            glerr = "invalid operation";
+            break;
+        case GL_INVALID_FRAMEBUFFER_OPERATION:
+            glerr = "invalid framebuffer operation";
+            break;
+        case GL_OUT_OF_MEMORY:
+            glerr = "out of memory";
+            break;
+        case GL_STACK_UNDERFLOW:
+            glerr = "stack underflow";
+            break;
+        case GL_STACK_OVERFLOW:
+            glerr = "stack overflow";
+            break;
+        default:
+            glerr = "unknown GL error";
+    }
+    cxmutstr msg = cx_strcat(3, message, CX_STR(" GL Error: "), cx_str(glerr));
+    asc_error_cxstr(cx_strcast(msg));
+    cx_strfree(&msg);
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/primitives.c	Wed Nov 15 22:51:40 2023 +0100
@@ -0,0 +1,93 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ * Copyright 2023 Mike Becker. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ascension/primitives.h"
+#include "ascension/error.h"
+#include "ascension/context.h"
+
+#include <string.h>
+#include <GL/glew.h>
+
+static void asc_primitives_init_plane(AscMesh *mesh) {
+    asc_dprintf("Create primitive plane in VBO %u and VAO %u", mesh->vbo, mesh->vao);
+    mesh->vertices = 4;
+    float data[8] = {
+            0.0f, 0.0f, // bottom left
+            0.0f, 1.0f,    // top left
+            1.0f, 0.0f, // bottom right
+            1.0f, 1.0f    // top right
+    };
+    glBindBuffer(GL_ARRAY_BUFFER, mesh->vbo);
+    glBufferData(GL_ARRAY_BUFFER, sizeof(data), data, GL_STATIC_DRAW);
+    glBindVertexArray(mesh->vao);
+    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, NULL);
+    glEnableVertexAttribArray(0);
+}
+
+bool asc_primitives_init(AscPrimitives *primitives) {
+    asc_dprintf("Create primitives for the GL context of active window.");
+    // TODO: more primitives
+
+    GLuint buffers[1];
+    GLuint arrays[1];
+    glGenBuffers(1, buffers);
+    glGenVertexArrays(1, arrays);
+
+    AscMesh *plane = &(primitives->plane);
+    plane->vbo = buffers[0];
+    plane->vao = arrays[0];
+    asc_primitives_init_plane(plane);
+
+    GLenum error = glGetError();
+    if (error == GL_NO_ERROR) {
+        return true;
+    } else {
+        asc_error_gl(error, CX_STR("Initialization of primitive meshes failed."));
+        return false;
+    }
+}
+
+void asc_primitives_destroy(AscPrimitives *primitives) {
+    asc_dprintf("Destroy primitives in GL context of active window.");
+
+    GLuint buffers[1];
+    GLuint arrays[1];
+
+    buffers[0] = primitives->plane.vbo;
+    arrays[0] = primitives->plane.vao;
+
+    glDeleteBuffers(1, buffers);
+    glDeleteVertexArrays(1, arrays);
+
+    memset(primitives, 0, sizeof(AscPrimitives));
+}
+
+void asc_primitives_draw_plane(void) {
+    AscMesh const *mesh = &(asc_context.active_window->primitives.plane);
+    glBindVertexArray(mesh->vao);
+    glDrawArrays(GL_TRIANGLE_STRIP, 0, mesh->vertices);
+}
\ No newline at end of file
--- a/src/shader.c	Wed Nov 08 23:17:07 2023 +0100
+++ b/src/shader.c	Wed Nov 15 22:51:40 2023 +0100
@@ -30,8 +30,9 @@
 #include "ascension/error.h"
 
 #include <GL/glew.h>
+#include <string.h>
 
-AscShaderProgram ASC_SHADER_FONT;
+AscShaderFont ASC_SHADER_FONT;
 
 
 AscShader asc_shader_compile(unsigned int type,
@@ -93,7 +94,12 @@
     glDetachShader(id, fragment.id);
     if (success) {
         asc_dprintf("Shader Program %u linked (vtf: %u, frag: %u)", id, vertex.id, fragment.id);
-        return (AscShaderProgram) {id};
+        AscShaderProgram prog;
+        prog.id = id;
+        prog.model = glGetUniformLocation(id, "model");
+        prog.view = glGetUniformLocation(id, "view");
+        prog.projection = glGetUniformLocation(id, "projection");
+        return prog;
     } else {
         char *log = malloc(1024);
         glGetProgramInfoLog(id, 1024, NULL, log);
@@ -131,9 +137,10 @@
 }
 
 void asc_shader_initialize_predefined(void) {
-    ASC_SHADER_FONT = asc_shader_compile_link_discard("shader/font_vtx.glsl", "shader/font_frag.glsl");
+    ASC_SHADER_FONT.base = asc_shader_compile_link_discard("shader/font_vtx.glsl", "shader/font_frag.glsl");
+    ASC_SHADER_FONT.surface = glGetUniformLocation(ASC_SHADER_FONT.base.id, "surface");
 }
 
 void asc_shader_destroy_predefined(void) {
-    asc_shader_program_destroy(ASC_SHADER_FONT);
+    asc_shader_program_destroy(ASC_SHADER_FONT.base);
 }
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/text.c	Wed Nov 15 22:51:40 2023 +0100
@@ -0,0 +1,129 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ * Copyright 2023 Mike Becker. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ascension/text.h"
+#include "ascension/context.h"
+#include "ascension/error.h"
+#include "ascension/shader.h"
+
+#include <GL/glew.h>
+
+void asc_text_draw_lb(
+        AscTextNode *node,
+        asc_vec2i position,
+        unsigned max_width,
+        cxmutstr text) {
+
+    // Generate new texture, if required
+    if (node->tex_id == 0) {
+        glGenTextures(1, &node->tex_id);
+        glBindTexture(GL_TEXTURE_RECTANGLE, node->tex_id);
+        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+        asc_dprintf("Generated new texture for text node: %u", node->tex_id);
+    }
+
+    // Render text onto a surface
+    SDL_Surface *surface = TTF_RenderText_Blended_Wrapped(
+            asc_context.active_font->ptr,
+            text.ptr,
+            (SDL_Color) {
+                    .r = asc_context.ink.red,
+                    .g = asc_context.ink.green,
+                    .b = asc_context.ink.blue,
+                    .a = asc_context.ink.alpha
+            },
+            max_width
+    );
+    if (surface == NULL) {
+        asc_error(SDL_GetError());
+        return;
+    }
+
+    // Store basic node information
+    node->position = position;
+    node->dimension.width = surface->w;
+    node->dimension.height = surface->h;
+
+    // Transfer Image Data
+    // TODO: move the image data transfer to a separate function - we will need it more often
+    glBindTexture(GL_TEXTURE_RECTANGLE, node->tex_id);
+    glPixelStorei(GL_UNPACK_ROW_LENGTH, surface->pitch / surface->format->BytesPerPixel);
+    glTexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_RGBA,
+                 surface->w, surface->h,
+                 0, GL_BGRA, GL_UNSIGNED_BYTE, surface->pixels);
+
+    // Free the surface
+    SDL_FreeSurface(surface);
+
+    // Redraw the node
+    asc_text_redraw(node);
+}
+
+void asc_text_draw(
+        AscTextNode *node,
+        asc_vec2i position,
+        cxmutstr text) {
+    unsigned max_width = asc_context.active_window->dimensions.width - position.width;
+    asc_text_draw_lb(node, position, max_width, text);
+}
+
+void asc_text_redraw(AscTextNode *node) {
+    if (node->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->dimension.width;
+    model[asc_mat4_index(1, 1)] = node->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->tex_id);
+    glUniform1i(ASC_SHADER_FONT.surface, 0);
+
+    // Draw mesh
+    asc_primitives_draw_plane();
+}
+
+void asc_text_destroy(AscTextNode *node) {
+    asc_dprintf("Release text node texture: %u", node->tex_id);
+    glDeleteTextures(1, &node->tex_id);
+    node->tex_id = 0;
+}
\ No newline at end of file
--- a/src/window.c	Wed Nov 08 23:17:07 2023 +0100
+++ b/src/window.c	Wed Nov 15 22:51:40 2023 +0100
@@ -45,61 +45,11 @@
     if (type == GL_DEBUG_TYPE_ERROR) {
         asc_error(buf.ptr);
     } else {
-        asc_dprintf("GL debug: %*.s", (int)buf.length, buf.ptr);
+        asc_dprintf("GL debug: %.*s", (int)buf.length, buf.ptr);
     }
     cx_strfree(&buf);
 }
 
-
-static void asc_event_window_resized(Uint32 id, Sint32 width, Sint32 height) {
-    for (unsigned int i = 0 ; i < ASC_MAX_WINDOWS ; i++) {
-        if (asc_context.windows[i].id == id) {
-            asc_context.windows[i].dimensions.width = width;
-            asc_context.windows[i].dimensions.height = height;
-            asc_mat4f_ortho(asc_context.windows[i].projection, 0, (float) width, (float) height, 0);
-            return;
-        }
-    }
-}
-
-bool asc_loop_next(void) {
-    // dispatch SDL events
-    SDL_Event event;
-    while (SDL_PollEvent(&event)) {
-        switch (event.type) {
-        case SDL_QUIT:
-            asc_set_flag(&asc_context.flags, ASC_FLAG_QUIT);
-            break;
-        case SDL_WINDOWEVENT: {
-            if (event.window.type == SDL_WINDOWEVENT_RESIZED)
-                asc_event_window_resized(
-                        event.window.windowID,
-                        event.window.data1,
-                        event.window.data2
-                );
-            break;
-        }
-        case SDL_KEYDOWN:
-            // TODO: remove this code and implement key press map instead
-            if (event.key.keysym.sym == SDLK_ESCAPE)
-                return false;
-            break;
-        case SDL_KEYUP:
-            // TODO: implement key press map
-            break;
-        }
-    }
-
-    // sync the windows
-    for (unsigned int i = 0 ; i < ASC_MAX_WINDOWS ; i++) {
-        if (asc_context.windows[i].id > 0) {
-            asc_window_sync(&asc_context.windows[i]);
-        }
-    }
-
-    return !asc_test_flag(asc_context.flags, ASC_FLAG_QUIT);
-}
-
 void asc_window_settings_init_defaults(AscWindowSettings* settings) {
     settings->depth_size = 24;
     settings->vsync = 1;
@@ -170,8 +120,14 @@
             glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
             glEnable(GL_DEBUG_OUTPUT);
             glDebugMessageCallback(asc_gl_debug_callback, NULL);
+
             asc_dprintf("Window %u initialized", window->id);
-            return window;
+            if (asc_primitives_init(&window->primitives)) {
+                asc_context.active_window = window;
+                return window;
+            } else {
+                asc_dprintf("!!! Creating primitives for window %u failed !!!", window->id);
+            }
         } else {
             asc_error(glewGetErrorString(err));
         }
@@ -191,6 +147,15 @@
     // safeguard
     if (window->id == 0) return;
 
+    // this window cannot be active anymore
+    if (asc_context.active_window == window) {
+        asc_context.active_window = NULL;
+    }
+
+    // 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);
+
     // destroy the GL context and the window
     if (window->glctx != NULL) {
         SDL_GL_DeleteContext(window->glctx);
@@ -199,15 +164,32 @@
         SDL_DestroyWindow(window->window);
     }
 
+    // if another window was active, make the other context current again
+    if (asc_context.active_window != NULL) {
+        AscWindow const *aw = asc_context.active_window;
+        SDL_GL_MakeCurrent(aw->window, aw->glctx);
+    }
+
     // clean the data
     asc_dprintf("Window %u and its OpenGL context destroyed.", window->id);
     memset(window, 0, sizeof(AscWindow));
 }
 
 void asc_window_sync(AscWindow const* window) {
-    SDL_GL_MakeCurrent(window->window, window->glctx);
+    AscWindow const *active_window = asc_context.active_window;
+    if (window != active_window) {
+        asc_window_activate(window);
+    }
     SDL_GL_SwapWindow(window->window);
     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);
+    if (window != active_window) {
+        asc_window_activate(active_window);
+    }
 }
+
+void asc_window_activate(AscWindow const *window) {
+    SDL_GL_MakeCurrent(window->window, window->glctx);
+    asc_context.active_window = window;
+}
--- a/test/Makefile	Wed Nov 08 23:17:07 2023 +0100
+++ b/test/Makefile	Wed Nov 15 22:51:40 2023 +0100
@@ -40,8 +40,10 @@
 
 $(BUILD_DIR)/sandbox.o: sandbox.c ../src/ascension/ascension.h \
  ../src/ascension/error.h ../src/ascension/context.h \
- ../src/ascension/window.h ../src/ascension/datatypes.h \
- ../src/ascension/font.h ../src/ascension/shader.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
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
--- a/test/sandbox.c	Wed Nov 08 23:17:07 2023 +0100
+++ b/test/sandbox.c	Wed Nov 15 22:51:40 2023 +0100
@@ -26,6 +26,7 @@
  */
 
 #include <ascension/ascension.h>
+#include <cx/printf.h>
 
 static bool show_message_box_on_error(SDL_Window* window) {
     if (asc_has_error()) {
@@ -50,13 +51,34 @@
 
     AscWindow *window = asc_window_initialize(0, &settings);
     asc_shader_initialize_predefined();
+
+    AscTextNode fps_counter = {0};
+    unsigned last_fps = 0;
+
     while (asc_loop_next()) {
         // quit application on any error
         if (show_message_box_on_error(window->window)) break;
 
 
+        // fps counter
+        if (asc_context.elapsed_millis > 0) {
+            unsigned fps = 1000u / asc_context.elapsed_millis;
+            if (fps != last_fps) {
+                last_fps = fps;
+                asc_set_font(asc_font(ASC_FONT_REGULAR, 24));
+                asc_ink_rgb(255, 0, 0);
+                cxmutstr fpstext = cx_asprintf("%u FPS", fps);
+                asc_text_draw(&fps_counter, (asc_vec2i) {50, 50}, fpstext);
+                cx_strfree(&fpstext);
+            } else {
+                asc_text_redraw(&fps_counter);
+            }
+        }
     }
 
+    // TODO: maybe nodes should also be "garbage collected"
+    asc_text_destroy(&fps_counter);
+
     asc_context_destroy();
     return 0;
 }

mercurial