Inject your own GPU work — pipeline, descriptors, barriers and draw — at a specific phase of the frame.
You will write a CustomRenderpass: a pass that injects arbitrary GPU work — graphics or compute — at a specific phase of the frame. It is the freest of the three tools: you control the pipeline, the descriptors, the barriers and the draw.
A custom pass runs after the built-in ShaderMaterialPass of the same phase. It has a single obligation: implement render().
class CustomRenderpass : public Renderpass {
public:
CustomRenderpass(const RendererConfiguration& config,
const std::string& name)
: Renderpass{config, name} {}
// Called once per frame for the associated phase
virtual void render(
const std::shared_ptr<vireo::CommandList>& commandList,
const std::shared_ptr<vireo::RenderTarget>& colorTarget,
const std::shared_ptr<vireo::RenderTarget>& depthTarget,
uint32 frameIndex) = 0;
};
Two hooks inherited from Renderpass are available: resize(extent, commandList) to recreate resolution-dependent resources, and update(frameIndex) to refresh per-frame state. Both are no-ops by default.
In the constructor, you create a descriptor layout, a graphics pipeline, and a per-frame-in-flight data structure. This example — a full-screen zone outline — reads depth, a camera uniform and a zones storage buffer.
ZoneOutlinePass::ZoneOutlinePass(
const RendererConfiguration& config,
vireo::ImageFormat outputFormat) :
CustomRenderpass{config, "ZoneOutlinePass"} {
descriptorLayout = ctx().vireo->createDescriptorLayout();
descriptorLayout->add(BINDING_DEPTH, vireo::DescriptorType::SAMPLED_IMAGE);
descriptorLayout->add(BINDING_CAMERA, vireo::DescriptorType::UNIFORM);
descriptorLayout->add(BINDING_ZONES, vireo::DescriptorType::DEVICE_STORAGE);
descriptorLayout->build();
pipelineConfig.resources = ctx().vireo->createPipelineResources(
{descriptorLayout, ctx().samplers.getDescriptorLayout()}, {}, name);
pipelineConfig.colorRenderFormats = {outputFormat};
pipelineConfig.vertexShader = loadShader("quad.vert");
pipelineConfig.fragmentShader = loadShader("zone_outline.frag");
pipeline = ctx().vireo->createGraphicPipeline(pipelineConfig, name);
renderingConfig.colorRenderTargets[0].clear = false; // compose, do not clear
framesData.resize(ctx().config.framesInFlight);
for (auto& frame : framesData) {
frame.descriptorSet = ctx().vireo->createDescriptorSet(descriptorLayout, name);
frame.cameraBuffer = ctx().vireo->createBuffer(
vireo::BufferType::UNIFORM, sizeof(CameraData), 1, name);
frame.cameraBuffer->map();
frame.descriptorSet->update(BINDING_CAMERA, frame.cameraBuffer);
}
}
The method receives the phase's color and depth targets. The typical pattern: early-out if there is nothing to draw, update the uniforms, transition barriers, draw, then restore the barriers. Note the barrier pair that moves the color from COMPUTE_READ to RENDER_TARGET_COLOR during the draw.
void ZoneOutlinePass::render(
const std::shared_ptr<vireo::CommandList>& commandList,
const std::shared_ptr<vireo::RenderTarget>& colorTarget,
const std::shared_ptr<vireo::RenderTarget>& depthTarget,
uint32 frameIndex) {
if (camera == nullptr) { return; } // early out
auto& frame = framesData[frameIndex];
frame.cameraData.projectionInverse = inverse(camera->projection);
frame.cameraData.viewInverse = camera->transform;
frame.cameraBuffer->write(&frame.cameraData);
frame.descriptorSet->update(BINDING_DEPTH, depthTarget->getImage());
renderingConfig.colorRenderTargets[0].renderTarget = colorTarget;
commandList->barrier(colorTarget,
vireo::ResourceState::COMPUTE_READ, vireo::ResourceState::RENDER_TARGET_COLOR);
commandList->barrier(depthTarget,
vireo::ResourceState::RENDER_TARGET_DEPTH, vireo::ResourceState::SHADER_READ);
commandList->beginRendering(renderingConfig);
commandList->bindPipeline(pipeline);
commandList->bindDescriptors({frame.descriptorSet, ctx().samplers.getDescriptorSet()});
commandList->draw(3); // full-screen triangle
commandList->endRendering();
commandList->barrier(depthTarget,
vireo::ResourceState::SHADER_READ, vireo::ResourceState::RENDER_TARGET_DEPTH);
commandList->barrier(colorTarget,
vireo::ResourceState::RENDER_TARGET_COLOR, vireo::ResourceState::COMPUTE_READ);
}
Always restore the targets' state at the end of the pass. The next pass expects to find the color in COMPUTE_READ and the depth in RENDER_TARGET_DEPTH.
Override resize() to recreate or record anything that depends on resolution. Here, we simply keep the screen size, used by the shader to reproject each pixel.
void ZoneOutlinePass::resize(const vireo::Extent& extent,
const std::shared_ptr<vireo::CommandList>&) {
currentScreenSize = float2{float(extent.width), float(extent.height)};
}
You attach the pass to a phase. The caller retains ownership: the pass must outlive the Renderer. The registration order fixes the execution order within the phase.
ZoneOutlinePass outline(
renderTarget.getRendererConfiguration(),
renderTarget.getImageFormat());
auto& renderer = renderTarget.getRenderer();
renderer.addRenderPass(lysa::RenderingPhase::AFTER_COLOR_PASS, outline);
// ... later, to remove it (same phase as the add) ...
renderer.removeRenderPass(lysa::RenderingPhase::AFTER_COLOR_PASS, outline);
On registration, addRenderPass() calls resize() on the pass to size it, then appends it to the phase list. Removal is by reference and requires the same phase as the add; a pass that is not registered is silently ignored.