Skip to content

fix(nui-core): release game-view compat renderers when their canvas is removed - #4351

Open
BingoWings247 wants to merge 1 commit into
citizenfx:masterfrom
BingoWings247:master
Open

BingoWings247 wants to merge 1 commit into
citizenfx:masterfrom
BingoWings247:master

Conversation

@BingoWings247

Copy link
Copy Markdown

Goal of this PR

Fix two defects in the application/x-cfx-game-view compatibility renderer (EpoxyScript.h) that keep a full-frame WebGL2 renderer running for every game-view element a page has ever mounted (#4342):

  1. The only teardown path is canvas.addEventListener('remove', ...). remove is not a DOM event, so it never fires when a page removes the element; the requestAnimationFrame loop and the WebGL2 context (powerPreference: 'high-performance') stay alive.
  2. CfxGameViewRenderer.destroy() calls this.#canvas.parentNode.removeChild(canvas), where canvas is not defined in that scope. Whenever the canvas still has a parent (including a container that was itself detached), destroy() throws ReferenceError: canvas is not defined after cancelling the rAF loop but before WEBGL_lose_context, so the context is never released and the listener never reaches resizeObserver.disconnect(). This also breaks resources that work around (1) by dispatching remove themselves.

How is this PR achieving the goal

  • destroy(): removeChild(canvas) → removeChild(this.#canvas).
  • The compat layer now tracks the canvases it created a renderer for (LiveCanvases). The existing MutationObserver, which already watches the whole document, checks them on each mutation batch. A canvas that is no longer isConnected gets a 1 second grace period, so moving it between containers or briefly detaching it keeps its renderer; after that the existing teardown runs by dispatching the remove event the listener was already waiting for. The listener is now { once: true } and removes the canvas from the set.
  • The check only runs while at least one compat renderer is alive and is O(live renderers).
  • Renderers created directly with new CfxGameViewRenderer(canvas) are unaffected (their owner calls destroy()), and resources that already dispatch remove keep working.
  • Known trade-off: a compat canvas that is re-inserted more than 1 second after being removed stays blank, the same as any destroyed renderer.

Before/after, using the g_gameViewScript extracted from master 88a4138 and injected into a page the way NUI does:

Scenario Before After
Mount and unmount a container 10 times 10 render loops running, 10 WebGL2 contexts alive 0 loops, all 10 contexts released
Remove the canvas element itself render loop keeps running released after the grace period
Page dispatches remove while the canvas is attached ReferenceError: canvas is not defined, context stays alive released, canvas removed
Move the canvas to another container in the same task keeps rendering keeps rendering
Detach, then re-attach after 300 ms keeps rendering keeps rendering
Element stays mounted (control) 1 loop 1 loop

This PR applies to the following area(s)

FiveM, RedM

Successfully tested on

Game builds: n/a. The change is JavaScript inside the g_gameViewScript raw string literal; I verified the literal still terminates in the same place and the extracted script parses.

Platforms: Headless Chromium (ANGLE). Not built in a client locally; CI will compile it.

Checklist

  • Code compiles and has been tested successfully.
  • Code explains itself well and/or is documented.
  • My commit message explains what the changes do and what they are for.
  • No extra compilation warnings are added by these changes.

Fixes issues

fixes #4342

@github-actions github-actions Bot added RedM Issues/PRs related to RedM invalid Requires changes before it's considered valid and can be (re)triaged labels Nov 5, 2026
@BingoWings247

Copy link
Copy Markdown
Author

The Windows build failures are from the CI runner, not this change: .github/workflows/ci/build_windows.sh
calls MSBuild at C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MsBuild.exe,
which isn't present on the current windows-latest image (exit 127 before compilation starts).
Any PR will hit this until the path or runner image is updated.

…s removed.

The x-cfx-game-view compat renderer's only teardown listened for a 'remove'
event, which is not a DOM event, so every mounted game view kept a
full-frame WebGL2 render loop running after its element was removed.
destroy() also referenced an undefined `canvas`, so it threw before
releasing the WebGL context whenever the canvas still had a parent.

Track compat canvases with a live renderer and, from the existing
MutationObserver, run the existing teardown for any that left the
document (after a 1s grace so moved or briefly detached canvases keep
their renderer). Fix destroy() to remove this.#canvas.

Signed-off-by: BingoWings247 <hilldavid71150@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

invalid Requires changes before it's considered valid and can be (re)triaged RedM Issues/PRs related to RedM

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[CEF 144] x-cfx-game-view compat renderer is never destroyed: 'remove' is not a DOM event, WebGL2 rAF loop leaks on unmount

1 participant