How to Customize the Render Pipeline Architecture in Pyrite64 for Advanced Scene Rendering
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) |
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) |
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, pipelineHDRBloom.cpp, 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) |
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, the editor demonstrates this workflow by binding multiple pipelines within a single render pass.
// 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:
#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:
// 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:
// 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:
// 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:
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 or additional render-targets for deferred shading increase GPU load compared to the lightweight 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::Sceneinsrc/renderer/scene.h) from rendering execution (Renderer::Pipelineinsrc/renderer/pipeline.h) to enable deep architectural customization. - Concrete pipelines such as
pipelineDefault.cpp,pipelineHDRBloom.cpp, andpipelineBigTex.cppdemonstrate 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, andpostDraw()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) 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) 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →