1. Introduction
This document outlines a strategic plan to refactor the rendering architecture of the yaze
application. The primary goals are:
- Decouple the application from the SDL2 rendering API.
- Create a clear and straightforward path for migrating to SDL3.
- Enable support for multiple rendering backends (e.g., OpenGL, Metal, DirectX) to improve cross-platform performance and leverage modern graphics APIs.
2. Current State Analysis
The current architecture exhibits tight coupling with the SDL2 rendering API.
- Direct Dependency: Components like
gfx::Bitmap
, gfx::Arena
, and gfx::AtlasRenderer
directly accept or call functions using an SDL_Renderer*
.
- Singleton Pattern: The
core::Renderer
singleton in src/app/core/window.h
provides global access to the SDL_Renderer
, making it difficult to manage, replace, or mock.
- Dual Rendering Pipelines: The main application (
yaze.cc
, app_delegate.mm
) and the standalone emulator (app/emu/emu.cc
) both perform their own separate, direct SDL initialization and rendering loops. This code duplication makes maintenance and migration efforts more complex.
This tight coupling makes it brittle, difficult to maintain, and nearly impossible to adapt to newer rendering APIs like SDL3 or other backends without a major, project-wide rewrite.
3. Proposed Architecture: The <tt>Renderer</tt> Abstraction
The core of this plan is to introduce a Renderer
interface (an abstract base class) that defines a set of rendering primitives. The application will be refactored to program against this interface, not a concrete SDL2 implementation.
3.1. The <tt>IRenderer</tt> Interface
A new interface, IRenderer
, will be created. It will define the contract for all rendering operations.
File: src/app/gfx/irenderer.h
#pragma once
#include <SDL.h>
#include <memory>
#include <vector>
namespace gfx {
class Bitmap;
class IRenderer {
public:
virtual void Clear() = 0;
};
}
}
virtual TextureHandle CreateTexture(int width, int height)=0
Creates a new, empty texture.
virtual bool Initialize(SDL_Window *window)=0
Initializes the renderer with a given window.
virtual void * GetBackendRenderer()=0
Provides an escape hatch to get the underlying, concrete renderer object.
virtual void UpdateTexture(TextureHandle texture, const Bitmap &bitmap)=0
Updates a texture with the pixel data from a Bitmap.
virtual ~IRenderer()=default
virtual void RenderCopy(TextureHandle texture, const SDL_Rect *srcrect, const SDL_Rect *dstrect)=0
Copies a portion of a texture to the current render target.
virtual void SetDrawColor(SDL_Color color)=0
Sets the color used for drawing operations (e.g., Clear).
virtual void Shutdown()=0
Shuts down the renderer and releases all associated resources.
virtual void Present()=0
Presents the back buffer to the screen, making the rendered content visible.
virtual void Clear()=0
Clears the entire render target with the current draw color.
virtual void DestroyTexture(TextureHandle texture)=0
Destroys a texture and frees its associated resources.
virtual void SetRenderTarget(TextureHandle texture)=0
Sets the render target for subsequent drawing operations.
void * TextureHandle
An abstract handle representing a texture.
Main namespace for the application.
3.2. The <tt>SDL2Renderer</tt> Implementation
A concrete class, SDL2Renderer
, will be the first implementation of the IRenderer
interface. It will encapsulate all the existing SDL2-specific rendering logic.
File: src/app/gfx/sdl2_renderer.h
& src/app/gfx/sdl2_renderer.cc
#include "app/gfx/irenderer.h"
namespace gfx {
class SDL2Renderer : public IRenderer {
public:
private:
std::unique_ptr<SDL_Renderer, util::SDL_Deleter>
renderer_;
};
}
}
void SetDrawColor(SDL_Color color) override
Sets the draw color.
std::unique_ptr< SDL_Renderer, util::SDL_Deleter > renderer_
bool Initialize(SDL_Window *window) override
Initializes the SDL2 renderer. This function creates an accelerated SDL2 renderer and attaches it to ...
void DestroyTexture(TextureHandle texture) override
Destroys an SDL_Texture.
void UpdateTexture(TextureHandle texture, const Bitmap &bitmap) override
Updates an SDL_Texture with data from a Bitmap. This involves converting the bitmap's surface to the ...
void SetRenderTarget(TextureHandle texture) override
Sets the render target.
void * GetBackendRenderer() override
Provides access to the underlying SDL_Renderer*.
TextureHandle CreateTexture(int width, int height) override
Creates an SDL_Texture. The texture is created with streaming access, which is suitable for textures ...
void Present() override
Presents the rendered frame to the screen.
void Clear() override
Clears the screen with the current draw color.
void Shutdown() override
Shuts down the renderer. The underlying SDL_Renderer is managed by a unique_ptr, so its destruction i...
void RenderCopy(TextureHandle texture, const SDL_Rect *srcrect, const SDL_Rect *dstrect) override
Copies a texture to the render target.
4. Migration Plan
The migration will be executed in phases to ensure stability and minimize disruption.
Phase 1: Implement the Abstraction Layer
- Create
IRenderer
and SDL2Renderer
: Implement the interface and concrete class as defined above.
- Refactor
core::Renderer
Singleton: The existing core::Renderer
singleton will be deprecated and removed. A new central mechanism (e.g., a service locator or passing the IRenderer
instance) will provide access to the active renderer.
- Update Application Entry Points:
- In
app/core/controller.cc
(for the main editor) and app/emu/emu.cc
(for the emulator), instantiate SDL2Renderer
during initialization. The Controller
will own the unique_ptr<IRenderer>
.
- This immediately unifies the rendering pipeline initialization for both application modes.
- Refactor
gfx
Library:
- **
gfx::Bitmap
:** Modify CreateTexture
and UpdateTexture
to accept an IRenderer*
instead of an SDL_Renderer*
. The SDL_Texture*
will be replaced with the abstract TextureHandle
.
- **
gfx::Arena
:** The AllocateTexture
method will now call renderer->CreateTexture()
. The internal pools will store TextureHandle
s.
- **
gfx::AtlasRenderer
:** The Initialize
method will take an IRenderer*
. All calls to SDL_RenderCopy
, SDL_SetRenderTarget
, etc., will be replaced with calls to the corresponding methods on the IRenderer
interface.
- Update ImGui Integration:
- The ImGui backend requires the concrete
SDL_Renderer*
. The GetBackendRenderer()
method on the interface provides a type-erased void*
for this purpose.
The ImGui initialization code will be modified as follows: ```cpp // Before ImGui_ImplSDLRenderer2_Init(sdl_renderer_ptr);
// After auto backend_renderer = renderer->GetBackendRenderer(); ImGui_ImplSDLRenderer2_Init(static_cast<SDL_Renderer*>(backend_renderer)); ```
Phase 2: Migrate to SDL3
With the abstraction layer in place, migrating to SDL3 becomes significantly simpler.
- Create
SDL3Renderer
: A new class, SDL3Renderer
, will be created that implements the IRenderer
interface using SDL3's rendering functions.
- This class will handle the differences in the SDL3 API (e.g.,
SDL_CreateRendererWithProperties
, float-based rendering functions, etc.) internally.
- The
TextureHandle
will now correspond to an SDL_Texture*
from SDL3.
- Update Build System: The CMake files will be updated to link against SDL3 instead of SDL2.
- Switch Implementation: The application entry points (
controller.cc
, emu.cc
) will be changed to instantiate SDL3Renderer
instead of SDL2Renderer
.
The rest of the application, which only knows about the IRenderer
interface, will require no changes.
Phase 3: Support for Multiple Rendering Backends
The IRenderer
interface makes adding new backends a modular task.
- Implement New Backends: Create new classes like
OpenGLRenderer
, MetalRenderer
, or VulkanRenderer
. Each will implement the IRenderer
interface using the corresponding graphics API.
- Backend Selection: Implement a factory function or a strategy in the main controller to select and create the desired renderer at startup, based on platform, user configuration, or command-line flags.
- ImGui Backend Alignment: When a specific backend is chosen for
yaze
, the corresponding ImGui backend implementation must also be used (e.g., ImGui_ImplOpenGL3_Init
, ImGui_ImplMetal_Init
). The GetBackendRenderer()
method will provide the necessary context (e.g., ID3D11Device*
, MTLDevice*
) for each implementation.
5. Conclusion
This plan transforms the rendering system from a tightly coupled, monolithic design into a flexible, modular, and future-proof architecture.
Benefits:
- Maintainability: Rendering logic is centralized and isolated, making it easier to debug and maintain.
- Extensibility: Adding support for new rendering APIs (like SDL3, Vulkan, Metal) becomes a matter of implementing a new interface, not refactoring the entire application.
- Testability: The rendering interface can be mocked, allowing for unit testing of graphics components without a live rendering context.
- Future-Proofing: The application is no longer tied to a specific version of SDL, ensuring a smooth transition to future graphics technologies.