diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..0790721
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,22 @@
+# SPDX-License-Identifier: GPL-2.0
+---
+BasedOnStyle: LLVM
+UseTab: ForIndentation
+IndentWidth: 8
+TabWidth: 8
+ContinuationIndentWidth: 8
+ColumnLimit: 80
+AlignConsecutiveDeclarations: AcrossComments
+AlignTrailingComments: true
+SpacesBeforeTrailingComments: 1
+PointerAlignment: Left
+SpaceBeforeParens: ControlStatements
+BreakBeforeBraces: Custom
+BraceWrapping:
+ AfterStruct: false
+ AfterEnum: false
+ AfterFunction: false
+ AfterControlStatement: false
+SortIncludes: true
+ReflowComments: true
+...
diff --git a/.gitignore b/.gitignore
index 4fa1c75..5027b01 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ CTestTestfile.cmake
_deps
CMakeUserPresets.json
+docs/
diff --git a/Doxyfile b/Doxyfile
new file mode 100644
index 0000000..ffc2e67
--- /dev/null
+++ b/Doxyfile
@@ -0,0 +1,15 @@
+PROJECT_NAME = "SKR"
+PROJECT_BRIEF = "Single-header 3D graphics engine"
+OUTPUT_DIRECTORY = docs
+
+INPUT = include skr
+FILE_PATTERNS = *.h *.c
+RECURSIVE = YES
+
+GENERATE_HTML = YES
+GENERATE_LATEX = NO
+GENERATE_MAN = NO
+GENERATE_RTF = NO
+
+QUIET = NO
+EXTRACT_ALL = YES
diff --git a/skr/skr.h b/skr/skr.h
new file mode 100644
index 0000000..a7bce53
--- /dev/null
+++ b/skr/skr.h
@@ -0,0 +1,964 @@
+/**
+ * @file skr.h
+ *
+ * Single-header 3D graphics engine
+ *
+ * @copyright (C) 2025 SKR Authors
+ *
+ * @license SPDX-License-Identifier: GPL-3.0-or-later (see LICENSE).
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later
+ * version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see .
+ */
+
+#ifndef SKR_H
+#define SKR_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @internal
+ * @brief Size of the global error buffer (in bytes).
+ *
+ * This constant defines the maximum length of the error string stored in
+ * `SKR_LAST_ERROR`, including the null terminator.
+ */
+#ifndef SKR_LAST_ERROR_SIZE
+#define SKR_LAST_ERROR_SIZE 1044
+#endif
+
+/**
+ * @brief Global error buffer.
+ *
+ * Holds the most recent error message set by the error handling system. Its
+ * size is fixed at `SKR_LAST_ERROR_SIZE`.
+ *
+ * @details
+ * If the buffer is empty (first character == '\0'),
+ * then no error is currently set.
+ *
+ * @see SKR_LAST_ERROR_CLEAR
+ * @see SKR_LAST_ERROR_SET
+ * @see SKR_OK
+ */
+static char SKR_LAST_ERROR[SKR_LAST_ERROR_SIZE];
+
+/**
+ * @internal
+ * @brief Clears the global error buffer.
+ *
+ * Sets the first character of `SKR_LAST_ERROR` to '\0', effectively removing
+ * any previously stored error message.
+ *
+ * @usage
+ * SKR_LAST_ERROR_CLEAR();
+ */
+#define SKR_LAST_ERROR_CLEAR() (SKR_LAST_ERROR[0] = '\0')
+
+/**
+ * @internal
+ * @brief Sets the global error message.
+ *
+ * Formats and writes a message into `SKR_LAST_ERROR` using `snprintf()`
+ * semantics. Any previous error is overwritten.
+ *
+ * @param fmt Format string (like printf).
+ * @param ... Optional arguments for the format string.
+ *
+ * @details
+ * This macro stores the last error encountered by the application in a global
+ * buffer of size `SKR_LAST_ERROR_SIZE`. It is safe and bounded by the buffer
+ * size.
+ *
+ * @usage
+ * if (!init_graphics()) {
+ * SKR_LAST_ERROR_SET("Graphics init failed: %s", reason);
+ * return false;
+ * }
+ */
+#define SKR_LAST_ERROR_SET(fmt, ...) \
+ snprintf(SKR_LAST_ERROR, SKR_LAST_ERROR_SIZE, fmt, ##__VA_ARGS__)
+
+/**
+ * @brief Checks if there is no error set.
+ *
+ * Returns true (nonzero) if `SKR_LAST_ERROR` is empty, false (0) otherwise.
+ *
+ * @return int 1 if no error, 0 if an error exists.
+ *
+ * @usage
+ * if (!SKR_OK()) {
+ * fprintf(stderr, "Error: %s\n", SKR_LAST_ERROR);
+ * }
+ */
+#define SKR_OK() (SKR_LAST_ERROR[0] == '\0')
+
+/**
+ * @brief Maximum number of bone influences per vertex.
+ *
+ * Each vertex can be affected by up to this many bones during skeletal
+ * animation. Commonly set to 4, since most real-time rendering pipelines
+ * balance flexibility with performance by limiting to four weights per vertex.
+ */
+#define MAX_BONE_INFLUENCE 4
+
+/**
+ * @brief Identifies the type of window backend in use.
+ */
+typedef enum SkrWindowBackendType {
+ SkrGLFW, /*!< Window created using GLFW. */
+ SkrSDL /*!< Window created using SDL. */
+} SkrWindowBackendType;
+
+/**
+ * @brief Tagged union that wraps a backend window handle.
+ *
+ * Contains both the backend type (`Tag`) and the backend-specific handle
+ * (`Handler`). Only the member corresponding to the active backend type is
+ * valid.
+ */
+typedef struct SkrWindowBackend {
+ const SkrWindowBackendType Type; /*!< Backend type. */
+
+ union {
+ struct GLFWwindow* GLFW; /*!< Handle to a GLFW window. */
+ /* TODO: more backends */
+ } Handler; /*!< Backend-specific handle. */
+} SkrWindowBackend;
+
+struct SkrWindow;
+
+/**
+ * @brief Function type for input event callbacks.
+ */
+typedef void SkrInputHandler(struct SkrWindow* w);
+
+/**
+ * @brief Generic engine window.
+ *
+ * A portable window structure that abstracts over different windowing backends.
+ * Encapsulates title, size, optional input handler, and backend data.
+ */
+typedef struct SkrWindow {
+ char* Title; /*!< Window title (UTF-8 string). */
+ int Width; /*!< Width of the window in pixels. */
+ int Height; /*!< Height of the window in pixels. */
+
+ SkrInputHandler* InputHandler; /*!< Pointer to input handler. */
+ SkrWindowBackend Backend; /*!< Backend type and handle. */
+} SkrWindow;
+
+/**
+ * @brief Shader object definition.
+ *
+ * Represents a GPU shader in the engine. A shader can be defined either
+ * directly from source code in memory or by referencing a file path.
+ */
+typedef struct SkrShader {
+ /**
+ * @brief Shader type.
+ *
+ * OpenGL shader type enum (e.g. GL_VERTEX_SHADER, GL_FRAGMENT_SHADER).
+ */
+ const unsigned int Type;
+
+ /**
+ * @brief GLSL source code (optional).
+ *
+ * If provided, the shader will be compiled directly from this string in
+ * memory. May be NULL if the shader is loaded from a file.
+ */
+ const char* Source;
+
+ /**
+ * @brief Path to shader file (optional).
+ *
+ * If provided, the shader source will be loaded from this file. May be
+ * NULL if the shader is provided directly via @ref Source.
+ */
+ const char* Path;
+} SkrShader;
+
+/**
+ * @brief Vertex structure used by the rendering engine.
+ *
+ * Encapsulates all per-vertex attributes commonly required in 3D rendering,
+ * including position, normals, texture coordinates, tangent space, and skeletal
+ * animation data.
+ */
+typedef struct SkrVertex {
+ /**
+ * @brief Vertex position in object space.
+ *
+ * Three-component vector (x, y, z).
+ */
+ float Position[3];
+
+ /**
+ * @brief Vertex normal vector.
+ *
+ * Used for lighting calculations. Three components (x, y, z).
+ */
+ float Normal[3];
+
+ /**
+ * @brief Texture coordinates (UV).
+ *
+ * Two-component vector (u, v), typically in the range [0, 1].
+ */
+ float UV[2];
+
+ /**
+ * @brief Tangent vector.
+ *
+ * Defines the direction of increasing U in the tangent space.
+ * Used for normal mapping. Three components (x, y, z).
+ */
+ float Tangent[3];
+
+ /**
+ * @brief Bitangent vector.
+ *
+ * Defines the direction of increasing V in the tangent space.
+ * Orthogonal to both the normal and tangent. Three components (x, y,
+ * z).
+ */
+ float Bitangent[3];
+
+ /**
+ * @brief Indices of influencing bones.
+ *
+ * Array of up to @ref MAX_BONE_INFLUENCE integers that reference bones
+ * in the skeleton.
+ * Used for skeletal animation.
+ */
+ int BoneIDs[MAX_BONE_INFLUENCE];
+
+ /**
+ * @brief Weights of influencing bones.
+ *
+ * Parallel array to @ref BoneIDs, with the corresponding influence
+ * weights.
+ * Values typically normalized so they sum to 1.0.
+ */
+ int BoneWeights[MAX_BONE_INFLUENCE];
+} SkrVertex;
+
+/**
+ * @brief Supported texture roles.
+ *
+ * Defines the semantic role of a texture in a material or shader.
+ */
+typedef enum SkrTextureType {
+ SKR_TEXTURE_DIFFUSE, /*!< Base color / albedo map. */
+ SKR_TEXTURE_SPECULAR, /*!< Specular intensity map. */
+ SKR_TEXTURE_NORMAL, /*!< Normal map (tangent-space). */
+ SKR_TEXTURE_HEIGHT, /*!< Height/displacement map. */
+ SKR_TEXTURE_EMISSIVE, /*!< Emissive (glow) map. */
+ SKR_TEXTURE_AMBIENT, /*!< Ambient occlusion map. */
+ SKR_TEXTURE_METALLIC, /*!< Metallic map (PBR). */
+ SKR_TEXTURE_ROUGHNESS, /*!< Roughness map (PBR). */
+ SKR_TEXTURE_REFLECTION, /*!< Reflection/environment map. */
+ SKR_TEXTURE_UNKNOWN /*!< Unknown/unsupported type. */
+} SkrTextureType;
+
+/**
+ * @brief Texture object used by the rendering engine.
+ *
+ * Encapsulates GPU texture data and the raw image source from which it was
+ * created. The texture may represent diffuse color, normals, specular, or other
+ * material properties (see @ref SkrTextureType).
+ */
+typedef struct SkrTexture {
+ /**
+ * @brief OpenGL texture object ID.
+ *
+ * Assigned by glGenTextures() and used to bind this texture to the GPU.
+ */
+ unsigned int ID;
+
+ /**
+ * @brief Texture semantic type.
+ *
+ * Indicates the role this texture plays in a material/shader.
+ * See @ref SkrTextureType for the list of supported values.
+ */
+ SkrTextureType Type;
+
+ /**
+ * @brief Raw image data in memory.
+ *
+ * Pointer to pixel data (e.g. from `stbi_load`). Used to upload the
+ * texture to the GPU. The format (channels, bit depth, layout) depends
+ * on the loader. May be NULL once the texture has been uploaded and the
+ * CPU copy freed.
+ */
+ char* Source;
+
+ /**
+ * @brief Filesystem path to the texture image.
+ *
+ * Original file location of the texture (e.g. "assets/diffuse.png").
+ */
+ char* Path;
+} SkrTexture;
+
+/**
+ * @brief Renderable mesh data.
+ *
+ * Represents a single drawable mesh in the engine. A mesh contains its own
+ * GPU buffer objects (VAO, VBO, EBO) and CPU-side data for vertices, indices,
+ * and associated textures.
+ */
+typedef struct SkrMesh {
+ /**
+ * @brief Vertex Array Object (VAO).
+ *
+ * Stores the vertex attribute configuration and references to VBO/EBO.
+ */
+ unsigned int VAO;
+
+ /**
+ * @brief Vertex Buffer Object (VBO).
+ *
+ * Stores vertex data (positions, normals, UVs, etc.) in GPU memory.
+ */
+ unsigned int VBO;
+
+ /**
+ * @brief Element Buffer Object (EBO).
+ *
+ * Stores mesh indices in GPU memory for indexed drawing.
+ */
+ unsigned int EBO;
+
+ /**
+ * @brief Vertex data.
+ *
+ * Pointer to an array of @ref SkrVertex structs stored on the CPU.
+ * Uploaded to GPU via the VBO. May be freed after upload if not needed.
+ */
+ SkrVertex* Vertices;
+
+ /**
+ * @brief Index data count.
+ *
+ * Number of indices used to draw this mesh.
+ */
+ unsigned int Indices;
+
+ /**
+ * @brief Associated textures.
+ *
+ * Pointer to an array of @ref SkrTexture objects that define the
+ * materials of this mesh.
+ */
+ SkrTexture* Textures;
+} SkrMesh;
+
+/**
+ * @brief 3D model representation.
+ *
+ * A model consists of one or more meshes, each with its own vertices, indices,
+ * and material textures. The model may also reference textures that are shared
+ * across meshes.
+ */
+typedef struct SkrModel {
+ SkrTexture* Textures; /*!< Array of textures used by model’s meshes. */
+ SkrMesh* Meshes; /*!< Array of meshes that compose the model. */
+ char* Path; /*!< Filesystem path of the model file. */
+} SkrModel;
+
+/**
+ * @internal
+ * @brief Load an image from a file into raw pixel memory.
+ *
+ * This function **must be implemented by the user** of the engine. The engine
+ * will call it when loading textures, but does not provide its own image
+ * loading backend.
+ *
+ * @param path Filesystem path to the image file.
+ * @param width Output pointer to receive image width in pixels.
+ * @param height Output pointer to receive image height in pixels.
+ * @param channels Output pointer to receive the number of color channels.
+ *
+ * @return Pointer to raw pixel data (heap-allocated). The exact format (e.g.
+ * RGB vs. RGBA) is determined by the user’s implementation. Returns
+ * NULL if the image could not be loaded.
+ *
+ * @note The returned memory must be freed with ::skr_free_image.
+ */
+extern unsigned char* m_skr_load_image_from_file(const char* path, int* width,
+ int* height, int* channels);
+
+/**
+ * @internal
+ * @brief Free pixel memory previously allocated by
+ * ::m_skr_load_image_from_file.
+ *
+ * This function **must be implemented by the user** of the engine to match the
+ * allocation strategy used in their m_skr_load_image_from_file.
+ *
+ * @param image_data Pointer to the pixel data to free. Safe to pass NULL.
+ */
+extern void m_skr_free_image(unsigned char* image_data);
+
+/**
+ * @internal
+ * @brief Read a whole file into memory.
+ *
+ * Opens the file in binary mode, reads its contents into a null-terminated
+ * buffer, and returns it. Caller must free the buffer with `free()`.
+ *
+ * @param path File path to read.
+ * @return Newly allocated buffer containing file contents, or NULL on error.
+ */
+static inline char* m_skr_read_file(const char* path) {
+ FILE* file = fopen(path, "rb");
+ if (!file) {
+ SKR_LAST_ERROR_SET("failed to open");
+ return NULL;
+ }
+
+ fseek(file, 0, SEEK_END);
+ long len = ftell(file);
+ rewind(file);
+
+ char* buffer = (char*)malloc(len + 1);
+ if (!buffer) {
+ fclose(file);
+ SKR_LAST_ERROR_SET("failed to open");
+ return NULL;
+ }
+
+ fread(buffer, 1, len, file);
+ buffer[len] = '\0';
+ fclose(file);
+
+ SKR_LAST_ERROR_CLEAR();
+ return buffer;
+}
+
+/**
+ * @internal
+ * @brief GL framebuffer resize callback
+ *
+ * Adjusts the OpenGL viewport when the window is resized.
+ *
+ * @param width New framebuffer width.
+ * @param height New framebuffer height.
+ */
+static inline void m_skr_gl_framebuffer_size_callback(const int width,
+ const int height) {
+ glViewport(0, 0, width, height);
+ SKR_LAST_ERROR_CLEAR();
+}
+
+/**
+ * @internal
+ * @brief GLFW framebuffer resize callback wrapper.
+ *
+ * @param window GLFW window handle.
+ * @param width New framebuffer width.
+ * @param height New framebuffer height.
+ */
+static inline void m_skr_gl_glfw_framebuffer_size_callback(GLFWwindow* window,
+ const int width,
+ const int height) {
+ m_skr_gl_framebuffer_size_callback(width, height);
+ SKR_LAST_ERROR_CLEAR();
+}
+
+/**
+ * @internal
+ * @brief Initialize a GLFW window for OpenGL rendering.
+ *
+ * @param w Pointer to SkrWindow to initialize (must not be NULL).
+ * @return 1 on success, 0 on failure.
+ */
+static inline int m_skr_gl_glfw_init(SkrWindow* w) {
+ if (!glfwInit() || !w) {
+ SKR_LAST_ERROR_SET("either glfwInit != 1 or SkrWindow == NULL");
+ return 0;
+ }
+
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
+ glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
+#ifdef __APPLE__
+ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
+#endif
+
+ w->Backend.Handler.GLFW = NULL;
+
+ w->Backend.Handler.GLFW =
+ glfwCreateWindow(w->Width, w->Height, w->Title, NULL, NULL);
+
+ if (!w->Backend.Handler.GLFW) {
+ SKR_LAST_ERROR_SET("window backend is NULL");
+ glfwTerminate();
+ return 0;
+ }
+
+ glfwSetFramebufferSizeCallback(w->Backend.Handler.GLFW,
+ m_skr_gl_glfw_framebuffer_size_callback);
+ glfwMakeContextCurrent(w->Backend.Handler.GLFW);
+
+ SKR_LAST_ERROR_CLEAR();
+ return 1;
+}
+
+/**
+ * @internal
+ * @brief GL check shader or program compile/link status.
+ *
+ * @param shader OpenGL shader or program ID.
+ * @param type String ("vert","frag","prog", etc.).
+ *
+ * @return 1 if compilation/link succeeded, 0 otherwise.
+ */
+static inline int m_skr_gl_check_compile_errors(const GLuint shader,
+ const char* type) {
+ GLint success;
+ GLchar infoLog[1024];
+
+ if (strcmp(type, "prog") == 0) {
+ glGetProgramiv(shader, GL_LINK_STATUS, &success);
+ if (!success) {
+ glGetProgramInfoLog(shader, sizeof(infoLog), NULL,
+ infoLog);
+ SKR_LAST_ERROR_SET("failed to link %s: %s", type,
+ infoLog);
+ return 0;
+ }
+ } else {
+ glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
+ if (!success) {
+ glGetShaderInfoLog(shader, sizeof(infoLog), NULL,
+ infoLog);
+ SKR_LAST_ERROR_SET("failed to compile %s: %s", type,
+ infoLog);
+ return 0;
+ }
+ }
+
+ return 1;
+}
+
+/**
+ * @internal
+ * @brief GL create an OpenGL shader from source.
+ *
+ * @param type Shader type (GL_VERTEX_SHADER, etc.).
+ * @param source Null-terminated GLSL source code.
+ *
+ * @return Shader ID, or 0 on failure.
+ */
+static inline GLuint m_skr_gl_create_shader(const GLenum type,
+ const char* source) {
+ GLuint shader = glCreateShader(type);
+ glShaderSource(shader, 1, &source, NULL);
+ glCompileShader(shader);
+
+ const char* type_str = NULL;
+ switch (type) {
+ case GL_VERTEX_SHADER:
+ type_str = "vert";
+ break;
+ case GL_FRAGMENT_SHADER:
+ type_str = "frag";
+ break;
+ case GL_GEOMETRY_SHADER:
+ type_str = "geom";
+ break;
+ case GL_COMPUTE_SHADER:
+ type_str = "comp";
+ break;
+ case GL_TESS_CONTROL_SHADER:
+ type_str = "tesc";
+ break;
+ case GL_TESS_EVALUATION_SHADER:
+ type_str = "tese";
+ break;
+ default:
+ type_str = "unknown";
+ break;
+ }
+
+ if (!m_skr_gl_check_compile_errors(shader, type_str)) {
+ glDeleteShader(shader);
+ return 0;
+ }
+
+ SKR_LAST_ERROR_CLEAR();
+ return shader;
+}
+
+/**
+ * @internal
+ * @brief GL create an OpenGL shader from a file.
+ *
+ * Reads the file contents and compiles it as a shader of the given type.
+ *
+ * @param type Shader type.
+ * @param path File path to GLSL source.
+ *
+ * @return Shader ID, or 0 on failure.
+ */
+static inline GLuint m_skr_gl_create_shader_from_file(const GLenum type,
+ const char* path) {
+ char* source = m_skr_read_file(path);
+ if (!source) {
+ return 0;
+ }
+
+ GLuint shader = m_skr_gl_create_shader(type, source);
+ free(source);
+
+ SKR_LAST_ERROR_CLEAR();
+ return shader;
+}
+
+/**
+ * @internal
+ * @brief GL link multiple shaders into a program.
+ *
+ * Attaches all shaders, links, deletes them, and returns the program.
+ *
+ * @param shaders Array of shader IDs.
+ * @param count Number of shaders.
+ *
+ * @return Program ID, or 0 on failure.
+ */
+static inline GLuint m_skr_gl_create_program(const GLuint* shaders,
+ const size_t count) {
+ GLuint program = glCreateProgram();
+ for (size_t i = 0; i < count; ++i) {
+ glAttachShader(program, shaders[i]);
+ }
+
+ glLinkProgram(program);
+ if (!m_skr_gl_check_compile_errors(program, "prog")) {
+ glDeleteProgram(program);
+ return 0;
+ }
+
+ for (size_t i = 0; i < count; ++i) {
+ glDetachShader(program, shaders[i]);
+ glDeleteShader(shaders[i]);
+ }
+
+ SKR_LAST_ERROR_CLEAR();
+ return program;
+}
+
+/**
+ * @internal
+ * @brief GL create program from SkrShader array (source or file).
+ *
+ * @param shaders_input Array of SkrShader descriptors.
+ * @param count Number of shaders.
+ *
+ * @return Program ID, or 0 on failure.
+ */
+static inline GLuint
+m_skr_gl_create_program_from_shaders(const SkrShader* shaders_input,
+ const size_t count) {
+ if (!shaders_input || count == 0) {
+ SKR_LAST_ERROR_SET("either shaders_input != 1 or count == 0");
+ return 0;
+ }
+
+ GLuint* shaders = (GLuint*)malloc(sizeof(GLuint) * count);
+ if (!shaders) {
+ SKR_LAST_ERROR_SET("shaders_input == NULL");
+ return 0;
+ }
+
+ for (size_t i = 0; i < count; ++i) {
+ const SkrShader* s = &shaders_input[i];
+ GLuint shader = 0;
+
+ if (s->Source) {
+ shader = m_skr_gl_create_shader(s->Type, s->Source);
+ } else if (s->Path) {
+ shader = m_skr_gl_create_shader_from_file(s->Type,
+ s->Path);
+ } else {
+ for (size_t j = 0; j < i; ++j) {
+ glDeleteShader(shaders[j]);
+ }
+ free(shaders);
+ SKR_LAST_ERROR_SET(
+ "shader.Source and shader.Path are NULL");
+ return 0;
+ }
+
+ if (!shader) {
+ for (size_t j = 0; j < i; ++j) {
+ glDeleteShader(shaders[j]);
+ }
+ free(shaders);
+ return 0;
+ }
+
+ shaders[i] = shader;
+ }
+
+ GLuint program = m_skr_gl_create_program(shaders, count);
+ free(shaders);
+
+ if (!program) {
+ return 0;
+ }
+
+ SKR_LAST_ERROR_CLEAR();
+ return program;
+}
+
+/**
+ * @internal
+ * @brief GL use an OpenGL shader program.
+ */
+static inline void m_skr_gl_shader_use(const GLuint program) {
+ glUseProgram(program);
+ SKR_LAST_ERROR_CLEAR();
+}
+
+/**
+ * @internal
+ * @brief GL destroy an OpenGL shader program.
+ *
+ * @param program Pointer to program ID. Resets to 0 on success.
+ */
+static inline void m_skr_gl_shader_destroy(GLuint* program) {
+ if (program && *program) {
+ glDeleteProgram(*program);
+ *program = 0;
+ SKR_LAST_ERROR_CLEAR();
+ }
+}
+
+static inline void m_skr_gl_shader_set_bool(const GLuint program,
+ const char* name, const int value) {
+ glUniform1i(glGetUniformLocation(program, name), value);
+ SKR_LAST_ERROR_CLEAR();
+}
+
+static inline void m_skr_gl_shader_set_int(const GLuint program,
+ const char* name, const int value) {
+ glUniform1i(glGetUniformLocation(program, name), value);
+ SKR_LAST_ERROR_CLEAR();
+}
+
+static inline void m_skr_gl_shader_set_float(const GLuint program,
+ const char* name,
+ const float value) {
+ glUniform1f(glGetUniformLocation(program, name), value);
+ SKR_LAST_ERROR_CLEAR();
+}
+
+static inline void m_skr_gl_shader_set_vec2(const GLuint program,
+ const char* name,
+ const vec2 value) {
+ glUniform2fv(glGetUniformLocation(program, name), 1, value);
+ SKR_LAST_ERROR_CLEAR();
+}
+
+static inline void m_skr_gl_shader_set_vec3(const GLuint program,
+ const char* name,
+ const vec3 value) {
+ glUniform3fv(glGetUniformLocation(program, name), 1, value);
+ SKR_LAST_ERROR_CLEAR();
+}
+
+static inline void m_skr_gl_shader_set_vec4(const GLuint program,
+ const char* name,
+ const vec4 value) {
+ glUniform4fv(glGetUniformLocation(program, name), 1, value);
+ SKR_LAST_ERROR_CLEAR();
+}
+
+static inline void m_skr_gl_shader_set_mat2(const GLuint program,
+ const char* name,
+ const mat2 value) {
+ glUniformMatrix2fv(glGetUniformLocation(program, name), 1, GL_FALSE,
+ (const float*)value);
+ SKR_LAST_ERROR_CLEAR();
+}
+
+static inline void m_skr_gl_shader_set_mat3(const GLuint program,
+ const char* name,
+ const mat3 value) {
+ glUniformMatrix3fv(glGetUniformLocation(program, name), 1, GL_FALSE,
+ (const float*)value);
+ SKR_LAST_ERROR_CLEAR();
+}
+
+static inline void m_skr_gl_shader_set_mat4(const GLuint program,
+ const char* name,
+ const mat4 value) {
+ glUniformMatrix4fv(glGetUniformLocation(program, name), 1, GL_FALSE,
+ (const float*)value);
+ SKR_LAST_ERROR_CLEAR();
+}
+
+static inline void m_skr_gl_renderer_init() {}
+
+/**
+ * @internal
+ * @brief GL clear screen (color + depth).
+ */
+static inline void m_skr_gl_renderer_render(void) {
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+}
+
+/**
+ * @internal
+ * @brief GL free VAO/VBO/EBO of a mesh.
+ *
+ * Usually called at shutdown, not per-frame.
+ */
+static inline void m_skr_gl_renderer_finalize(SkrMesh* m) {
+ glDeleteVertexArrays(1, &m->VAO);
+ glDeleteBuffers(1, &m->VBO);
+ glDeleteBuffers(1, &m->EBO);
+}
+
+/**
+ * @internal
+ * @brief GLFW check if a GLFW window should close.
+ */
+static inline int m_skr_gl_glfw_should_close(SkrWindow* w) {
+ return glfwWindowShouldClose(w->Backend.Handler.GLFW);
+}
+
+/**
+ * @internal
+ * @brief GLFW Render a frame with GLFW with OpenGL.
+ *
+ * Calls input handler, polls events, renders, swaps buffers.
+ */
+static inline void m_skr_gl_glfw_renderer_render(SkrWindow* w, SkrMesh* m) {
+ if (w->InputHandler) {
+ w->InputHandler(w);
+ }
+
+ glfwPollEvents();
+ glfwGetFramebufferSize(w->Backend.Handler.GLFW, &w->Width, &w->Height);
+
+ m_skr_gl_renderer_render();
+
+ glfwSwapBuffers(w->Backend.Handler.GLFW);
+}
+
+/**
+ * @internal
+ * @brief GLFW Shutdown OpenGL renderer and GLFW.
+ */
+static inline void m_skr_gl_glfw_renderer_finalize(SkrMesh* m) {
+ m_skr_gl_renderer_finalize(m);
+
+ glfwTerminate();
+}
+
+/**
+ * @internal
+ * @brief GL load a 2D texture from file path.
+ *
+ * @param path Path to image file.
+ * @param texture Output texture ID.
+ *
+ * @return 1 on success, 0 on failure.
+ */
+static inline int m_skr_gl_load_texture_2d_from_path(const char* path,
+ unsigned int* texture) {
+ glGenTextures(1, texture);
+ glBindTexture(GL_TEXTURE_2D, *texture);
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
+ GL_LINEAR_MIPMAP_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+ int width, height, nrChannels;
+ unsigned char* data =
+ m_skr_load_image_from_file(path, &width, &height, &nrChannels);
+ if (!data) {
+ SKR_LAST_ERROR_SET("failed to load texture");
+ return 0;
+ }
+
+ GLenum format = GL_RGB;
+ if (nrChannels == 1)
+ format = GL_RED;
+ else if (nrChannels == 3)
+ format = GL_RGB;
+ else if (nrChannels == 4)
+ format = GL_RGBA;
+
+ glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format,
+ GL_UNSIGNED_BYTE, data);
+ glGenerateMipmap(GL_TEXTURE_2D);
+
+ m_skr_free_image(data);
+
+ SKR_LAST_ERROR_CLEAR();
+ return 1;
+}
+
+/**
+ * @internal
+ * @brief GL load multiple 2D textures from file paths.
+ */
+static inline int m_skr_gl_load_textures_2d_from_paths(const char** paths,
+ unsigned int* textures,
+ const int count) {
+ for (int i = 0; i < count; i++) {
+ if (!m_skr_gl_load_texture_2d_from_path(paths[i],
+ &textures[i])) {
+ return 0;
+ }
+ }
+
+ SKR_LAST_ERROR_CLEAR();
+ return 1;
+}
+
+/**
+ * @internal
+ * @brief GL free an array of textures.
+ */
+static inline void m_skr_free_textures_2d(unsigned int* textures,
+ const int count) {
+ if (count > 0 && textures) {
+ glDeleteTextures(count, textures);
+ SKR_LAST_ERROR_CLEAR();
+ }
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // SKR_H
diff --git a/tests/main.c b/tests/main.c
new file mode 100644
index 0000000..64cbce4
--- /dev/null
+++ b/tests/main.c
@@ -0,0 +1,33 @@
+#include
+
+#include "skr.h"
+
+#include
+
+int main(void) {
+ SkrWindow window = {
+ .Title = "Hello SKR",
+ .Width = 800,
+ .Height = 600,
+ };
+
+ if (!m_skr_gl_glfw_init(&window)) {
+ fprintf(stderr, "Failed to init GLFW: %s\n", SKR_LAST_ERROR);
+ return 1;
+ }
+
+ // Initialize GLEW after creating context
+ if (glewInit() != GLEW_OK) {
+ fprintf(stderr, "Failed to init GLEW\n");
+ return 1;
+ }
+
+ while (!m_skr_gl_glfw_should_close(&window)) {
+ m_skr_gl_renderer_render();
+ glfwSwapBuffers(window.Backend.Handler.GLFW);
+ glfwPollEvents();
+ }
+
+ glfwTerminate();
+ return 0;
+}