Where to find the code:
- Header:
include/utils/WorldRenderPipeline.hpp - Implementation:
src/utils/WorldRenderPipeline.cpp
Note: This documentation covers the SDL_Renderer path only. For GPU rendering, see GPURendering.md which uses GPUSceneRenderer as its parallel implementation.
WorldRenderPipeline is a unified facade that coordinates chunk management and scene composition for the SDL_Renderer rendering path. It wraps SceneRenderer and provides a four-phase architecture for clean, predictable world rendering.
- Four-Phase Architecture: prepareChunks → beginScene → renderWorld → endScene
- Unified Coordination: Single point of control for TileRenderer and SceneRenderer
- RenderContext: All render parameters computed once and shared
- Loading-Time Pre-warm: Renders visible chunks during loading to prevent hitches
Without WorldRenderPipeline, game states must:
- Manually coordinate dirty chunk processing
- Call SceneRenderer begin/end with correct parameters
- Track render state across multiple systems
- Duplicate camera calculations
WorldRenderPipeline provides a simple four-phase API that handles all coordination internally.
void update(float dt) {
updateCamera(dt);
m_renderPipeline->prepareChunks(*m_camera, dt);
}Processes dirty chunks (from season changes, tile modifications) with proper render target management. Call this in update(), not render().
void render(float interpolation) {
auto ctx = m_renderPipeline->beginScene(renderer, *m_camera, interpolation);
if (!ctx) return; // Handle error
// ...
}Sets up SceneRenderer intermediate texture and calculates all render parameters once. Returns a RenderContext containing floored camera positions, view dimensions, and zoom.
void render(float interpolation) {
auto ctx = m_renderPipeline->beginScene(renderer, *m_camera, interpolation);
if (!ctx) return;
m_renderPipeline->renderWorld(renderer, ctx);
// Render entities using ctx.cameraX, ctx.cameraY
for (auto& entity : m_entities) {
entity->render(renderer, ctx.cameraX, ctx.cameraY, interpolation);
}
// ...
}Renders visible tile chunks to the current render target using pre-computed context.
void render(float interpolation) {
// ... beginScene, renderWorld, entities ...
m_renderPipeline->endScene(renderer);
// UI renders after endScene (at 1.0 scale)
UIManager::Instance().render();
}Composites the intermediate texture to screen with zoom and sub-pixel offset. Resets render scale to 1.0 for UI rendering.
struct RenderContext {
// Camera position for entities (floored - sub-pixel via composite offset)
float cameraX{0.0f};
float cameraY{0.0f};
// Camera position for tiles (floored - pixel-aligned, same as cameraX/Y)
float flooredCameraX{0.0f};
float flooredCameraY{0.0f};
// Sub-pixel offset for smooth scrolling (applied in endScene)
float subPixelOffsetX{0.0f};
float subPixelOffsetY{0.0f};
// View dimensions at 1x scale (divide by zoom for effective view)
float viewWidth{0.0f};
float viewHeight{0.0f};
// Current zoom level
float zoom{1.0f};
// Camera world position (for followed entity - avoids double-interpolation jitter)
Vector2D cameraCenter{0.0f, 0.0f};
// Whether the context is valid (beginScene succeeded)
bool valid{false};
explicit operator bool() const { return valid; }
};| Field | Use For | Notes |
|---|---|---|
cameraX/Y |
Entity rendering | Floored for pixel alignment |
flooredCameraX/Y |
Tile rendering | Same as cameraX/Y, explicit name |
subPixelOffsetX/Y |
Internal | Applied in endScene composite |
viewWidth/Height |
Visibility culling | At 1x scale |
zoom |
Scale queries | Applied automatically |
cameraCenter |
Followed entity | Use instead of entity.getPosition() |
valid |
Error checking | False if beginScene failed |
class GamePlayState : public GameState {
private:
Camera m_camera;
std::unique_ptr<WorldRenderPipeline> m_renderPipeline;
public:
bool enter() override {
m_renderPipeline = std::make_unique<WorldRenderPipeline>();
return true;
}
void update(float dt) override {
// Update camera first
m_camera.update(dt);
// Phase 1: Prepare chunks (process dirty chunks)
m_renderPipeline->prepareChunks(m_camera, dt);
// Other update logic...
}
void render(float interpolation) override {
auto& renderer = GameEngine::Instance().getRenderer();
// Phase 2: Begin scene - get render context
auto ctx = m_renderPipeline->beginScene(renderer, m_camera, interpolation);
if (!ctx) {
GAMESTATE_WARN("RenderPipeline unavailable");
return;
}
// Phase 3: Render world tiles
m_renderPipeline->renderWorld(renderer, ctx);
// Render entities using context coordinates
for (auto& npc : m_npcs) {
npc->render(renderer, ctx.cameraX, ctx.cameraY, interpolation);
}
// Render followed entity using cameraCenter (avoids jitter)
if (mp_Player) {
mp_Player->renderAtCameraCenter(renderer, ctx.cameraCenter, interpolation);
}
// Render particles
ParticleManager::Instance().render(renderer, ctx.cameraX, ctx.cameraY);
// Phase 4: End scene - composite with zoom and sub-pixel offset
m_renderPipeline->endScene(renderer);
// UI renders at native resolution (after endScene)
UIManager::Instance().render();
}
};Prevent hitches when gameplay starts by pre-warming visible chunks during loading:
void LoadingState::onWorldGenerationComplete() {
// Pre-warm chunks that will be visible at spawn point
m_renderPipeline->prewarmVisibleChunks(
renderer,
spawnPoint.x, spawnPoint.y,
static_cast<float>(m_viewportWidth),
static_cast<float>(m_viewportHeight)
);
}WorldRenderPipeline();
~WorldRenderPipeline();
// Non-copyable (owns resources)
WorldRenderPipeline(const WorldRenderPipeline&) = delete;
WorldRenderPipeline& operator=(const WorldRenderPipeline&) = delete;
// Movable
WorldRenderPipeline(WorldRenderPipeline&&) noexcept;
WorldRenderPipeline& operator=(WorldRenderPipeline&&) noexcept;// Phase 1: Call in update()
void prepareChunks(Camera& camera, float deltaTime);
// Phase 2: Call in render(), returns context
RenderContext beginScene(SDL_Renderer* renderer, Camera& camera, float interpolationAlpha);
// Phase 3: Call after beginScene
void renderWorld(SDL_Renderer* renderer, const RenderContext& ctx);
// Phase 4: Call after all scene rendering
void endScene(SDL_Renderer* renderer);// Pre-warm chunks during loading
void prewarmVisibleChunks(SDL_Renderer* renderer, float centerX, float centerY,
float viewWidth, float viewHeight);
// Access underlying SceneRenderer (advanced use)
SceneRenderer* getSceneRenderer();
// Check if between beginScene/endScene
bool isSceneActive() const;| Metric | Value | Notes |
|---|---|---|
| prepareChunks | ~0.5ms (dirty) | 0ms if no dirty chunks |
| beginScene | ~0.1ms | Render target switch |
| renderWorld | ~1-3ms | Depends on visible chunks |
| endScene | ~0.1ms | Composite to screen |
WorldRenderPipeline owns a SceneRenderer internally. The pipeline manages the SceneRenderer lifecycle and provides a simpler API.
The pipeline requires a Camera reference for:
- Position (via
getRenderOffset()) - Zoom level (via
getZoom()) - Viewport dimensions (via
getViewport())
The pipeline coordinates with WorldManager for:
- Dirty chunk processing (season changes)
- Tile rendering (via TileRenderer)
- Chunk visibility calculations
For GPU rendering, use GPUSceneRenderer instead. The two systems are parallel implementations:
| SDL_Renderer Path | GPU Path |
|---|---|
| WorldRenderPipeline | GPUSceneRenderer |
| SceneRenderer | GPURenderer (scene texture) |
| RenderContext | GPUSceneContext |
- SceneRenderer - Underlying scene rendering
- Camera - Camera position and zoom
- WorldManager - Chunk and tile management
- GPURendering - GPU path equivalent