Skip to content

Architecture

This document outlines the C++20 design patterns, techniques, and architectural principles used to create safe, efficient wrappers around SDL3's C API.


Core Principles

1. RAII (Resource Acquisition Is Initialization)

Every SDL resource is wrapped in a C++ class that automatically manages its lifetime.

// Bad: Manual resource management
SDL_Window* window = SDL_CreateWindow("Title", 800, 600, 0);
// ... use window
SDL_DestroyWindow(window);  // Easy to forget or skip on error paths

// Good: RAII wrapper
class window {
    SDL_Window* m_window;
public:
    window(std::string_view title, size sz, window_flags flags = window_flags::none)
        : m_window(SDL_CreateWindow(title.data(), sz.width, sz.height,
                                     static_cast<uint32_t>(flags)))
    {
        if (!m_window) {
            throw error::from_sdl();
        }
    }

    ~window() {
        if (m_window) {
            SDL_DestroyWindow(m_window);
        }
    }

    // Non-copyable, movable
    window(const window&) = delete;
    window& operator=(const window&) = delete;

    window(window&& other) noexcept
        : m_window(std::exchange(other.m_window, nullptr)) {}

    window& operator=(window&& other) noexcept {
        if (this != &other) {
            if (m_window) SDL_DestroyWindow(m_window);
            m_window = std::exchange(other.m_window, nullptr);
        }
        return *this;
    }
};

2. Strong Type Safety

Replace SDL's loose typing with C++ strong types to prevent logical errors at compile time.

// Bad: Easy to mix up parameters
SDL_SetWindowSize(window, 600, 800);  // Oops, swapped width/height

// Good: Strong types prevent errors
struct size { int width, height; };
struct position { int x, y; };

void window::set_size(size sz) {
    SDL_SetWindowSize(m_window, sz.width, sz.height);
}

void window::set_position(position pos) {
    SDL_SetWindowPosition(m_window, pos.x, pos.y);
}

// Usage - impossible to mix up
win.set_size({800, 600});
win.set_position({100, 50});

3. Enum Class for Constants

Replace SDL's C-style defines with type-safe enum classes.

// Bad: C-style defines, no type safety
SDL_CreateWindow("Title", 800, 600, SDL_WINDOW_RESIZABLE | SDL_WINDOW_BORDERLESS);

// Good: Type-safe enum class with bitwise operations
enum class window_flags : uint32_t {
    none = 0,
    fullscreen = SDL_WINDOW_FULLSCREEN,
    resizable = SDL_WINDOW_RESIZABLE,
    borderless = SDL_WINDOW_BORDERLESS,
    always_on_top = SDL_WINDOW_ALWAYS_ON_TOP,
};

// Enable bitwise operations
constexpr window_flags operator|(window_flags lhs, window_flags rhs) {
    return static_cast<window_flags>(
        static_cast<uint32_t>(lhs) | static_cast<uint32_t>(rhs)
    );
}

// Usage
auto flags = window_flags::resizable | window_flags::borderless;
window win("Title", {800, 600}, flags);

Error Handling

Exception-Based Error Handling

Convert SDL's error codes into C++ exceptions for automatic error propagation.

class error : public std::runtime_error {
public:
    explicit error(const std::string& msg) : std::runtime_error(msg) {}

    static error from_sdl() {
        const char* sdl_error = SDL_GetError();
        return error(sdl_error ? sdl_error : "Unknown SDL error");
    }
};

// Usage in constructors
window::window(std::string_view title, size sz, window_flags flags) {
    m_window = SDL_CreateWindow(title.data(), sz.width, sz.height,
                                 static_cast<uint32_t>(flags));
    if (!m_window) {
        throw error::from_sdl();
    }
}

Modern C++20 Features

1. Concepts for Type Constraints

Use C++20 concepts to constrain template parameters and provide better error messages.

#include <concepts>

template<typename T>
concept ColorLike = requires(T t) {
    { t.r } -> std::convertible_to<uint8_t>;
    { t.g } -> std::convertible_to<uint8_t>;
    { t.b } -> std::convertible_to<uint8_t>;
    { t.a } -> std::convertible_to<uint8_t>;
};

template<ColorLike Color>
void renderer::set_draw_color(const Color& c) {
    SDL_SetRenderDrawColor(m_renderer, c.r, c.g, c.b, c.a);
}

2. Constexpr for Compile-Time Computation

Use constexpr to move computations to compile time when possible.

struct color {
    uint8_t r, g, b, a;

    // Compile-time color creation
    static constexpr color rgb(uint8_t red, uint8_t green, uint8_t blue) {
        return {red, green, blue, 255};
    }

    // Compile-time color from hex
    static constexpr color from_hex(uint32_t hex) {
        return {
            static_cast<uint8_t>((hex >> 16) & 0xFF),
            static_cast<uint8_t>((hex >> 8) & 0xFF),
            static_cast<uint8_t>(hex & 0xFF),
            255
        };
    }

    // Common colors as compile-time constants
    static constexpr color white() { return from_hex(0xFFFFFF); }
    static constexpr color black() { return from_hex(0x000000); }
    static constexpr color red() { return from_hex(0xFF0000); }
};

// Usage - computed at compile time
constexpr auto bg_color = color::from_hex(0x2E3440);

3. std::format for Type-Safe Formatting

Leverage std::format for compile-time checked string formatting in logging.

template <class... Args>
inline void log_info(std::format_string<Args...> fmt, Args&&... args) {
    auto message = std::format(fmt, std::forward<Args>(args)...);
    SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "%s", message.c_str());
}

// Usage - format checked at compile time
laya::log_info("Window size: {}x{}", width, height);

Event System Design

Type-Safe Event Variants

Use std::variant to create a type-safe event system.

#include <variant>

// Individual event types
struct quit_event {
    uint32_t timestamp;
};

struct key_event {
    enum class state { pressed, released };
    uint32_t timestamp;
    state key_state;
    // ... key details
};

struct mouse_button_event {
    enum class state { pressed, released };
    enum class button { left = 1, middle = 2, right = 3 };
    uint32_t timestamp;
    state button_state;
    button mouse_button;
    int x, y;
};

// Event variant
using event = std::variant<
    quit_event,
    key_event,
    mouse_button_event
    // ... other event types
>;

Range-Based Event Polling

Provide a range-based interface for event polling.

class event_range {
    std::vector<event> m_events;

public:
    event_range() {
        SDL_Event sdl_event;
        while (SDL_PollEvent(&sdl_event)) {
            m_events.push_back(from_sdl_event(sdl_event));
        }
    }

    auto begin() const { return m_events.begin(); }
    auto end() const { return m_events.end(); }
    bool empty() const { return m_events.empty(); }
};

// Usage
for (const auto& ev : laya::poll_events()) {
    std::visit([](const auto& event) {
        using T = std::decay_t<decltype(event)>;
        if constexpr (std::is_same_v<T, quit_event>) {
            // Handle quit
        } else if constexpr (std::is_same_v<T, key_event>) {
            // Handle key
        }
    }, ev);
}

Efficient Abstractions

1. Inline Functions for Simple Wrappers

Mark simple wrapper functions as inline to minimize overhead.

class window {
    SDL_Window* m_window;

public:
    inline void show() { SDL_ShowWindow(m_window); }
    inline void hide() { SDL_HideWindow(m_window); }
    inline void maximize() { SDL_MaximizeWindow(m_window); }
    inline void minimize() { SDL_MinimizeWindow(m_window); }

    inline size size() const {
        int w, h;
        SDL_GetWindowSize(m_window, &w, &h);
        return {w, h};
    }
};

2. Compile-Time Tests

Use static_assert to validate assumptions at compile time.

// Ensure our wrappers have the same size as SDL types
static_assert(sizeof(laya::color) == 4, "Color should be 4 bytes");
static_assert(sizeof(laya::point) == 8, "Point should be 8 bytes");
static_assert(sizeof(laya::rect) == 16, "Rect should be 16 bytes");

// Ensure enum values match SDL constants
static_assert(static_cast<uint32_t>(window_flags::resizable)
              == SDL_WINDOW_RESIZABLE);

Memory Management

RAII with Move Semantics

All resource-owning types are non-copyable but movable.

class window {
    SDL_Window* m_window;

public:
    // Non-copyable
    window(const window&) = delete;
    window& operator=(const window&) = delete;

    // Movable
    window(window&& other) noexcept
        : m_window(std::exchange(other.m_window, nullptr)) {}

    window& operator=(window&& other) noexcept {
        if (this != &other) {
            if (m_window) SDL_DestroyWindow(m_window);
            m_window = std::exchange(other.m_window, nullptr);
        }
        return *this;
    }

    ~window() {
        if (m_window) {
            SDL_DestroyWindow(m_window);
        }
    }
};

Design Patterns

1. Thin Wrapper Philosophy

Laya provides thin wrappers that map directly to SDL3 functions:

  • Direct mapping to SDL3 API
  • Minimal overhead - just type safety and convenience
  • No additional abstractions beyond what SDL provides
  • Optional QoL features don't interfere with core functionality

2. No SDL Exposure in Public Headers

SDL types never appear in public headers:

// In public header (window.hpp):
void set_title(std::string_view title);

// In implementation (window.cpp):
void window::set_title(std::string_view title) {
    SDL_SetWindowTitle(m_window, title.data());
}

Users never see SDL_Window* or other SDL types directly.

3. Type Conversion Helpers

Conversion functions are internal and not exposed:

namespace {  // anonymous namespace - internal only
    constexpr int to_sdl_category(log_category cat) noexcept {
        return static_cast<int>(cat);
    }

    constexpr SDL_LogPriority to_sdl_priority(log_priority pri) noexcept {
        return static_cast<SDL_LogPriority>(pri);
    }
}

File Organization

include/laya/
├── laya.hpp              # Main include file
├── errors.hpp            # Error handling types
├── subsystems.hpp        # Subsystem initialization
├── bitmask.hpp           # Bitmask utilities
├── events/
│   ├── event_types.hpp   # Event variant types
│   ├── event_polling.hpp # Event polling API
│   └── event_window.hpp  # Window events
├── windows/
│   ├── window.hpp        # Window class
│   ├── window_flags.hpp  # Window flags enum
│   └── window_id.hpp     # Window ID type
├── renderers/
│   ├── renderer.hpp      # Renderer class
│   ├── renderer_flags.hpp
│   └── renderer_types.hpp
└── logging/
    ├── log.hpp           # Logging API
    ├── log_priority.hpp  # Priority enum
    └── log_category.hpp  # Category enum

src/laya/
├── window.cpp            # Window implementation
├── renderer.cpp          # Renderer implementation
├── log.cpp              # Logging implementation
├── event_polling.cpp    # Event polling implementation
└── subsystems.cpp       # Subsystem initialization

Testing Strategy

1. Unit Tests

Test individual components in isolation:

TEST_CASE("Window RAII") {
    laya::context ctx{laya::subsystem::video};

    {
        laya::window win{"Test", {800, 600}};
        CHECK(win.native_handle() != nullptr);
    }
    // Window automatically destroyed
}

2. Performance Benchmarks

Measure overhead and optimize hot paths:

void benchmark_laya_wrapper() {
    laya::renderer ren{window};
    auto start = std::chrono::high_resolution_clock::now();

    for (int i = 0; i < 1000000; ++i) {
        ren.set_draw_color(laya::color::red());
    }

    auto end = std::chrono::high_resolution_clock::now();
    // Compare with raw SDL to measure wrapper overhead
}

3. Integration Tests

Test real-world usage patterns in the examples/ directory.


Summary

Laya's architecture demonstrates these key principles:

RAII - Automatic resource management ✅ Type Safety - Strong enums, compile-time checks ✅ Modern C++20 - Concepts, std::format, constexpr ✅ Minimal Overhead - Inline functions, efficient design ✅ Thin Wrapper - Direct mapping to SDL3 ✅ No SDL Exposure - Clean public API

This architecture serves as the foundation for all Laya components, ensuring consistency, safety, and performance across the entire library.