Conversation
PGE3 Multiple Render Targets (MRT) SupportThis document describes the changes made to olcPixelGameEngine3 to support Multiple Render Targets (MRT). MRT allows a single draw call to write to several framebuffer color attachments simultaneously, enabling techniques like deferred rendering (G-buffer). The existing engine already had most of the GL plumbing in place (8-attachment array, Summary of Changes
Change 1: GL_NONE_X ConstantLocation: GL constants section of the static constexpr GLenum GL_NONE_X = 0;Why: Change 2: nActiveAttachmentMask MemberLocation: uint8_t nActiveAttachmentMask = 0x01; // Bitmask of active MRT color attachment slotsWhy: Tracks which of the 8 possible color attachment slots currently have a texture/renderbuffer attached. Bit 0 = slot 0, bit 1 = slot 1, etc. Defaults to Change 3 & 4: DetachTextureTargetInterface DeclarationLocation: // Detaches any texture from the specified render target slot
virtual bool DetachTextureTarget(const uint32_t slot) = 0;OGL33 Class DeclarationLocation: bool DetachTextureTarget(const uint32_t slot) override;OGL33 ImplementationLocation: After bool Renderer_OGL33::DetachTextureTarget(const uint32_t slot)
{
// No-op if slot is not active
if (!(nActiveAttachmentMask & (1 << slot)))
return true;
auto& gl = olc::apis::opengl::gl::Get();
// Detach texture from this attachment slot
gl.glFramebufferTexture2D(
gl.GL_FRAMEBUFFER_X,
gl.GL_COLOR_ATTACHMENT0_X + slot,
GL_TEXTURE_2D,
0,
0
);
// Also detach any renderbuffer (MSAA case)
gl.glFramebufferRenderbuffer(
gl.GL_FRAMEBUFFER_X,
gl.GL_COLOR_ATTACHMENT0_X + slot,
gl.GL_RENDERBUFFER_X,
0
);
// Clear the bit and rebuild draw buffers
nActiveAttachmentMask &= ~(1 << slot);
std::array<GLenum, 8> drawBuffers;
int maxSlot = 0;
for (int i = 0; i < 8; i++)
{
if (nActiveAttachmentMask & (1 << i))
{
drawBuffers[i] = gl.GL_COLOR_ATTACHMENT0_X + i;
maxSlot = i + 1;
}
else
{
drawBuffers[i] = gl.GL_NONE_X;
}
}
if (maxSlot > 0)
gl.glDrawBuffers(maxSlot, drawBuffers.data());
return true;
}Why: Needed to cleanly tear down MRT state when switching back to single-target rendering. Detaches both the texture (regular path) and renderbuffer (MSAA path) from the specified FBO slot, then rebuilds the The no-op guard on the bitmask means calling Change 5: Modified AssignTextureTargetLocation: 5a. Reset mask when binding screen framebufferIn the if (texid == 0)
{
nActiveAttachmentMask = 0x01; // <-- NEW
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER_X, nScreenFBO);
return true;
}Why: The screen framebuffer has its own draw buffer configuration managed by the OS/driver. Resetting the mask ensures that subsequent 5b. Replace single-slot glDrawBuffers with bitmask-driven rebuildThe original code selected a single attachment for // BEFORE (removed):
std::array<GLenum, 8> attachments = { /* GL_COLOR_ATTACHMENT0..7 */ };
GLenum draw = attachments[slot];
gl.glDrawBuffers(1, &draw);Replaced with: // AFTER:
// Update active attachment bitmask
nActiveAttachmentMask |= (1 << slot);
// Rebuild draw buffers list from all active slots (GL_NONE for inactive)
std::array<GLenum, 8> drawBuffers;
int maxSlot = 0;
for (int i = 0; i < 8; i++)
{
if (nActiveAttachmentMask & (1 << i))
{
drawBuffers[i] = gl.GL_COLOR_ATTACHMENT0_X + i;
maxSlot = i + 1;
}
else
{
drawBuffers[i] = gl.GL_NONE_X;
}
}
gl.glDrawBuffers(maxSlot, drawBuffers.data());Why: The original code only ever enabled a single draw buffer, even though the FBO could have multiple textures attached. With MRT, after attaching textures to slots 0, 1, and 2 via successive calls, Backward compatibility: When only slot 0 is used (the existing single-target path), the mask is Change 6: Draw::SetTargetsDeclarationLocation: // Sets multiple drawing targets (MRT) for this drawing toolbox
void SetTargets(std::initializer_list<olc::Image*> targets);ImplementationLocation: After void Draw::SetTargets(std::initializer_list<olc::Image*> targets)
{
// Perform any outstanding tasks for current target
ProcessGPUTasks();
// MSAA resolve for previous target if needed
if (pTarget && pTarget->GetConfig().MSAA)
pRenderer->ResolveMSAA(uint32_t(pTarget->GetGPUID()));
// Attach each image to its corresponding slot
olc::vi2d size = {0, 0};
uint32_t slot = 0;
for (auto* img : targets)
{
PrepareImageForHW(*img);
if (slot == 0)
size = img->Size();
pRenderer->AssignTextureTarget(slot, uint32_t(img->GetGPUID()));
slot++;
}
// Detach any previously-used higher slots
for (uint32_t i = slot; i < 8; i++)
pRenderer->DetachTextureTarget(i);
// Track slot 0 as the "primary" target
pTarget = *targets.begin();
WorldReset();
pRenderer->SetViewport({0, 0}, size);
}Why: User-facing API for MRT. Takes an initializer list of Image pointers, attaches each to consecutive FBO slots (0, 1, 2, ...), and cleans up any higher slots that may have been active from a previous call. The first image (slot 0) is used as the "primary" target for Usage example: draw.SetTargets({&albedo, &normal, &depth}); // attach 3 render targets
draw.Clear(olc::BLACK); // clears all 3 + depth buffer
draw.Mesh(...); // fragment shader writes to all 3
draw.SetTarget(screen); // back to single target (cleans up slots 1-2)Constraints: All target images must be the same size (OpenGL requirement for FBO completeness). The shared depth renderbuffer is automatically sized to match slot 0's dimensions by Change 7: Stale Attachment Cleanup in SetTargetLocation: // Configure default render target
pRenderer->AssignTextureTarget(0, uint32_t(pTarget->GetGPUID()));
pRenderer->SetViewport({ 0,0 }, pTarget->Size());
// Detach any MRT attachments from a previous SetTargets call
for (uint32_t i = 1; i < 8; i++)
pRenderer->DetachTextureTarget(i);Why: Without this, switching from The Fragment Shader AuthoringWith MRT enabled, fragment shaders that write to multiple targets must declare explicit output locations instead of using the default // Instead of using PS_DefaultHeader (which declares: layout(location = 0) out vec4 pixel)
// write a custom header with multiple outputs:
layout(location = 0) out vec4 outAlbedo;
layout(location = 1) out vec4 outNormal;
layout(location = 2) out vec4 outDistance;The Shaders that only write to a single target (including the default shader) continue to work unchanged — |
|
Brilliant!!! Just brilliant 👏. I didn't fully understand it, I say I will need to debug it to understand it better Do you think we should add this to PGE 3? I see you have it setup so that the existing functionality is not affected Again thank you, impressive work |
|
I can't claim any credit, I do think it was eventually intended to be added though - all the scaffolding was already in place |
|
I say a lot of the credit is yours, you might of worked off an example but it was you who put all the pieces together to get it running smooth |
Not handwritten - probably not mergeable, pushing as a reference