diff --git a/CMakeLists.txt b/CMakeLists.txt index faff68c..265f910 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,10 +11,22 @@ set(CMAKE_CXX_STANDARD_REQUIRED True) # Headless build option (no SFML, no graphics - for server/testing/Emscripten prep) option(MCRF_HEADLESS "Build without graphics dependencies (SFML, ImGui)" OFF) -# Emscripten builds are always headless (no SFML yet - using stubs) +# SDL2 backend option (SDL2 + OpenGL ES 2 - for Emscripten/WebGL, Android, cross-platform) +option(MCRF_SDL2 "Build with SDL2+OpenGL ES 2 backend instead of SFML" OFF) + +# Emscripten builds: use SDL2 if specified, otherwise fall back to headless if(EMSCRIPTEN) - set(MCRF_HEADLESS ON) - message(STATUS "Emscripten detected - forcing HEADLESS mode") + if(MCRF_SDL2) + message(STATUS "Emscripten detected - using SDL2 backend") + set(MCRF_HEADLESS OFF) + else() + set(MCRF_HEADLESS ON) + message(STATUS "Emscripten detected - forcing HEADLESS mode (use -DMCRF_SDL2=ON for graphics)") + endif() +endif() + +if(MCRF_SDL2) + message(STATUS "Building with SDL2 backend - SDL2+OpenGL ES 2") endif() if(MCRF_HEADLESS) @@ -36,9 +48,13 @@ if(EMSCRIPTEN) # Emscripten build: use Python headers compiled for wasm32-emscripten # The pyconfig.h from cross-build has correct LONG_BIT and other settings set(PYTHON_WASM_BUILD "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/build/python") + # Force-include wasm pyconfig.h BEFORE anything else to set correct platform defines add_compile_options(-include ${PYTHON_WASM_BUILD}/pyconfig.h) + # Override LONG_BIT - Emscripten's limits.h incorrectly defines it as 64 for wasm32 + add_compile_definitions(LONG_BIT=32) + # Include wasm build directory FIRST so its pyconfig.h is found by #include "pyconfig.h" + include_directories(BEFORE ${PYTHON_WASM_BUILD}) include_directories(${CMAKE_SOURCE_DIR}/deps/cpython/Include) - include_directories(${PYTHON_WASM_BUILD}) # For generated headers message(STATUS "Using Emscripten Python from: ${PYTHON_WASM_BUILD}") elseif(MCRF_CROSS_WINDOWS) # Windows cross-compilation: use cpython headers with PC/pyconfig.h @@ -57,8 +73,9 @@ else() include_directories(${CMAKE_SOURCE_DIR}/deps/Python) endif() -# ImGui and ImGui-SFML include directories (not needed in headless mode) -if(NOT MCRF_HEADLESS) +# ImGui and ImGui-SFML include directories (not needed in headless or SDL2 mode) +# SDL2 builds will use ImGui with SDL2 backend later; for now, no ImGui +if(NOT MCRF_HEADLESS AND NOT MCRF_SDL2) include_directories(${CMAKE_SOURCE_DIR}/modules/imgui) include_directories(${CMAKE_SOURCE_DIR}/modules/imgui-sfml) @@ -75,13 +92,14 @@ endif() # Collect all the source files file(GLOB_RECURSE SOURCES "src/*.cpp") -# Add ImGui sources to the build (only if not headless) -if(NOT MCRF_HEADLESS) +# Add ImGui sources to the build (only if using SFML) +if(NOT MCRF_HEADLESS AND NOT MCRF_SDL2) list(APPEND SOURCES ${IMGUI_SOURCES}) endif() # Find OpenGL (required by ImGui-SFML) - not needed in headless mode -if(NOT MCRF_HEADLESS) +# SDL2 builds handle OpenGL ES 2 differently (via SDL2 or Emscripten) +if(NOT MCRF_HEADLESS AND NOT MCRF_SDL2) if(MCRF_CROSS_WINDOWS) # For cross-compilation, OpenGL is provided by MinGW set(OPENGL_LIBRARIES opengl32) @@ -109,8 +127,27 @@ if(EMSCRIPTEN) ${LIBTCOD_WASM_BUILD}/_deps/lodepng-c-build/liblodepng-c.a ${LIBTCOD_WASM_BUILD}/_deps/utf8proc-build/libutf8proc.a) include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux) # Use Linux platform stubs for now + # For SDL2 builds, add stb headers for image/font loading + if(MCRF_SDL2) + include_directories(${CMAKE_SOURCE_DIR}/deps/stb) + endif() message(STATUS "Linking Emscripten Python: ${PYTHON_WASM_BUILD}/libpython3.14.a") message(STATUS "Linking Emscripten libtcod: ${LIBTCOD_WASM_BUILD}/libtcod.a") +elseif(MCRF_SDL2) + # SDL2 build (non-Emscripten): link against SDL2 and system libraries + # Note: For desktop SDL2 builds in the future + find_package(SDL2 REQUIRED) + find_package(OpenGL REQUIRED) + set(LINK_LIBS + SDL2::SDL2 + OpenGL::GL + tcod + python3.14 + m dl util pthread) + include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux) + include_directories(${CMAKE_SOURCE_DIR}/deps/stb) # stb_image.h, stb_truetype.h + link_directories(${CMAKE_SOURCE_DIR}/__lib) + message(STATUS "Building with SDL2 backend (desktop)") elseif(MCRF_HEADLESS) # Headless build: no SFML, no OpenGL if(WIN32 OR MCRF_CROSS_WINDOWS) @@ -195,6 +232,8 @@ endif() add_executable(mcrogueface ${SOURCES}) # Define NO_SDL for libtcod-headless headers (excludes SDL-dependent code) +# We ALWAYS need this because libtcod headers expect SDL3, not SDL2 +# Our SDL2 backend is separate from libtcod's SDL3 renderer target_compile_definitions(mcrogueface PRIVATE NO_SDL) # Define MCRF_HEADLESS for headless builds (excludes SFML/ImGui code) @@ -202,9 +241,15 @@ if(MCRF_HEADLESS) target_compile_definitions(mcrogueface PRIVATE MCRF_HEADLESS) endif() +# Define MCRF_SDL2 for SDL2 builds (uses SDL2+OpenGL ES 2 instead of SFML) +if(MCRF_SDL2) + target_compile_definitions(mcrogueface PRIVATE MCRF_SDL2) +endif() + # Emscripten-specific link options (use ports for zlib, bzip2, sqlite3) if(EMSCRIPTEN) - target_link_options(mcrogueface PRIVATE + # Base Emscripten options + set(EMSCRIPTEN_LINK_OPTIONS -sUSE_ZLIB=1 -sUSE_BZIP2=1 -sUSE_SQLITE3=1 @@ -215,6 +260,9 @@ if(EMSCRIPTEN) -sSTACK_OVERFLOW_CHECK=2 -fexceptions -sNO_DISABLE_EXCEPTION_CATCHING + # Disable features that require dynamic linking support + -sERROR_ON_UNDEFINED_SYMBOLS=0 + -sALLOW_UNIMPLEMENTED_SYSCALLS=1 # Preload Python stdlib into virtual filesystem at /lib/python3.14 --preload-file=${CMAKE_SOURCE_DIR}/wasm_stdlib/lib@/lib # Preload game scripts into /scripts @@ -222,6 +270,24 @@ if(EMSCRIPTEN) # Preload assets --preload-file=${CMAKE_SOURCE_DIR}/assets@/assets ) + + # Add SDL2 options if using SDL2 backend + if(MCRF_SDL2) + list(APPEND EMSCRIPTEN_LINK_OPTIONS + -sUSE_SDL=2 + -sFULL_ES2=1 + -sMIN_WEBGL_VERSION=2 + -sMAX_WEBGL_VERSION=2 + ) + # SDL2 flags are also needed at compile time for headers + target_compile_options(mcrogueface PRIVATE + -sUSE_SDL=2 + ) + message(STATUS "Emscripten SDL2 options enabled: -sUSE_SDL=2 -sFULL_ES2=1") + endif() + + target_link_options(mcrogueface PRIVATE ${EMSCRIPTEN_LINK_OPTIONS}) + # Set Python home for the embedded interpreter target_compile_definitions(mcrogueface PRIVATE MCRF_WASM_PYTHON_HOME="/lib/python3.14" diff --git a/src/Common.h b/src/Common.h index 2827d94..b77c7be 100644 --- a/src/Common.h +++ b/src/Common.h @@ -10,11 +10,15 @@ // ============================================================================= #ifdef MCRF_HEADLESS - // Use headless type stubs instead of SFML + // Use headless type stubs instead of SFML (no graphics, for CI/testing) #include "platform/HeadlessTypes.h" #define MCRF_GRAPHICS_BACKEND "headless" +#elif defined(MCRF_SDL2) + // Use SDL2 + OpenGL ES 2 backend (for Emscripten/WebGL, Android, cross-platform) + #include "platform/SDL2Types.h" + #define MCRF_GRAPHICS_BACKEND "sdl2" #else - // Use SFML for graphics and audio + // Use SFML for graphics and audio (default desktop build) #include #include #define MCRF_GRAPHICS_BACKEND "sfml" diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index 774ed6a..68c3f8e 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -7,7 +7,8 @@ #include "Animation.h" #include "Timer.h" #include "BenchmarkLogger.h" -#ifndef MCRF_HEADLESS +// ImGui is only available for SFML builds (not headless, not SDL2) +#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2) #include "imgui.h" #include "imgui-SFML.h" #endif @@ -86,8 +87,8 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg) window->setFramerateLimit(60); render_target = window.get(); -#ifndef MCRF_HEADLESS - // Initialize ImGui for the window +#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2) + // Initialize ImGui for the window (SFML builds only) if (ImGui::SFML::Init(*window)) { imguiInitialized = true; // Register settings handler before .ini is loaded (happens on first frame) @@ -199,7 +200,7 @@ void GameEngine::cleanup() } // Shutdown ImGui AFTER window is closed to avoid X11 BadCursor errors -#ifndef MCRF_HEADLESS +#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2) if (imguiInitialized) { ImGui::SFML::Shutdown(); imguiInitialized = false; @@ -361,8 +362,8 @@ void GameEngine::doFrame() if (!headless) { sUserInput(); -#ifndef MCRF_HEADLESS - // Update ImGui +#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2) + // Update ImGui (SFML builds only) if (imguiInitialized) { ImGui::SFML::Update(*window, clock.getElapsedTime()); } @@ -405,8 +406,8 @@ void GameEngine::doFrame() profilerOverlay->render(*render_target); } -#ifndef MCRF_HEADLESS - // Render ImGui overlays (console and scene explorer) +#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2) + // Render ImGui overlays (console and scene explorer) - SFML builds only if (imguiInitialized && !headless) { console.render(); sceneExplorer.render(*this); @@ -593,8 +594,8 @@ void GameEngine::sUserInput() sf::Event event; while (window && window->pollEvent(event)) { -#ifndef MCRF_HEADLESS - // Process event through ImGui first +#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2) + // Process event through ImGui first (SFML builds only) if (imguiInitialized) { ImGui::SFML::ProcessEvent(*window, event); } diff --git a/src/GameEngine.h b/src/GameEngine.h index f9d7a40..36d507c 100644 --- a/src/GameEngine.h +++ b/src/GameEngine.h @@ -10,7 +10,8 @@ #include "HeadlessRenderer.h" #include "SceneTransition.h" #include "Profiler.h" -#ifndef MCRF_HEADLESS +// ImGui is only available for SFML builds (not headless, not SDL2) +#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2) #include "ImGuiConsole.h" #include "ImGuiSceneExplorer.h" #endif @@ -196,7 +197,8 @@ private: int overlayUpdateCounter = 0; // Only update overlay every N frames ProfilerOverlay* profilerOverlay = nullptr; // The actual overlay renderer -#ifndef MCRF_HEADLESS +// ImGui is only available for SFML builds +#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2) // ImGui console overlay ImGuiConsole console; ImGuiSceneExplorer sceneExplorer; diff --git a/src/ImGuiConsole.cpp b/src/ImGuiConsole.cpp index 6f5f149..284f8d0 100644 --- a/src/ImGuiConsole.cpp +++ b/src/ImGuiConsole.cpp @@ -1,7 +1,7 @@ // ImGuiConsole.cpp - Debug console using ImGui -// This file is excluded from headless builds (no GUI/debug interface needed) +// This file is excluded from headless and SDL2 builds (ImGui-SFML only) -#ifndef MCRF_HEADLESS +#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2) #include "ImGuiConsole.h" #include "imgui.h" diff --git a/src/ImGuiConsole.h b/src/ImGuiConsole.h index e705936..0184bb4 100644 --- a/src/ImGuiConsole.h +++ b/src/ImGuiConsole.h @@ -1,7 +1,7 @@ #pragma once -// ImGuiConsole - excluded from headless builds (no GUI/debug interface) -#ifndef MCRF_HEADLESS +// ImGuiConsole - excluded from headless and SDL2 builds (ImGui-SFML only) +#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2) #include #include diff --git a/src/ImGuiSceneExplorer.cpp b/src/ImGuiSceneExplorer.cpp index 0eb7c03..e255bbf 100644 --- a/src/ImGuiSceneExplorer.cpp +++ b/src/ImGuiSceneExplorer.cpp @@ -1,7 +1,7 @@ // ImGuiSceneExplorer.cpp - Debug scene hierarchy explorer using ImGui -// This file is excluded from headless builds (no GUI/debug interface needed) +// This file is excluded from headless and SDL2 builds (ImGui-SFML only) -#ifndef MCRF_HEADLESS +#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2) #include "ImGuiSceneExplorer.h" #include "imgui.h" diff --git a/src/ImGuiSceneExplorer.h b/src/ImGuiSceneExplorer.h index 7293ed3..76ef22e 100644 --- a/src/ImGuiSceneExplorer.h +++ b/src/ImGuiSceneExplorer.h @@ -1,7 +1,7 @@ #pragma once -// ImGuiSceneExplorer - excluded from headless builds (no GUI/debug interface) -#ifndef MCRF_HEADLESS +// ImGuiSceneExplorer - excluded from headless and SDL2 builds (ImGui-SFML only) +#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2) #include #include diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index c06eee3..292236d 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -32,7 +32,8 @@ #include "PyUniformCollection.h" // Shader uniform collection (#106) #include "McRogueFaceVersion.h" #include "GameEngine.h" -#ifndef MCRF_HEADLESS +// ImGui is only available for SFML builds +#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2) #include "ImGuiConsole.h" #endif #include "BenchmarkLogger.h" @@ -1627,7 +1628,7 @@ PyObject* McRFPy_API::_setDevConsole(PyObject* self, PyObject* args) { return NULL; } -#ifndef MCRF_HEADLESS +#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2) ImGuiConsole::setEnabled(enabled); #endif Py_RETURN_NONE; diff --git a/src/platform/EmscriptenStubs.cpp b/src/platform/EmscriptenStubs.cpp new file mode 100644 index 0000000..105c79d --- /dev/null +++ b/src/platform/EmscriptenStubs.cpp @@ -0,0 +1,38 @@ +// EmscriptenStubs.cpp - Stub implementations for missing POSIX functions in Emscripten +// These functions are referenced by Python's posixmodule and timemodule but not available in WASM + +#ifdef __EMSCRIPTEN__ + +#include +#include +#include +#include + +extern "C" { + +// wait3 and wait4 are BSD-style wait functions not available in Emscripten +// Python's posixmodule references them but they're not critical for WASM +pid_t wait3(int *status, int options, struct rusage *rusage) { + errno = ENOSYS; + return -1; +} + +pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage) { + errno = ENOSYS; + return -1; +} + +// wcsftime - wide character strftime, used by Python's timemodule +// Provide a minimal implementation that returns 0 (no characters written) +size_t wcsftime(wchar_t *wcs, size_t maxsize, const wchar_t *format, const struct tm *timeptr) { + // In a full implementation, this would format time as wide chars + // For WASM, just return 0 to indicate nothing written + if (maxsize > 0) { + wcs[0] = L'\0'; + } + return 0; +} + +} // extern "C" + +#endif // __EMSCRIPTEN__ diff --git a/src/platform/SDL2Renderer.cpp b/src/platform/SDL2Renderer.cpp new file mode 100644 index 0000000..158018c --- /dev/null +++ b/src/platform/SDL2Renderer.cpp @@ -0,0 +1,1875 @@ +// SDL2Renderer.cpp - OpenGL ES 2 rendering implementation for SDL2 backend +// Implements the SDL2 types defined in SDL2Types.h using SDL2 and OpenGL ES 2 + +#ifdef MCRF_SDL2 + +#include "SDL2Renderer.h" +#include "SDL2Types.h" + +#include +#include +#include +#include + +// SDL2 and OpenGL ES 2 headers +#ifdef __EMSCRIPTEN__ +#include +#include +// Emscripten's USE_SDL=2 port puts headers directly in include path +#include +#include +#else +#include +#include +// Desktop OpenGL - we'll use GL 2.1 compatible subset that matches GLES2 +#define GL_GLEXT_PROTOTYPES +#include +#endif + +// stb libraries for image/font loading (from deps/stb/) +#define STB_IMAGE_IMPLEMENTATION +#include + +#define STB_TRUETYPE_IMPLEMENTATION +#include + +namespace sf { + +// ============================================================================= +// Built-in Shaders (GLSL ES 2.0 / GLSL 1.20 compatible) +// ============================================================================= + +static const char* SHAPE_VERTEX_SHADER = R"( +#ifdef GL_ES +precision mediump float; +#endif +attribute vec2 a_position; +attribute vec4 a_color; +uniform mat4 u_projection; +varying vec4 v_color; + +void main() { + gl_Position = u_projection * vec4(a_position, 0.0, 1.0); + v_color = a_color; +} +)"; + +static const char* SHAPE_FRAGMENT_SHADER = R"( +#ifdef GL_ES +precision mediump float; +#endif +varying vec4 v_color; + +void main() { + gl_FragColor = v_color; +} +)"; + +static const char* SPRITE_VERTEX_SHADER = R"( +#ifdef GL_ES +precision mediump float; +#endif +attribute vec2 a_position; +attribute vec4 a_color; +attribute vec2 a_texcoord; +uniform mat4 u_projection; +varying vec4 v_color; +varying vec2 v_texcoord; + +void main() { + gl_Position = u_projection * vec4(a_position, 0.0, 1.0); + v_color = a_color; + v_texcoord = a_texcoord; +} +)"; + +static const char* SPRITE_FRAGMENT_SHADER = R"( +#ifdef GL_ES +precision mediump float; +#endif +varying vec4 v_color; +varying vec2 v_texcoord; +uniform sampler2D u_texture; + +void main() { + gl_FragColor = texture2D(u_texture, v_texcoord) * v_color; +} +)"; + +// Text shader is same as sprite for now +static const char* TEXT_VERTEX_SHADER = SPRITE_VERTEX_SHADER; +static const char* TEXT_FRAGMENT_SHADER = R"( +#ifdef GL_ES +precision mediump float; +#endif +varying vec4 v_color; +varying vec2 v_texcoord; +uniform sampler2D u_texture; + +void main() { + // Text rendering: use texture alpha as coverage + float alpha = texture2D(u_texture, v_texcoord).a; + gl_FragColor = vec4(v_color.rgb, v_color.a * alpha); +} +)"; + +// ============================================================================= +// SDL2Renderer Implementation +// ============================================================================= + +SDL2Renderer& SDL2Renderer::getInstance() { + static SDL2Renderer instance; + return instance; +} + +bool SDL2Renderer::init() { + if (initialized_) return true; + + // Initialize SDL2 if not already done + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) < 0) { + std::cerr << "SDL2Renderer: Failed to initialize SDL: " << SDL_GetError() << std::endl; + return false; + } + + // Note: Shaders are initialized in initGL() after GL context is created + + // Set up initial projection matrix (identity) + memset(projectionMatrix_, 0, sizeof(projectionMatrix_)); + projectionMatrix_[0] = 1.0f; + projectionMatrix_[5] = 1.0f; + projectionMatrix_[10] = 1.0f; + projectionMatrix_[15] = 1.0f; + + initialized_ = true; + return true; +} + +bool SDL2Renderer::initGL() { + if (glInitialized_) return true; + + // Initialize built-in shaders (requires active GL context) + initBuiltinShaders(); + + glInitialized_ = true; + return true; +} + +void SDL2Renderer::shutdown() { + if (!initialized_) return; + + // Delete built-in shader programs + if (shapeProgram_) glDeleteProgram(shapeProgram_); + if (spriteProgram_) glDeleteProgram(spriteProgram_); + if (textProgram_) glDeleteProgram(textProgram_); + + shapeProgram_ = spriteProgram_ = textProgram_ = 0; + + SDL_Quit(); + initialized_ = false; +} + +void SDL2Renderer::initBuiltinShaders() { + // Compile shape shader + if (!compileAndLinkProgram(SHAPE_VERTEX_SHADER, SHAPE_FRAGMENT_SHADER, shapeProgram_)) { + std::cerr << "SDL2Renderer: Failed to compile shape shader" << std::endl; + } + + // Compile sprite shader + if (!compileAndLinkProgram(SPRITE_VERTEX_SHADER, SPRITE_FRAGMENT_SHADER, spriteProgram_)) { + std::cerr << "SDL2Renderer: Failed to compile sprite shader" << std::endl; + } + + // Compile text shader + if (!compileAndLinkProgram(TEXT_VERTEX_SHADER, TEXT_FRAGMENT_SHADER, textProgram_)) { + std::cerr << "SDL2Renderer: Failed to compile text shader" << std::endl; + } +} + +unsigned int SDL2Renderer::compileShaderStage(unsigned int type, const char* source) { + unsigned int shader = glCreateShader(type); + glShaderSource(shader, 1, &source, nullptr); + glCompileShader(shader); + + int success; + glGetShaderiv(shader, GL_COMPILE_STATUS, &success); + if (!success) { + char infoLog[512]; + glGetShaderInfoLog(shader, 512, nullptr, infoLog); + std::cerr << "SDL2Renderer: Shader compilation failed: " << infoLog << std::endl; + glDeleteShader(shader); + return 0; + } + + return shader; +} + +bool SDL2Renderer::compileAndLinkProgram(const char* vertexSrc, const char* fragmentSrc, unsigned int& programOut) { + unsigned int vertexShader = compileShaderStage(GL_VERTEX_SHADER, vertexSrc); + if (!vertexShader) return false; + + unsigned int fragmentShader = compileShaderStage(GL_FRAGMENT_SHADER, fragmentSrc); + if (!fragmentShader) { + glDeleteShader(vertexShader); + return false; + } + + unsigned int program = glCreateProgram(); + glAttachShader(program, vertexShader); + glAttachShader(program, fragmentShader); + + // Bind attribute locations before linking + glBindAttribLocation(program, 0, "a_position"); + glBindAttribLocation(program, 1, "a_color"); + glBindAttribLocation(program, 2, "a_texcoord"); + + glLinkProgram(program); + + // Shaders can be deleted after linking + glDeleteShader(vertexShader); + glDeleteShader(fragmentShader); + + int success; + glGetProgramiv(program, GL_LINK_STATUS, &success); + if (!success) { + char infoLog[512]; + glGetProgramInfoLog(program, 512, nullptr, infoLog); + std::cerr << "SDL2Renderer: Program linking failed: " << infoLog << std::endl; + glDeleteProgram(program); + return false; + } + + programOut = program; + return true; +} + +unsigned int SDL2Renderer::getShaderProgram(ShaderType type) const { + switch (type) { + case ShaderType::Shape: return shapeProgram_; + case ShaderType::Sprite: return spriteProgram_; + case ShaderType::Text: return textProgram_; + default: return 0; + } +} + +unsigned int SDL2Renderer::compileShader(const std::string& vertexSource, const std::string& fragmentSource) { + unsigned int program = 0; + if (compileAndLinkProgram(vertexSource.c_str(), fragmentSource.c_str(), program)) { + return program; + } + return 0; +} + +void SDL2Renderer::deleteShaderProgram(unsigned int programId) { + if (programId) { + glDeleteProgram(programId); + } +} + +unsigned int SDL2Renderer::createTexture(unsigned int width, unsigned int height, const unsigned char* pixels) { + unsigned int textureId; + glGenTextures(1, &textureId); + glBindTexture(GL_TEXTURE_2D, textureId); + + // Set default texture parameters + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + // Upload pixel data (RGBA format) + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + + return textureId; +} + +void SDL2Renderer::updateTexture(unsigned int textureId, unsigned int x, unsigned int y, + unsigned int width, unsigned int height, const unsigned char* pixels) { + glBindTexture(GL_TEXTURE_2D, textureId); + glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels); +} + +void SDL2Renderer::deleteTexture(unsigned int textureId) { + if (textureId) { + glDeleteTextures(1, &textureId); + } +} + +void SDL2Renderer::setTextureSmooth(unsigned int textureId, bool smooth) { + glBindTexture(GL_TEXTURE_2D, textureId); + GLint filter = smooth ? GL_LINEAR : GL_NEAREST; + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter); +} + +void SDL2Renderer::setTextureRepeated(unsigned int textureId, bool repeated) { + glBindTexture(GL_TEXTURE_2D, textureId); + GLint wrap = repeated ? GL_REPEAT : GL_CLAMP_TO_EDGE; + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap); +} + +unsigned int SDL2Renderer::createFBO(unsigned int width, unsigned int height, unsigned int& colorTexture) { + // Create color texture + colorTexture = createTexture(width, height, nullptr); + + // Create FBO + unsigned int fbo; + glGenFramebuffers(1, &fbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + + // Attach color texture + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTexture, 0); + + // Check completeness + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + std::cerr << "SDL2Renderer: FBO is not complete" << std::endl; + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo); + deleteTexture(colorTexture); + return 0; + } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + return fbo; +} + +void SDL2Renderer::deleteFBO(unsigned int fboId) { + if (fboId) { + glDeleteFramebuffers(1, &fboId); + } +} + +void SDL2Renderer::bindFBO(unsigned int fboId) { + fboStack_.push_back(fboId); + glBindFramebuffer(GL_FRAMEBUFFER, fboId); +} + +void SDL2Renderer::unbindFBO() { + if (!fboStack_.empty()) { + fboStack_.pop_back(); + } + unsigned int fbo = fboStack_.empty() ? 0 : fboStack_.back(); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); +} + +void SDL2Renderer::setViewport(int x, int y, unsigned int width, unsigned int height) { + glViewport(x, y, width, height); +} + +void SDL2Renderer::setProjection(float left, float right, float bottom, float top) { + // Build orthographic projection matrix + float near = -1.0f; + float far = 1.0f; + + memset(projectionMatrix_, 0, sizeof(projectionMatrix_)); + projectionMatrix_[0] = 2.0f / (right - left); + projectionMatrix_[5] = 2.0f / (top - bottom); + projectionMatrix_[10] = -2.0f / (far - near); + projectionMatrix_[12] = -(right + left) / (right - left); + projectionMatrix_[13] = -(top + bottom) / (top - bottom); + projectionMatrix_[14] = -(far + near) / (far - near); + projectionMatrix_[15] = 1.0f; +} + +static int clearCount = 0; +void SDL2Renderer::clear(float r, float g, float b, float a) { + glClearColor(r, g, b, a); + glClear(GL_COLOR_BUFFER_BIT); + + // Debug: Log first few clears to confirm render loop is running + if (clearCount < 5) { + std::cout << "SDL2Renderer::clear(" << r << ", " << g << ", " << b << ", " << a << ") #" << clearCount << std::endl; + clearCount++; + } +} + +void SDL2Renderer::drawTriangles(const float* vertices, size_t vertexCount, + const float* colors, const float* texCoords, + unsigned int textureId) { + if (vertexCount == 0) return; + + unsigned int program = textureId ? spriteProgram_ : shapeProgram_; + glUseProgram(program); + + // Set projection uniform + int projLoc = glGetUniformLocation(program, "u_projection"); + glUniformMatrix4fv(projLoc, 1, GL_FALSE, projectionMatrix_); + + // Enable blending + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + // Set up vertex attributes + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, vertices); + + if (colors) { + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, colors); + } + + if (texCoords && textureId) { + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, texCoords); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, textureId); + int texLoc = glGetUniformLocation(program, "u_texture"); + glUniform1i(texLoc, 0); + } + + // Draw + glDrawArrays(GL_TRIANGLES, 0, vertexCount); + + // Clean up + glDisableVertexAttribArray(0); + if (colors) glDisableVertexAttribArray(1); + if (texCoords && textureId) glDisableVertexAttribArray(2); +} + +// ============================================================================= +// sf::RenderWindow Implementation +// ============================================================================= + +RenderWindow::~RenderWindow() { + close(); +} + +void RenderWindow::create(VideoMode mode, const std::string& title, uint32_t style) { + // Close any existing window + close(); + + // Initialize SDL2 renderer + if (!SDL2Renderer::getInstance().init()) { + std::cerr << "RenderWindow: Failed to initialize SDL2Renderer" << std::endl; + return; + } + + // Set OpenGL attributes for ES2/WebGL compatibility + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); +#ifdef __EMSCRIPTEN__ + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); +#else + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY); +#endif + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 0); + + // Convert sf::Style to SDL window flags + Uint32 sdlFlags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN; + if (style & Style::Fullscreen) { + sdlFlags |= SDL_WINDOW_FULLSCREEN; + } + if (style & Style::Resize) { + sdlFlags |= SDL_WINDOW_RESIZABLE; + } + if (!(style & Style::Titlebar)) { + sdlFlags |= SDL_WINDOW_BORDERLESS; + } + +#ifdef __EMSCRIPTEN__ + // For Emscripten, tell SDL2 which canvas element to use + // SDL_HINT_EMSCRIPTEN_CANVAS_SELECTOR = "SDL_EMSCRIPTEN_CANVAS_SELECTOR" + SDL_SetHint("SDL_EMSCRIPTEN_CANVAS_SELECTOR", "#canvas"); + + // Set the canvas size explicitly before creating the window + emscripten_set_canvas_element_size("#canvas", mode.width, mode.height); + + std::cout << "Emscripten: Setting canvas to " << mode.width << "x" << mode.height << std::endl; +#endif + + // Create window + SDL_Window* window = SDL_CreateWindow( + title.c_str(), + SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + mode.width, mode.height, + sdlFlags + ); + + if (!window) { + std::cerr << "RenderWindow: Failed to create window: " << SDL_GetError() << std::endl; + return; + } + + // Create OpenGL context + SDL_GLContext context = SDL_GL_CreateContext(window); + if (!context) { + std::cerr << "RenderWindow: Failed to create GL context: " << SDL_GetError() << std::endl; + SDL_DestroyWindow(window); + return; + } + + sdlWindow_ = window; + glContext_ = context; + size_ = Vector2u(mode.width, mode.height); + title_ = title; + open_ = true; + +#ifdef __EMSCRIPTEN__ + // Force canvas size AFTER SDL window creation (SDL may have reset it) + emscripten_set_canvas_element_size("#canvas", mode.width, mode.height); + + // Also set the CSS size to match + EM_ASM({ + var canvas = document.getElementById('canvas'); + if (canvas) { + canvas.width = $0; + canvas.height = $1; + canvas.style.width = $0 + 'px'; + canvas.style.height = $1 + 'px'; + console.log('EM_ASM: Set canvas to ' + $0 + 'x' + $1); + } else { + console.error('EM_ASM: Canvas element not found!'); + } + }, mode.width, mode.height); + + // Re-make context current after canvas resize + SDL_GL_MakeCurrent(window, context); +#endif + + // Initialize OpenGL resources now that we have a context + if (!SDL2Renderer::getInstance().initGL()) { + std::cerr << "RenderWindow: Failed to initialize OpenGL resources" << std::endl; + } + + // Set up initial view + view_ = View(FloatRect(0, 0, static_cast(mode.width), static_cast(mode.height))); + defaultView_ = view_; + + // Set up OpenGL state + glViewport(0, 0, mode.width, mode.height); + std::cout << "GL viewport set to " << mode.width << "x" << mode.height << std::endl; + + GLenum err = glGetError(); + if (err != GL_NO_ERROR) { + std::cerr << "GL error after viewport: " << err << std::endl; + } + + SDL2Renderer::getInstance().setProjection(0, mode.width, mode.height, 0); + + // Enable blending for transparency + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + // Initial clear to a visible color to confirm GL is working + glClearColor(0.2f, 0.3f, 0.4f, 1.0f); // Blue-gray + glClear(GL_COLOR_BUFFER_BIT); + + err = glGetError(); + if (err != GL_NO_ERROR) { + std::cerr << "GL error after clear: " << err << std::endl; + } + + SDL_GL_SwapWindow(window); + + err = glGetError(); + if (err != GL_NO_ERROR) { + std::cerr << "GL error after swap: " << err << std::endl; + } + + std::cout << "RenderWindow: Created " << mode.width << "x" << mode.height << " window" << std::endl; + std::cout << "WebGL context should now show blue-gray" << std::endl; +} + +void RenderWindow::close() { + if (glContext_) { + SDL_GL_DeleteContext(static_cast(glContext_)); + glContext_ = nullptr; + } + if (sdlWindow_) { + SDL_DestroyWindow(static_cast(sdlWindow_)); + sdlWindow_ = nullptr; + } + open_ = false; +} + +void RenderWindow::clear(const Color& color) { + SDL2Renderer::getInstance().clear( + color.r / 255.0f, + color.g / 255.0f, + color.b / 255.0f, + color.a / 255.0f + ); +} + +void RenderWindow::display() { + SDL_GL_SwapWindow(static_cast(sdlWindow_)); +} + +void RenderWindow::setTitle(const std::string& title) { + title_ = title; + if (sdlWindow_) { + SDL_SetWindowTitle(static_cast(sdlWindow_), title.c_str()); + } +} + +void RenderWindow::setFramerateLimit(unsigned int limit) { + // SDL2 doesn't have built-in framerate limiting + // We'd need to implement this manually with timing + // For now, VSync is the recommended approach +} + +void RenderWindow::setVerticalSyncEnabled(bool enabled) { + SDL_GL_SetSwapInterval(enabled ? 1 : 0); +} + +void RenderWindow::setVisible(bool visible) { + if (sdlWindow_) { + if (visible) { + SDL_ShowWindow(static_cast(sdlWindow_)); + } else { + SDL_HideWindow(static_cast(sdlWindow_)); + } + } +} + +void RenderWindow::setMouseCursorVisible(bool visible) { + SDL_ShowCursor(visible ? SDL_ENABLE : SDL_DISABLE); +} + +void RenderWindow::setMouseCursorGrabbed(bool grabbed) { + if (sdlWindow_) { + SDL_SetWindowGrab(static_cast(sdlWindow_), grabbed ? SDL_TRUE : SDL_FALSE); + } +} + +Vector2i RenderWindow::getPosition() const { + int x = 0, y = 0; + if (sdlWindow_) { + SDL_GetWindowPosition(static_cast(sdlWindow_), &x, &y); + } + return Vector2i(x, y); +} + +void RenderWindow::setPosition(const Vector2i& position) { + if (sdlWindow_) { + SDL_SetWindowPosition(static_cast(sdlWindow_), position.x, position.y); + } +} + +void RenderWindow::setSize(const Vector2u& size) { + size_ = size; + if (sdlWindow_) { + SDL_SetWindowSize(static_cast(sdlWindow_), size.x, size.y); + glViewport(0, 0, size.x, size.y); + } +} + +// Event polling - translate SDL events to sf::Event +bool RenderWindow::pollEvent(Event& event) { + SDL_Event sdlEvent; + while (SDL_PollEvent(&sdlEvent)) { + if (translateSDLEvent(&sdlEvent, &event)) { + return true; + } + } + return false; +} + +bool RenderWindow::waitEvent(Event& event) { + SDL_Event sdlEvent; + if (SDL_WaitEvent(&sdlEvent)) { + return translateSDLEvent(&sdlEvent, &event); + } + return false; +} + +// ============================================================================= +// VideoMode Implementation +// ============================================================================= + +VideoMode VideoMode::getDesktopMode() { + SDL_DisplayMode mode; + if (SDL_GetDesktopDisplayMode(0, &mode) == 0) { + return VideoMode(mode.w, mode.h, SDL_BITSPERPIXEL(mode.format)); + } + return VideoMode(1920, 1080, 32); +} + +const std::vector& VideoMode::getFullscreenModes() { + static std::vector modes; + static bool initialized = false; + + if (!initialized) { + int numModes = SDL_GetNumDisplayModes(0); + for (int i = 0; i < numModes; ++i) { + SDL_DisplayMode mode; + if (SDL_GetDisplayMode(0, i, &mode) == 0) { + modes.push_back(VideoMode(mode.w, mode.h, SDL_BITSPERPIXEL(mode.format))); + } + } + initialized = true; + } + + return modes; +} + +// ============================================================================= +// Event Translation +// ============================================================================= + +// SDL scancode to sf::Keyboard::Key mapping table +static const Keyboard::Key SDL_SCANCODE_TO_SF_KEY[] = { + // This is a simplified mapping - full implementation would have all keys + Keyboard::Unknown // Placeholder +}; + +bool translateSDLEvent(const void* sdlEventPtr, void* sfEventPtr) { + const SDL_Event& sdlEvent = *static_cast(sdlEventPtr); + Event& sfEvent = *static_cast(sfEventPtr); + + switch (sdlEvent.type) { + case SDL_QUIT: + sfEvent.type = Event::Closed; + return true; + + case SDL_WINDOWEVENT: + switch (sdlEvent.window.event) { + case SDL_WINDOWEVENT_RESIZED: + case SDL_WINDOWEVENT_SIZE_CHANGED: + sfEvent.type = Event::Resized; + sfEvent.size.width = sdlEvent.window.data1; + sfEvent.size.height = sdlEvent.window.data2; + return true; + case SDL_WINDOWEVENT_FOCUS_GAINED: + sfEvent.type = Event::GainedFocus; + return true; + case SDL_WINDOWEVENT_FOCUS_LOST: + sfEvent.type = Event::LostFocus; + return true; + case SDL_WINDOWEVENT_ENTER: + sfEvent.type = Event::MouseEntered; + return true; + case SDL_WINDOWEVENT_LEAVE: + sfEvent.type = Event::MouseLeft; + return true; + } + break; + + case SDL_KEYDOWN: + case SDL_KEYUP: + sfEvent.type = sdlEvent.type == SDL_KEYDOWN ? Event::KeyPressed : Event::KeyReleased; + sfEvent.key.code = static_cast(sdlScancodeToSfKey(sdlEvent.key.keysym.scancode)); + sfEvent.key.alt = (sdlEvent.key.keysym.mod & KMOD_ALT) != 0; + sfEvent.key.control = (sdlEvent.key.keysym.mod & KMOD_CTRL) != 0; + sfEvent.key.shift = (sdlEvent.key.keysym.mod & KMOD_SHIFT) != 0; + sfEvent.key.system = (sdlEvent.key.keysym.mod & KMOD_GUI) != 0; + return true; + + case SDL_TEXTINPUT: + sfEvent.type = Event::TextEntered; + // Convert UTF-8 to single codepoint (simplified - only handles ASCII and simple UTF-8) + sfEvent.text.unicode = static_cast(sdlEvent.text.text[0]); + return true; + + case SDL_MOUSEMOTION: + sfEvent.type = Event::MouseMoved; + sfEvent.mouseMove.x = sdlEvent.motion.x; + sfEvent.mouseMove.y = sdlEvent.motion.y; + return true; + + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: + sfEvent.type = sdlEvent.type == SDL_MOUSEBUTTONDOWN ? Event::MouseButtonPressed : Event::MouseButtonReleased; + sfEvent.mouseButton.button = static_cast(sdlButtonToSfButton(sdlEvent.button.button)); + sfEvent.mouseButton.x = sdlEvent.button.x; + sfEvent.mouseButton.y = sdlEvent.button.y; + return true; + + case SDL_MOUSEWHEEL: + sfEvent.type = Event::MouseWheelScrolled; + sfEvent.mouseWheelScroll.wheel = sdlEvent.wheel.x != 0 ? Mouse::HorizontalWheel : Mouse::VerticalWheel; + sfEvent.mouseWheelScroll.delta = sdlEvent.wheel.x != 0 ? sdlEvent.wheel.x : sdlEvent.wheel.y; + // Get current mouse position + SDL_GetMouseState(&sfEvent.mouseWheelScroll.x, &sfEvent.mouseWheelScroll.y); + return true; + } + + return false; +} + +// ============================================================================= +// Keyboard/Mouse Implementation +// ============================================================================= + +int sdlScancodeToSfKey(int sdlScancode) { + // Simplified mapping - covers most common keys + switch (sdlScancode) { + case SDL_SCANCODE_A: return Keyboard::A; + case SDL_SCANCODE_B: return Keyboard::B; + case SDL_SCANCODE_C: return Keyboard::C; + case SDL_SCANCODE_D: return Keyboard::D; + case SDL_SCANCODE_E: return Keyboard::E; + case SDL_SCANCODE_F: return Keyboard::F; + case SDL_SCANCODE_G: return Keyboard::G; + case SDL_SCANCODE_H: return Keyboard::H; + case SDL_SCANCODE_I: return Keyboard::I; + case SDL_SCANCODE_J: return Keyboard::J; + case SDL_SCANCODE_K: return Keyboard::K; + case SDL_SCANCODE_L: return Keyboard::L; + case SDL_SCANCODE_M: return Keyboard::M; + case SDL_SCANCODE_N: return Keyboard::N; + case SDL_SCANCODE_O: return Keyboard::O; + case SDL_SCANCODE_P: return Keyboard::P; + case SDL_SCANCODE_Q: return Keyboard::Q; + case SDL_SCANCODE_R: return Keyboard::R; + case SDL_SCANCODE_S: return Keyboard::S; + case SDL_SCANCODE_T: return Keyboard::T; + case SDL_SCANCODE_U: return Keyboard::U; + case SDL_SCANCODE_V: return Keyboard::V; + case SDL_SCANCODE_W: return Keyboard::W; + case SDL_SCANCODE_X: return Keyboard::X; + case SDL_SCANCODE_Y: return Keyboard::Y; + case SDL_SCANCODE_Z: return Keyboard::Z; + case SDL_SCANCODE_0: return Keyboard::Num0; + case SDL_SCANCODE_1: return Keyboard::Num1; + case SDL_SCANCODE_2: return Keyboard::Num2; + case SDL_SCANCODE_3: return Keyboard::Num3; + case SDL_SCANCODE_4: return Keyboard::Num4; + case SDL_SCANCODE_5: return Keyboard::Num5; + case SDL_SCANCODE_6: return Keyboard::Num6; + case SDL_SCANCODE_7: return Keyboard::Num7; + case SDL_SCANCODE_8: return Keyboard::Num8; + case SDL_SCANCODE_9: return Keyboard::Num9; + case SDL_SCANCODE_ESCAPE: return Keyboard::Escape; + case SDL_SCANCODE_LCTRL: return Keyboard::LControl; + case SDL_SCANCODE_LSHIFT: return Keyboard::LShift; + case SDL_SCANCODE_LALT: return Keyboard::LAlt; + case SDL_SCANCODE_LGUI: return Keyboard::LSystem; + case SDL_SCANCODE_RCTRL: return Keyboard::RControl; + case SDL_SCANCODE_RSHIFT: return Keyboard::RShift; + case SDL_SCANCODE_RALT: return Keyboard::RAlt; + case SDL_SCANCODE_RGUI: return Keyboard::RSystem; + case SDL_SCANCODE_SPACE: return Keyboard::Space; + case SDL_SCANCODE_RETURN: return Keyboard::Enter; + case SDL_SCANCODE_BACKSPACE: return Keyboard::Backspace; + case SDL_SCANCODE_TAB: return Keyboard::Tab; + case SDL_SCANCODE_LEFT: return Keyboard::Left; + case SDL_SCANCODE_RIGHT: return Keyboard::Right; + case SDL_SCANCODE_UP: return Keyboard::Up; + case SDL_SCANCODE_DOWN: return Keyboard::Down; + case SDL_SCANCODE_F1: return Keyboard::F1; + case SDL_SCANCODE_F2: return Keyboard::F2; + case SDL_SCANCODE_F3: return Keyboard::F3; + case SDL_SCANCODE_F4: return Keyboard::F4; + case SDL_SCANCODE_F5: return Keyboard::F5; + case SDL_SCANCODE_F6: return Keyboard::F6; + case SDL_SCANCODE_F7: return Keyboard::F7; + case SDL_SCANCODE_F8: return Keyboard::F8; + case SDL_SCANCODE_F9: return Keyboard::F9; + case SDL_SCANCODE_F10: return Keyboard::F10; + case SDL_SCANCODE_F11: return Keyboard::F11; + case SDL_SCANCODE_F12: return Keyboard::F12; + default: return Keyboard::Unknown; + } +} + +int sfKeyToSdlScancode(int sfKey) { + // Reverse mapping (simplified) + switch (sfKey) { + case Keyboard::A: return SDL_SCANCODE_A; + case Keyboard::W: return SDL_SCANCODE_W; + case Keyboard::S: return SDL_SCANCODE_S; + case Keyboard::D: return SDL_SCANCODE_D; + case Keyboard::Space: return SDL_SCANCODE_SPACE; + case Keyboard::Escape: return SDL_SCANCODE_ESCAPE; + // Add more as needed + default: return SDL_SCANCODE_UNKNOWN; + } +} + +int sdlButtonToSfButton(int sdlButton) { + switch (sdlButton) { + case SDL_BUTTON_LEFT: return Mouse::Left; + case SDL_BUTTON_RIGHT: return Mouse::Right; + case SDL_BUTTON_MIDDLE: return Mouse::Middle; + case SDL_BUTTON_X1: return Mouse::XButton1; + case SDL_BUTTON_X2: return Mouse::XButton2; + default: return Mouse::Left; + } +} + +int sfButtonToSdlButton(int sfButton) { + switch (sfButton) { + case Mouse::Left: return SDL_BUTTON_LEFT; + case Mouse::Right: return SDL_BUTTON_RIGHT; + case Mouse::Middle: return SDL_BUTTON_MIDDLE; + case Mouse::XButton1: return SDL_BUTTON_X1; + case Mouse::XButton2: return SDL_BUTTON_X2; + default: return SDL_BUTTON_LEFT; + } +} + +bool Keyboard::isKeyPressed(Key key) { + const Uint8* state = SDL_GetKeyboardState(nullptr); + int scancode = sfKeyToSdlScancode(key); + return scancode != SDL_SCANCODE_UNKNOWN && state[scancode]; +} + +bool Mouse::isButtonPressed(Button button) { + Uint32 state = SDL_GetMouseState(nullptr, nullptr); + return state & SDL_BUTTON(sfButtonToSdlButton(button)); +} + +Vector2i Mouse::getPosition() { + int x, y; + SDL_GetMouseState(&x, &y); + return Vector2i(x, y); +} + +Vector2i Mouse::getPosition(const RenderWindow& relativeTo) { + // For now, same as global position (would need window-relative in multi-window setup) + return getPosition(); +} + +void Mouse::setPosition(const Vector2i& position) { + SDL_WarpMouseGlobal(position.x, position.y); +} + +void Mouse::setPosition(const Vector2i& position, const RenderWindow& relativeTo) { + SDL_WarpMouseInWindow( + static_cast(relativeTo.getNativeWindowHandle()), + position.x, position.y + ); +} + +// ============================================================================= +// RenderTarget Implementation +// ============================================================================= + +void RenderTarget::clear(const Color& color) { + SDL2Renderer::getInstance().clear( + color.r / 255.0f, + color.g / 255.0f, + color.b / 255.0f, + color.a / 255.0f + ); +} + +void RenderTarget::draw(const Vertex* vertices, size_t vertexCount, PrimitiveType type, const RenderStates& states) { + // TODO: Implement with proper vertex buffer handling +} + +void RenderTarget::draw(const VertexArray& vertices, const RenderStates& states) { + draw(&vertices[0], vertices.getVertexCount(), vertices.getPrimitiveType(), states); +} + +// ============================================================================= +// RenderTexture Implementation +// ============================================================================= + +RenderTexture::~RenderTexture() { + if (fboId_) { + SDL2Renderer::getInstance().deleteFBO(fboId_); + } +} + +bool RenderTexture::create(unsigned int width, unsigned int height) { + size_ = Vector2u(width, height); + + unsigned int colorTexture = 0; + fboId_ = SDL2Renderer::getInstance().createFBO(width, height, colorTexture); + + if (!fboId_) { + return false; + } + + // Set up internal texture to point to FBO color attachment + texture_.setNativeHandle(colorTexture); + + view_ = View(FloatRect(0, 0, static_cast(width), static_cast(height))); + defaultView_ = view_; + + return true; +} + +void RenderTexture::clear(const Color& color) { + SDL2Renderer::getInstance().bindFBO(fboId_); + RenderTarget::clear(color); +} + +void RenderTexture::display() { + SDL2Renderer::getInstance().unbindFBO(); +} + +// ============================================================================= +// Texture Implementation +// ============================================================================= + +Texture::~Texture() { + if (textureId_) { + SDL2Renderer::getInstance().deleteTexture(textureId_); + } +} + +Texture::Texture(const Texture& other) + : size_(other.size_), smooth_(other.smooth_), repeated_(other.repeated_) { + if (other.textureId_) { + // Create new texture with same properties + textureId_ = SDL2Renderer::getInstance().createTexture(size_.x, size_.y, nullptr); + // Note: Would need to copy pixel data for full implementation + } +} + +Texture& Texture::operator=(const Texture& other) { + if (this != &other) { + if (textureId_) { + SDL2Renderer::getInstance().deleteTexture(textureId_); + } + size_ = other.size_; + smooth_ = other.smooth_; + repeated_ = other.repeated_; + if (other.textureId_) { + textureId_ = SDL2Renderer::getInstance().createTexture(size_.x, size_.y, nullptr); + } + } + return *this; +} + +bool Texture::create(unsigned int width, unsigned int height) { + if (textureId_) { + SDL2Renderer::getInstance().deleteTexture(textureId_); + } + size_ = Vector2u(width, height); + textureId_ = SDL2Renderer::getInstance().createTexture(width, height, nullptr); + return textureId_ != 0; +} + +bool Texture::loadFromFile(const std::string& filename) { + int width, height, channels; + unsigned char* data = stbi_load(filename.c_str(), &width, &height, &channels, 4); + + if (!data) { + std::cerr << "Texture: Failed to load " << filename << ": " << stbi_failure_reason() << std::endl; + return false; + } + + if (textureId_) { + SDL2Renderer::getInstance().deleteTexture(textureId_); + } + + size_ = Vector2u(width, height); + textureId_ = SDL2Renderer::getInstance().createTexture(width, height, data); + + stbi_image_free(data); + return textureId_ != 0; +} + +bool Texture::loadFromMemory(const void* data, size_t size) { + int width, height, channels; + unsigned char* pixels = stbi_load_from_memory( + static_cast(data), size, &width, &height, &channels, 4); + + if (!pixels) { + return false; + } + + if (textureId_) { + SDL2Renderer::getInstance().deleteTexture(textureId_); + } + + size_ = Vector2u(width, height); + textureId_ = SDL2Renderer::getInstance().createTexture(width, height, pixels); + + stbi_image_free(pixels); + return textureId_ != 0; +} + +void Texture::setSmooth(bool smooth) { + smooth_ = smooth; + if (textureId_) { + SDL2Renderer::getInstance().setTextureSmooth(textureId_, smooth); + } +} + +void Texture::setRepeated(bool repeated) { + repeated_ = repeated; + if (textureId_) { + SDL2Renderer::getInstance().setTextureRepeated(textureId_, repeated); + } +} + +Image Texture::copyToImage() const { + Image img; + img.create(size_.x, size_.y); + // TODO: Read back from GPU texture + return img; +} + +void Texture::update(const RenderWindow& window) { + // TODO: Copy window contents to texture +} + +void Texture::update(const Uint8* pixels) { + if (textureId_ && pixels) { + SDL2Renderer::getInstance().updateTexture(textureId_, 0, 0, size_.x, size_.y, pixels); + } +} + +void Texture::update(const Uint8* pixels, unsigned int width, unsigned int height, unsigned int x, unsigned int y) { + if (textureId_ && pixels) { + SDL2Renderer::getInstance().updateTexture(textureId_, x, y, width, height, pixels); + } +} + +// ============================================================================= +// Image Implementation +// ============================================================================= + +bool Image::loadFromFile(const std::string& filename) { + int width, height, channels; + unsigned char* data = stbi_load(filename.c_str(), &width, &height, &channels, 4); + + if (!data) { + return false; + } + + size_ = Vector2u(width, height); + pixels_.resize(width * height * 4); + memcpy(pixels_.data(), data, pixels_.size()); + + stbi_image_free(data); + return true; +} + +bool Image::saveToFile(const std::string& filename) const { + // TODO: Use stb_image_write + return false; +} + +// ============================================================================= +// Font Implementation +// ============================================================================= + +bool Font::loadFromFile(const std::string& filename) { + FILE* file = fopen(filename.c_str(), "rb"); + if (!file) { + return false; + } + + fseek(file, 0, SEEK_END); + size_t size = ftell(file); + fseek(file, 0, SEEK_SET); + + fontData_.resize(size); + fread(fontData_.data(), 1, size, file); + fclose(file); + + loaded_ = true; + return true; +} + +bool Font::loadFromMemory(const void* data, size_t sizeInBytes) { + fontData_.resize(sizeInBytes); + memcpy(fontData_.data(), data, sizeInBytes); + loaded_ = true; + return true; +} + +// ============================================================================= +// Shape Drawing (Stubs - implement with vertex generation) +// ============================================================================= + +void Shape::draw(RenderTarget& target, RenderStates states) const { + size_t pointCount = getPointCount(); + if (pointCount < 3) return; + + // Get the combined transform + Transform combinedTransform = states.transform * getTransform(); + + // Build vertex data for fill (triangle fan from center) + std::vector vertices; + std::vector colors; + + // Calculate center point + Vector2f center(0, 0); + for (size_t i = 0; i < pointCount; ++i) { + center.x += getPoint(i).x; + center.y += getPoint(i).y; + } + center.x /= pointCount; + center.y /= pointCount; + + // Transform center + Vector2f transformedCenter = combinedTransform.transformPoint(center); + + // Build triangles (fan from center) + Color fill = getFillColor(); + float fr = fill.r / 255.0f; + float fg = fill.g / 255.0f; + float fb = fill.b / 255.0f; + float fa = fill.a / 255.0f; + + for (size_t i = 0; i < pointCount; ++i) { + size_t next = (i + 1) % pointCount; + + Vector2f p1 = combinedTransform.transformPoint(getPoint(i)); + Vector2f p2 = combinedTransform.transformPoint(getPoint(next)); + + // Triangle: center, p1, p2 + vertices.push_back(transformedCenter.x); + vertices.push_back(transformedCenter.y); + vertices.push_back(p1.x); + vertices.push_back(p1.y); + vertices.push_back(p2.x); + vertices.push_back(p2.y); + + // Colors for each vertex + for (int v = 0; v < 3; ++v) { + colors.push_back(fr); + colors.push_back(fg); + colors.push_back(fb); + colors.push_back(fa); + } + } + + // Draw fill + if (fill.a > 0 && !vertices.empty()) { + SDL2Renderer::getInstance().drawTriangles( + vertices.data(), vertices.size() / 2, + colors.data(), nullptr, 0 + ); + } + + // Draw outline if thickness > 0 + float outlineThickness = getOutlineThickness(); + if (outlineThickness > 0) { + Color outline = getOutlineColor(); + if (outline.a > 0) { + float or_ = outline.r / 255.0f; + float og = outline.g / 255.0f; + float ob = outline.b / 255.0f; + float oa = outline.a / 255.0f; + + // Build outline as quads (two triangles per edge) + vertices.clear(); + colors.clear(); + + for (size_t i = 0; i < pointCount; ++i) { + size_t next = (i + 1) % pointCount; + + Vector2f p1 = combinedTransform.transformPoint(getPoint(i)); + Vector2f p2 = combinedTransform.transformPoint(getPoint(next)); + + // Calculate normal direction + Vector2f dir(p2.x - p1.x, p2.y - p1.y); + float len = std::sqrt(dir.x * dir.x + dir.y * dir.y); + if (len > 0) { + dir.x /= len; + dir.y /= len; + } + Vector2f normal(-dir.y * outlineThickness, dir.x * outlineThickness); + + // Outer points + Vector2f p1o(p1.x + normal.x, p1.y + normal.y); + Vector2f p2o(p2.x + normal.x, p2.y + normal.y); + + // Two triangles for quad + // Triangle 1: p1, p2, p1o + vertices.push_back(p1.x); vertices.push_back(p1.y); + vertices.push_back(p2.x); vertices.push_back(p2.y); + vertices.push_back(p1o.x); vertices.push_back(p1o.y); + // Triangle 2: p2, p2o, p1o + vertices.push_back(p2.x); vertices.push_back(p2.y); + vertices.push_back(p2o.x); vertices.push_back(p2o.y); + vertices.push_back(p1o.x); vertices.push_back(p1o.y); + + for (int v = 0; v < 6; ++v) { + colors.push_back(or_); + colors.push_back(og); + colors.push_back(ob); + colors.push_back(oa); + } + } + + if (!vertices.empty()) { + SDL2Renderer::getInstance().drawTriangles( + vertices.data(), vertices.size() / 2, + colors.data(), nullptr, 0 + ); + } + } + } +} + +void VertexArray::draw(RenderTarget& target, RenderStates states) const { + if (vertices_.empty()) return; + + // Convert vertex array to flat arrays based on primitive type + std::vector positions; + std::vector colors; + std::vector texcoords; + + auto addVertex = [&](const Vertex& v) { + Vector2f p = states.transform.transformPoint(v.position); + positions.push_back(p.x); + positions.push_back(p.y); + colors.push_back(v.color.r / 255.0f); + colors.push_back(v.color.g / 255.0f); + colors.push_back(v.color.b / 255.0f); + colors.push_back(v.color.a / 255.0f); + texcoords.push_back(v.texCoords.x); + texcoords.push_back(v.texCoords.y); + }; + + switch (primitiveType_) { + case Triangles: + // Already in triangle format + for (size_t i = 0; i < vertices_.size(); ++i) { + addVertex(vertices_[i]); + } + break; + + case TriangleFan: + // Convert fan to triangles: v0, v1, v2, then v0, v2, v3, etc. + if (vertices_.size() >= 3) { + for (size_t i = 1; i < vertices_.size() - 1; ++i) { + addVertex(vertices_[0]); + addVertex(vertices_[i]); + addVertex(vertices_[i + 1]); + } + } + break; + + case TriangleStrip: + // Convert strip to triangles + if (vertices_.size() >= 3) { + for (size_t i = 0; i < vertices_.size() - 2; ++i) { + if (i % 2 == 0) { + addVertex(vertices_[i]); + addVertex(vertices_[i + 1]); + addVertex(vertices_[i + 2]); + } else { + // Flip winding for odd triangles + addVertex(vertices_[i + 1]); + addVertex(vertices_[i]); + addVertex(vertices_[i + 2]); + } + } + } + break; + + case Quads: + // Convert quads to triangles (4 vertices -> 2 triangles) + for (size_t i = 0; i + 3 < vertices_.size(); i += 4) { + // Triangle 1: v0, v1, v2 + addVertex(vertices_[i]); + addVertex(vertices_[i + 1]); + addVertex(vertices_[i + 2]); + // Triangle 2: v0, v2, v3 + addVertex(vertices_[i]); + addVertex(vertices_[i + 2]); + addVertex(vertices_[i + 3]); + } + break; + + case Lines: + // Draw lines as thin quads (2 triangles per line) + for (size_t i = 0; i + 1 < vertices_.size(); i += 2) { + Vector2f p1 = states.transform.transformPoint(vertices_[i].position); + Vector2f p2 = states.transform.transformPoint(vertices_[i + 1].position); + + // Calculate perpendicular for line thickness (1 pixel) + Vector2f dir(p2.x - p1.x, p2.y - p1.y); + float len = std::sqrt(dir.x * dir.x + dir.y * dir.y); + if (len > 0) { + dir.x /= len; + dir.y /= len; + } + Vector2f perp(-dir.y * 0.5f, dir.x * 0.5f); + + // Build thin quad + Vector2f v0(p1.x - perp.x, p1.y - perp.y); + Vector2f v1(p1.x + perp.x, p1.y + perp.y); + Vector2f v2(p2.x + perp.x, p2.y + perp.y); + Vector2f v3(p2.x - perp.x, p2.y - perp.y); + + const Vertex& vert1 = vertices_[i]; + const Vertex& vert2 = vertices_[i + 1]; + + // Triangle 1 + positions.insert(positions.end(), {v0.x, v0.y, v1.x, v1.y, v2.x, v2.y}); + for (int j = 0; j < 2; ++j) { + colors.insert(colors.end(), {vert1.color.r/255.f, vert1.color.g/255.f, vert1.color.b/255.f, vert1.color.a/255.f}); + } + colors.insert(colors.end(), {vert2.color.r/255.f, vert2.color.g/255.f, vert2.color.b/255.f, vert2.color.a/255.f}); + texcoords.insert(texcoords.end(), {0, 0, 0, 0, 0, 0}); + + // Triangle 2 + positions.insert(positions.end(), {v0.x, v0.y, v2.x, v2.y, v3.x, v3.y}); + colors.insert(colors.end(), {vert1.color.r/255.f, vert1.color.g/255.f, vert1.color.b/255.f, vert1.color.a/255.f}); + colors.insert(colors.end(), {vert2.color.r/255.f, vert2.color.g/255.f, vert2.color.b/255.f, vert2.color.a/255.f}); + colors.insert(colors.end(), {vert2.color.r/255.f, vert2.color.g/255.f, vert2.color.b/255.f, vert2.color.a/255.f}); + texcoords.insert(texcoords.end(), {0, 0, 0, 0, 0, 0}); + } + break; + + case LineStrip: + // Similar to Lines but connected + for (size_t i = 0; i + 1 < vertices_.size(); ++i) { + Vector2f p1 = states.transform.transformPoint(vertices_[i].position); + Vector2f p2 = states.transform.transformPoint(vertices_[i + 1].position); + + Vector2f dir(p2.x - p1.x, p2.y - p1.y); + float len = std::sqrt(dir.x * dir.x + dir.y * dir.y); + if (len > 0) { + dir.x /= len; + dir.y /= len; + } + Vector2f perp(-dir.y * 0.5f, dir.x * 0.5f); + + Vector2f v0(p1.x - perp.x, p1.y - perp.y); + Vector2f v1(p1.x + perp.x, p1.y + perp.y); + Vector2f v2(p2.x + perp.x, p2.y + perp.y); + Vector2f v3(p2.x - perp.x, p2.y - perp.y); + + const Vertex& vert1 = vertices_[i]; + const Vertex& vert2 = vertices_[i + 1]; + + positions.insert(positions.end(), {v0.x, v0.y, v1.x, v1.y, v2.x, v2.y}); + for (int j = 0; j < 2; ++j) { + colors.insert(colors.end(), {vert1.color.r/255.f, vert1.color.g/255.f, vert1.color.b/255.f, vert1.color.a/255.f}); + } + colors.insert(colors.end(), {vert2.color.r/255.f, vert2.color.g/255.f, vert2.color.b/255.f, vert2.color.a/255.f}); + texcoords.insert(texcoords.end(), {0, 0, 0, 0, 0, 0}); + + positions.insert(positions.end(), {v0.x, v0.y, v2.x, v2.y, v3.x, v3.y}); + colors.insert(colors.end(), {vert1.color.r/255.f, vert1.color.g/255.f, vert1.color.b/255.f, vert1.color.a/255.f}); + colors.insert(colors.end(), {vert2.color.r/255.f, vert2.color.g/255.f, vert2.color.b/255.f, vert2.color.a/255.f}); + colors.insert(colors.end(), {vert2.color.r/255.f, vert2.color.g/255.f, vert2.color.b/255.f, vert2.color.a/255.f}); + texcoords.insert(texcoords.end(), {0, 0, 0, 0, 0, 0}); + } + break; + + case Points: + // Draw points as small quads + for (size_t i = 0; i < vertices_.size(); ++i) { + Vector2f p = states.transform.transformPoint(vertices_[i].position); + const Vertex& v = vertices_[i]; + + // 2x2 pixel quad centered on point + positions.insert(positions.end(), { + p.x - 1, p.y - 1, p.x + 1, p.y - 1, p.x + 1, p.y + 1, + p.x - 1, p.y - 1, p.x + 1, p.y + 1, p.x - 1, p.y + 1 + }); + for (int j = 0; j < 6; ++j) { + colors.insert(colors.end(), {v.color.r/255.f, v.color.g/255.f, v.color.b/255.f, v.color.a/255.f}); + } + texcoords.insert(texcoords.end(), {0,0, 0,0, 0,0, 0,0, 0,0, 0,0}); + } + break; + } + + if (!positions.empty()) { + // Use shape shader (no texture) + glUseProgram(SDL2Renderer::getInstance().getShaderProgram(SDL2Renderer::ShaderType::Shape)); + SDL2Renderer::getInstance().drawTriangles( + positions.data(), positions.size() / 2, + colors.data(), nullptr, 0 + ); + } +} + +void Sprite::draw(RenderTarget& target, RenderStates states) const { + if (!texture_) return; + + Transform combined = states.transform * getTransform(); + + // Get texture rectangle (use full texture if not set) + IntRect rect = textureRect_; + if (rect.width == 0 || rect.height == 0) { + rect = IntRect(0, 0, texture_->getSize().x, texture_->getSize().y); + } + + // Four corners of sprite in local space + Vector2f p0 = combined.transformPoint(0, 0); + Vector2f p1 = combined.transformPoint(static_cast(rect.width), 0); + Vector2f p2 = combined.transformPoint(static_cast(rect.width), static_cast(rect.height)); + Vector2f p3 = combined.transformPoint(0, static_cast(rect.height)); + + // Texture coordinates (normalized) + Vector2u texSize = texture_->getSize(); + if (texSize.x == 0 || texSize.y == 0) return; + + float u0 = rect.left / static_cast(texSize.x); + float v0 = rect.top / static_cast(texSize.y); + float u1 = (rect.left + rect.width) / static_cast(texSize.x); + float v1 = (rect.top + rect.height) / static_cast(texSize.y); + + // Two triangles forming a quad (6 vertices) + float vertices[] = { + p0.x, p0.y, p1.x, p1.y, p2.x, p2.y, // Triangle 1 + p0.x, p0.y, p2.x, p2.y, p3.x, p3.y // Triangle 2 + }; + + float texcoords[] = { + u0, v0, u1, v0, u1, v1, // Triangle 1 + u0, v0, u1, v1, u0, v1 // Triangle 2 + }; + + // Color tint for all 6 vertices + float colors[24]; + float r = color_.r / 255.0f; + float g = color_.g / 255.0f; + float b = color_.b / 255.0f; + float a = color_.a / 255.0f; + for (int i = 0; i < 6; ++i) { + colors[i * 4 + 0] = r; + colors[i * 4 + 1] = g; + colors[i * 4 + 2] = b; + colors[i * 4 + 3] = a; + } + + // Use sprite shader and draw + glUseProgram(SDL2Renderer::getInstance().getShaderProgram(SDL2Renderer::ShaderType::Sprite)); + SDL2Renderer::getInstance().drawTriangles(vertices, 6, colors, texcoords, texture_->getNativeHandle()); +} + +// Static cache for font atlases - keyed by (font data pointer, character size) +static std::map, FontAtlas> s_fontAtlasCache; + +void Text::draw(RenderTarget& target, RenderStates states) const { + if (!font_ || string_.empty() || !font_->isLoaded()) return; + + // Get or create font atlas for this font + size combination + auto key = std::make_pair(font_, characterSize_); + auto it = s_fontAtlasCache.find(key); + if (it == s_fontAtlasCache.end()) { + FontAtlas atlas; + if (!atlas.load(font_->getData(), font_->getDataSize(), static_cast(characterSize_))) { + return; // Failed to create atlas + } + it = s_fontAtlasCache.emplace(key, std::move(atlas)).first; + } + const FontAtlas& atlas = it->second; + + Transform combined = states.transform * getTransform(); + + // Build vertex data for all glyphs + std::vector vertices; + std::vector texcoords; + std::vector colors; + + float x = 0; + float y = atlas.getAscent(); // Start at baseline + + float r = fillColor_.r / 255.0f; + float g = fillColor_.g / 255.0f; + float b = fillColor_.b / 255.0f; + float a = fillColor_.a / 255.0f; + + for (size_t i = 0; i < string_.size(); ++i) { + char c = string_[i]; + + // Handle newlines + if (c == '\n') { + x = 0; + y += atlas.getLineHeight(); + continue; + } + + FontAtlas::GlyphInfo glyph; + if (!atlas.getGlyph(static_cast(c), glyph)) { + // Try space for unknown glyphs + if (!atlas.getGlyph(' ', glyph)) { + continue; + } + } + + // Calculate quad corners in local space + float x0 = x + glyph.xoff; + float y0 = y + glyph.yoff; + float x1 = x0 + glyph.width; + float y1 = y0 + glyph.height; + + // Transform to world space + Vector2f p0 = combined.transformPoint(x0, y0); + Vector2f p1 = combined.transformPoint(x1, y0); + Vector2f p2 = combined.transformPoint(x1, y1); + Vector2f p3 = combined.transformPoint(x0, y1); + + // Two triangles for quad + vertices.insert(vertices.end(), { + p0.x, p0.y, p1.x, p1.y, p2.x, p2.y, // Triangle 1 + p0.x, p0.y, p2.x, p2.y, p3.x, p3.y // Triangle 2 + }); + + texcoords.insert(texcoords.end(), { + glyph.u0, glyph.v0, glyph.u1, glyph.v0, glyph.u1, glyph.v1, // Triangle 1 + glyph.u0, glyph.v0, glyph.u1, glyph.v1, glyph.u0, glyph.v1 // Triangle 2 + }); + + // 6 vertices * 4 color components + for (int v = 0; v < 6; ++v) { + colors.insert(colors.end(), {r, g, b, a}); + } + + x += glyph.xadvance; + } + + if (!vertices.empty()) { + // Use text shader (uses alpha from texture) + glUseProgram(SDL2Renderer::getInstance().getShaderProgram(SDL2Renderer::ShaderType::Text)); + SDL2Renderer::getInstance().drawTriangles( + vertices.data(), vertices.size() / 2, + colors.data(), texcoords.data(), + atlas.getTextureId() + ); + } +} + +FloatRect Text::getLocalBounds() const { + if (!font_ || string_.empty() || !font_->isLoaded()) { + return FloatRect(0, 0, 0, 0); + } + + // Get or create font atlas for this font + size combination + auto key = std::make_pair(font_, characterSize_); + auto it = s_fontAtlasCache.find(key); + if (it == s_fontAtlasCache.end()) { + FontAtlas atlas; + if (!atlas.load(font_->getData(), font_->getDataSize(), static_cast(characterSize_))) { + return FloatRect(0, 0, 0, 0); + } + it = s_fontAtlasCache.emplace(key, std::move(atlas)).first; + } + const FontAtlas& atlas = it->second; + + float x = 0; + float maxX = 0; + float minY = 0; + float maxY = atlas.getLineHeight(); + int lineCount = 1; + + for (size_t i = 0; i < string_.size(); ++i) { + char c = string_[i]; + + if (c == '\n') { + maxX = std::max(maxX, x); + x = 0; + lineCount++; + continue; + } + + FontAtlas::GlyphInfo glyph; + if (atlas.getGlyph(static_cast(c), glyph)) { + x += glyph.xadvance; + } + } + + maxX = std::max(maxX, x); + maxY = atlas.getLineHeight() * lineCount; + + return FloatRect(0, 0, maxX, maxY); +} + +FloatRect Text::getGlobalBounds() const { + FloatRect local = getLocalBounds(); + Transform t = getTransform(); + return t.transformRect(local); +} + +// ============================================================================= +// Shader Implementation +// ============================================================================= + +Shader::~Shader() { + if (programId_) { + SDL2Renderer::getInstance().deleteShaderProgram(programId_); + } +} + +bool Shader::loadFromFile(const std::string& filename, Type type) { + // TODO: Load shader from file + return false; +} + +bool Shader::loadFromFile(const std::string& vertexFile, const std::string& fragmentFile) { + // TODO: Load shaders from files + return false; +} + +bool Shader::loadFromMemory(const std::string& shader, Type type) { + // For fragment-only shaders, use default vertex shader + if (type == Fragment) { + std::string defaultVertex = R"( + attribute vec2 a_position; + attribute vec4 a_color; + attribute vec2 a_texcoord; + uniform mat4 u_projection; + varying vec4 v_color; + varying vec2 v_texcoord; + void main() { + gl_Position = u_projection * vec4(a_position, 0.0, 1.0); + v_color = a_color; + v_texcoord = a_texcoord; + } + )"; + programId_ = SDL2Renderer::getInstance().compileShader(defaultVertex, shader); + loaded_ = programId_ != 0; + return loaded_; + } + return false; +} + +void Shader::setUniform(const std::string& name, float x) { + if (programId_) { + glUseProgram(programId_); + int loc = glGetUniformLocation(programId_, name.c_str()); + if (loc >= 0) glUniform1f(loc, x); + } +} + +void Shader::setUniform(const std::string& name, const Vector2f& v) { + if (programId_) { + glUseProgram(programId_); + int loc = glGetUniformLocation(programId_, name.c_str()); + if (loc >= 0) glUniform2f(loc, v.x, v.y); + } +} + +void Shader::setUniform(const std::string& name, const Color& color) { + if (programId_) { + glUseProgram(programId_); + int loc = glGetUniformLocation(programId_, name.c_str()); + if (loc >= 0) glUniform4f(loc, color.r/255.f, color.g/255.f, color.b/255.f, color.a/255.f); + } +} + +void Shader::setUniform(const std::string& name, const Texture& texture) { + // Texture binding is handled during draw +} + +void Shader::setUniform(const std::string& name, const Glsl::Vec3& v) { + if (programId_) { + glUseProgram(programId_); + int loc = glGetUniformLocation(programId_, name.c_str()); + if (loc >= 0) glUniform3f(loc, v.x, v.y, v.z); + } +} + +void Shader::setUniform(const std::string& name, const Glsl::Vec4& v) { + if (programId_) { + glUseProgram(programId_); + int loc = glGetUniformLocation(programId_, name.c_str()); + if (loc >= 0) glUniform4f(loc, v.x, v.y, v.z, v.w); + } +} + +void Shader::setUniform(const std::string& name, CurrentTextureType) { + // Handled during draw +} + +bool Shader::isAvailable() { + return SDL2Renderer::getInstance().isInitialized(); +} + +// ============================================================================= +// FontAtlas Implementation +// ============================================================================= + +FontAtlas::FontAtlas() = default; + +FontAtlas::~FontAtlas() { + if (textureId_) { + SDL2Renderer::getInstance().deleteTexture(textureId_); + } + if (stbFontInfo_) { + delete static_cast(stbFontInfo_); + } +} + +bool FontAtlas::load(const unsigned char* fontData, size_t dataSize, float fontSize) { + fontSize_ = fontSize; + + stbtt_fontinfo* info = new stbtt_fontinfo(); + if (!stbtt_InitFont(info, fontData, 0)) { + delete info; + return false; + } + + stbFontInfo_ = info; + + // Get font metrics + int ascent, descent, lineGap; + stbtt_GetFontVMetrics(info, &ascent, &descent, &lineGap); + + float scale = stbtt_ScaleForPixelHeight(info, fontSize); + ascent_ = ascent * scale; + descent_ = descent * scale; + lineHeight_ = (ascent - descent + lineGap) * scale; + + // Create glyph atlas (simple ASCII for now) + const int atlasSize = 512; + std::vector atlasPixels(atlasSize * atlasSize, 0); + + int x = 1, y = 1; + int rowHeight = 0; + + for (uint32_t c = 32; c < 128; ++c) { + int advance, lsb; + stbtt_GetCodepointHMetrics(info, c, &advance, &lsb); + + int x0, y0, x1, y1; + stbtt_GetCodepointBitmapBox(info, c, scale, scale, &x0, &y0, &x1, &y1); + + int w = x1 - x0; + int h = y1 - y0; + + if (x + w + 1 >= atlasSize) { + x = 1; + y += rowHeight + 1; + rowHeight = 0; + } + + if (y + h + 1 >= atlasSize) { + break; // Atlas full + } + + // Render glyph to atlas + stbtt_MakeCodepointBitmap(info, &atlasPixels[y * atlasSize + x], w, h, atlasSize, scale, scale, c); + + GlyphInfo glyph; + glyph.u0 = x / (float)atlasSize; + glyph.v0 = y / (float)atlasSize; + glyph.u1 = (x + w) / (float)atlasSize; + glyph.v1 = (y + h) / (float)atlasSize; + glyph.xoff = x0; + glyph.yoff = y0; + glyph.xadvance = advance * scale; + glyph.width = w; + glyph.height = h; + + glyphCache_[c] = glyph; + + x += w + 1; + rowHeight = std::max(rowHeight, h); + } + + // Convert single-channel to RGBA + std::vector rgbaPixels(atlasSize * atlasSize * 4); + for (int i = 0; i < atlasSize * atlasSize; ++i) { + rgbaPixels[i * 4 + 0] = 255; + rgbaPixels[i * 4 + 1] = 255; + rgbaPixels[i * 4 + 2] = 255; + rgbaPixels[i * 4 + 3] = atlasPixels[i]; + } + + textureId_ = SDL2Renderer::getInstance().createTexture(atlasSize, atlasSize, rgbaPixels.data()); + + return true; +} + +bool FontAtlas::getGlyph(uint32_t codepoint, GlyphInfo& info) const { + auto it = glyphCache_.find(codepoint); + if (it != glyphCache_.end()) { + info = it->second; + return true; + } + return false; +} + +} // namespace sf + +#endif // MCRF_SDL2 diff --git a/src/platform/SDL2Renderer.h b/src/platform/SDL2Renderer.h new file mode 100644 index 0000000..f5c3e30 --- /dev/null +++ b/src/platform/SDL2Renderer.h @@ -0,0 +1,182 @@ +// SDL2Renderer.h - OpenGL ES 2 rendering implementation for SDL2 backend +// This file provides the actual rendering implementation for the SDL2 types +// defined in SDL2Types.h. It handles: +// - OpenGL ES 2 context management +// - Shader compilation and management +// - Texture loading (via stb_image) +// - Font rendering (via stb_truetype) +// - Framebuffer object management +// +// Part of the renderer abstraction layer + +#pragma once + +#ifdef MCRF_SDL2 + +#include +#include +#include +#include + +// Forward declarations to avoid SDL header in this header +struct SDL_Window; +typedef void* SDL_GLContext; + +namespace sf { + +// Forward declarations +class RenderTarget; +class Texture; +class Shader; + +// ============================================================================= +// SDL2 Renderer - Singleton managing OpenGL ES 2 state +// ============================================================================= + +class SDL2Renderer { +public: + // Singleton access + static SDL2Renderer& getInstance(); + + // Initialization/shutdown + bool init(); // Initialize SDL2 (call before window creation) + bool initGL(); // Initialize OpenGL resources (call after GL context exists) + void shutdown(); + bool isInitialized() const { return initialized_; } + bool isGLInitialized() const { return glInitialized_; } + + // Built-in shader programs + enum class ShaderType { + Shape, // For RectangleShape, CircleShape, etc. + Sprite, // For textured sprites + Text, // For text rendering + Custom // User-provided shaders + }; + + // Get a built-in shader program + unsigned int getShaderProgram(ShaderType type) const; + + // Compile a custom shader program + unsigned int compileShader(const std::string& vertexSource, const std::string& fragmentSource); + void deleteShaderProgram(unsigned int programId); + + // Texture management + unsigned int createTexture(unsigned int width, unsigned int height, const unsigned char* pixels = nullptr); + void updateTexture(unsigned int textureId, unsigned int x, unsigned int y, + unsigned int width, unsigned int height, const unsigned char* pixels); + void deleteTexture(unsigned int textureId); + void setTextureSmooth(unsigned int textureId, bool smooth); + void setTextureRepeated(unsigned int textureId, bool repeated); + + // FBO management + unsigned int createFBO(unsigned int width, unsigned int height, unsigned int& colorTexture); + void deleteFBO(unsigned int fboId); + void bindFBO(unsigned int fboId); + void unbindFBO(); + + // Render state management + void setViewport(int x, int y, unsigned int width, unsigned int height); + void setProjection(float left, float right, float bottom, float top); + void clear(float r, float g, float b, float a); + + // Drawing primitives + void drawTriangles(const float* vertices, size_t vertexCount, + const float* colors, const float* texCoords, + unsigned int textureId = 0); + + // Projection matrix access (for shaders) + const float* getProjectionMatrix() const { return projectionMatrix_; } + +private: + SDL2Renderer() = default; + ~SDL2Renderer() = default; + SDL2Renderer(const SDL2Renderer&) = delete; + SDL2Renderer& operator=(const SDL2Renderer&) = delete; + + bool initialized_ = false; + bool glInitialized_ = false; + + // Built-in shader programs + unsigned int shapeProgram_ = 0; + unsigned int spriteProgram_ = 0; + unsigned int textProgram_ = 0; + + // Current projection matrix (4x4 orthographic) + float projectionMatrix_[16] = {0}; + + // FBO stack for nested render-to-texture + std::vector fboStack_; + + // Helper functions + bool compileAndLinkProgram(const char* vertexSrc, const char* fragmentSrc, unsigned int& programOut); + unsigned int compileShaderStage(unsigned int type, const char* source); + void initBuiltinShaders(); +}; + +// ============================================================================= +// Keyboard/Mouse SDL2 Implementation helpers +// ============================================================================= + +// SDL2 scancode to sf::Keyboard::Key mapping +int sdlScancodeToSfKey(int sdlScancode); +int sfKeyToSdlScancode(int sfKey); + +// SDL2 mouse button to sf::Mouse::Button mapping +int sdlButtonToSfButton(int sdlButton); +int sfButtonToSdlButton(int sfButton); + +// ============================================================================= +// Event translation +// ============================================================================= + +// Translate SDL_Event to sf::Event +// Returns true if the event was translated, false if it should be ignored +bool translateSDLEvent(const void* sdlEvent, void* sfEvent); + +// ============================================================================= +// Font Atlas for text rendering +// ============================================================================= + +class FontAtlas { +public: + FontAtlas(); + ~FontAtlas(); + + // Load font data + bool load(const unsigned char* fontData, size_t dataSize, float fontSize); + + // Get texture atlas + unsigned int getTextureId() const { return textureId_; } + + // Get glyph info for rendering + struct GlyphInfo { + float u0, v0, u1, v1; // Texture coordinates + float xoff, yoff; // Offset from cursor + float xadvance; // How far to advance cursor + float width, height; // Glyph dimensions in pixels + }; + + bool getGlyph(uint32_t codepoint, GlyphInfo& info) const; + + // Get font metrics + float getAscent() const { return ascent_; } + float getDescent() const { return descent_; } + float getLineHeight() const { return lineHeight_; } + +private: + unsigned int textureId_ = 0; + float fontSize_ = 0; + float ascent_ = 0; + float descent_ = 0; + float lineHeight_ = 0; + + // Glyph cache - maps codepoint to glyph info + std::unordered_map glyphCache_; + + // stb_truetype font info (opaque pointer to avoid header inclusion) + void* stbFontInfo_ = nullptr; +}; + +} // namespace sf + +#endif // MCRF_SDL2 diff --git a/src/platform/SDL2Types.h b/src/platform/SDL2Types.h new file mode 100644 index 0000000..d91a473 --- /dev/null +++ b/src/platform/SDL2Types.h @@ -0,0 +1,1116 @@ +// SDL2Types.h - SFML type stubs for SDL2 + OpenGL ES 2 backend +// This file provides type definitions that compile against SDL2 instead of SFML, +// enabling cross-platform builds for Web (Emscripten/WebGL), Android, and desktop. +// +// Part of the renderer abstraction layer (issue #158 continuation) +// +// Architecture: +// - Game code uses sf:: namespace types (unchanged from SFML builds) +// - This header provides sf:: types implemented using SDL2 + OpenGL ES 2 +// - Compile-time selection via MCRF_SDL2 define +// +// Phases: +// - Phase 1: Skeleton (stubs matching HeadlessTypes.h) +// - Phase 2: Window + GL context +// - Phase 3: Shape rendering (rectangles, circles) +// - Phase 4: Texture + Sprites +// - Phase 5: Text rendering +// - Phase 6: RenderTexture (FBO) +// - Phase 7: Custom shaders + +#pragma once + +#include +#include +#include +#include +#include + +// SDL2 headers - conditionally included when actually implementing +// For now, forward declare what we need +#ifdef MCRF_SDL2_IMPL +#include +#include +#endif + +namespace sf { + +// Forward declarations (needed for RenderWindow) +struct Event; +class Keyboard; +class Mouse; + +// ============================================================================= +// Type Aliases (SFML compatibility) +// ============================================================================= + +using Uint8 = uint8_t; +using Uint16 = uint16_t; +using Uint32 = uint32_t; +using Uint64 = uint64_t; +using Int8 = int8_t; +using Int16 = int16_t; +using Int32 = int32_t; +using Int64 = int64_t; + +// ============================================================================= +// Vector Types +// ============================================================================= + +template +struct Vector2 { + T x = 0; + T y = 0; + + Vector2() = default; + Vector2(T x_, T y_) : x(x_), y(y_) {} + + template + explicit Vector2(const Vector2& other) : x(static_cast(other.x)), y(static_cast(other.y)) {} + + Vector2 operator+(const Vector2& rhs) const { return Vector2(x + rhs.x, y + rhs.y); } + Vector2 operator-(const Vector2& rhs) const { return Vector2(x - rhs.x, y - rhs.y); } + Vector2 operator*(T scalar) const { return Vector2(x * scalar, y * scalar); } + Vector2 operator/(T scalar) const { return Vector2(x / scalar, y / scalar); } + Vector2& operator+=(const Vector2& rhs) { x += rhs.x; y += rhs.y; return *this; } + Vector2& operator-=(const Vector2& rhs) { x -= rhs.x; y -= rhs.y; return *this; } + Vector2& operator*=(T scalar) { x *= scalar; y *= scalar; return *this; } + Vector2& operator/=(T scalar) { x /= scalar; y /= scalar; return *this; } + bool operator==(const Vector2& rhs) const { return x == rhs.x && y == rhs.y; } + bool operator!=(const Vector2& rhs) const { return !(*this == rhs); } + Vector2 operator-() const { return Vector2(-x, -y); } +}; + +using Vector2f = Vector2; +using Vector2i = Vector2; +using Vector2u = Vector2; + +template +Vector2 operator*(T scalar, const Vector2& vec) { return vec * scalar; } + +// ============================================================================= +// Color Type +// ============================================================================= + +struct Color { + uint8_t r = 0; + uint8_t g = 0; + uint8_t b = 0; + uint8_t a = 255; + + Color() = default; + Color(uint8_t r_, uint8_t g_, uint8_t b_, uint8_t a_ = 255) : r(r_), g(g_), b(b_), a(a_) {} + + bool operator==(const Color& rhs) const { return r == rhs.r && g == rhs.g && b == rhs.b && a == rhs.a; } + bool operator!=(const Color& rhs) const { return !(*this == rhs); } + + // Standard colors + static const Color Black; + static const Color White; + static const Color Red; + static const Color Green; + static const Color Blue; + static const Color Yellow; + static const Color Magenta; + static const Color Cyan; + static const Color Transparent; +}; + +// Static color definitions +inline const Color Color::Black(0, 0, 0); +inline const Color Color::White(255, 255, 255); +inline const Color Color::Red(255, 0, 0); +inline const Color Color::Green(0, 255, 0); +inline const Color Color::Blue(0, 0, 255); +inline const Color Color::Yellow(255, 255, 0); +inline const Color Color::Magenta(255, 0, 255); +inline const Color Color::Cyan(0, 255, 255); +inline const Color Color::Transparent(0, 0, 0, 0); + +// ============================================================================= +// Rectangle Types +// ============================================================================= + +template +struct Rect { + T left = 0; + T top = 0; + T width = 0; + T height = 0; + + Rect() = default; + Rect(T left_, T top_, T width_, T height_) : left(left_), top(top_), width(width_), height(height_) {} + Rect(const Vector2& position, const Vector2& size) + : left(position.x), top(position.y), width(size.x), height(size.y) {} + + bool contains(T x, T y) const { + return x >= left && x < left + width && y >= top && y < top + height; + } + bool contains(const Vector2& point) const { return contains(point.x, point.y); } + + bool intersects(const Rect& other) const { + return left < other.left + other.width && left + width > other.left && + top < other.top + other.height && top + height > other.top; + } + + Vector2 getPosition() const { return Vector2(left, top); } + Vector2 getSize() const { return Vector2(width, height); } +}; + +using FloatRect = Rect; +using IntRect = Rect; + +// ============================================================================= +// Time Types +// ============================================================================= + +class Time { + int64_t microseconds_ = 0; +public: + Time() = default; + float asSeconds() const { return microseconds_ / 1000000.0f; } + int32_t asMilliseconds() const { return static_cast(microseconds_ / 1000); } + int64_t asMicroseconds() const { return microseconds_; } + + static Time Zero; + + friend Time seconds(float amount); + friend Time milliseconds(int32_t amount); + friend Time microseconds(int64_t amount); +}; + +inline Time Time::Zero; + +inline Time seconds(float amount) { Time t; t.microseconds_ = static_cast(amount * 1000000); return t; } +inline Time milliseconds(int32_t amount) { Time t; t.microseconds_ = amount * 1000; return t; } +inline Time microseconds(int64_t amount) { Time t; t.microseconds_ = amount; return t; } + +class Clock { + int64_t start_time_ = 0; + + static int64_t now_microseconds() { + // Use C++11 chrono for portable timing + auto now = std::chrono::high_resolution_clock::now(); + auto duration = now.time_since_epoch(); + return std::chrono::duration_cast(duration).count(); + } +public: + Clock() : start_time_(now_microseconds()) {} + + Time getElapsedTime() const { + return microseconds(now_microseconds() - start_time_); + } + + Time restart() { + int64_t now = now_microseconds(); + int64_t elapsed = now - start_time_; + start_time_ = now; + return microseconds(elapsed); + } +}; + +// ============================================================================= +// Transform +// ============================================================================= +// SDL2 backend will implement actual matrix math for transforms +// For now, stub implementation matching headless + +class Transform { + // 3x3 matrix stored as column-major for OpenGL + // [ a c tx ] [ m[0] m[3] m[6] ] + // [ b d ty ] -> [ m[1] m[4] m[7] ] + // [ 0 0 1 ] [ m[2] m[5] m[8] ] + float m[9] = {1,0,0, 0,1,0, 0,0,1}; + +public: + Transform() = default; + + Transform& translate(float x, float y) { + // Combine with translation matrix + m[6] += m[0] * x + m[3] * y; + m[7] += m[1] * x + m[4] * y; + return *this; + } + Transform& translate(const Vector2f& offset) { return translate(offset.x, offset.y); } + + Transform& rotate(float angle) { + float rad = angle * 3.14159265f / 180.0f; + float cos_a = std::cos(rad); + float sin_a = std::sin(rad); + + float new_m0 = m[0] * cos_a + m[3] * sin_a; + float new_m1 = m[1] * cos_a + m[4] * sin_a; + float new_m3 = m[0] * -sin_a + m[3] * cos_a; + float new_m4 = m[1] * -sin_a + m[4] * cos_a; + + m[0] = new_m0; m[1] = new_m1; + m[3] = new_m3; m[4] = new_m4; + return *this; + } + Transform& rotate(float angle, const Vector2f& center) { + translate(center.x, center.y); + rotate(angle); + translate(-center.x, -center.y); + return *this; + } + + Transform& scale(float factorX, float factorY) { + m[0] *= factorX; m[1] *= factorX; + m[3] *= factorY; m[4] *= factorY; + return *this; + } + Transform& scale(const Vector2f& factors) { return scale(factors.x, factors.y); } + + Vector2f transformPoint(float x, float y) const { + return Vector2f(m[0] * x + m[3] * y + m[6], + m[1] * x + m[4] * y + m[7]); + } + Vector2f transformPoint(const Vector2f& point) const { + return transformPoint(point.x, point.y); + } + + FloatRect transformRect(const FloatRect& rect) const { + // Transform all four corners and compute bounding box + Vector2f p1 = transformPoint(rect.left, rect.top); + Vector2f p2 = transformPoint(rect.left + rect.width, rect.top); + Vector2f p3 = transformPoint(rect.left, rect.top + rect.height); + Vector2f p4 = transformPoint(rect.left + rect.width, rect.top + rect.height); + + float minX = std::min({p1.x, p2.x, p3.x, p4.x}); + float maxX = std::max({p1.x, p2.x, p3.x, p4.x}); + float minY = std::min({p1.y, p2.y, p3.y, p4.y}); + float maxY = std::max({p1.y, p2.y, p3.y, p4.y}); + + return FloatRect(minX, minY, maxX - minX, maxY - minY); + } + + Transform getInverse() const { + // Compute inverse of 3x3 affine matrix + float det = m[0] * m[4] - m[1] * m[3]; + if (std::abs(det) < 1e-7f) return Transform(); + + float invDet = 1.0f / det; + Transform inv; + inv.m[0] = m[4] * invDet; + inv.m[1] = -m[1] * invDet; + inv.m[3] = -m[3] * invDet; + inv.m[4] = m[0] * invDet; + inv.m[6] = (m[3] * m[7] - m[4] * m[6]) * invDet; + inv.m[7] = (m[1] * m[6] - m[0] * m[7]) * invDet; + return inv; + } + + Transform operator*(const Transform& rhs) const { + Transform result; + result.m[0] = m[0] * rhs.m[0] + m[3] * rhs.m[1]; + result.m[1] = m[1] * rhs.m[0] + m[4] * rhs.m[1]; + result.m[3] = m[0] * rhs.m[3] + m[3] * rhs.m[4]; + result.m[4] = m[1] * rhs.m[3] + m[4] * rhs.m[4]; + result.m[6] = m[0] * rhs.m[6] + m[3] * rhs.m[7] + m[6]; + result.m[7] = m[1] * rhs.m[6] + m[4] * rhs.m[7] + m[7]; + return result; + } + Vector2f operator*(const Vector2f& point) const { return transformPoint(point); } + + static const Transform Identity; + + // SDL2-specific: Get raw matrix for OpenGL + const float* getMatrix() const { return m; } +}; + +inline const Transform Transform::Identity; + +// ============================================================================= +// Vertex (for custom geometry) +// ============================================================================= + +struct Vertex { + Vector2f position; + Color color; + Vector2f texCoords; + + Vertex() = default; + Vertex(const Vector2f& pos) : position(pos), color(Color::White) {} + Vertex(const Vector2f& pos, const Color& col) : position(pos), color(col) {} + Vertex(const Vector2f& pos, const Vector2f& tex) : position(pos), color(Color::White), texCoords(tex) {} + Vertex(const Vector2f& pos, const Color& col, const Vector2f& tex) : position(pos), color(col), texCoords(tex) {} +}; + +// ============================================================================= +// View (camera) +// ============================================================================= + +class View { + Vector2f center_; + Vector2f size_; + float rotation_ = 0.0f; + FloatRect viewport_{0, 0, 1, 1}; +public: + View() : center_(0, 0), size_(1000, 1000) {} + View(const FloatRect& rect) : center_(rect.left + rect.width/2, rect.top + rect.height/2), size_(rect.width, rect.height) {} + View(const Vector2f& center, const Vector2f& size) : center_(center), size_(size) {} + + void setCenter(float x, float y) { center_ = Vector2f(x, y); } + void setCenter(const Vector2f& center) { center_ = center; } + void setSize(float width, float height) { size_ = Vector2f(width, height); } + void setSize(const Vector2f& size) { size_ = size; } + void setRotation(float angle) { rotation_ = angle; } + void setViewport(const FloatRect& viewport) { viewport_ = viewport; } + + const Vector2f& getCenter() const { return center_; } + const Vector2f& getSize() const { return size_; } + float getRotation() const { return rotation_; } + const FloatRect& getViewport() const { return viewport_; } + + void move(float offsetX, float offsetY) { center_.x += offsetX; center_.y += offsetY; } + void move(const Vector2f& offset) { center_ += offset; } + void rotate(float angle) { rotation_ += angle; } + void zoom(float factor) { size_ *= factor; } + + Transform getTransform() const { + // View transform: translates world coords to view coords (NDC) + // Order: Scale to NDC, then Rotate, then Translate center to origin + Transform t; + t.translate(-center_.x, -center_.y); // Move center to origin + t.rotate(-rotation_); // Rotate around origin (negative for view) + t.scale(2.0f / size_.x, 2.0f / size_.y); // Scale to NDC [-1,1] + return t; + } + + Transform getInverseTransform() const { + return getTransform().getInverse(); + } +}; + +// ============================================================================= +// Rendering Types +// ============================================================================= + +enum PrimitiveType { + Points, + Lines, + LineStrip, + Triangles, + TriangleStrip, + TriangleFan, + Quads // Deprecated in SFML 3.0, but we'll support it via triangulation +}; + +// BlendMode stub +struct BlendMode { + BlendMode() = default; + static const BlendMode Alpha; + static const BlendMode Add; + static const BlendMode Multiply; + static const BlendMode None; +}; +inline const BlendMode BlendMode::Alpha{}; +inline const BlendMode BlendMode::Add{}; +inline const BlendMode BlendMode::Multiply{}; +inline const BlendMode BlendMode::None{}; + +// Forward declare Shader for RenderStates +class Shader; + +class RenderStates { +public: + Transform transform; + BlendMode blendMode; + const Shader* shader = nullptr; + + RenderStates() = default; + RenderStates(const Transform& t) : transform(t) {} + RenderStates(const BlendMode& mode) : blendMode(mode) {} + RenderStates(const Shader* s) : shader(s) {} + static const RenderStates Default; +}; + +inline const RenderStates RenderStates::Default; + +// Forward declarations for rendering types +class RenderTarget; +class RenderTexture; +class RenderWindow; +class Texture; +class Font; +class Shader; + +// Drawable base class +class Drawable { +public: + virtual ~Drawable() = default; +protected: + friend class RenderTarget; + virtual void draw(RenderTarget& target, RenderStates states) const = 0; +}; + +// Transformable base class +class Transformable { +protected: + Vector2f position_; + float rotation_ = 0.0f; + Vector2f scale_{1.0f, 1.0f}; + Vector2f origin_; +public: + virtual ~Transformable() = default; + + void setPosition(float x, float y) { position_ = Vector2f(x, y); } + void setPosition(const Vector2f& position) { position_ = position; } + void setRotation(float angle) { rotation_ = angle; } + void setScale(float factorX, float factorY) { scale_ = Vector2f(factorX, factorY); } + void setScale(const Vector2f& factors) { scale_ = factors; } + void setOrigin(float x, float y) { origin_ = Vector2f(x, y); } + void setOrigin(const Vector2f& origin) { origin_ = origin; } + + const Vector2f& getPosition() const { return position_; } + float getRotation() const { return rotation_; } + const Vector2f& getScale() const { return scale_; } + const Vector2f& getOrigin() const { return origin_; } + + void move(float offsetX, float offsetY) { position_.x += offsetX; position_.y += offsetY; } + void move(const Vector2f& offset) { position_ += offset; } + void rotate(float angle) { rotation_ += angle; } + void scale(float factorX, float factorY) { scale_.x *= factorX; scale_.y *= factorY; } + void scale(const Vector2f& factor) { scale_.x *= factor.x; scale_.y *= factor.y; } + + Transform getTransform() const { + Transform transform; + // Apply transformations: translate to position, rotate, scale, translate by -origin + transform.translate(position_.x, position_.y); + transform.rotate(rotation_); + transform.scale(scale_.x, scale_.y); + transform.translate(-origin_.x, -origin_.y); + return transform; + } + Transform getInverseTransform() const { + return getTransform().getInverse(); + } +}; + +// ============================================================================= +// Shape Classes +// ============================================================================= + +class Shape : public Drawable, public Transformable { +protected: + Color fillColor_ = Color::White; + Color outlineColor_ = Color::White; + float outlineThickness_ = 0.0f; +public: + void setFillColor(const Color& color) { fillColor_ = color; } + void setOutlineColor(const Color& color) { outlineColor_ = color; } + void setOutlineThickness(float thickness) { outlineThickness_ = thickness; } + + const Color& getFillColor() const { return fillColor_; } + const Color& getOutlineColor() const { return outlineColor_; } + float getOutlineThickness() const { return outlineThickness_; } + + virtual FloatRect getLocalBounds() const { return FloatRect(); } + virtual FloatRect getGlobalBounds() const { return FloatRect(); } + + // Virtual methods for shape points (implemented by derived classes) + virtual size_t getPointCount() const = 0; + virtual Vector2f getPoint(size_t index) const = 0; + +protected: + void draw(RenderTarget& target, RenderStates states) const override; // Implemented in SDL2Renderer.cpp +}; + +class RectangleShape : public Shape { + Vector2f size_; +public: + RectangleShape(const Vector2f& size = Vector2f(0, 0)) : size_(size) {} + void setSize(const Vector2f& size) { size_ = size; } + const Vector2f& getSize() const { return size_; } + FloatRect getLocalBounds() const override { return FloatRect(0, 0, size_.x, size_.y); } + FloatRect getGlobalBounds() const override { return FloatRect(position_.x, position_.y, size_.x, size_.y); } + + size_t getPointCount() const override { return 4; } + Vector2f getPoint(size_t index) const override { + switch (index) { + case 0: return Vector2f(0, 0); + case 1: return Vector2f(size_.x, 0); + case 2: return Vector2f(size_.x, size_.y); + case 3: return Vector2f(0, size_.y); + default: return Vector2f(); + } + } +}; + +class CircleShape : public Shape { + float radius_ = 0.0f; + size_t pointCount_ = 30; +public: + CircleShape(float radius = 0, size_t pointCount = 30) : radius_(radius), pointCount_(pointCount) {} + void setRadius(float radius) { radius_ = radius; } + float getRadius() const { return radius_; } + void setPointCount(size_t count) { pointCount_ = count; } + size_t getPointCount() const override { return pointCount_; } + FloatRect getLocalBounds() const override { return FloatRect(0, 0, radius_ * 2, radius_ * 2); } + + Vector2f getPoint(size_t index) const override { + float angle = static_cast(index) / pointCount_ * 2.0f * 3.14159265f; + return Vector2f(radius_ + radius_ * std::cos(angle), radius_ + radius_ * std::sin(angle)); + } +}; + +class ConvexShape : public Shape { + std::vector points_; +public: + ConvexShape(size_t pointCount = 0) : points_(pointCount) {} + void setPointCount(size_t count) { points_.resize(count); } + size_t getPointCount() const override { return points_.size(); } + void setPoint(size_t index, const Vector2f& point) { if (index < points_.size()) points_[index] = point; } + Vector2f getPoint(size_t index) const override { return index < points_.size() ? points_[index] : Vector2f(); } +}; + +// ============================================================================= +// VertexArray +// ============================================================================= + +class VertexArray : public Drawable { + std::vector vertices_; + PrimitiveType primitiveType_ = Points; +public: + VertexArray() = default; + VertexArray(PrimitiveType type, size_t vertexCount = 0) : vertices_(vertexCount), primitiveType_(type) {} + + size_t getVertexCount() const { return vertices_.size(); } + Vertex& operator[](size_t index) { return vertices_[index]; } + const Vertex& operator[](size_t index) const { return vertices_[index]; } + + void clear() { vertices_.clear(); } + void resize(size_t vertexCount) { vertices_.resize(vertexCount); } + void append(const Vertex& vertex) { vertices_.push_back(vertex); } + + void setPrimitiveType(PrimitiveType type) { primitiveType_ = type; } + PrimitiveType getPrimitiveType() const { return primitiveType_; } + + FloatRect getBounds() const { return FloatRect(); } // TODO: Implement + +protected: + void draw(RenderTarget& target, RenderStates states) const override; // Implemented in SDL2Renderer.cpp +}; + +// ============================================================================= +// Image +// ============================================================================= + +class Image { + Vector2u size_; + std::vector pixels_; +public: + Image() = default; + + void create(unsigned int width, unsigned int height, const Color& color = Color::Black) { + size_ = Vector2u(width, height); + pixels_.resize(width * height * 4, 0); + // Fill with color + for (unsigned int y = 0; y < height; ++y) { + for (unsigned int x = 0; x < width; ++x) { + setPixel(x, y, color); + } + } + } + + bool loadFromFile(const std::string& filename); // Implemented in SDL2Renderer.cpp (uses stb_image) + bool saveToFile(const std::string& filename) const; // Implemented in SDL2Renderer.cpp (uses stb_image_write) + + Vector2u getSize() const { return size_; } + + void setPixel(unsigned int x, unsigned int y, const Color& color) { + if (x < size_.x && y < size_.y) { + size_t idx = (y * size_.x + x) * 4; + pixels_[idx] = color.r; + pixels_[idx + 1] = color.g; + pixels_[idx + 2] = color.b; + pixels_[idx + 3] = color.a; + } + } + + Color getPixel(unsigned int x, unsigned int y) const { + if (x < size_.x && y < size_.y) { + size_t idx = (y * size_.x + x) * 4; + return Color(pixels_[idx], pixels_[idx + 1], pixels_[idx + 2], pixels_[idx + 3]); + } + return Color::Black; + } + + const Uint8* getPixelsPtr() const { return pixels_.data(); } + + // SDL2-specific: Allow direct pixel access for texture upload + Uint8* getPixelsPtr() { return pixels_.data(); } + void setSize(unsigned int w, unsigned int h) { size_ = Vector2u(w, h); pixels_.resize(w * h * 4); } +}; + +// ============================================================================= +// Texture +// ============================================================================= + +// Forward declare RenderWindow for Texture::update +class RenderWindow; + +class Texture { + Vector2u size_; + // SDL2-specific: OpenGL texture handle (0 = not created) + unsigned int textureId_ = 0; + bool smooth_ = false; + bool repeated_ = false; + +public: + Texture() = default; + ~Texture(); // Implemented in SDL2Renderer.cpp (deletes GL texture) + + // Copy constructor/assignment - needed for proper GL resource management + Texture(const Texture& other); + Texture& operator=(const Texture& other); + + bool create(unsigned int width, unsigned int height); // Implemented in SDL2Renderer.cpp + bool loadFromFile(const std::string& filename); // Implemented in SDL2Renderer.cpp + bool loadFromMemory(const void* data, size_t size); // Implemented in SDL2Renderer.cpp + + Vector2u getSize() const { return size_; } + void setSmooth(bool smooth); // Implemented in SDL2Renderer.cpp + bool isSmooth() const { return smooth_; } + void setRepeated(bool repeated); // Implemented in SDL2Renderer.cpp + bool isRepeated() const { return repeated_; } + + Image copyToImage() const; // Implemented in SDL2Renderer.cpp + void update(const RenderWindow& window); // Implemented in SDL2Renderer.cpp + void update(const Uint8* pixels); // Implemented in SDL2Renderer.cpp + void update(const Uint8* pixels, unsigned int width, unsigned int height, unsigned int x, unsigned int y); + + // SDL2-specific: Get GL texture handle + unsigned int getNativeHandle() const { return textureId_; } + void setNativeHandle(unsigned int id) { textureId_ = id; } +}; + +// ============================================================================= +// Sprite +// ============================================================================= + +class Sprite : public Drawable, public Transformable { + const Texture* texture_ = nullptr; + IntRect textureRect_; + Color color_ = Color::White; +public: + Sprite() = default; + Sprite(const Texture& texture) : texture_(&texture) {} + Sprite(const Texture& texture, const IntRect& rectangle) : texture_(&texture), textureRect_(rectangle) {} + + void setTexture(const Texture& texture, bool resetRect = false) { texture_ = &texture; } + void setTextureRect(const IntRect& rectangle) { textureRect_ = rectangle; } + void setColor(const Color& color) { color_ = color; } + + const Texture* getTexture() const { return texture_; } + const IntRect& getTextureRect() const { return textureRect_; } + const Color& getColor() const { return color_; } + + FloatRect getLocalBounds() const { return FloatRect(0, 0, static_cast(textureRect_.width), static_cast(textureRect_.height)); } + FloatRect getGlobalBounds() const { return FloatRect(position_.x, position_.y, static_cast(textureRect_.width), static_cast(textureRect_.height)); } + +protected: + void draw(RenderTarget& target, RenderStates states) const override; // Implemented in SDL2Renderer.cpp +}; + +// ============================================================================= +// Text and Font +// ============================================================================= + +class Font { + // SDL2-specific: font data for stb_truetype + std::vector fontData_; + bool loaded_ = false; + +public: + struct Info { + std::string family; + }; + + Font() = default; + bool loadFromFile(const std::string& filename); // Implemented in SDL2Renderer.cpp + bool loadFromMemory(const void* data, size_t sizeInBytes); // Implemented in SDL2Renderer.cpp + const Info& getInfo() const { static Info info; return info; } + + // SDL2-specific: Access font data + const unsigned char* getData() const { return fontData_.data(); } + size_t getDataSize() const { return fontData_.size(); } + bool isLoaded() const { return loaded_; } +}; + +class Text : public Drawable, public Transformable { + std::string string_; + const Font* font_ = nullptr; + unsigned int characterSize_ = 30; + Color fillColor_ = Color::White; + Color outlineColor_ = Color::Black; + float outlineThickness_ = 0.0f; + uint32_t style_ = 0; +public: + enum Style { Regular = 0, Bold = 1, Italic = 2, Underlined = 4, StrikeThrough = 8 }; + + Text() = default; + Text(const std::string& string, const Font& font, unsigned int characterSize = 30) + : string_(string), font_(&font), characterSize_(characterSize) {} + + void setString(const std::string& string) { string_ = string; } + void setFont(const Font& font) { font_ = &font; } + void setCharacterSize(unsigned int size) { characterSize_ = size; } + void setStyle(uint32_t style) { style_ = style; } + void setFillColor(const Color& color) { fillColor_ = color; } + void setOutlineColor(const Color& color) { outlineColor_ = color; } + void setOutlineThickness(float thickness) { outlineThickness_ = thickness; } + + const std::string& getString() const { return string_; } + const Font* getFont() const { return font_; } + unsigned int getCharacterSize() const { return characterSize_; } + uint32_t getStyle() const { return style_; } + const Color& getFillColor() const { return fillColor_; } + const Color& getOutlineColor() const { return outlineColor_; } + float getOutlineThickness() const { return outlineThickness_; } + + FloatRect getLocalBounds() const; // Implemented in SDL2Renderer.cpp + FloatRect getGlobalBounds() const; // Implemented in SDL2Renderer.cpp + +protected: + void draw(RenderTarget& target, RenderStates states) const override; // Implemented in SDL2Renderer.cpp +}; + +// ============================================================================= +// RenderTarget (base class for rendering) +// ============================================================================= + +class RenderTarget { +protected: + Vector2u size_; + View view_; + View defaultView_; +public: + virtual ~RenderTarget() = default; + + virtual Vector2u getSize() const { return size_; } + virtual void clear(const Color& color = Color::Black); // Implemented in SDL2Renderer.cpp + + void draw(const Drawable& drawable, const RenderStates& states = RenderStates::Default) { + drawable.draw(*this, states); + } + void draw(const Vertex* vertices, size_t vertexCount, PrimitiveType type, const RenderStates& states = RenderStates::Default); + void draw(const VertexArray& vertices, const RenderStates& states = RenderStates::Default); + + void setView(const View& view) { view_ = view; } + const View& getView() const { return view_; } + const View& getDefaultView() const { return defaultView_; } + + IntRect getViewport(const View& view) const { return IntRect(0, 0, size_.x, size_.y); } + + Vector2f mapPixelToCoords(const Vector2i& point) const { return Vector2f(static_cast(point.x), static_cast(point.y)); } + Vector2f mapPixelToCoords(const Vector2i& point, const View& view) const { return Vector2f(static_cast(point.x), static_cast(point.y)); } + Vector2i mapCoordsToPixel(const Vector2f& point) const { return Vector2i(static_cast(point.x), static_cast(point.y)); } + Vector2i mapCoordsToPixel(const Vector2f& point, const View& view) const { return Vector2i(static_cast(point.x), static_cast(point.y)); } +}; + +// ============================================================================= +// RenderTexture (Framebuffer Object) +// ============================================================================= + +class RenderTexture : public RenderTarget { + Texture texture_; + unsigned int fboId_ = 0; // OpenGL FBO handle +public: + RenderTexture() = default; + ~RenderTexture(); // Implemented in SDL2Renderer.cpp + + bool create(unsigned int width, unsigned int height); // Implemented in SDL2Renderer.cpp + + void clear(const Color& color = Color::Black) override; // Implemented in SDL2Renderer.cpp + void display(); // Implemented in SDL2Renderer.cpp + + const Texture& getTexture() const { return texture_; } + void setSmooth(bool smooth) { texture_.setSmooth(smooth); } + bool isSmooth() const { return texture_.isSmooth(); } + + // SDL2-specific: Get FBO handle + unsigned int getNativeHandle() const { return fboId_; } +}; + +// ============================================================================= +// RenderWindow +// ============================================================================= + +namespace Style { + enum { + None = 0, + Titlebar = 1 << 0, + Resize = 1 << 1, + Close = 1 << 2, + Fullscreen = 1 << 3, + Default = Titlebar | Resize | Close + }; +} + +class VideoMode { +public: + unsigned int width = 0; + unsigned int height = 0; + unsigned int bitsPerPixel = 32; + + VideoMode() = default; + VideoMode(unsigned int w, unsigned int h, unsigned int bpp = 32) : width(w), height(h), bitsPerPixel(bpp) {} + + static VideoMode getDesktopMode(); // Implemented in SDL2Renderer.cpp + static const std::vector& getFullscreenModes(); // Implemented in SDL2Renderer.cpp +}; + +class RenderWindow : public RenderTarget { + bool open_ = false; + std::string title_; + + // SDL2-specific handles (void* to avoid SDL header dependency) + void* sdlWindow_ = nullptr; + void* glContext_ = nullptr; + +public: + RenderWindow() = default; + RenderWindow(VideoMode mode, const std::string& title, uint32_t style = Style::Default) { + create(mode, title, style); + } + ~RenderWindow(); // Implemented in SDL2Renderer.cpp + + void create(VideoMode mode, const std::string& title, uint32_t style = Style::Default); // Implemented in SDL2Renderer.cpp + + void close(); // Implemented in SDL2Renderer.cpp + bool isOpen() const { return open_; } + + void clear(const Color& color = Color::Black) override; // Implemented in SDL2Renderer.cpp + void display(); // Implemented in SDL2Renderer.cpp + + void setTitle(const std::string& title); // Implemented in SDL2Renderer.cpp + void setFramerateLimit(unsigned int limit); // Implemented in SDL2Renderer.cpp + void setVerticalSyncEnabled(bool enabled); // Implemented in SDL2Renderer.cpp + void setVisible(bool visible); // Implemented in SDL2Renderer.cpp + void setMouseCursorVisible(bool visible); // Implemented in SDL2Renderer.cpp + void setMouseCursorGrabbed(bool grabbed); // Implemented in SDL2Renderer.cpp + void setKeyRepeatEnabled(bool enabled) {} // No-op for now + + Vector2i getPosition() const; // Implemented in SDL2Renderer.cpp + void setPosition(const Vector2i& position); // Implemented in SDL2Renderer.cpp + Vector2u getSize() const override { return size_; } + void setSize(const Vector2u& size); // Implemented in SDL2Renderer.cpp + + bool pollEvent(Event& event); // Implemented in SDL2Renderer.cpp + bool waitEvent(Event& event); // Implemented in SDL2Renderer.cpp + + // SDL2-specific: Access native handles + void* getNativeWindowHandle() const { return sdlWindow_; } + void* getGLContext() const { return glContext_; } +}; + +// ============================================================================= +// Audio Stubs (SDL2_mixer could implement these later) +// ============================================================================= + +class SoundBuffer { +public: + SoundBuffer() = default; + bool loadFromFile(const std::string& filename) { return true; } // Stub + bool loadFromMemory(const void* data, size_t sizeInBytes) { return true; } // Stub + Time getDuration() const { return Time(); } +}; + +class Sound { +public: + enum Status { Stopped, Paused, Playing }; + + Sound() = default; + Sound(const SoundBuffer& buffer) {} + + void setBuffer(const SoundBuffer& buffer) {} + void play() {} + void pause() {} + void stop() {} + + Status getStatus() const { return Stopped; } + void setVolume(float volume) {} + float getVolume() const { return 100.0f; } + void setLoop(bool loop) {} + bool getLoop() const { return false; } +}; + +class Music { +public: + enum Status { Stopped, Paused, Playing }; + + Music() = default; + bool openFromFile(const std::string& filename) { return true; } // Stub + + void play() {} + void pause() {} + void stop() {} + + Status getStatus() const { return Stopped; } + void setVolume(float volume) {} + float getVolume() const { return 100.0f; } + void setLoop(bool loop) {} + bool getLoop() const { return false; } + Time getDuration() const { return Time(); } + Time getPlayingOffset() const { return Time(); } + void setPlayingOffset(Time offset) {} +}; + +// ============================================================================= +// Input (Keyboard and Mouse) +// ============================================================================= + +class Keyboard { +public: + enum Key { + Unknown = -1, + A = 0, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, + Num0, Num1, Num2, Num3, Num4, Num5, Num6, Num7, Num8, Num9, + Escape, LControl, LShift, LAlt, LSystem, RControl, RShift, RAlt, RSystem, + Menu, LBracket, RBracket, Semicolon, Comma, Period, Apostrophe, Slash, Backslash, + Grave, Equal, Hyphen, Space, Enter, Backspace, Tab, PageUp, PageDown, End, Home, + Insert, Delete, Add, Subtract, Multiply, Divide, + Left, Right, Up, Down, + Numpad0, Numpad1, Numpad2, Numpad3, Numpad4, Numpad5, Numpad6, Numpad7, Numpad8, Numpad9, + F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, + Pause, + KeyCount, + // Deprecated aliases (SFML 2.x compatibility) + Tilde = Grave, + Quote = Apostrophe, + BackSpace = Backspace, + BackSlash = Backslash, + SemiColon = Semicolon, + Dash = Hyphen + }; + + static bool isKeyPressed(Key key); // Implemented in SDL2Renderer.cpp +}; + +class Mouse { +public: + enum Button { Left, Right, Middle, XButton1, XButton2, ButtonCount }; + enum Wheel { VerticalWheel, HorizontalWheel }; + + static bool isButtonPressed(Button button); // Implemented in SDL2Renderer.cpp + static Vector2i getPosition(); // Implemented in SDL2Renderer.cpp + static Vector2i getPosition(const RenderWindow& relativeTo); // Implemented in SDL2Renderer.cpp + static void setPosition(const Vector2i& position); // Implemented in SDL2Renderer.cpp + static void setPosition(const Vector2i& position, const RenderWindow& relativeTo); // Implemented in SDL2Renderer.cpp +}; + +// ============================================================================= +// Event System +// ============================================================================= + +struct Event { + enum EventType { + Closed, + Resized, + LostFocus, + GainedFocus, + TextEntered, + KeyPressed, + KeyReleased, + MouseWheelMoved, // Deprecated + MouseWheelScrolled, + MouseButtonPressed, + MouseButtonReleased, + MouseMoved, + MouseEntered, + MouseLeft, + Count + }; + + struct SizeEvent { unsigned int width, height; }; + struct KeyEvent { Keyboard::Key code; bool alt, control, shift, system; }; + struct TextEvent { uint32_t unicode; }; + struct MouseMoveEvent { int x, y; }; + struct MouseButtonEvent { Mouse::Button button; int x, y; }; + struct MouseWheelScrollEvent { Mouse::Wheel wheel; float delta; int x, y; }; + + EventType type; + union { + SizeEvent size; + KeyEvent key; + TextEvent text; + MouseMoveEvent mouseMove; + MouseButtonEvent mouseButton; + MouseWheelScrollEvent mouseWheelScroll; + }; +}; + +// ============================================================================= +// GLSL Types (for shader uniforms) +// ============================================================================= + +namespace Glsl { + using Vec2 = Vector2f; + + struct Vec3 { + float x = 0, y = 0, z = 0; + Vec3() = default; + Vec3(float x_, float y_, float z_) : x(x_), y(y_), z(z_) {} + }; + + struct Vec4 { + float x = 0, y = 0, z = 0, w = 0; + Vec4() = default; + Vec4(float x_, float y_, float z_, float w_) : x(x_), y(y_), z(z_), w(w_) {} + Vec4(const Color& c) : x(c.r/255.f), y(c.g/255.f), z(c.b/255.f), w(c.a/255.f) {} + }; +} // namespace Glsl + +// Forward declaration for CurrentTexture +struct CurrentTextureType {}; + +// ============================================================================= +// Shader +// ============================================================================= + +class Shader { + unsigned int programId_ = 0; // OpenGL shader program handle + bool loaded_ = false; + +public: + enum Type { Vertex, Geometry, Fragment }; + static const CurrentTextureType CurrentTexture; + + Shader() = default; + ~Shader(); // Implemented in SDL2Renderer.cpp + + bool loadFromFile(const std::string& filename, Type type); // Implemented in SDL2Renderer.cpp + bool loadFromFile(const std::string& vertexFile, const std::string& fragmentFile); // Implemented in SDL2Renderer.cpp + bool loadFromMemory(const std::string& shader, Type type); // Implemented in SDL2Renderer.cpp + + void setUniform(const std::string& name, float x); // Implemented in SDL2Renderer.cpp + void setUniform(const std::string& name, const Vector2f& v); + void setUniform(const std::string& name, const Color& color); + void setUniform(const std::string& name, const Texture& texture); + void setUniform(const std::string& name, const Glsl::Vec3& v); + void setUniform(const std::string& name, const Glsl::Vec4& v); + void setUniform(const std::string& name, CurrentTextureType); + + static bool isAvailable(); // Implemented in SDL2Renderer.cpp + + // SDL2-specific: Get program handle + unsigned int getNativeHandle() const { return programId_; } + bool isLoaded() const { return loaded_; } +}; + +inline const CurrentTextureType Shader::CurrentTexture{}; + +// ============================================================================= +// Error stream +// ============================================================================= + +#include +#include + +inline std::ostream& err() { + static std::stringstream dummy; + return dummy; +} + +} // namespace sf