# How to Customize the Render Pipeline Architecture in Pyrite64 for Advanced Scene Rendering

> Deeply customize Pyrite64 render pipeline architecture by inheriting Renderer::Pipeline override shader loading and post-process hooks for advanced scene rendering.

- Repository: [Max Bebök/pyrite64](https://github.com/hailtododongo/pyrite64)
- Tags: deep-dive
- Published: 2026-02-19

---

**You can deeply customize Pyrite64’s render pipeline architecture by inheriting from `Renderer::Pipeline` to override shader loading, render-target configuration, and post-process hooks, while `Renderer::Scene` orchestrates these pipelines through modular render-pass callbacks that directly impact scene rendering performance, visual fidelity, and extensibility.**

Pyrite64 is an open-source N64-style graphics engine that separates scene management from rendering execution to enable flexible pipeline customization. Understanding how to customize the render pipeline architecture allows developers to implement custom shaders, post-processing effects like HDR bloom, and specialized rendering paths for debug visualization or particle systems.

## Core Render Pipeline Architecture in Pyrite64

Pyrite64’s design strictly separates *scene management* from *pipeline execution*. The `Renderer::Scene` class acts as a dispatcher that owns render-pass callbacks and light data, while concrete `Renderer::Pipeline` subclasses encapsulate the actual GPU state, shaders, and drawing logic.

### Key Classes and File Locations

| Class / File | Role | Key Link |
|--------------|------|----------|
| `Renderer::Scene` ([`src/renderer/scene.h`](https://github.com/HailToDodongo/pyrite64/blob/main/src/renderer/scene.h)) | Holds render-pass callbacks, light data, and owns the pipelines used for a frame. | <https://github.com/HailToDodongo/pyrite64/blob/main/src/renderer/scene.h> |
| `Renderer::Pipeline` ([`src/renderer/pipeline.h`](https://github.com/HailToDodongo/pyrite64/blob/main/src/renderer/pipeline.h)) | Abstract base for concrete pipelines (N64, line-draw, sprite-draw). Implements `init()`, `preDraw()`, `draw()`, and `postDraw()` hooks. | <https://github.com/HailToDodongo/pyrite64/blob/main/src/renderer/pipeline.h> |
| Concrete pipelines ([`pipelineDefault.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/pipelineDefault.cpp), [`pipelineHDRBloom.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/pipelineHDRBloom.cpp), [`pipelineBigTex.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/pipelineBigTex.cpp)) | Full rendering stacks under `n64/engine/src/renderer/` providing shader sets, descriptor sets, render-targets, and post-process. | <https://github.com/HailToDodongo/pyrite64/blob/main/n64/engine/src/renderer/pipelineDefault.cpp> |
| `Renderer::Shader` ([`src/renderer/shader.h`](https://github.com/HailToDodongo/pyrite64/blob/main/src/renderer/shader.h)) | Wraps compiled GLSL/SPIR-V shader modules bound by pipelines. | <https://github.com/HailToDodongo/pyrite64/blob/main/src/renderer/shader.h> |
| `Renderer::Object`, `Renderer::Mesh`, `Renderer::Texture` | Geometry, vertex-buffer, and texture resources consumed by pipelines. | <https://github.com/HailToDodongo/pyrite64/blob/main/src/renderer/object.h> |

## Render-Pass Workflow and Execution Order

The `Renderer::Scene` class orchestrates the frame by invoking pipeline hooks and registered callbacks. In [`src/editor/pages/parts/viewport3D.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/src/editor/pages/parts/viewport3D.cpp), the editor demonstrates this workflow by binding multiple pipelines within a single render pass.

```cpp
// Simplified workflow from Editor::Viewport3D::onRenderPass
SDL_GPUCommandBuffer* cmd = /* obtain command buffer */;
Renderer::Scene& scene = *ctx.scene;

// 1. Begin GPU render pass for the 3D target
SDL_GPURenderPass* rp = SDL_BeginGPURenderPass(cmd, &targetInfo, 1, nullptr);

// 2. Bind specific pipelines by name
scene.getPipeline("n64").bind(rp);      // N64-style rasterizer
scene.getPipeline("lines").bind(rp);    // Wire-frame debug lines
scene.getPipeline("sprites").bind(rp);  // 2D sprite overlay

// 3. Execute registered render-pass callbacks
for (auto& [id, pass] : scene.renderPasses) {
    pass(cmd, scene);
}

// 4. End render pass
SDL_EndGPURenderPass(rp);

```

The `Scene` object acts as a thin orchestration layer containing **no rendering logic itself**. All GPU state management, shader binding, and draw commands reside within the concrete `Pipeline` subclasses.

## How to Customize the Render Pipeline Architecture

Because `Renderer::Pipeline` is an abstract base class, you can deeply customize the render pipeline architecture by overriding specific virtual methods that control initialization, per-frame setup, drawing, and post-processing.

### Customization Points and Override Targets

| Customization Point | Override Target | Typical Effect |
|---------------------|-----------------|----------------|
| **Shader Modules** | `Pipeline::init()` | Load custom vertex/fragment shaders via `Shader::loadFromFile()` to change lighting models or shading languages. |
| **Descriptor Sets / Uniform Buffers** | `Pipeline::preDraw()` | Populate extra uniform buffers (e.g., per-object material indices) to enable skeletal animation or custom per-frame data. |
| **Render-Target Configuration** | `Pipeline::init()` | Change `SDL_GPUTexture` formats or attach extra render targets for MSAA, HDR, or deferred shading. |
| **Render-Pass Order** | `Scene::addRenderPass()` registration | Re-order callbacks to render opaque geometry before transparent objects or UI overlays. |
| **Post-Process Effects** | `Pipeline::postDraw()` | Insert full-screen quads with custom fragment shaders for bloom, tone-mapping, or edge detection. |
| **Pipeline Selection Logic** | `Scene::getPipeline()` mapping | Switch between "low-poly", "high-res", or debug pipelines at runtime based on scene configuration. |

### Implementing a Custom Post-Process Pipeline

To add a grayscale post-process effect, inherit from `Renderer::Pipeline` and override the initialization and post-draw hooks:

```cpp
#include "renderer/pipeline.h"
#include "renderer/shader.h"

class PipelineGrayScale : public Renderer::Pipeline {
public:
    void init() override {
        // Load fullscreen quad shaders
        shader = std::make_unique<Renderer::Shader>(
            "shaders/grayscale.vert.spv",
            "shaders/grayscale.frag.spv"
        );
        
        // Create intermediate render target for scene color
        createRenderTarget(SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
    }

    void postDraw(SDL_GPUCommandBuffer* cmd) override {
        // Bind grayscale shader and draw fullscreen triangle
        shader->bind(cmd);
        drawFullScreenTriangle(cmd);
    }
};

```

Register the custom pipeline in your scene configuration:

```cpp
// In scene setup or configuration parsing
if (conf.renderPipeline.value == 99) {  // Custom ID for grayscale
    pipelineGray = std::make_unique<PipelineGrayScale>();
    pipelineGray->init();
}

```

### Registering Custom Render Passes

For scene-specific rendering logic that does not belong in a pipeline, use the callback system in [`src/renderer/scene.h`](https://github.com/HailToDodongo/pyrite64/blob/main/src/renderer/scene.h):

```cpp
// Register a custom render pass with unique ID
ctx.scene->addRenderPass(42, [](SDL_GPUCommandBuffer* cmd, Renderer::Scene& sc) {
    // Custom geometry drawing logic
    myCustomObject.draw(cmd);
});

```

### Runtime Pipeline Switching

Change pipelines dynamically based on user input or quality settings:

```cpp
// UI toggle or quality setting change
if (newPipelineName != currentPipelineName) {
    currentPipeline = &ctx.scene->getPipeline(newPipelineName);
    // Pipeline binding occurs automatically in the next render pass
}

```

### One-Time Copy Passes for G-Buffer Readback

For debugging or CPU analysis of GPU data, register a one-time copy pass:

```cpp
ctx.scene->addOneTimeCopyPass([](SDL_GPUCommandBuffer* cmd,
                                SDL_GPUCopyPass* copy) {
    // Copy depth buffer to staging texture for CPU analysis
    SDL_GPUCopyPassAddCopyTexture(copy,
        depthTexture, stagingTexture,
        nullptr, nullptr);
});

```

## Effects of Pipeline Customization on Scene Rendering

Modifying the render pipeline architecture directly impacts three critical aspects of scene rendering: GPU performance, visual output, and system extensibility.

### Performance Implications

The concrete pipeline implementation determines GPU state changes, shader complexity, and memory bandwidth usage. Heavy fragment shaders in [`pipelineHDRBloom.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/pipelineHDRBloom.cpp) or additional render-targets for deferred shading increase GPU load compared to the lightweight [`pipelineDefault.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/pipelineDefault.cpp). Efficient pipeline customization requires balancing visual quality against the N64-style hardware constraints that Pyrite64 emulates.

### Visual Fidelity and HDR Support

Switching between pipeline implementations fundamentally alters the visual output. The default N64 pipeline produces authentic low-poly rasterization, while `pipelineHDRBloom` enables high dynamic range rendering with bloom effects and tone mapping. Custom pipelines can implement alternative shading languages or lighting models by overriding `Pipeline::init()` to load specialized SPIR-V shaders via `Renderer::Shader`.

### Extensibility for Debug and Particle Systems

Because `Renderer::Scene` stores only function pointers (`CbRenderPass`) rather than hardcoded rendering logic, you can inject custom drawing routines without modifying core engine files. This architecture makes it straightforward to add particle systems, bounding-box debug overlays, or G-buffer visualization by registering new render-pass callbacks. The separation between scene orchestration and pipeline execution ensures that experimental rendering features remain isolated from stable scene management code.

## Summary

- **Pyrite64** separates scene management (`Renderer::Scene` in [`src/renderer/scene.h`](https://github.com/HailToDodongo/pyrite64/blob/main/src/renderer/scene.h)) from rendering execution (`Renderer::Pipeline` in [`src/renderer/pipeline.h`](https://github.com/HailToDodongo/pyrite64/blob/main/src/renderer/pipeline.h)) to enable deep architectural customization.
- **Concrete pipelines** such as [`pipelineDefault.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/pipelineDefault.cpp), [`pipelineHDRBloom.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/pipelineHDRBloom.cpp), and [`pipelineBigTex.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/pipelineBigTex.cpp) demonstrate how to implement full rendering stacks with custom shaders, descriptor sets, and post-processing.
- **Customization points** include overriding `init()` for shader loading, `preDraw()` for uniform buffer updates, and `postDraw()` for effects like grayscale or bloom.
- **Scene rendering impact** depends on pipeline complexity—custom shaders affect GPU performance, render-target configuration determines visual fidelity (HDR vs. N64-style), and the callback-based architecture enables extensible debug and particle rendering.

## Frequently Asked Questions

### How do I switch between different render pipelines at runtime in Pyrite64?

You can switch pipelines at runtime by updating the reference returned from `Renderer::Scene::getPipeline()`. Store the pipeline name in your configuration, then when the user selects a different quality preset (e.g., switching from `"n64"` to `"hdr"`), update your local pipeline pointer to point to `ctx.scene->getPipeline("hdr")`. The next render pass will automatically bind the new pipeline state.

### What is the difference between Renderer::Scene and Renderer::Pipeline?

`Renderer::Scene` (defined in [`src/renderer/scene.h`](https://github.com/HailToDodongo/pyrite64/blob/main/src/renderer/scene.h)) acts as an orchestration layer that owns light data, render-pass callbacks, and pipeline instances, but contains **no GPU rendering logic itself**. `Renderer::Pipeline` (defined in [`src/renderer/pipeline.h`](https://github.com/HailToDodongo/pyrite64/blob/main/src/renderer/pipeline.h)) is an abstract base class that concrete implementations inherit from to encapsulate actual GPU state management, shader binding, and draw commands. The Scene dispatches work; the Pipeline executes it.

### How can I add post-processing effects like bloom or tone mapping?

To add post-processing effects, create a class inheriting from `Renderer::Pipeline` and override the `postDraw()` method. In `init()`, load fullscreen quad shaders using `Renderer::Shader::loadFromFile()` and create an intermediate render-target. In `postDraw()`, bind your shader and draw a fullscreen triangle to apply effects like bloom, tone mapping, or edge detection to the scene color texture.

### Where should I implement custom geometry rendering for particle systems?

Implement custom geometry rendering by registering a render-pass callback via `Renderer::Scene::addRenderPass()` rather than modifying core pipeline files. In your particle system manager, call `ctx.scene->addRenderPass(100, [](SDL_GPUCommandBuffer* cmd, Renderer::Scene& sc){ /* draw particles */ })` where `100` is a unique ID. This keeps particle rendering logic isolated from the core engine while ensuring it executes during the correct phase of scene rendering.