← index
libmx2/MX2_Project/webmx/mx2/include/gl.hpp
Source: libmx2/MX2_Project/webmx/mx2/include/gl.hpp
/**
 * @file gl.hpp
 * @brief OpenGL window, shader program, sprite, and text rendering classes.
 *
 * Provides the core OpenGL abstractions used by libmx applications:
 * - ShaderProgram  – GLSL vertex/fragment shader compilation and uniform upload.
 * - GLText         – SDL_ttf text rendered into an OpenGL texture.
 * - GLSprite       – A textured quad rendered with an arbitrary shader.
 * - GLWindow       – SDL2 + OpenGL/ES window and event loop.
 * - GLObject       – Abstract interface for scene objects attached to GLWindow.
 *
 * Requires the WITH_GL compile-time flag.
 */
#ifndef GL_H__
#define GL_H__

#ifdef __EMSCRIPTEN__
#include "config.hpp"
#else
#include "config.h"
#endif

#ifdef WITH_GL
#include "mx.hpp"
#include <memory>
#include <string>

#ifdef __EMSCRIPTEN__
#include "glm.hpp"
#include "gtc/type_ptr.hpp"
#include <GLES3/gl3.h>
#include <emscripten/emscripten.h>
#include <emscripten/html5.h>
#else
#include <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#endif

#include "console.hpp"

namespace gl {

    extern const char *vSource; ///< Default vertex shader source.
    extern const char *fSource; ///< Default fragment shader source.

    /**
     * @class ShaderProgram
     * @brief GLSL shader program wrapper (shared-ownership).
     *
     * Compiles a vertex and fragment shader, links them into a GL program, and
     * exposes uniform-upload helpers.  Uses shared_ptr internally so multiple
     * copies refer to the same underlying GL object; the program is deleted when
     * the last copy is destroyed.
     */
    class ShaderProgram {
      private:
        struct SharedState {
            GLuint vertex_shader = 0;
            GLuint fragment_shader = 0;
            GLuint shader_id = 0;
            std::string name_;
            bool silent_ = false;

            ~SharedState();
        };
        std::shared_ptr<SharedState> state;

      public:
        /** @brief Default constructor — allocates shared state, no program compiled yet. */
        ShaderProgram();

        /**
         * @brief Wrap an existing GL program ID.
         * @param id Pre-compiled GL program identifier.
         */
        ShaderProgram(GLuint id);
        ~ShaderProgram() = default;
        ShaderProgram(const ShaderProgram &) = default;
        ShaderProgram &operator=(const ShaderProgram &) = default;
        ShaderProgram(ShaderProgram &&) noexcept = default;
        ShaderProgram &operator=(ShaderProgram &&) noexcept = default;

        /** @brief Release the underlying GL program (decrements shared count). */
        void release();

        /** @return The GL program ID, or 0 if not loaded. */
        GLuint id() const;

        /** @return @c true if the program has been successfully linked. */
        bool loaded() const;

        /** @brief Bind this shader program for subsequent draw calls. */
        void useProgram();

        /**
         * @brief Assign a human-readable name used in error messages.
         * @param n Name string.
         */
        void setName(const std::string &n);

        /**
         * @brief Suppress compilation/link error output.
         * @param s If @c true, errors are not printed.
         */
        void setSilent(bool s);

        /**
         * @brief Compile and link shaders from file paths.
         * @param v Path to the GLSL vertex shader file.
         * @param f Path to the GLSL fragment shader file.
         * @return @c true on success.
         */
        bool loadProgram(const std::string &v, const std::string &f);

        /**
         * @brief Compile and link shaders from source-code strings.
         * @param v Vertex shader GLSL source.
         * @param f Fragment shader GLSL source.
         * @return @c true on success.
         */
        bool loadProgramFromText(const std::string &v, const std::string &f);

        /**
         * @brief Upload an integer uniform variable.
         * @param name  Uniform name in the shader.
         * @param value Value to upload.
         */
        void setUniform(const std::string &name, int value);

        /**
         * @brief Upload a float uniform variable.
         * @param name  Uniform name.
         * @param value Float value.
         */
        void setUniform(const std::string &name, float value);

        /**
         * @brief Upload a vec2 uniform.
         * @param name  Uniform name.
         * @param value glm::vec2 value.
         */
        void setUniform(const std::string &name, const glm::vec2 &value);

        /**
         * @brief Upload a vec3 uniform.
         * @param name  Uniform name.
         * @param value glm::vec3 value.
         */
        void setUniform(const std::string &name, const glm::vec3 &value);

        /**
         * @brief Upload a vec4 uniform.
         * @param name  Uniform name.
         * @param value glm::vec4 value.
         */
        void setUniform(const std::string &name, const glm::vec4 &value);

        /**
         * @brief Upload a mat4 uniform.
         * @param name  Uniform name.
         * @param value glm::mat4 value.
         */
        void setUniform(const std::string &name, const glm::mat4 &value);

      private:
        GLuint createProgram(const char *vshaderSource, const char *fshaderSource);
        GLuint createProgramFromFile(const std::string &vert, const std::string &frag);
        int printShaderLog(GLuint shader);
        void printProgramLog(int p);
        bool checkError();
    };

    /**
     * @class GLText
     * @brief Render SDL_ttf text into an OpenGL texture and display it on screen.
     *
     * Manages a simple text shader and provides helpers that rasterise a string
     * to an SDL_Surface, upload it as an OpenGL texture, and draw a screen-space
     * quad at the specified position.
     */
    class GLText {
      public:
        /** @brief Default constructor. */
        GLText();

        /**
         * @brief Initialise internal state with the viewport dimensions.
         * @param w Viewport width in pixels.
         * @param h Viewport height in pixels.
         */
        void init(int w, int h);

        /**
         * @brief Rasterise a string to an OpenGL texture.
         * @param text        String to render.
         * @param font        SDL_ttf font handle.
         * @param color       Text colour.
         * @param textWidth   Output: texture pixel width.
         * @param textHeight  Output: texture pixel height.
         * @param solid       If @c true, use solid (fast) rendering; otherwise blended.
         * @return GL texture ID.
         */
        GLuint createText(const std::string &text, TTF_Font *font, SDL_Color color, int &textWidth, int &textHeight, bool solid = true);

        /**
         * @brief Draw a pre-created text texture at a given position.
         * @param texture      GL texture ID from createText().
         * @param x            Destination X (pixels).
         * @param y            Destination Y (pixels).
         * @param textWidth    Texture pixel width.
         * @param textHeight   Texture pixel height.
         * @param screenWidth  Current viewport width.
         * @param screenHeight Current viewport height.
         */
        void renderText(GLuint texture, float x, float y, int textWidth, int textHeight, int screenWidth, int screenHeight);

        /**
         * @brief Convenience: create and immediately draw solid text.
         * @param f    mx::Font reference.
         * @param x    X position.
         * @param y    Y position.
         * @param text String to render.
         */
        void printText_Solid(const mx::Font &f, float x, float y, const std::string &text);

        /**
         * @brief Convenience: create and immediately draw blended (anti-aliased) text.
         * @param f    mx::Font reference.
         * @param x    X position.
         * @param y    Y position.
         * @param text String to render.
         */
        void printText_Blended(const mx::Font &f, float x, float y, const std::string &text);

        /**
         * @brief Set the colour used for subsequent printText calls.
         * @param col SDL_Color.
         */
        void setColor(SDL_Color col);

      private:
        ShaderProgram textShader;               ///< Internal text quad shader.
        SDL_Color color = {255, 255, 255, 255}; ///< Current text colour.
        int w = 0, h = 0;                       ///< Viewport dimensions.
    };

    /**
     * @class GLSprite
     * @brief A textured screen-space quad drawn with an OpenGL shader.
     *
     * Manages a VAO/VBO pair that holds the quad geometry and a GL texture.
     * Supports loading from file, drawing at arbitrary positions/sizes, and
     * updating the texture from an SDL_Surface or raw pixel buffer at runtime.
     * Copy and move are disabled; create on the heap or as a member.
     */
    class GLSprite {
      public:
        /** @brief Default constructor. */
        GLSprite();
        /** @brief Destructor — releases GL buffers and texture. */
        ~GLSprite();

        GLSprite(const GLSprite &) = delete;
        GLSprite &operator=(const GLSprite &) = delete;
        GLSprite(GLSprite &&) = delete;
        GLSprite &operator=(GLSprite &&) = delete;

        /** @brief Free all GL resources associated with this sprite. */
        void release();

        /**
         * @brief Set the native display size of the sprite.
         * @param w Width in pixels.
         * @param h Height in pixels.
         */
        void initSize(float w, float h);

        /**
         * @brief Assign a debug name (used in log messages).
         * @param name Name string.
         */
        void setName(const std::string &name);

        /**
         * @brief Bind a shader program by extracting its GL ID.
         * @tparam ShaderT Any type exposing id() -> GLuint.
         * @param program  Pointer to the shader (nullptr clears the binding).
         */
        template <typename ShaderT>
        void setShader(ShaderT *program) {
            active_shader_id = program ? program->id() : 0;
        }

        /**
         * @brief Initialise the sprite from an existing GL texture.
         * @tparam ShaderT    Shader type.
         * @param program     Active shader.
         * @param texture     Existing GL texture ID.
         * @param x           Destination X.
         * @param y           Destination Y.
         * @param textWidth   Texture width.
         * @param textHeight  Texture height.
         */
        template <typename ShaderT>
        void initWithTexture(ShaderT *program, GLuint texture, float x, float y, int textWidth, int textHeight) {
            active_shader_id = program ? program->id() : 0;
            initWithTextureImpl(texture, x, y, textWidth, textHeight);
        }

        /**
         * @brief Load a texture from a file and set the sprite geometry.
         * @tparam ShaderT    Shader type.
         * @param shader      Active shader.
         * @param tex         Image file path.
         * @param x           Display X.
         * @param y           Display Y.
         * @param textWidth   Display width.
         * @param textHeight  Display height.
         */
        template <typename ShaderT>
        void loadTexture(ShaderT *shader, const std::string &tex, float x, float y, int textWidth, int textHeight) {
            active_shader_id = shader ? shader->id() : 0;
            loadTextureImpl(tex, x, y, textWidth, textHeight);
        }

        /**
         * @brief Load a texture from a file (natural dimensions).
         * @tparam ShaderT Shader type.
         * @param shader   Active shader.
         * @param tex      Image file path.
         * @param x        Display X.
         * @param y        Display Y.
         */
        template <typename ShaderT>
        void loadTexture(ShaderT *shader, const std::string &tex, float x, float y) {
            active_shader_id = shader ? shader->id() : 0;
            loadTextureImpl(tex, x, y);
        }

        /** @brief Draw at the sprite's stored position and size. */
        void draw();

        /**
         * @brief Draw at the given position (stored size).
         * @param x Destination X.
         * @param y Destination Y.
         */
        void draw(int x, int y);

        /**
         * @brief Draw at an explicit position and size.
         * @param x Destination X.
         * @param y Destination Y.
         * @param w Display width.
         * @param h Display height.
         */
        void draw(int x, int y, int w, int h);

        /**
         * @brief Draw an arbitrary texture at a position and size.
         * @param texture_id External GL texture ID.
         * @param x          Destination X.
         * @param y          Destination Y.
         * @param w          Display width.
         * @param h          Display height.
         */
        void draw(GLuint texture_id, float x, float y, int w, int h);

        /**
         * @brief Replace the sprite texture with the contents of an SDL_Surface.
         * @param surf New surface (not consumed; caller still owns it).
         */
        void updateTexture(SDL_Surface *surf);

        /**
         * @brief Replace the sprite texture from a raw pixel buffer.
         * @param buffer Pointer to RGBA pixel data.
         * @param width  Buffer width in pixels.
         * @param height Buffer height in pixels.
         */
        void updateTexture(void *buffer, int width, int height);

        int width = 0;  ///< Texture pixel width.
        int height = 0; ///< Texture pixel height.
      private:
        void initWithTextureImpl(GLuint texture, float x, float y, int textWidth, int textHeight);
        void loadTextureImpl(const std::string &tex, float x, float y, int textWidth, int textHeight);
        void loadTextureImpl(const std::string &tex, float x, float y);
        GLuint active_shader_id = 0;
        GLuint texture = 0;
        GLuint VBO = 0, VAO = 0;
        std::vector<float> vertices;
        float screenWidth = 0.0f, screenHeight = 0.0f;
        int texWidth = 0, texHeight = 0;
        std::string textureName;
    };

    /** @brief OpenGL mode: desktop GL or OpenGL ES. */
    enum class GLMode {
        DESKTOP,
        ES
    };

    class GLObject;

    /**
     * @class GLWindow
     * @brief SDL2 + OpenGL/ES window with event loop and console support.
     *
     * Creates an SDL2 window with an OpenGL (or GLES) context.  Derived classes
     * implement event() and draw().  An optional GLObject can be attached to
     * delegate rendering.  A built-in GLConsole can be activated for in-app
     * debugging.
     */
    class GLWindow {
      private:
        GLMode gl_mode;

      public:
#ifdef __EMSCRIPTEN__
        EMSCRIPTEN_WEBGL_CONTEXT_HANDLE webglContext = 0;
        void restoreContext();
#endif
        /**
         * @brief Construct and open an SDL2 + OpenGL window.
         * @param text   Window title.
         * @param width  Width in pixels.
         * @param height Height in pixels.
         * @param resize_ Allow window resizing if @c true.
         * @param mode   GL mode (DESKTOP or ES).
         */
        GLWindow(const std::string &text, int width, int height, bool resize_ = true, GLMode mode = GLMode::DESKTOP) : gl_mode(mode), glContext{nullptr}, window{nullptr} {
            initGL(text, width, height, resize_);
        }

        /**
         * @brief Construct a window from dimensions and mode (internal use).
         * @param width  Width.
         * @param height Height.
         * @param mode   GL mode.
         */
        GLWindow(int width, int height, GLMode mode);

        /** @brief Destructor — destroys the GL context and SDL window. */
        virtual ~GLWindow();

        GLWindow(const GLWindow &) = delete;
        GLWindow &operator=(const GLWindow &) = delete;
        GLWindow(GLWindow &&) = delete;
        GLWindow &operator=(GLWindow &&) = delete;

        /**
         * @brief Initialise the GL context and SDL window.
         * @param title  Window title.
         * @param width  Width.
         * @param height Height.
         * @param resize_ Allow resizing.
         */
        void initGL(const std::string &title, int width, int height, bool resize_ = true);

        /**
         * @brief Reinitialise with only dimensions (used by Emscripten).
         * @param width  New width.
         * @param height New height.
         */
        void initGL(int width, int height);

        /** @brief Update the GL viewport to match the current window size. */
        void updateViewport();

        /** @brief Swap front and back buffers to display the rendered frame. */
        void swap();

        /**
         * @brief Process a single SDL event (pure virtual).
         * @param e SDL event.
         */
        virtual void event(SDL_Event &e) = 0;

        /** @brief Render the current frame (pure virtual). */
        virtual void draw() = 0;

        /**
         * @brief Called when the window is resized (optional override).
         * @param w New width.
         * @param h New height.
         */
        virtual void resize(int w, int h) {}

        /**
         * @brief Attach a GLObject to receive draw/event callbacks.
         * @param o Pointer to the object (window does not take ownership).
         */
        void setObject(GLObject *o);

        /** @brief Request the event loop to stop. */
        void quit();

        /** @brief Run the event+draw loop until quit() is called. */
        void proc();

        /** @brief Start the blocking main loop. */
        void loop();

        /** @brief Insert a small sleep to cap frame rate. */
        void delay();

        /**
         * @brief Set the asset search path.
         * @param path Directory containing assets.
         */
        void setPath(const std::string &path) { util.path = path; }

        /**
         * @brief Change the window title bar text.
         * @param title New title.
         */
        void setWindowTitle(const std::string &title);

        /**
         * @brief Resize the SDL window.
         * @param w New width.
         * @param h New height.
         */
        void setWindowSize(int w, int h);

        /**
         * @brief Set the window icon from an SDL_Surface.
         * @param ico Icon surface.
         */
        void setWindowIcon(SDL_Surface *ico);

        /**
         * @brief Toggle fullscreen mode.
         * @param full @c true for fullscreen.
         */
        void setFullScreen(bool full);

        /**
         * @brief Activate the built-in debug console.
         * @param fnt  Font file path.
         * @param size Font size in points.
         * @param color Text colour.
         */
        void activateConsole(const std::string &fnt, int size, const SDL_Color &color);

        /**
         * @brief Activate the console at a specific screen region.
         * @param rc   Console rectangle.
         * @param fnt  Font file path.
         * @param size Font size.
         * @param color Text colour.
         */
        void activateConsole(const SDL_Rect &rc, const std::string &fnt, int size, const SDL_Color &color);

        /** @brief Draw the console overlay onto the current frame. */
        void drawConsole();

        /**
         * @brief Show or hide the console overlay.
         * @param show @c true to show, @c false to hide.
         */
        void showConsole(bool show);

        std::unique_ptr<gl::GLObject> object; ///< Optional attached scene object.
        mx::mxUtil util;                      ///< Asset path and utility helpers.
        console::GLConsole console;           ///< Built-in debug console.
        int w = 0;                            ///< Current window width.
        int h = 0;                            ///< Current window height.

        /** @return Pointer to the underlying SDL_Window. */
        SDL_Window *getWindow() { return window; }

        /** @brief Restore this window's GL context as current on the calling thread. */
        void makeCurrent();
#ifdef WITH_MIXER
        mx::Mixer mixer;
#endif
        GLText text;
        bool console_visible = false;
        bool console_active = false;

      private:
        SDL_GLContext glContext;
        SDL_Window *window = nullptr;
        bool active = false;
        SDL_Event e;
        bool hide_console = false;
    };

    /**
     * @class GLObject
     * @brief Abstract interface for scene objects managed by a GLWindow.
     *
     * Derived classes implement load(), draw(), event(), and optionally resize()
     * to participate in the GLWindow render/event loop.  Attach an instance via
     * GLWindow::setObject().
     */
    class GLObject {
      public:
        GLObject() = default;
        virtual ~GLObject() = default;

        /**
         * @brief Load GPU resources (textures, shaders, geometry).
         * @param win Parent window.
         */
        virtual void load(GLWindow *win) = 0;

        /**
         * @brief Render the scene for the current frame.
         * @param win Parent window.
         */
        virtual void draw(GLWindow *win) = 0;

        /**
         * @brief Handle an SDL event.
         * @param window Parent window.
         * @param e      SDL event.
         */
        virtual void event(GLWindow *window, SDL_Event &e) = 0;

        /**
         * @brief Called when the window is resized (optional).
         * @param win Parent window.
         * @param w   New width.
         * @param h   New height.
         */
        virtual void resize(gl::GLWindow *win, int w, int h) {}
    };

    /**
     * @brief Load a GL texture from a file.
     * @param filename Image file path.
     * @return GL texture ID.
     */
    GLuint loadTexture(const std::string &filename);

    /**
     * @brief Load a GL texture from a file and retrieve its dimensions.
     * @param filename Image file path.
     * @param w        Output: width.
     * @param h        Output: height.
     * @return GL texture ID.
     */
    GLuint loadTexture(const std::string &filename, int &w, int &h);

    /**
     * @brief Replace a GL texture's contents from an SDL_Surface.
     * @param texture Target GL texture ID.
     * @param surface Source surface.
     * @param flip    If @c true, flip vertically before uploading.
     */
    void updateTexture(GLuint texture, SDL_Surface *surface, bool flip);

    /**
     * @brief Create a new GL texture from an SDL_Surface.
     * @param surface Source surface.
     * @param flip    If @c true, flip vertically.
     * @return New GL texture ID.
     */
    GLuint createTexture(SDL_Surface *surface, bool flip);

    /**
     * @brief Create a new GL texture from a raw pixel buffer.
     * @param buffer Pointer to RGBA data.
     * @param width  Image width.
     * @param height Image height.
     * @return New GL texture ID.
     */
    GLuint createTexture(void *buffer, int width, int height);

    /**
     * @brief Allocate a blank RGBA SDL_Surface.
     * @param w Width in pixels.
     * @param h Height in pixels.
     * @return Newly allocated surface (caller owns it).
     */
    SDL_Surface *createSurface(int w, int h);
} // namespace gl
#endif
#endif