Repository navigation
Description
Summary
The #485 GL-error-state listener surfaced this: after a run of GPU render tests churns Renderer3D's process-wide render-graph / asset state, a subsequent full-scene render emits GL_INVALID_OPERATION — "<texture> is not a valid texture name" — via the GL debug callback, every frame. The render output is correct (all victim tests pass their pixel assertions); only the GL error queue is dirtied. So it is benign today but indicates a stale texture handle being bound somewhere in the full render path.
Evidence (from #485)
- Victim tests (
SceneRenders3D,SphereAreaLightShadowEvidenceTest,MetallicSphereUnderNebula,EASUVisualEvidenceTest.*,AutoExposureBrightScene,OccludedInstanceFieldScene) all pass in isolation, and fail only in the full single-process suite → the bad state is inherited from earlier tests, not self-generated. - It is diffuse / accumulative with multiple sources: fixing one confirmed source (
GPUOcclusionCullGPUTest, which left an HZB bound to a texture unit then deleted it) removed it for some victims, but others still tripped and the failing set shifted → more than one upstream test leaves shared renderer state that makes a later render bind an invalid texture name. - The debug message is
id 1282 (GL_INVALID_OPERATION) "<texture> is not a valid texture name", i.e.glBindTextureUnit(unit, name)with a non-zero name that is no longer a valid texture.
Current containment (not a fix)
RendererAttachedTest::RunFrames / RunEditorFrames now drain the GL error queue after each guarded render tick (mirroring the #485 production fix 69aa935 that drains stray errors in the readback helpers), so a render self-contains its error queue and cannot poison a later GPU test. The #485 listener stays strict everywhere else. This masks the symptom in tests — the underlying stale-handle bind is unfixed.
What to do
Root-cause which shared Renderer3D resource (render-graph target / material / IBL / HZB / bindless handle) is left referencing a deleted texture across a render-graph resize / path switch / asset churn, and fix the lifetime so the full pipeline binds no stale texture name. Then the per-tick drain in RendererAttachedTest can be removed and the listener will guard render-attached tests too.
Likely approach: run the suite with a synchronous GL debug context + a break/stack on id 1282 to capture the exact bind site, then trace the handle back to the resource whose owner was freed.
Score
capability: 1
craft: 1
stability: 8
decay: 8
effort: 5
confidence: 0.5
learning: 3
fun: 2
kano: table-stakes
blocked_by: []
blocks: []
Benign today (correct pixels) but a real stale-handle bug in a core render path; effort is a diffuse multi-source hunt, confidence low. Surfaced by #485.
Activity
coderabbitai commentedon Jul 1, 2026
🔗 Related PRs
#249 - fixing state leak via policy change [merged]
#250 - Fix viewport-resize rendering bugs (GTAO ghost halo + transient pool … [merged]
#263 - fix(tests): untangle Renderer3D test coupling for full-pipeline render tests (
#258) [merged]#465 - Fix/flaky fog visual evidence test [merged]
#487 - Feature/renderer hzb occlusion cull [merged]
🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
test-infra: address code-review findings on the #485 GL-error guard
test-infra: examine the full render-tick error queue, not just the first
drsnuggles8 commentedon Jul 3, 2026
Root-caused and fixed in #528 (auto-closes this on merge).
It was not diffuse — a
std::stacktracecapture wired into the synchronous GL debug callback attributed all 248 in-renderid 1282errors from a full-suite run to one bind site:ToneMapRenderPass::ExecutebindingRenderer3D::GetWaterSurfaceDepthTextureID()for the underwater-fog stage.The lifecycle bug:
WaterRenderPasspublishes its depth FB's raw GL name and cleared it only at the top of its ownExecute()— but the render graph culls the water pass on no-water frames, so the publication from the last water-rendering test survived while graph churn deleted the owning FB. ToneMap (never culled) re-bound the dead name every frame.PlanarReflectionTextureIDhad the identical lifecycle — that's why fixing individual upstream tests only shifted the victim set: the actual variable was which water/reflection test ran last.Fix: both publications are reset every frame in
RenderPipeline::PrepareFrame; the executing pass re-publishes. Also fixed en route:GLStateGuard::ApplyCorenow validates FBO/program/VAO names before restoring (the restore path itself emitted one id-1282 when a tick deleted an entry-bound VAO).The per-tick
GL_INVALID_OPERATIONcarve-out inRendererAttachedTestis removed — the tick check is now strict (any GL error fails the tick that produced it). Verified: 3 consecutive full-suite runs with 0 id-1282 messages (baseline: 230). New deterministic regression testWaterStaleTexturePublicationTest(L4) fails with the fix disabled.The stack-capture diagnostics stay in
OpenGLDebug.cpppermanently — any future ERROR-type GL debug message now logs the offending call site with file:line.Merge pull request #528 from drsnuggles8/feature/renderer-stale-textu…
build(cmake): link libstdc++exp for std::stacktrace on Linux/Clang + GCC