Repository navigation
Fix stale framebuffer attachments, leaked renderbuffers, and nonzero-default copies - #2984
toaster0123 wants to merge 8 commits into
Conversation
jaime-jmebot
left a comment
There was a problem hiding this comment.
Nice work on this one — the three fixes map cleanly onto real API expectations, and the recording-GL tests (attachments, renderbuffer lifetimes, blit endpoints) are the right kind of proof for this: they assert the actual GL calls rather than just Java-side bookkeeping.
A few things worth a look:
NativeObjectManagerqueue guard — the newif (refMap.get(ref.objClone.getUniqueId()) != ref) continue;is a good guard, but it dereferencesobjClonebefore checking it's still there. SinceunregisterObjectnow clears the reference, a stale queuedReferencecould reach this branch withobjClone == null. A cheapref.objClone == null ||on the same condition makes it null-safe with no behavior change otherwise.- Track map cleanup — could you confirm the renderbuffer bookkeeping keyed by framebuffer ID is also dropped in
deleteFrameBuffer, alongside theglDeleteFramebuffersEXTcall? If an entry survives and a later framebuffer is handed the same GL name, it would inherit renderbuffer IDs it never allocated — the exact id-reuse hazard this PR already guards against inrefMap. - Minor nits (no action strictly required):
FrameBuffer.clearColorTargets(): theif (colorBufIndex >= 0)branch reads like a leftover from an older version — the field starts at0, so the condition is always true andcolorBufIndex = 0;could be a plain assignment.- In
mipmappedReplacementGeneratesOldRenderedTextureBeforeAllocatingNewOne, the old texture's mipmaps are generated twice (once when the slot is replaced, again on unbind). Harmless, but it's a fullglGenerateMipmapon a texture nothing will read again.
When this gets combined with #2983, the mipmap loop is the one spot worth merging by hand: keep this PR's actual-attachment snapshot iteration with #2983's image-binding/generation body, as you noted.
|
I checked these points against f1646c1 and reran the 34 attachment/lifecycle/default-framebuffer regressions; all pass.
For the eventual combination with #2983, agreed: retain this PR's actual-attachment snapshot iteration and #2983's direct image binding, mip-range restoration and generated-state update. The standalone attachment test checks texture IDs; it does not claim to cover #2983's storage-redefinition or pixel-preservation behavior. No source change was needed for these review points. Both current-head GitHub workflows also pass. |
Track native framebuffer attachments and all owned renderbuffers across target mutation, allocation failure, explicit deletion, managed cleanup, and context reset. Preserve existing storage and guard stale references when native names are reused. Use discovered presentation framebuffer names for copies, restoration, and overrides. Build directly on upstream master without the mipmap/null-storage or GLES3 capability changes from PR jMonkeyEngine#2983. Add 34 recording-GL regressions in three dedicated test classes. Local core/effects validation: 522 passed, one existing test skipped, zero failures.
ca0de89 to
c185a0f
Compare
jaime-jmebot
left a comment
There was a problem hiding this comment.
- Excellent set of fixes addressing tricky framebuffer lifecycle, attachment update, and default framebuffer copy edge cases.
- Comprehensive recording-GL tests validate the exact GL calls and resource lifetimes.
- Code changes are clean, well-tested, and robust.
Reject shared target instances before mutation while allowing distinct targets on the same texture. Retain ownership after removal/disposal using an opaque token independent of the framebuffer and its phantom-reference handle. Replace boxed framebuffer maps and temporary sets with IntMap, a slot mask, and reusable primitive renderbuffer state. Pass state through attachment updates and use indexed mipmap iteration. Deduplicate disposal requests by identity and avoid repeated queue scans in managed deletion. Clear stale requests during context reset and full cleanup so they cannot delete a subsequently reinitialized resource. Add 23 regressions covering ownership, shared-texture survival, real owner GC, highest color slots, duplicate disposal, batch limits, reset and cleanup. Validation: core 605 passed / 1 existing skipped; effects 6 passed. Core main/test checkstyle and git diff --check pass; no checkstyle warnings on added/modified lines. No native GPU or frame-rate benchmark claimed.
jaime-jmebot
left a comment
There was a problem hiding this comment.
- The code changes are extremely clean, thorough, and fully covered by robust recording-GL and ownership regression tests.
- All review items and edge cases have been verified and confirmed.
- Great work! Ready to go.
Snapshot the actual attached Image and native name per color slot, and invalidate snapshots and texture-unit bindings when an image is deleted. Avoid mipmap generation for deleted or recreated images, including native name reuse, while retaining the rendered image across Texture.setImage. Keep untouched attachment snapshots on partial update failures. Reconcile public attachment/texture update helpers through the full framebuffer update path, including initialization, binding and ownership validation. Retain allocation-free snapshot reuse for stable slots. Drain pending CPU-buffer disposal on context reset and full cleanup when UNSAFE is enabled, without deleting old GL names in a new context. Preserve queued CPU cleanup on direct unregistration without double-freeing during managed deletion. Keep ordinary per-frame deletion limits and deduplication. Treat invalidated framebuffer binding as unknown and restore the discovered presentation framebuffer for binding, copies and main overrides. Add 26 regressions. Validation: core 642 passed / 1 existing skipped; effects 6 passed. All 32 targeted cases pass with Java assertions disabled. Core main/test Checkstyle and git diff --check completed, with no warnings on changed lines. Recording-GL tests only; no native GPU/FPS claim.
|
Fixed the second-review findings in f0c3d4b, pushed directly to this PR branch (no merge). Fixes
Also corrected default-framebuffer binding/copy/override behavior after state invalidation, including a nonzero presentation framebuffer. Target exclusivity and sharing textures through separate target instances are unchanged. Validation on the pushed sources
The original 12 new snapshot reproductions failed before the production fix. The fixed tests cover deletion/disposal, native-name reuse, shared images, direct updates, partial failures, CPU cleanup and stale-request safety. GL validation uses recording fixtures; no native GPU rendering, driver-memory measurement or FPS benchmark is claimed. |
Handle the window-system framebuffer without unsupported FBO calls after invalidation, reset and cleanup. Reject actual offscreen targets as before. Avoid querying GL_FRAMEBUFFER_BINDING when the capability is absent. Seven new fallback cases failed before this fix and now pass. Integrate six lifecycle edge regressions covering shared Images, stale phantom references, 151-resource cleanup, reset, texture-to-buffer replacement and UNSAFE cleanup. Core/effects: 661 passed, one existing skipped; no Checkstyle warnings on modified lines.
Resolve GLRenderer.invalidateState by retaining unknown FBO binding plus upstream vertex-divisor invalidation. Master is integrated into the PR branch; no upstream merge is performed. Re-reviewed resource ownership, deferred deletion, snapshots, failure recovery, legacy fallback and the conflict resolution. Core 747 passed / 1 existing skipped, effects 6 passed, LWJGL3 4 passed. All 158 targeted tests also pass with Java assertions disabled. No Checkstyle warnings on PR-modified lines. Recording-GL verification, not native GPU rendering.
Latest follow-up and review
Current head:
a485967fb37674260a53a915f0afaed951ba736a.56e53704be8e4fcbdfddc786cfe9a1e95f23852d: preserve screen rendering without framebuffer-object support. After invalidation, reset or cleanup,setFrameBuffer(null)restores the cache without calling unsupported FBO functions. Real offscreen targets and main overrides still throw before allocation.GL_FRAMEBUFFER_BINDINGduring initialization whenCaps.FrameBufferis absent. A strict OpenGL 2.1 recording fixture checks both unsupported queries and entry points.a485967fb37674260a53a915f0afaed951ba736a: integrate current upstream master (f89b4e0) into this PR branch. Resolve the conflict ininvalidateState()by keeping both unknown-FBO binding and upstream vertex-divisor invalidation. This does not merge the PR into master.Latest validation
Added 13 regression tests: seven legacy-backend cases (all failed before the fix) and six previously local lifecycle edge cases, now committed. The edge cases cover distinct texture wrappers sharing an Image, stale phantom references and native-name reuse, cleanup of 151 mixed framebuffers, reset/reinitialization, texture-to-renderbuffer replacement and explicit UNSAFE cleanup.
Validated the integrated sources with:
git diff --check; unrelated existing upstream whitespace was left unchanged.The follow-up review covered ownership, shared-image lifetime, deferred/GC cleanup, native-name reuse, attachment snapshots, failure recovery, legacy fallback, and the upstream conflict resolution. No additional confirmed blocking regression was found in that pass. The exclusive-target contract is unchanged; use distinct targets to share a texture.
GL checks use recording fixtures. Native GPU rendering, pixel output and FPS were not validated; the sandbox's EGL/GLX context setup remains unavailable. GitHub CI for this head is separate from the local results above. Earlier results below are historical.
Review follow-up — 2026-10-26
Commit: 7e86c0f — Enforce framebuffer target ownership and reduce lifecycle overhead.
Target ownership contract
IllegalArgumentExceptionin the add/replace/depth API, before changing either framebuffer or the target's slot/face.Bookkeeping and cleanup
IntMapfor framebuffer state, an attachment-slot bitmask, and reusable renderbuffer records with primitive cached formats instead of boxed maps and per-update temporary hash sets.Earlier validation (historical)
Executed locally on this revision:
git diff --checkcompleted. Existing repository warnings remain; no Checkstyle warnings on the added/modified lines.GL behavior is checked with recording-GL fixtures. No native GPU rendering, driver-memory measurements or frame-rate benchmark is claimed. The original validation below is historical.
What is broken
This fixes three framebuffer API behaviors: changing a render target can leave OpenGL using the old attachment, disposing a framebuffer can leave its renderbuffers undeleted, and copying to/from the screen can use the wrong framebuffer when the platform's default ID is nonzero.
1. Replacing or removing an initialized attachment does not update OpenGL
Trigger: Render into a framebuffer, then change its targets through
replaceColorTarget,removeColorTarget, or the related add/depth setters. A caller expects the nextsetFrameBuffer(fb)to apply the new targets.For example, after
fbhas been initialized with texture A:Before: The recording-GL reproduction still has A attached. The setters do not mark the framebuffer for an update; switching away and back does not help. Removal has a second bug: even after an explicit
fb.setUpdateNeeded(), the removed color slot remains attached because the update code only visits surviving targets. Rendering can therefore go to the old texture, and the native attachments can disagree with the Java target list.Fix: Mark target/MRT changes for update, explicitly detach removed slots, and reattach surviving renderbuffers when their slot changes. Keep the selected color target valid after removal. When switching away, generate mipmaps for the textures that were actually attached, rather than a newly replaced Java target. Regression tests assert that B becomes attached and removed slots become zero.
2. Framebuffer disposal misses owned renderbuffers
Trigger: Dispose a framebuffer with several color renderbuffers, or let the native-object manager clean it up. Callers expect every renderbuffer owned by that framebuffer to be released.
Before, measured in the GL-call recording:
fb.dispose(); renderer.postFrame();deletes only 2, leaving the second color renderbuffer live.renderer.cleanup()deletes the framebuffer but 0 renderbuffers: the manager's destructible framebuffer clone contains no attachment information.These missed deletes can retain resources while the GL context remains alive. Copying attachment IDs into the clone once is insufficient: registration occurs before initial attachment allocation, and targets may change later.
Fix: Track allocated renderbuffers in renderer-owned state keyed by framebuffer ID, without retaining the owning
FrameBuffer. Direct disposal, managed cleanup, and the GC-queue path can then delete all owned IDs, including late additions, removed targets not yet rebound, and allocations made before a later failure. Unregister direct deletions and reject stale queued references so a reused GL ID does not cause deletion of a new object. Tests now record all 3/3 and 2/2 deletions, and also cover context reset, ID reuse, and simulated GC cleanup. Texture attachments remain separately owned.3. Screen copies bind framebuffer zero instead of the discovered default
Trigger: The platform starts with framebuffer 77 bound, so initialization records 77 as the screen framebuffer. With no main-framebuffer override, a null copy endpoint should refer to that same framebuffer.
Before: Normal screen binding uses 77, but at
glBlitFramebufferEXTthe recorded draw/read endpoint is 0. The copy therefore targets a different framebuffer; it cannot be relied on to copy to/from the presentation surface.Fix: Use the discovered default ID for null copy endpoints, and keep initialization, restoration, bound-framebuffer deletion, and main-framebuffer overrides consistent with it. The tests now record 77 at the relevant blit endpoint and verify restoration; the default-zero control still passes.
Initial validation (historical)
git diff --checkcompleted. Existing checkstyle warnings remain; no new warnings in the added tests or production lines. Independent review found no blocking issues.These tests record GL calls, attachment IDs, and resource lifetimes. They do not rasterize pixels or measure driver memory. No native GPU, iOS hardware, or visual-output validation is claimed.
Original relation to #2983 (historical)
This branch is based directly on master and excludes #2983's mipmap null-storage fix, GLES3 half-float change, and their tests. It passes independently. Combining the PRs has one expected textual overlap in the mipmap loop: retain this PR's actual-attachment snapshot iteration and #2983's image-binding/generation body.
The earlier fork branch
fix/framebuffer-attachment-resource-lifecycleis retained unchanged; this independent branch supersedes it for upstream review.