Post processing silently breaks on HDR cameras: unordered post_process_write() races tonemapping
- Dominant language
- Rust
- Stars
- 48.2k
- Forks
- 4.8k
- Avg merge
- 3d 22h
- Merged PRs (30d)
- 161
Description
## Bevy version and features
0.19.0, default features.
Note: I was debating if this is a bug or a feature request / documentation update / example update request, but opted for a bug as nothing gave me an indication that there was a race now suddenly in the scenario of HDR and post processing, and the example `custom_post_processing ` does not allude to this race.
## What you did
Ported a custom post-process pass from the 0.18 render graph to 0.19 systems, following `examples/shader_advanced/custom_post_processing.rs`:
```rust
render_app.add_systems(
Core3d,
post_process_system.in_set(Core3dSystems::PostProcess),
);
```
Ran it on a camera with `Bloom` (which requires `Hdr`, so `tonemapping` runs).
## What went wrong
Expected: the effect applies, as in 0.18 where graph edges (`Node3d::Tonemapping → MyLabel → Node3d::EndMainPassPostProcessing`) ordered the pass.
Actual: the entire 3D output is lost — a flat gray frame (UI still renders). No errors, no warnings. Hardcoding the pass's fragment shader to output solid red changes nothing, which makes this very hard to debug.
Cause: `tonemapping` also runs `.in_set(Core3dSystems::PostProcess)`, and the custom system has no ordering against it. Both only take read-only ECS access (`ViewQuery<(&ViewTarget, ...)>` + `RenderContext`), so the multithreaded executor runs them concurrently. Both call `ViewTarget::post_process_write()`, whose atomic ping-pong flips race: tonemapping can sample the cleared half of the pair (→ gray), and the custom pass writes into the half that upscaling never presents.
Fix on the user side is one line:
```rust
post_process_system
.in_set(Core3dSystems::PostProcess)
.after(bevy::core_pipeline::tonemapping::tonemapping)
```
## Additional information
The example cannot reproduce this because its camera is SDR — `tonemapping` early-outs on `!camera.hdr`, so there is no second flip to race. Add `Bloom` to the example camera and the effect breaks.
Bevy's own passes pin their order explicitly (`bloom.before(tonemapping)`, `motion_blur.before(bloom)`), so internal code never hits this. Only the example pattern is affected.
Suggestions (any of these would have saved a long debugging session):
- Make the example use an HDR camera and include the `.after(tonemapping)` ordering, with a comment explaining that every `post_process_write()` caller on the same view must be ordered against every other one.
- Document the ordering requirement on `ViewTarget::post_process_write`.
- Ideally: detect concurrent unordered flips (e.g. debug assertion or ambiguity warning between systems that call `post_process_write` on the same view), since the current failure mode is a silent lost frame.
Contributor guide
Research direction
Reproduce the failure with the custom_post_processing.rs example using an HDR camera or Bloom. Read the example, Core3dSystems::PostProcess ordering, tonemapping, and ViewTarget::post_process_write; then choose whether the fix is an ordered HDR example, documentation, or concurrent-flip detection and verify that the post-processing effect renders reliably.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- computer-graphics, game-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100