bevyengine / bevyengine/bevy

GPU light clustering is disabled on Android but not iOS, where it costs 5 passes per frame and ~45% more CPU power with a single point light

Open
#25,198 1 comment 0 reactions 0 assignees View on GitHub
A-Rendering C-Bug C-Performance D-Straightforward I-Regression O-iOS S-Ready-For-Implementation
Dominant language
Rust
Stars
48.2k
Forks
4.8k
Avg merge
3d 16h
Merged PRs (30d)
171

Description

## Bevy version

* Original: 0.18.1
* Current: 0.19.0

## Relevant system information

* Rust: `cargo 1.98.0-nightly (fbb61be30 2026-05-26)`
* Device: iPhone 15 Pro Max (A17 Pro), iOS 27.0
* Built on macOS 26.5.1, Xcode Release configuration, `aarch64-apple-ios`
* Renderer: forward, no shadows anywhere, no bloom, no HDR, MSAA off, one `PointLight` plus directional lights in the measured scene

## What's performing poorly?

`bevy_pbr`'s GPU light clustering runs unconditionally on physical iOS; for a basic scene with a single light is seems to be pure overhead.

The platform gate excludes Android and the iOS simulator but not physical iOS `bevy_pbr-0.19.0/src/cluster/mod.rs:79`
https://github.com/bevyengine/bevy/blob/v0.19.0/crates/bevy_pbr/src/cluster/mod.rs#L79

```rust
let gpu_clustering_supported = !(cfg!(target_os = "android") || cfg!(target_abi = "sim"))
```

What it costs per frame, from a Metal System Trace, in a scene that has _one_ clusterable object (a single `PointLight`):
| encoder | 0.18.1 | 0.19.0 |
|---|---|---|
| `clustering Z slicing pass` | 0/s | 59/s |
| `clustering count pass` (render) | 0/s | 59/s |
| `clustering allocation local pass` | 0/s | 59/s |
| `clustering allocation global pass` | 0/s | 59/s |
| `clustering populate pass` (render) | 0/s | 59/s |
| `(wgpu internal) Transit` (the readback) | 0/s | 59/s |

## Before and After Traces

Instruments, `ProcessSubsystemPowerImpact` summed over a 120s capture, identical idle in-game scene, screen Auto-Brightness off, scene held constant, captures taken back to back & two or more captures per configuration.

| | CPU | GPU |
|---|---|---|
| 0.18.1 | 244, 247, 247 | 475, 475, 476 |
| 0.19.0, unmodified | 356, 366 | 593, 603 |
| 0.19.0 with `gpu_clustering = None` | 243, 248, 260, 243 | 592, 594, 604, 592 |

Metal System Trace, 30s, same scene:

| | GPU busy | fragment channel |
|---|---|---|
| 0.18.1 | 12.73s | 11.47s |
| 0.19.0, unmodified | 18.36s | 16.70s |
| 0.19.0 with `gpu_clustering = None` | 13.27s | 12.21s |

So disabling it _recovered the entire CPU regression_ (361 to 249 mean, against a 0.18.1 baseline of 244-247) and _all of the excess GPU busy time_ (18.36s to 13.27s, baseline 12.73s).

Frame rate was ~56 fps in every configuration, so this is not a frame-pacing artifact.

## Additional information

**Workaround (no fork needed)**: a Startup system setting `GlobalClusterSettings.gpu_clustering = None`. It routes to the CPU clustering path 0.18 used and every Android device on 0.19 still uses. Lighting output is unchanged: `clustered_forward.wgsl` is the same code path in both modes and both fill the same `ViewClusterBindings`. It has to be `Startup` rather than plugin-build time, because `PbrPlugin::finish` inserts the resource and would overwrite an earlier write.

The opt-out is documented only in a comment, and doesn't appear in the 0.18-to-0.19 migration guide. I found it by reading the source after measuring the power regression, not before.
https://github.com/bevyengine/bevy/blob/v0.19.0/crates/bevy_light/src/cluster/mod.rs#L48

Context: I'm porting my commercial Steam game to iOS and squeezing out every bit of power efficiency I can, so I'm unusually attentive to where it goes.

Would an early-out at low clusterable-object counts be reasonable? Or excluding physical iOS by default the way Android already is? Or entioning the opt-out in the migration guide would save the next person tracking this down.

Contributor guide

Open the contributing guide

Research direction

Start with bevy_pbr/src/cluster/mod.rs:79 and compare its physical-iOS gate with the Android and simulator cases. Read bevy_light/src/cluster/mod.rs:48 for GlobalClusterSettings, then reproduce the reported single-PointLight iOS scene and profile its passes. Done means an agreed low-cost iOS behavior or a documented migration path for the existing opt-out, with unchanged lighting output.

Written by the indexing model from the issue text.

Assessment

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.