Compare commits

..

3 commits

Author SHA1 Message Date
be6fe23499 Implement Sprite, Text, and VertexArray draw() for SDL2 backend
- Sprite::draw(): Textured quad rendering with UV coordinates and color tint
- Text::draw(): Glyph rendering using FontAtlas cache, supports multi-line
- Text::getLocalBounds()/getGlobalBounds(): Calculate text dimensions
- VertexArray::draw(): Full primitive support (Triangles, TriangleFan,
  TriangleStrip, Quads, Lines, LineStrip, Points)
- View::getTransform(): Proper camera transform using translate/rotate/scale

Frames and sprites now render in browser. Text shows glyph positions but
texture sampling needs debugging. Grid rendering not yet working.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-31 12:38:03 -05:00
a702d3cab4 Implement Shape rendering and Transform math for SDL2 backend
Shape rendering now works:
- Shape::draw() generates triangle vertices for fill and outline
- RectangleShape, CircleShape, ConvexShape provide getPointCount()/getPoint()
- Shapes render with correct fill color, outline color, and outline thickness

Transform class fully implemented:
- translate(), rotate(), scale() modify the 3x3 affine matrix
- transformPoint() applies transform to Vector2f
- operator* combines transforms
- getInverse() computes inverse transform

Transformable::getTransform() now computes proper transform from:
- position, rotation, scale, and origin

RenderStates now has transform, blendMode, shader members

Canvas sizing fixed for Emscripten:
- EM_ASM sets canvas size after SDL window creation
- SDL_GL_MakeCurrent called after canvas resize

Result: RectangleShape UI elements render in correct positions!

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 11:58:42 -05:00
c5cc022aa2 Add SDL2+OpenGL ES 2 renderer backend for Emscripten/WebGL
Implements Phase 1 of renderer abstraction plan:
- SDL2Types.h: SFML-compatible type stubs in sf:: namespace
- SDL2Renderer.h/cpp: OpenGL ES 2 rendering implementation
- EmscriptenStubs.cpp: Stubs for missing POSIX functions (wait3, wait4, wcsftime)

Build system changes:
- Add MCRF_SDL2 compile-time backend selection
- Add Emscripten SDL2 link options (-sUSE_SDL=2, -sFULL_ES2=1)
- Fix LONG_BIT mismatch for Emscripten in pyport.h

Code changes for SDL2/headless compatibility:
- Guard ImGui includes with !MCRF_HEADLESS && !MCRF_SDL2
- Defer GL shader initialization until after context creation

Current status: Python runs in browser, rendering WIP (canvas sizing issues)

Build commands:
  emcmake cmake -DMCRF_SDL2=ON -B build-emscripten
  emmake make -C build-emscripten

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 11:13:15 -05:00
13 changed files with 3319 additions and 34 deletions

View file

@ -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)
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")
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"

View file

@ -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 <SFML/Graphics.hpp>
#include <SFML/Audio.hpp>
#define MCRF_GRAPHICS_BACKEND "sfml"

View file

@ -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);
}

View file

@ -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;

View file

@ -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"

View file

@ -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 <string>
#include <vector>

View file

@ -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"

View file

@ -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 <string>
#include <memory>

View file

@ -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;

View file

@ -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 <sys/types.h>
#include <sys/resource.h>
#include <wchar.h>
#include <errno.h>
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__

File diff suppressed because it is too large Load diff

182
src/platform/SDL2Renderer.h Normal file
View file

@ -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 <string>
#include <vector>
#include <unordered_map>
#include <memory>
// 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<unsigned int> 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<uint32_t, GlyphInfo> glyphCache_;
// stb_truetype font info (opaque pointer to avoid header inclusion)
void* stbFontInfo_ = nullptr;
};
} // namespace sf
#endif // MCRF_SDL2

1116
src/platform/SDL2Types.h Normal file

File diff suppressed because it is too large Load diff