yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
SDL2 to SDL3 Migration and Rendering Abstraction Plan

1. Introduction

This document outlines a strategic plan to refactor the rendering architecture of the yaze application. The primary goals are:

  1. Decouple the application from the SDL2 rendering API.
  2. Create a clear and straightforward path for migrating to SDL3.
  3. 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> // For SDL_Rect, SDL_Color, etc.
#include <memory>
#include <vector>
#include "app/gfx/bitmap.h"
namespace yaze {
namespace gfx {
// Forward declarations
class Bitmap;
// A handle to a texture, abstracting away the underlying implementation
using TextureHandle = void*;
class IRenderer {
public:
virtual ~IRenderer() = default;
// --- Initialization and Lifecycle ---
virtual bool Initialize(SDL_Window* window) = 0;
virtual void Shutdown() = 0;
// --- Texture Management ---
virtual TextureHandle CreateTexture(int width, int height) = 0;
virtual void UpdateTexture(TextureHandle texture, const Bitmap& bitmap) = 0;
virtual void DestroyTexture(TextureHandle texture) = 0;
// --- Rendering Primitives ---
virtual void Clear() = 0;
virtual void Present() = 0;
virtual void RenderCopy(TextureHandle texture, const SDL_Rect* srcrect, const SDL_Rect* dstrect) = 0;
virtual void SetRenderTarget(TextureHandle texture) = 0;
virtual void SetDrawColor(SDL_Color color) = 0;
// --- Backend-specific Access ---
// Provides an escape hatch for libraries like ImGui that need the concrete renderer.
virtual void* GetBackendRenderer() = 0;
};
} // namespace gfx
} // namespace yaze
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.
Definition irenderer.h:24
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

// sdl2_renderer.h
#include "app/gfx/irenderer.h"
namespace yaze {
namespace gfx {
class SDL2Renderer : public IRenderer {
public:
~SDL2Renderer() override;
bool Initialize(SDL_Window* window) override;
void Shutdown() override;
TextureHandle CreateTexture(int width, int height) override;
void UpdateTexture(TextureHandle texture, const Bitmap& bitmap) override;
void DestroyTexture(TextureHandle texture) override;
void Clear() override;
void Present() override;
void RenderCopy(TextureHandle texture, const SDL_Rect* srcrect, const SDL_Rect* dstrect) override;
void SetRenderTarget(TextureHandle texture) override;
void SetDrawColor(SDL_Color color) override;
void* GetBackendRenderer() override { return renderer_.get(); }
private:
std::unique_ptr<SDL_Renderer, util::SDL_Deleter> renderer_;
};
} // namespace gfx
} // namespace yaze
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

  1. Create IRenderer and SDL2Renderer: Implement the interface and concrete class as defined above.
  2. 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.
  3. 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.
  4. 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 TextureHandles.
    • **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.
  5. 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.

  1. 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.
  2. Update Build System: The CMake files will be updated to link against SDL3 instead of SDL2.
  3. 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.

  1. Implement New Backends: Create new classes like OpenGLRenderer, MetalRenderer, or VulkanRenderer. Each will implement the IRenderer interface using the corresponding graphics API.
  2. 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.
  3. 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.