[BUG] Nested FBO end() corrupts render-surface state, causing incorrect texture Y-flip

Author: thegreeneylCreated Apr 21, 2026Updated Jun 12, 2026

Summary

ofFbo::end() inside another FBO corrupts the matrix stack's render-surface state, causing all subsequent ofTexture::draw() calls within the parent FBO to make wrong Y-flip decisions. Textures rendered after a nested FBO end are vertically flipped.

openFrameworks version

0.12.1 (also present on current master — the code is unchanged)

Platform

All (the affected code is platform-independent C++)

Root cause

In both ofGLProgrammableRenderer::end() and ofGLRenderer::end(), the render surface is unconditionally reset to the window:

cpp
// ofGLProgrammableRenderer.cpp, line 1619–1625
void ofGLProgrammableRenderer::end(const ofFbo & fbo) {
    unbind(fbo);
    matrixStack.setRenderSurface(*window);   // ← always resets to window
    uploadMatrices();
    popStyle();
    popView();
}

setRenderSurface(*window) sets currentRenderSurface = nullptr, which changes the return value of customMatrixNeedsFlip():

cpp
// ofMatrixStack.cpp, line 117–118
bool ofMatrixStack::customMatrixNeedsFlip() const{
    return vFlipped != (bool(currentRenderSurface) && flipRenderSurfaceMatrix);
}

When inside a parent FBO, currentRenderSurface should point to that parent FBO, making the expression (true && true) = true. After the nested end() resets it to nullptr, the expression becomes (false && true) = false, and customMatrixNeedsFlip() returns the wrong value. This corrupts the orientationMatrix (which is recomputed by setOrientation() during popView()), and downstream, ofTexture::draw() makes the wrong flip decision at:

cpp
// ofTexture.cpp, getMeshForSubsection(), line 1103
if (texData.bFlipTexture == vflipped) {
    std::swap(py0, py1);  // flip vertex Y
}

Although popView() does restore vFlipped and orientation from orientationStack, it calls setOrientation() which calls customMatrixNeedsFlip() — and at that point currentRenderSurface has already been clobbered to nullptr.

Steps to reproduce

cpp
ofFbo outerFbo, innerFbo;
outerFbo.allocate(512, 512, GL_RGBA);
innerFbo.allocate(512, 512, GL_RGBA);

ofFbo contentFbo;
contentFbo.allocate(512, 512, GL_RGBA);
contentFbo.begin();
// ... draw some content ...
contentFbo.end();

outerFbo.begin();
    // Draw something into a nested FBO (e.g. a blur pass)
    innerFbo.begin();
    ofClear(0);
    contentFbo.draw(0, 0);
    innerFbo.end();

    // Now draw another FBO texture — this will be vertically flipped
    contentFbo.draw(0, 0);  // ← WRONG: vertically flipped
outerFbo.end();

Any real-world scenario with nested FBOs triggers this: blur passes, post-processing chains, multi-layer compositing, etc.

Expected behavior

Drawing contentFbo after innerFbo.end() should produce the same result as drawing it before. The render-surface state should be transparently restored to the parent FBO.

Proposed fix

Add a render-surface stack to ofMatrixStack so that pushView() / popView() save and restore currentRenderSurface and flipRenderSurfaceMatrix, just as they already do for orientation, viewport, and matrices. Then remove the explicit setRenderSurface(*window) from the renderers' end() methods.

ofMatrixStack.h — add to the private section:

cpp
struct RenderSurfaceState {
    ofBaseDraws * surface = nullptr;
    bool flipMatrix = false;
};
std::stack<RenderSurfaceState> renderSurfaceStack;

ofMatrixStack.cpp — save in pushView(), restore in popView() (before setOrientation() so customMatrixNeedsFlip() sees the correct surface):

cpp
// In pushView(), append:
renderSurfaceStack.push({currentRenderSurface, flipRenderSurfaceMatrix});

// In popView(), insert BEFORE the orientationStack restore:
if(!renderSurfaceStack.empty()){
    auto saved = renderSurfaceStack.top();
    renderSurfaceStack.pop();
    currentRenderSurface = saved.surface;
    flipRenderSurfaceMatrix = saved.flipMatrix;
}

// In clearStacks(), append:
while (!renderSurfaceStack.empty()){
    renderSurfaceStack.pop();
}

ofGLProgrammableRenderer.cpp — remove setRenderSurface(*window), move uploadMatrices() after popView():

cpp
void ofGLProgrammableRenderer::end(const ofFbo & fbo) {
    unbind(fbo);
    popStyle();
    popView();
    uploadMatrices();
}

ofGLRenderer.cpp — same change:

cpp
void ofGLRenderer::end(const ofFbo & fbo) {
    unbind(fbo);
    popStyle();
    popView();
}

The fix is backward-compatible: without nesting, the stack simply saves and restores the window state, producing identical behavior to stock OF.

Source: openframeworks/openFrameworks