- Introduced a complete guide for the Dungeon Editor, detailing features, architecture, and usage instructions, ensuring users can effectively utilize the tool for dungeon creation. - Added an Overworld Agent Guide to facilitate AI interaction with the overworld editor, covering tools, commands, and best practices for automation and AI-generated edits. - Included a migration plan for transitioning from SDL2 to SDL3, outlining the necessary steps for refactoring the rendering architecture to support modern graphics APIs. - Enhanced the palette system overview, detailing SNES color formats, palette organization, and common issues, providing developers with essential insights for effective color management. - Updated the emulator development guide to reflect the latest status and improvements, confirming production readiness and outlining future enhancements.
8.7 KiB
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:
- 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, andgfx::AtlasRendererdirectly accept or call functions using anSDL_Renderer*. - Singleton Pattern: The
core::Renderersingleton insrc/app/core/window.hprovides global access to theSDL_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 Renderer 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 IRenderer 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
3.2. The SDL2Renderer 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"
#include "util/sdl_deleter.h"
namespace yaze {
namespace gfx {
class SDL2Renderer : public IRenderer {
public:
SDL2Renderer();
~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
4. Migration Plan
The migration will be executed in phases to ensure stability and minimize disruption.
Phase 1: Implement the Abstraction Layer
- Create
IRendererandSDL2Renderer: Implement the interface and concrete class as defined above. - Refactor
core::RendererSingleton: The existingcore::Renderersingleton will be deprecated and removed. A new central mechanism (e.g., a service locator or passing theIRendererinstance) will provide access to the active renderer. - Update Application Entry Points:
- In
app/core/controller.cc(for the main editor) andapp/emu/emu.cc(for the emulator), instantiateSDL2Rendererduring initialization. TheControllerwill own theunique_ptr<IRenderer>. - This immediately unifies the rendering pipeline initialization for both application modes.
- In
- Refactor
gfxLibrary:gfx::Bitmap: ModifyCreateTextureandUpdateTextureto accept anIRenderer*instead of anSDL_Renderer*. TheSDL_Texture*will be replaced with the abstractTextureHandle.gfx::Arena: TheAllocateTexturemethod will now callrenderer->CreateTexture(). The internal pools will storeTextureHandles.gfx::AtlasRenderer: TheInitializemethod will take anIRenderer*. All calls toSDL_RenderCopy,SDL_SetRenderTarget, etc., will be replaced with calls to the corresponding methods on theIRendererinterface.
- Update ImGui Integration:
- The ImGui backend requires the concrete
SDL_Renderer*. TheGetBackendRenderer()method on the interface provides a type-erasedvoid*for this purpose. - The ImGui initialization code will be modified as follows:
// Before ImGui_ImplSDLRenderer2_Init(sdl_renderer_ptr); // After auto backend_renderer = renderer->GetBackendRenderer(); ImGui_ImplSDLRenderer2_Init(static_cast<SDL_Renderer*>(backend_renderer));
- The ImGui backend requires the concrete
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 theIRendererinterface 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
TextureHandlewill now correspond to anSDL_Texture*from SDL3.
- This class will handle the differences in the SDL3 API (e.g.,
- 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 instantiateSDL3Rendererinstead ofSDL2Renderer.
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, orVulkanRenderer. Each will implement theIRendererinterface 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). TheGetBackendRenderer()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.