godotengine / godotengine/godot

`MultiMeshInstance2D` culling stale after `set_buffer` mutation

Open
#119,151 0 comments 0 reactions 0 assignees View on GitHub
bug topic:2d topic:rendering
Dominant language
C++
Stars
117k
Forks
26.8k
PR merge metrics
PR metrics pending

Description

### Tested versions

Godot 4.6.x stable. Reproduces on **both** Vulkan and Compatibility (OpenGL) renderers.

### System information

Windows 11, RTX 5070 Ti, both render drivers (Vulkan + Compatibility).

### Issue description

`MultiMeshInstance2D` instances are intermittently culled despite being well within the camera viewport. The CanvasItem's cached visibility rect does not update when the underlying `MultiMesh.buffer` is mutated per-frame (or when the parent `Node2D`'s transform — e.g., `scale` — is mutated per-frame). The renderer reads the stale cached rect and skips the draw call, while every visibility / modulate / z_index property reads as "should draw."

This is the same root cause as #54854 (fixed in 3.x via #54899) and #66849 (closed referencing PR #67116, which was closed unmerged). Sibling: #94151 (same staleness pattern with `ImmediateMesh` + `MeshInstance2D`).

### Two confirmed triggers

1. **Per-frame `MultiMesh.buffer = ...` (or `set_buffer`) mutation.** Capture-and-replay of a frozen buffer renders solidly forever; resuming per-frame buffer assignment intermittently culls all instances.
2. **Per-frame parent-transform mutation.** Even with the buffer frozen, mutating `scale` on the `MultiMeshInstance2D`'s parent `Node2D` per-frame reproduces the cull. Both triggers point to the same stale-CanvasItem-rect mechanism.

### Workaround

Calling `queue_redraw()` on the node holding the `MultiMesh` (the `MultiMeshInstance2D` itself, or its parent if scripts are attached there) immediately after the buffer/transform mutation marks the canvas-item rect dirty so it recomputes. Verified at full 500-instance load on both renderers.

```gdscript
multimesh_instance.multimesh.buffer = new_xf_floats
multimesh_instance.queue_redraw() # workaround
```

### Steps to reproduce

Minimal repro (will attach MRP on request):

1. Add a `MultiMeshInstance2D` with an arbitrary mesh and N instances.
2. From `_physics_process`, mutate `multimesh.buffer` each frame to update instance transforms.
3. Move the camera around — instances disappear intermittently for sustained windows of frames despite remaining well within view.

Adding `queue_redraw()` after the buffer assignment eliminates the issue.

### Notes

- This affects games that use `MultiMeshInstance2D` for crowd / particle / agent rendering with per-frame transform updates — the standard use case.
- Filing as a fresh issue rather than reopening #66849 because that issue closed COMPLETED in 2022 and the actual fix landed in 3.x only; the 4.x port (#67116) was closed unmerged. Happy to consolidate if a maintainer prefers.
- Glad to send an MRP and/or a PR with the equivalent of the 3.x fix ported to 4.x if a maintainer gives a direction.

Contributor guide

Open the contributing guide

Research direction

Start at MultiMeshInstance2D and the CanvasItem visibility-rect invalidation path, then compare the 3.x fix in #54899 with the unmerged 4.x attempt in #67116. Reproduce the per-frame buffer and parent-transform mutations using the listed steps, and verify that instances remain visible without queue_redraw() on both Vulkan and Compatibility renderers.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
game-dev, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.