C++, SDL, and RAII – Code Review Stack Exchange

Learning Modern C++ patterns and this ended up being the direction I went to to create an encapsulation of SDL’s window management, while trying to stay with RAII practices. This is meant to be a base for a software renderer (and possibly expanding to OpenGL after that).

window/input.h

#pragma once

#include <map>

// State of a keyboard key
enum class KeyboardKeyState {
    Up,
    Down,
    Pressed, // Key was previously down last check and just changed to the up state
};

// Keyboard keys that we are listening for events for
enum class KeyboardKey {
    W, S, A, D, LeftArrow, RightArrow, UpArrow, DownArrow
};

struct InputState {
    std::map<KeyboardKey, KeyboardKeyState> Keys{
            {KeyboardKey::W, KeyboardKeyState::Up},
            {KeyboardKey::S, KeyboardKeyState::Up},
            {KeyboardKey::A, KeyboardKeyState::Up},
            {KeyboardKey::D, KeyboardKeyState::Up},
            {KeyboardKey::LeftArrow, KeyboardKeyState::Up},
            {KeyboardKey::UpArrow, KeyboardKeyState::Up},
            {KeyboardKey::DownArrow, KeyboardKeyState::Up},
            {KeyboardKey::RightArrow, KeyboardKeyState::Up},
    };

    bool LeftMouseClicked{};
    bool RightMouseClicked{};
    int MouseDragX{};
    int MouseDragY{};
    int MouseScrollAmount{};
};

window/window_state.h

#pragma once

struct WindowState {
    bool quitRequested{false};
};

window/window_settings.h

#pragma once

#include <optional>
#include <string>
#include <SDL.h>

// How the window will be rendered to (e.g. manually via a pixel buffer, OGL context, Vulkan context, etc...)
enum class WindowRenderMode {
    Unspecified, // No render mode selected.  Will throw exceptions
    ByPixelBuffer, // Rendered using software rendering directly to a window's pixel buffer
};

// Contains options for the window display
struct WindowSettings {
    // How many pixels wide the window should be. If unspecified it will match the display's width.
    std::optional<int> Width;

    // How many pixels tall the window should be. If unspecified it will match the display's height
    std::optional<int> Height;

    // If the window should be borderless or not
    bool Borderless{false};

    // The title to give the window
    std::string Title{};

    // How we intend to render to the window
    WindowRenderMode RenderMode{WindowRenderMode::Unspecified};
};

window/sdl/sdl_raii.h

#pragma once

// C++ can't deal with classes that use forward declared types, so we need all these RAII types
// in it's own header to keep it out of sdl_app_window.h

#include <memory>
#include <vector>
#include <SDL.h>
#include "window/window_settings.h"

namespace sdl_raii {
    class SdlWindow {
    public:
        std::unique_ptr<SDL_Window, void(*)(SDL_Window*)> pointer;

        explicit SdlWindow(const WindowSettings& windowSettings);
        SdlWindow(const SdlWindow& other) = delete;
        SdlWindow(SdlWindow&& other) = default;
    };

    class SdlRenderer {
    public:
        std::unique_ptr<SDL_Renderer, void(*)(SDL_Renderer*)> pointer;

        explicit SdlRenderer(const SdlWindow& window);
        SdlRenderer(const SdlRenderer& other) = delete;
        SdlRenderer(SdlRenderer&& other) = default;
    };

    class SdlFullWindowTexture {
    public:
        std::unique_ptr<SDL_Texture, void(*)(SDL_Texture*)> pointer;

        SdlFullWindowTexture(const WindowSettings& windowSettings, const SdlRenderer& renderer);
        SdlFullWindowTexture(const SdlFullWindowTexture& other) = delete;
        SdlFullWindowTexture(SdlFullWindowTexture&& other) = default;
    };
}

window/sdl/sdl_raii.cpp

#include <stdexcept>
#include "sdl_raii.h"

using namespace sdl_raii;

std::unique_ptr<SDL_Window, void (*)(SDL_Window*)> CreateSdlWindowPointer(const WindowSettings &windowSettings) {
    if (windowSettings.RenderMode != WindowRenderMode::ByPixelBuffer) {
        std::string error = "Unsupported render mode: ";
        error += std::to_string((int) windowSettings.RenderMode);
        throw std::runtime_error{error};
    }

    if (SDL_Init(SDL_INIT_EVERYTHING) != 0) {
        std::string error{"Error initializing SDL: "};
        error += SDL_GetError();

        throw std::runtime_error{error};
    }

    unsigned int flags = windowSettings.Borderless ? SDL_WINDOW_BORDERLESS : 0;

    auto sdlWin = SDL_CreateWindow(windowSettings.Title.c_str(),
                                   SDL_WINDOWPOS_CENTERED,
                                   SDL_WINDOWPOS_CENTERED,
                                   windowSettings.Width.value(),
                                   windowSettings.Height.value(),
                                   flags);

    if (sdlWin == nullptr) {
        std::string error = "Error creating SDL window: ";
        error += SDL_GetError();
        throw std::runtime_error{error};
    }

    return std::unique_ptr<SDL_Window, void(*)(SDL_Window*)>(sdlWin, &SDL_DestroyWindow);
}

std::unique_ptr<SDL_Renderer, void (*)(SDL_Renderer*)> CreateSdlRendererPointer(const SdlWindow& sdlWindow) {
    auto renderer = SDL_CreateRenderer(sdlWindow.pointer.get(), -1, 0);
    if (!renderer) {
        std::string error{"Error creating renderer: "};
        error += SDL_GetError();

        throw std::runtime_error{error};
    }

    return std::unique_ptr<SDL_Renderer, void (*)(SDL_Renderer*)>(renderer, &SDL_DestroyRenderer);
}

std::unique_ptr<SDL_Texture, void (*)(SDL_Texture*)>
CreateSdlTexturePointer(const WindowSettings &windowSettings, const SdlRenderer& sdlRenderer) {
    auto pointer = SDL_CreateTexture(sdlRenderer.pointer.get(),
                                     SDL_PIXELFORMAT_ARGB8888,
                                     SDL_TEXTUREACCESS_STREAMING,
                                     windowSettings.Width.value(),
                                     windowSettings.Height.value());

    return std::unique_ptr<SDL_Texture, void (*)(SDL_Texture*)>(pointer, &SDL_DestroyTexture);
}

SdlWindow::SdlWindow(const WindowSettings &windowSettings)
    : pointer{CreateSdlWindowPointer(windowSettings)} {
}

SdlRenderer::SdlRenderer(const SdlWindow& window)
    : pointer{CreateSdlRendererPointer(window)} {
}

SdlFullWindowTexture::SdlFullWindowTexture(const WindowSettings &windowSettings, const SdlRenderer &renderer)
    : pointer{CreateSdlTexturePointer(windowSettings, renderer)}{
}

window/sdl/sdl_app_window.h

#pragma once

#include <SDL.h>
#include <memory>
#include <vector>
#include <span>
#include "window/input.h"
#include "window/window_state.h"
#include "window/window_settings.h"
#include "sdl_raii.h"

// A window that's managed by SDL
class SdlAppWindow {
public:
    explicit SdlAppWindow(WindowSettings settings);
    SdlAppWindow(const SdlAppWindow& other) = delete;
    SdlAppWindow(SdlAppWindow&& other) = default;

    // Retrieves a mutable view of the raw full screen pixel buffer.
    std::span<unsigned int> GetPixelBuffer();

    // Performs any work that needs to be done at the beginning of a frame.
    void BeginFrame();

    // Called after all render operations have occurred, in order to push the renderings to the window
    void PresentFrame();

    void HandleWindowEvents(WindowState& windowState, InputState& inputState);

private:
    const WindowSettings windowSettings;
    sdl_raii::SdlWindow sdlWindow;
    sdl_raii::SdlRenderer sdlRenderer;
    sdl_raii::SdlFullWindowTexture sdlFullWindowTexture;

    // Full screen render buffer for use in the ByPixelBuffer render mode
    std::vector<unsigned int> pixelBuffer;

    static WindowSettings GetUpdatedWindowSettings(WindowSettings windowSettings);
    std::vector<unsigned int> CreatePixelBuffer();
};

window/sdl/sdl_app_window.cpp

#include <utility>
#include <stdexcept>
#include "sdl_app_window.h"

using std::vector;

SdlAppWindow::SdlAppWindow(WindowSettings settings) :
        windowSettings{GetUpdatedWindowSettings(std::move(settings))},
        sdlWindow{windowSettings},
        sdlRenderer{sdlWindow},
        sdlFullWindowTexture(windowSettings, sdlRenderer),
        pixelBuffer{CreatePixelBuffer()} {
}

void SdlAppWindow::BeginFrame() {
    switch (windowSettings.RenderMode) {
        case WindowRenderMode::ByPixelBuffer:
            // fill to black
            std::fill(pixelBuffer.begin(), pixelBuffer.end(), 0xFF000000);
            break;

        default:
            std::string error{"No begin frame support for render mode: "};
            error += std::to_string((int) windowSettings.RenderMode);
            throw std::runtime_error{error};
    }
}

void SdlAppWindow::PresentFrame() {
    int pitch = windowSettings.Width.value() * (int) sizeof (int);
    SDL_UpdateTexture(sdlFullWindowTexture.pointer.get(),
                      nullptr,
                      pixelBuffer.data(),
                      pitch);

    SDL_RenderCopy(sdlRenderer.pointer.get(), sdlFullWindowTexture.pointer.get(), nullptr, nullptr);
    SDL_RenderPresent(sdlRenderer.pointer.get());
}

WindowSettings SdlAppWindow::GetUpdatedWindowSettings(WindowSettings windowSettings) {
    SDL_DisplayMode displayMode;
    SDL_GetCurrentDisplayMode(0, &displayMode);

    if (!windowSettings.Width.has_value()) {
        windowSettings.Width = displayMode.w;
    }

    if (!windowSettings.Height.has_value()) {
        windowSettings.Height = displayMode.h;
    }

    return windowSettings;
}

std::vector<unsigned int> SdlAppWindow::CreatePixelBuffer() {
    std::vector<unsigned int> result(windowSettings.Width.value() * windowSettings.Height.value());

    return result;
}

std::span<unsigned int> SdlAppWindow::GetPixelBuffer() {
    return std::span<unsigned int>{pixelBuffer};
}

void SdlAppWindow::HandleWindowEvents(WindowState& windowState, InputState &inputState) {
    SDL_Event sdlEvent;
    while (SDL_PollEvent(&sdlEvent))
    {
        switch (sdlEvent.type) {
            case SDL_QUIT:
                windowState.quitRequested = true;
                break;

            // todo: add case statements for keyboard/mouse processing
        }
    }
}

main.cpp

#include <iostream>
#include <SDL.h>
#include "window/window_settings.h"
#include "window/sdl/sdl_app_window.h"

int main(int argc, char* argv()) {
    constexpr unsigned int targetFps = 30;
    constexpr unsigned int targetFrameTime = 1000 / targetFps;

    WindowSettings settings{
        1024,
        768,
        false,
        std::string{"Test Window"},
        WindowRenderMode::ByPixelBuffer,
    };

    SdlAppWindow appWindow{settings};

    InputState inputState;
    WindowState windowState;

    unsigned int previousFrameTime = 0;
    bool isRunning = true;
    while(isRunning) {
        unsigned int timeSinceLastFrame = SDL_GetTicks() - previousFrameTime;
        int timeToWait = targetFrameTime - timeSinceLastFrame;
        if (timeToWait > 0 && timeToWait <= targetFrameTime) {
            SDL_Delay(timeToWait);
        }

        previousFrameTime = SDL_GetTicks();

        appWindow.HandleWindowEvents(windowState, inputState);
        appWindow.BeginFrame();

        auto buffer = appWindow.GetPixelBuffer();
        std::fill(buffer.begin(), buffer.end(), 0xFFFF0000);

        appWindow.PresentFrame();

        isRunning = !windowState.quitRequested;
    }

    return 0;
}
```