5
\$\begingroup\$

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;
}
```
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Note, this is just a partial review of your code.

Use namespaces and nested classes

The way you named things in your code is a bit inconsistent. There is the namespace sdl_raii which contains classes like SdlWindow, but then there is also SdlAppWindow which is not in a namespace, and there are also classes like WindowState which are not in a namespace and don't have the Sdl prefix.

I would recommend putting everything in the namespace SDL, and nesting classes where appropriate. For example, since WindowSettings seems to be holding configuration specific to SdlWindow, the following hierarchy makes more sense:

namespace SDL {
    class Window {
    public:
        class Settings {
            ...
        };

        Window(const Settings &settings);
        ...
   }
};

The application code would then look like:


int main(...) {
    SDL::Window::Settings settings{
        1024,
        768,
        false,
        "Test Window",
        SDL::Window::RenderMode::ByPixelBuffer,
    };

    SDL::AppWindow appWindow{settings};
    ...
}

This also allows the application to use using namespace SDL or using SDL::Window for example, to reduce the amount of typing necessary, if desired.

Move the code from Create...Pointer() into the constructors

The constructors of the classes in sdl_raii don't do anything except call Create...Pointer(), and the latter functions are not called by anything else, so this separation seems quite unnecessary to me. I would just move all the code from those Create functions into the constructors of the corresponding classes.

\$\endgroup\$
3
  • \$\begingroup\$ The reason I used sdl_raii namespace for the 3 classes but not SdlAppWindow is because SdlAppWindow is application specific while the others are very specific for RAII purposes. So I moved the 3 resource classes into their own namespace so when you are referring to SdlAppWindow you aren't given the option for the others (since 0% chance you want the raii classes. I could not get nesting to work properly due to ordering issues (I could not get forward declarations with nested classes to work properly and the whole thing looked like a mess) \$\endgroup\$
    – KallDrexx
    Apr 5, 2021 at 1:37
  • \$\begingroup\$ Re constructors: I did it this way as otherwise you get double allocations. Maybe that's not a big deal though? \$\endgroup\$
    – KallDrexx
    Apr 5, 2021 at 1:38
  • 1
    \$\begingroup\$ If you want to abstract the lower level window handling away, then sure, but someone might actually want to construct a SdlWindow directly if SdlAppWindow doesn't provide what they need. As for the constructors: there are no double allocations. \$\endgroup\$
    – G. Sliepen
    Apr 5, 2021 at 9:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.