refactor shader management - resolves #684 default tip

Sun, 08 Jun 2025 14:58:19 +0200

author
Mike Becker <universe@uap-core.de>
date
Sun, 08 Jun 2025 14:58:19 +0200
changeset 139
5d655459db85
parent 138
2ceb0368b02d

refactor shader management - resolves #684

src/Makefile file | annotate | diff | comparison | revisions
src/ascension/2d/sprite.h file | annotate | diff | comparison | revisions
src/ascension/constants.h file | annotate | diff | comparison | revisions
src/ascension/datatypes.h file | annotate | diff | comparison | revisions
src/ascension/glcontext.h file | annotate | diff | comparison | revisions
src/ascension/shader.h file | annotate | diff | comparison | revisions
src/glcontext.c file | annotate | diff | comparison | revisions
src/scene.c file | annotate | diff | comparison | revisions
src/shader.c file | annotate | diff | comparison | revisions
src/sprite.c file | annotate | diff | comparison | revisions
test/snake/Makefile file | annotate | diff | comparison | revisions
--- a/src/Makefile	Sun Jun 08 14:57:54 2025 +0200
+++ b/src/Makefile	Sun Jun 08 14:58:19 2025 +0200
@@ -52,34 +52,24 @@
 
 $(BUILD_DIR)/camera.o: camera.c ascension/error.h ascension/context.h \
  ascension/datatypes.h ascension/window.h ascension/glcontext.h \
- ascension/2d/sprite.h ascension/2d/../scene_node.h \
- ascension/2d/../datatypes.h ascension/2d/../transform.h \
- ascension/2d/../mesh.h ascension/2d/../texture.h \
- ascension/2d/../shader.h ascension/2d/../camera.h ascension/texture.h \
- ascension/scene.h ascension/scene_node.h ascension/camera.h \
- ascension/input.h ascension/ui/font.h ascension/camera.h
+ ascension/scene.h ascension/scene_node.h ascension/transform.h \
+ ascension/camera.h ascension/input.h ascension/ui/font.h \
+ ascension/camera.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
 $(BUILD_DIR)/context.o: context.c ascension/context.h \
  ascension/datatypes.h ascension/window.h ascension/glcontext.h \
- ascension/2d/sprite.h ascension/2d/../scene_node.h \
- ascension/2d/../datatypes.h ascension/2d/../transform.h \
- ascension/2d/../mesh.h ascension/2d/../texture.h \
- ascension/2d/../shader.h ascension/2d/../camera.h ascension/texture.h \
- ascension/scene.h ascension/scene_node.h ascension/camera.h \
- ascension/input.h ascension/ui/font.h ascension/error.h
+ ascension/scene.h ascension/scene_node.h ascension/transform.h \
+ ascension/camera.h ascension/input.h ascension/ui/font.h \
+ ascension/error.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
 $(BUILD_DIR)/error.o: error.c ascension/context.h ascension/datatypes.h \
- ascension/window.h ascension/glcontext.h ascension/2d/sprite.h \
- ascension/2d/../scene_node.h ascension/2d/../datatypes.h \
- ascension/2d/../transform.h ascension/2d/../mesh.h \
- ascension/2d/../texture.h ascension/2d/../shader.h \
- ascension/2d/../camera.h ascension/texture.h ascension/scene.h \
- ascension/scene_node.h ascension/camera.h ascension/input.h \
- ascension/ui/font.h ascension/error.h
+ ascension/window.h ascension/glcontext.h ascension/scene.h \
+ ascension/scene_node.h ascension/transform.h ascension/camera.h \
+ ascension/input.h ascension/ui/font.h ascension/error.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
@@ -89,22 +79,17 @@
 
 $(BUILD_DIR)/font.o: font.c ascension/error.h ascension/context.h \
  ascension/datatypes.h ascension/window.h ascension/glcontext.h \
- ascension/2d/sprite.h ascension/2d/../scene_node.h \
- ascension/2d/../datatypes.h ascension/2d/../transform.h \
- ascension/2d/../mesh.h ascension/2d/../texture.h \
- ascension/2d/../shader.h ascension/2d/../camera.h ascension/texture.h \
- ascension/scene.h ascension/scene_node.h ascension/camera.h \
- ascension/input.h ascension/ui/font.h ascension/filesystem.h \
- ascension/ui/font.h
+ ascension/scene.h ascension/scene_node.h ascension/transform.h \
+ ascension/camera.h ascension/input.h ascension/ui/font.h \
+ ascension/filesystem.h ascension/ui/font.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
 $(BUILD_DIR)/glcontext.o: glcontext.c ascension/glcontext.h \
- ascension/2d/sprite.h ascension/2d/../scene_node.h \
+ ascension/error.h ascension/2d/sprite.h ascension/2d/../scene_node.h \
  ascension/2d/../datatypes.h ascension/2d/../transform.h \
  ascension/2d/../mesh.h ascension/2d/../texture.h \
- ascension/2d/../shader.h ascension/2d/../camera.h ascension/texture.h \
- ascension/error.h ascension/2d/sprite.h
+ ascension/2d/../shader.h ascension/2d/../camera.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
@@ -115,36 +100,28 @@
 
 $(BUILD_DIR)/scene.o: scene.c ascension/error.h ascension/context.h \
  ascension/datatypes.h ascension/window.h ascension/glcontext.h \
+ ascension/scene.h ascension/scene_node.h ascension/transform.h \
+ ascension/camera.h ascension/input.h ascension/ui/font.h \
+ ascension/scene.h ascension/behavior.h ascension/shader.h ascension/2d.h \
  ascension/2d/sprite.h ascension/2d/../scene_node.h \
- ascension/2d/../datatypes.h ascension/2d/../transform.h \
- ascension/2d/../mesh.h ascension/2d/../texture.h \
- ascension/2d/../shader.h ascension/2d/../camera.h ascension/texture.h \
- ascension/scene.h ascension/scene_node.h ascension/camera.h \
- ascension/input.h ascension/ui/font.h ascension/scene.h \
- ascension/behavior.h ascension/shader.h ascension/2d.h
+ ascension/2d/../mesh.h ascension/2d/../datatypes.h \
+ ascension/2d/../texture.h ascension/2d/../shader.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
 $(BUILD_DIR)/scene_node.o: scene_node.c ascension/scene_node.h \
  ascension/datatypes.h ascension/transform.h ascension/context.h \
- ascension/window.h ascension/glcontext.h ascension/2d/sprite.h \
- ascension/2d/../scene_node.h ascension/2d/../mesh.h \
- ascension/2d/../datatypes.h ascension/2d/../texture.h \
- ascension/2d/../shader.h ascension/2d/../camera.h ascension/texture.h \
- ascension/scene.h ascension/scene_node.h ascension/camera.h \
- ascension/input.h ascension/ui/font.h ascension/error.h
+ ascension/window.h ascension/glcontext.h ascension/scene.h \
+ ascension/scene_node.h ascension/camera.h ascension/input.h \
+ ascension/ui/font.h ascension/error.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
 $(BUILD_DIR)/shader.o: shader.c ascension/context.h ascension/datatypes.h \
- ascension/window.h ascension/glcontext.h ascension/2d/sprite.h \
- ascension/2d/../scene_node.h ascension/2d/../datatypes.h \
- ascension/2d/../transform.h ascension/2d/../mesh.h \
- ascension/2d/../texture.h ascension/2d/../shader.h \
- ascension/2d/../camera.h ascension/texture.h ascension/scene.h \
- ascension/scene_node.h ascension/camera.h ascension/input.h \
- ascension/ui/font.h ascension/error.h ascension/shader.h \
- ascension/filesystem.h
+ ascension/window.h ascension/glcontext.h ascension/scene.h \
+ ascension/scene_node.h ascension/transform.h ascension/camera.h \
+ ascension/input.h ascension/ui/font.h ascension/error.h \
+ ascension/shader.h ascension/filesystem.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
@@ -153,45 +130,37 @@
  ascension/2d/../transform.h ascension/2d/../mesh.h \
  ascension/2d/../texture.h ascension/2d/../shader.h \
  ascension/2d/../camera.h ascension/context.h ascension/datatypes.h \
- ascension/window.h ascension/glcontext.h ascension/2d/sprite.h \
- ascension/texture.h ascension/scene.h ascension/scene_node.h \
- ascension/camera.h ascension/input.h ascension/ui/font.h \
- ascension/glcontext.h ascension/error.h ascension/mesh.h
+ ascension/window.h ascension/glcontext.h ascension/scene.h \
+ ascension/scene_node.h ascension/camera.h ascension/input.h \
+ ascension/ui/font.h ascension/glcontext.h ascension/error.h \
+ ascension/mesh.h ascension/constants.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
 $(BUILD_DIR)/text.o: text.c ascension/error.h ascension/context.h \
  ascension/datatypes.h ascension/window.h ascension/glcontext.h \
- ascension/2d/sprite.h ascension/2d/../scene_node.h \
- ascension/2d/../datatypes.h ascension/2d/../transform.h \
- ascension/2d/../mesh.h ascension/2d/../texture.h \
- ascension/2d/../shader.h ascension/2d/../camera.h ascension/texture.h \
- ascension/scene.h ascension/scene_node.h ascension/camera.h \
- ascension/input.h ascension/ui/font.h ascension/ui/text.h \
- ascension/ui/font.h ascension/ui/../2d/sprite.h
+ ascension/scene.h ascension/scene_node.h ascension/transform.h \
+ ascension/camera.h ascension/input.h ascension/ui/font.h \
+ ascension/ui/text.h ascension/ui/font.h ascension/ui/../2d/sprite.h \
+ ascension/ui/../2d/../scene_node.h ascension/ui/../2d/../mesh.h \
+ ascension/ui/../2d/../datatypes.h ascension/ui/../2d/../texture.h \
+ ascension/ui/../2d/../shader.h ascension/ui/../2d/../camera.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
 $(BUILD_DIR)/texture.o: texture.c ascension/error.h ascension/context.h \
  ascension/datatypes.h ascension/window.h ascension/glcontext.h \
- ascension/2d/sprite.h ascension/2d/../scene_node.h \
- ascension/2d/../datatypes.h ascension/2d/../transform.h \
- ascension/2d/../mesh.h ascension/2d/../texture.h \
- ascension/2d/../shader.h ascension/2d/../camera.h ascension/texture.h \
- ascension/scene.h ascension/scene_node.h ascension/camera.h \
- ascension/input.h ascension/ui/font.h ascension/texture.h \
- ascension/filesystem.h
+ ascension/scene.h ascension/scene_node.h ascension/transform.h \
+ ascension/camera.h ascension/input.h ascension/ui/font.h \
+ ascension/texture.h ascension/filesystem.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
 $(BUILD_DIR)/window.o: window.c ascension/error.h ascension/window.h \
- ascension/datatypes.h ascension/glcontext.h ascension/2d/sprite.h \
- ascension/2d/../scene_node.h ascension/2d/../datatypes.h \
- ascension/2d/../transform.h ascension/2d/../mesh.h \
- ascension/2d/../texture.h ascension/2d/../shader.h \
- ascension/2d/../camera.h ascension/texture.h ascension/scene.h \
- ascension/scene_node.h ascension/camera.h ascension/context.h \
- ascension/window.h ascension/input.h ascension/ui/font.h
+ ascension/datatypes.h ascension/glcontext.h ascension/scene.h \
+ ascension/scene_node.h ascension/transform.h ascension/camera.h \
+ ascension/context.h ascension/window.h ascension/input.h \
+ ascension/ui/font.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 
--- a/src/ascension/2d/sprite.h	Sun Jun 08 14:57:54 2025 +0200
+++ b/src/ascension/2d/sprite.h	Sun Jun 08 14:58:19 2025 +0200
@@ -71,23 +71,20 @@
 
 void asc_sprite_set_size(AscSceneNode *node, unsigned width, unsigned height);
 
-
-
-
-typedef struct AscShaderSprite {
-    AscShaderProgram program;
-    int tex;
-} AscShaderSprite;
+void asc_sprite_draw(const AscShaderProgram *shader, const AscSprite *node);
 
 /**
- * Loads and initializes the sprite shader.
+ * Returns a shader program that can draw sprites with rectangle textures.
  *
- * @param sprite the structure to initialize
- * @param rect true if the version for rectangular textures shall be compiled
- * @return zero on success, non-zero on failure
+ * @return the shader program
  */
-int asc_shader_sprite_init(AscShaderSprite *sprite, bool rect);
+const AscShaderProgram *asc_sprite_shader_rect(void);
 
-void asc_sprite_draw(const AscShaderSprite *shader, const AscSprite *node);
+/**
+ * Returns a shader program that can draw sprites with 2D textures.
+ *
+ * @return the shader program
+ */
+const AscShaderProgram *asc_sprite_shader_uv(void);
 
 #endif //ASCENSION_SPRITE_H
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ascension/constants.h	Sun Jun 08 14:58:19 2025 +0200
@@ -0,0 +1,42 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ * Copyright 2025 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 ASC_CONSTANTS_H
+#define ASC_CONSTANTS_H
+
+// --------------------------------------
+// Internally used shader IDs.
+// --------------------------------------
+
+#define ASC_SHADER_INTERNAL_ID(id)  (1000000000u+id)
+
+#define ASC_SHADER_SPRITE_RECT   ASC_SHADER_INTERNAL_ID(1)
+#define ASC_SHADER_SPRITE_UV     ASC_SHADER_INTERNAL_ID(2)
+
+
+
+#endif // ASC_CONSTANTS_H
--- a/src/ascension/datatypes.h	Sun Jun 08 14:57:54 2025 +0200
+++ b/src/ascension/datatypes.h	Sun Jun 08 14:58:19 2025 +0200
@@ -59,6 +59,8 @@
 #define asc_set_flag(reg, flag) (reg |= flag)
 #define asc_set_flag_masked(reg, mask, flag) (reg = (reg & ~(mask)) | flag)
 
+#define asc_ptr_cast(type, lvalue, rvalue)  type *lvalue = (type *)(rvalue);
+
 // --------------------------------------------------------------------------
 //    Datatype Definitions
 // --------------------------------------------------------------------------
--- a/src/ascension/glcontext.h	Sun Jun 08 14:57:54 2025 +0200
+++ b/src/ascension/glcontext.h	Sun Jun 08 14:58:19 2025 +0200
@@ -32,9 +32,6 @@
 
 #include <cx/list.h>
 
-#include "2d/sprite.h" // TODO: this shouldn't be included here
-#include "texture.h"
-
 typedef struct AscGLContextSettings {
     int gl_major_version;
     int gl_minor_version;
@@ -42,32 +39,22 @@
     int depth_size;
 } AscGLContextSettings;
 
-enum AscDefaultTextures2d {
-    ASC_TEXTURE_2D_EMPTY_1X1_IDX = 0,
-    ASC_TEXTURE_2D_COUNT
-};
-
-enum AscDefaultTexturesRect {
-    ASC_TEXTURE_RECT_EMPTY_1X1_IDX = 0,
-    ASC_TEXTURE_RECT_COUNT
-};
-
 typedef struct AscGLContext {
     SDL_Window *window;
     SDL_GLContext glctx;
+    /**
+     * List of registered cleanup functions.
+     * Use the API to register functions, don't access manually.
+     */
     CxList *cleanup_funcs;
-    // TODO: replace with something similar to the font cache
-    struct {
-        AscShaderSprite sprite_rect;
-        AscShaderSprite sprite_uv;
-    } shader;
+    /**
+     * List of pointers to AscShaderProgram.
+     */
+    CxList *shaders;
 } AscGLContext;
 
 #define asc_active_glctx (&asc_active_window->glctx)
 
-#define ASC_SHADER_SPRITE_RECT (&asc_active_glctx->shader.sprite_rect)
-#define ASC_SHADER_SPRITE_UV (&asc_active_glctx->shader.sprite_uv)
-
 bool asc_gl_context_initialize(
         AscGLContext *ctx,
         SDL_Window *window,
--- a/src/ascension/shader.h	Sun Jun 08 14:57:54 2025 +0200
+++ b/src/ascension/shader.h	Sun Jun 08 14:58:19 2025 +0200
@@ -72,12 +72,23 @@
 
 typedef struct AscShaderProgram {
     unsigned int id;
+    unsigned int gl_id;
     int model;
     int view;
     int projection;
+    cx_destructor_func destr_func;
 } AscShaderProgram;
 
 /**
+ * The maximum ID for a user defined shader.
+ *
+ * The IDs above this number are reserved for the engine.
+ */
+#define ASC_SHADER_ID_USER_MAX  1'000'000'000u
+
+typedef AscShaderProgram*(*asc_shader_create_func)(void);
+
+/**
  * Loads shader codes from files.
  *
  * @param info the structure containing the file names and preprocessing info
@@ -96,18 +107,27 @@
 /**
  * Creates a shader program.
  *
+ * @note This function intentionally has an unspecified pointer return type
+ * so that you can assign the return value to your shader struct pointer without casts.
+ *
  * @param codes the (zero-terminated) source codes
- * @return the compiled and linked shader program
+ * @param mem_size the memory size required for the structure
+ * @return the compiled and linked shader program with @c AscShaderProgram as base struct
  */
-AscShaderProgram asc_shader_program_create(AscShaderCodes codes);
+void *asc_shader_create(AscShaderCodes codes, size_t mem_size);
 
 /**
- * Destroys a shader program.
+ * Frees a shader program.
  *
  * @param program the program
  */
-void asc_shader_program_destroy(AscShaderProgram *program);
+void asc_shader_free(AscShaderProgram *program);
+
+void asc_shader_use(const AscShaderProgram *shader, const AscCamera *camera);
 
-void asc_shader_program_use(const AscShaderProgram *shader, const AscCamera *camera);
+
+AscShaderProgram *asc_shader_register(unsigned int id, asc_shader_create_func create_func);
+AscShaderProgram *asc_shader_lookup(unsigned int id);
+AscShaderProgram *asc_shader_lookup_or_create(unsigned int id, asc_shader_create_func create_func);
 
 #endif //ASCENSION_SHADER_H
--- a/src/glcontext.c	Sun Jun 08 14:57:54 2025 +0200
+++ b/src/glcontext.c	Sun Jun 08 14:58:19 2025 +0200
@@ -47,20 +47,6 @@
     }
 }
 
-static int asc_shader_initialize_predefined(AscGLContext *ctx) {
-    // TODO: check if we can replace that by lazy loading shaders just like fonts
-    int ret = 0;
-    ret |= asc_shader_sprite_init(&ctx->shader.sprite_uv, false);
-    ret |= asc_shader_sprite_init(&ctx->shader.sprite_rect, true);
-    ret |= asc_error_catch_all_gl();
-    return ret;
-}
-
-static void asc_shader_destroy_predefined(AscGLContext *ctx) {
-    asc_shader_program_destroy(&ctx->shader.sprite_uv.program);
-    asc_shader_program_destroy(&ctx->shader.sprite_rect.program);
-}
-
 struct asc_gl_context_cleanup_data {
     int type;
     union {
@@ -109,15 +95,14 @@
         glEnable(GL_DEBUG_OUTPUT);
         glDebugMessageCallback(asc_gl_debug_callback, NULL);
 
-        if (asc_shader_initialize_predefined(ctx)) {
-            asc_error("Initializing predefined shaders failed");
-            SDL_GL_DeleteContext(ctx->glctx);
-            return false;
-        }
-
+        // Create the cleanup functions array
         ctx->cleanup_funcs = cxArrayListCreateSimple(sizeof(struct asc_gl_context_cleanup_data), 8);
         cxDefineDestructor(ctx->cleanup_funcs, asc_gl_context_cleanup);
 
+        // Create the shaders array
+        ctx->shaders = cxArrayListCreateSimple(CX_STORE_POINTERS, 32);
+        cxDefineDestructor(ctx->shaders, asc_shader_free);
+
         return true;
     } else {
         asc_error("glewInit failed: %s", glewGetErrorString(err));
@@ -130,8 +115,8 @@
     if (ctx->glctx == NULL) return;
     SDL_GL_MakeCurrent(ctx->window, ctx->glctx);
 
+    cxListFree(ctx->shaders);
     cxListFree(ctx->cleanup_funcs);
-    asc_shader_destroy_predefined(ctx);
 
     // destroy the GL context and the window
     SDL_GL_DeleteContext(ctx->glctx);
--- a/src/scene.c	Sun Jun 08 14:57:54 2025 +0200
+++ b/src/scene.c	Sun Jun 08 14:58:19 2025 +0200
@@ -96,16 +96,16 @@
     // TODO: implement interleaving by depth
     if (cxIteratorValid(iter_opaque_rect)) {
         glDisable(GL_BLEND);
-        AscShaderSprite *shader = ASC_SHADER_SPRITE_RECT;
-        asc_shader_program_use(&shader->program, &scene->camera);
+        const AscShaderProgram *shader = asc_sprite_shader_rect();
+        asc_shader_use(shader, &scene->camera);
         cx_foreach(const AscSprite*, node, iter_opaque_rect) {
             asc_sprite_draw(shader, node);
         }
     }
     if (cxIteratorValid(iter_opaque_uv)) {
         glDisable(GL_BLEND);
-        AscShaderSprite *shader = ASC_SHADER_SPRITE_UV;
-        asc_shader_program_use(&shader->program, &scene->camera);
+        const AscShaderProgram *shader = asc_sprite_shader_uv();
+        asc_shader_use(shader, &scene->camera);
         cx_foreach(const AscSprite*, node, iter_opaque_uv) {
             asc_sprite_draw(shader, node);
         }
@@ -113,8 +113,8 @@
     if (cxIteratorValid(iter_blend_rect)) {
         glEnable(GL_BLEND);
         glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
-        AscShaderSprite *shader = ASC_SHADER_SPRITE_RECT;
-        asc_shader_program_use(&shader->program, &scene->camera);
+        const AscShaderProgram *shader = asc_sprite_shader_rect();
+        asc_shader_use(shader, &scene->camera);
         cx_foreach(const AscSprite*, node, iter_blend_rect) {
             asc_sprite_draw(shader, node);
         }
@@ -122,8 +122,8 @@
     if (cxIteratorValid(iter_blend_uv)) {
         glEnable(GL_BLEND);
         glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
-        AscShaderSprite *shader = ASC_SHADER_SPRITE_UV;
-        asc_shader_program_use(&shader->program, &scene->camera);
+        const AscShaderProgram *shader = asc_sprite_shader_uv();
+        asc_shader_use(shader, &scene->camera);
         cx_foreach(const AscSprite*, node, iter_blend_uv) {
             asc_sprite_draw(shader, node);
         }
--- a/src/shader.c	Sun Jun 08 14:57:54 2025 +0200
+++ b/src/shader.c	Sun Jun 08 14:58:19 2025 +0200
@@ -87,14 +87,16 @@
  *
  * @param shader the shader IDs to link
  * @param n the number of shaders
- * @return a compiled program
+ * @param prog the struct where to store the result
+ * @retval zero success
+ * @retval non-zero failure
  */
-static AscShaderProgram asc_shader_link(unsigned shader[], unsigned n) {
+static int asc_shader_link(unsigned shader[], unsigned n, AscShaderProgram *prog) {
     GLint success;
     GLint id = glCreateProgram();
     if (id <= 0) {
         asc_error("glCreateProgram failed: %s", glGetError());
-        return (AscShaderProgram) {0};
+        return -1;
     }
     for (unsigned i = 0; i < n; i++) {
         glAttachShader(id, shader[i]);
@@ -106,33 +108,36 @@
     }
     if (success) {
         asc_dprintf("Shader Program %u linked.", id);
-        AscShaderProgram prog;
-        prog.id = id;
+        prog->gl_id = id;
         // by convention every shader shall have MVP matrices
-        prog.model = glGetUniformLocation(id, "model");
-        prog.view = glGetUniformLocation(id, "view");
-        prog.projection = glGetUniformLocation(id, "projection");
-        return prog;
+        prog->model = glGetUniformLocation(id, "model");
+        prog->view = glGetUniformLocation(id, "view");
+        prog->projection = glGetUniformLocation(id, "projection");
+        return 0;
     } else {
         char *log = malloc(1024);
         glGetProgramInfoLog(id, 1024, NULL, log);
         glDeleteProgram(id);
         asc_error("Linking shader program %u failed.\n%s", id, log);
         free(log);
-        return (AscShaderProgram) {0};
+        return -1;
     }
 }
 
-void asc_shader_program_destroy(AscShaderProgram *program) {
-    if (program->id > 0) {
-        asc_dprintf("Delete Shader Program %u", program->id);
-        glDeleteProgram(program->id);
+void asc_shader_free(AscShaderProgram *program) {
+    if (program->gl_id > 0) {
+        asc_dprintf("Delete Shader Program %u", program->gl_id);
+        glDeleteProgram(program->gl_id);
     }
-    program->id = 0;
+    if (program->destr_func) {
+        program->destr_func(program);
+    }
+    cxFreeDefault(program);
 }
 
-AscShaderProgram asc_shader_program_create(AscShaderCodes codes) {
-    unsigned shader[4];
+void *asc_shader_create(AscShaderCodes codes, size_t mem_size) {
+    AscShaderProgram *prog = cxZallocDefault(mem_size);
+    unsigned shader[2];
     unsigned n = 0;
     if (codes.vtx) {
         shader[n++] = asc_shader_compile(GL_VERTEX_SHADER, codes.vtx, codes.vtx_pp);
@@ -140,15 +145,19 @@
     if (codes.frag) {
         shader[n++] = asc_shader_compile(GL_FRAGMENT_SHADER, codes.frag, codes.frag_pp);
     }
-    const AscShaderProgram prog = asc_shader_link(shader, n);
+    if (asc_shader_link(shader, n, prog)) {
+        cxFreeDefault(prog);
+        prog = NULL;
+    }
     for (unsigned i = 0; i < n; i++) {
+        asc_dprintf("Delete shader: %u", shader[i]);
         glDeleteShader(shader[i]);
     }
     return prog;
 }
 
-void asc_shader_program_use(const AscShaderProgram *shader, const AscCamera *camera) {
-    glUseProgram(shader->id);
+void asc_shader_use(const AscShaderProgram *shader, const AscCamera *camera) {
+    glUseProgram(shader->gl_id);
     glUniformMatrix4fv(shader->projection, 1, GL_FALSE, camera->projection);
     glUniformMatrix4fv(shader->view, 1, GL_FALSE, camera->view);
 }
@@ -185,3 +194,36 @@
     cxFreeDefault(codes.vtx);
     cxFreeDefault(codes.frag);
 }
+
+AscShaderProgram *asc_shader_register(unsigned int id, asc_shader_create_func create_func) {
+    AscGLContext *glctx = asc_active_glctx;
+    AscShaderProgram *prog = NULL;
+#ifndef NDEBUG
+    prog = asc_shader_lookup(id);
+    if (prog != NULL) {
+        asc_error("Shader program %u already exists. This is a bug!", id);
+        // still return it, so that the caller does not die immediately
+        return prog;
+    }
+#endif
+    prog = create_func();
+    prog->id = id;
+    cxListAdd(glctx->shaders, prog);
+    return prog;
+}
+
+AscShaderProgram *asc_shader_lookup(unsigned int id) {
+    CxIterator iter = cxListIterator(asc_active_glctx->shaders);
+    cx_foreach(AscShaderProgram *, prog, iter) {
+        if (prog->id == id) return prog;
+    }
+    return NULL;
+}
+
+AscShaderProgram *asc_shader_lookup_or_create(unsigned int id, asc_shader_create_func create_func) {
+    AscShaderProgram *prog = asc_shader_lookup(id);
+    if (prog == NULL) {
+        return asc_shader_register(id, create_func);
+    }
+    return prog;
+}
\ No newline at end of file
--- a/src/sprite.c	Sun Jun 08 14:57:54 2025 +0200
+++ b/src/sprite.c	Sun Jun 08 14:58:19 2025 +0200
@@ -29,11 +29,48 @@
 
 #include "ascension/context.h"
 #include "ascension/glcontext.h"
+#include "ascension/error.h"
+#include "ascension/mesh.h"
+#include "ascension/constants.h"
 
 #include <GL/glew.h>
 
-#include "ascension/error.h"
-#include "ascension/mesh.h"
+
+struct asc_sprite_shader_s {
+    AscShaderProgram program;
+    int tex;
+};
+
+static void *asc_sprite_shader_create(bool rect) {
+    AscShaderCodes codes;
+    if (asc_shader_load_code_files((AscShaderCodeInfo){
+        .files.vtx = "sprite_vtx.glsl",
+        .files.frag = "sprite_frag.glsl",
+        .defines.frag = rect ? "#define USE_RECT" : NULL,
+    }, &codes)) {
+        asc_error("Loading sprite shader failed.");
+        return NULL;
+    }
+    struct asc_sprite_shader_s *shader = asc_shader_create(codes, sizeof(*shader));
+    if (asc_has_error()) {
+        asc_shader_free_codes(codes);
+        return NULL;
+    }
+    shader->tex = glGetUniformLocation(shader->program.gl_id, "tex");
+    asc_shader_free_codes(codes);
+
+    asc_error_catch_all_gl();
+
+    return shader;
+}
+
+static AscShaderProgram *asc_sprite_shader_rect_create() {
+    return asc_sprite_shader_create(true);
+}
+
+static AscShaderProgram *asc_sprite_shader_uv_create() {
+    return asc_sprite_shader_create(false);
+}
 
 static void asc_sprite_destroy(AscSceneNode *node) {
     AscSprite *sprite = (AscSprite *) node;
@@ -88,7 +125,17 @@
     return node;
 }
 
-void asc_sprite_draw(const AscShaderSprite *shader, const AscSprite *node) {
+const AscShaderProgram *asc_sprite_shader_rect(void) {
+    return asc_shader_lookup_or_create(ASC_SHADER_SPRITE_RECT, asc_sprite_shader_rect_create);
+}
+
+const AscShaderProgram *asc_sprite_shader_uv(void) {
+    return asc_shader_lookup_or_create(ASC_SHADER_SPRITE_UV, asc_sprite_shader_uv_create);
+}
+
+void asc_sprite_draw(const AscShaderProgram *program, const AscSprite *node) {
+    asc_ptr_cast(const struct asc_sprite_shader_s, shader, program);
+
     // Upload model matrix
     glUniformMatrix4fv(shader->program.model, 1,
                        GL_FALSE, node->data.world_transform);
@@ -107,23 +154,3 @@
     asc_node_update(node);
 }
 
-int asc_shader_sprite_init(AscShaderSprite *sprite, bool rect) {
-    AscShaderCodes codes;
-    if (asc_shader_load_code_files((AscShaderCodeInfo){
-        .files.vtx = "sprite_vtx.glsl",
-        .files.frag = "sprite_frag.glsl",
-        .defines.frag = rect ? "#define USE_RECT" : NULL,
-    }, &codes)) {
-        asc_error("Loading sprite shader failed.");
-        return 1;
-    }
-    sprite->program = asc_shader_program_create(codes);
-    if (asc_has_error()) {
-        asc_shader_free_codes(codes);
-        return 1;
-    }
-    sprite->tex = glGetUniformLocation(sprite->program.id, "tex");
-    asc_shader_free_codes(codes);
-
-    return asc_error_catch_all_gl();
-}
--- a/test/snake/Makefile	Sun Jun 08 14:57:54 2025 +0200
+++ b/test/snake/Makefile	Sun Jun 08 14:58:19 2025 +0200
@@ -47,17 +47,18 @@
 $(BUILD_DIR)/snake.o: snake.c ../../src/ascension/core.h \
  ../../src/ascension/error.h ../../src/ascension/context.h \
  ../../src/ascension/datatypes.h ../../src/ascension/window.h \
- ../../src/ascension/glcontext.h ../../src/ascension/2d/sprite.h \
- ../../src/ascension/2d/../scene_node.h \
- ../../src/ascension/2d/../datatypes.h \
- ../../src/ascension/2d/../transform.h ../../src/ascension/2d/../mesh.h \
- ../../src/ascension/2d/../texture.h ../../src/ascension/2d/../shader.h \
- ../../src/ascension/2d/../camera.h ../../src/ascension/texture.h \
- ../../src/ascension/scene.h ../../src/ascension/scene_node.h \
+ ../../src/ascension/glcontext.h ../../src/ascension/scene.h \
+ ../../src/ascension/scene_node.h ../../src/ascension/transform.h \
  ../../src/ascension/camera.h ../../src/ascension/input.h \
  ../../src/ascension/ui/font.h ../../src/ascension/behavior.h \
  ../../src/ascension/ui.h ../../src/ascension/ui/text.h \
- ../../src/ascension/ui/font.h ../../src/ascension/ui/../2d/sprite.h
+ ../../src/ascension/ui/font.h ../../src/ascension/ui/../2d/sprite.h \
+ ../../src/ascension/ui/../2d/../scene_node.h \
+ ../../src/ascension/ui/../2d/../mesh.h \
+ ../../src/ascension/ui/../2d/../datatypes.h \
+ ../../src/ascension/ui/../2d/../texture.h \
+ ../../src/ascension/ui/../2d/../shader.h \
+ ../../src/ascension/ui/../2d/../camera.h
 	@echo "Compiling $<"
 	$(CC) -o $@ $(CFLAGS) -c $<
 

mercurial