Repository navigation
ofFbo::end() logs a false "unbalanced fbo bind/unbind" error when finishRender() clears the framebuffer stack #8564
Description
Summary
ofGLProgrammableRenderer::finishRender() unconditionally clears
framebufferIdStack at the end of every frame. unbind() then treats an empty
stack as a user error and prints:
[ error ] unbalanced fbo bind/unbind binding default framebuffer
Because the stack is cleared by the framework itself, this error can be emitted
for a perfectly balanced begin()/end() pair, and when a genuine mistake
happens it is reported one frame late with no context. The same code exists in
the fixed-function renderer.
Where
libs/openFrameworks/gl/ofGLProgrammableRenderer.cpp
// startRender() — pushes the default framebuffer once per frame
void ofGLProgrammableRenderer::startRender() {
currentFramebufferId = defaultFramebufferId;
framebufferIdStack.push_back(defaultFramebufferId); // line 88
...
}
// finishRender() — clears it again, every frame
void ofGLProgrammableRenderer::finishRender() {
...
matrixStack.clearStacks();
framebufferIdStack.clear(); // line 113
...
}
// bind() — pushes
void ofGLProgrammableRenderer::bind(const ofFbo & fbo) {
...
framebufferIdStack.push_back(currentFramebufferId); // line 1639
currentFramebufferId = fbo.getId();
glBindFramebuffer(GL_FRAMEBUFFER, currentFramebufferId);
}
// unbind() — pops, and errors if the stack is empty
void ofGLProgrammableRenderer::unbind(const ofFbo & fbo) {
if (framebufferIdStack.empty()) { // line 1668
ofLogError() << "unbalanced fbo bind/unbind binding default framebuffer";
currentFramebufferId = defaultFramebufferId;
} else {
currentFramebufferId = framebufferIdStack.back();
framebufferIdStack.pop_back();
}
glBindFramebuffer(GL_FRAMEBUFFER, currentFramebufferId);
fbo.flagDirty();
}The fixed-function renderer has the same shape:
ofGLRenderer::finishRender() line 81 (framebufferIdStack.clear()),
ofGLRenderer::unbind(const ofFbo &) line 536.
Why it fires
The frame lifecycle in ofAppEGLWindow::draw()
(libs/openFrameworks/app/ofAppEGLWindow.cpp, ~line 846) is:
currentRenderer->startRender(); // stack = [defaultFramebufferId]
coreEvents.notifyDraw(); // <- user ofApp::draw() runs here
currentRenderer->finishRender(); // stack.clear()
eglSwapBuffers(...);Two consequences:
- A
begin()that is notend()ed within the same frame is silently
discarded byfinishRender(). The nextend()then finds an empty stack
and reportsunbalanced ...— even though nothing was unbalanced in the code
the user wrote; the framework removed the entry. - Any
ofFbo::end()that runs when the stack is already empty is logged as an
error, even though the intended end state (bind the default framebuffer) is
exactly what the fallback branch does. So the error is emitted for a state the
code then handles correctly.
Reproduction
The bug is in the tracking state machine itself, so it reproduces without any
GL context. A faithful copy of the four methods (startRender, finishRender,
bind, unbind) shows that a balanced pair is fine, but the same unbind()
after finishRender() clears the stack emits the error even though the result
is the correct default framebuffer:
// mirrors ofGLProgrammableRenderer line-for-line
startRender(); // stack = [default]
bind(fbo); unbind(); // balanced -> no error
finishRender(); // stack.clear()
startRender(); // stack = [default]
finishRender(); // stack.clear() -> []
unbind(); // -> "[ error ] unbalanced fbo ..."Output:
frame 1 (balanced begin/end): stack=0
[ error ] unbalanced fbo bind/unbind binding default framebuffer
frame 2 (end after clear): error emitted above, final fbo=0 (default)
In a real app this surfaces whenever an ofFbo is left bound across a frame
boundary (bound in update(), used in draw()), or when any stray end() runs
while the stack happens to be empty — the fallback binding is then correct, yet
the error is logged.
Observed in the wild on openFrameworks 0.12.1 linuxaarch64 with the KMS/GBM
window (ofAppEGLWindow + TARGET_RASPBERRY_PI) on a Raspberry Pi 4, GLES2 /
ofGLProgrammableRenderer (via a GBM/DRM window patch); the fixed-function
renderer has the identical code path.
Note the same class of issue exists one function up, in bind(): if
currentFramebufferId == fbo.getId() it logs a warning and returns without
pushing, so the paired end() under-pops the stack and the next end()
reports unbalanced. The two routines thus disagree about balanced state.
Expected behaviour
Ending with the stack empty should not be reported as an error when the result
is already the default framebuffer, and a genuine leak should be reported at the
point it occurs (in finishRender()) rather than one frame later.
Suggested fix
unbind() — only warn when a different framebuffer was actually left bound,
and downgrade to a warning since the fallback is correct:
void ofGLProgrammableRenderer::unbind(const ofFbo & fbo) {
if (framebufferIdStack.empty()) {
if (currentFramebufferId != defaultFramebufferId) {
ofLogWarning() << "ofFbo::end() with no matching begin() "
<< "(framebuffer stack empty); binding default framebuffer";
}
currentFramebufferId = defaultFramebufferId;
} else {
currentFramebufferId = framebufferIdStack.back();
framebufferIdStack.pop_back();
}
glBindFramebuffer(GL_FRAMEBUFFER, currentFramebufferId);
fbo.flagDirty();
}finishRender() — report a real leak where it happens, then reset to a known
state instead of blindly clearing:
void ofGLProgrammableRenderer::finishRender() {
...
if (!framebufferIdStack.empty() && framebufferIdStack.back() != defaultFramebufferId) {
ofLogWarning() << "ofFbo left bound at end of frame (id "
<< framebufferIdStack.back() << "); did you forget end()?";
}
framebufferIdStack.clear();
...
}Optionally, ofFbo could track whether it was the instance that pushed, making
end() on a never-begin()ed FBO a no-op. The two changes above are smaller and
suffice.
Apply the same to ofGLRenderer (lines 81 and 536), and make bind()'s
early-return path (the "cannot be bound onto itself" guard) either push a
placeholder or mark the FBO as not-pushed so its end() does not under-pop.
Impact / workaround
Cosmetic in most cases (the fallback binding is correct), but noisy and
misleading — it reads like an app bug when it is a framework lifecycle issue.
Workaround: ensure every ofFbo::begin() is matched by an end() in the same
update()/draw() call, and never hold an FBO bound across a frame boundary.
Filing notes
- Repo:
openframeworks/openFrameworks - Suggested labels:
bug,gl - No local patch is carried yet; if we ship one it would be a new
0005-fbo-unbind-stack-guard.patchalongside the existingof-0.12.1patches.
Activity