1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
//! Effects render pass execution.
use awsm_renderer_core::command::compute_pass::ComputePassDescriptor;
use crate::{
error::Result,
render::RenderContext,
render_passes::{
effects::{
bind_group::EffectsBindGroups,
pipeline::{EffectsPipelines, BLOOM_BLUR_PASSES},
shader::cache_key::BloomPhase,
},
RenderPassInitContext,
},
};
/// Effects pass bind groups and pipelines.
pub struct EffectsRenderPass {
pub bind_groups: EffectsBindGroups,
pub pipelines: EffectsPipelines,
}
impl EffectsRenderPass {
/// Creates the effects render pass resources.
pub async fn new(ctx: &mut RenderPassInitContext<'_>) -> Result<Self> {
let bind_groups = EffectsBindGroups::new(ctx).await?;
let pipelines = EffectsPipelines::new(ctx, &bind_groups).await?;
Ok(Self {
bind_groups,
pipelines,
})
}
/// Executes the effects pass.
pub fn render(&self, ctx: &RenderContext) -> Result<()> {
// PR #103 review note: T1.5 originally attempted to early-exit
// here when bloom/dof/smaa are all off, on the theory that the
// `BloomPhase::None` dispatch is a no-op. It isn't. The shader
// unconditionally does `textureLoad(composite_tex)` followed
// by `textureStore(effects_tex, …)` — i.e. it's the only thing
// that puts pixels into `effects_tex`, which the display pass
// then samples at binding 0 (see `display/bind_group.rs:99`).
//
// Skipping the dispatch left `effects_tex` with stale /
// driver-defined contents and the display pass showed garbage
// (or last-known-effects-frame contents) on every frame where
// no effects were enabled. Reinstating the dispatch is the
// safe path. The bandwidth cost is small (~5 MB at 400×800)
// and the only theoretical win was the compute-pass
// open/close overhead (~30 µs on mobile), recovering which
// would require plumbing post-processing state into the
// display bind-group recreation flow so display could sample
// `composite` directly — disproportionate complexity for the
// saving. Re-evaluate in a follow-up if profiling justifies.
let workgroup_size = (
ctx.render_texture_views.width.div_ceil(8),
ctx.render_texture_views.height.div_ceil(8),
);
if ctx.post_processing.bloom {
// Multi-pass bloom
// Pass 0: Extract bright pixels (ping_pong=false, writes to effects_tex)
self.dispatch_pass(ctx, BloomPhase::Extract, false, workgroup_size)?;
// Passes 1..=BLOOM_BLUR_PASSES: Blur passes (alternating ping_pong)
for i in 0..BLOOM_BLUR_PASSES {
let ping_pong = (i + 1) % 2 == 1; // Pass 1 is ping_pong=true, pass 2 is false, etc.
self.dispatch_pass(ctx, BloomPhase::Blur, ping_pong, workgroup_size)?;
}
// Final pass: Blend with original
// ping_pong must match what we calculated in pipeline.rs
let blend_ping_pong = (1 + BLOOM_BLUR_PASSES) % 2 == 1;
self.dispatch_pass(ctx, BloomPhase::Blend, blend_ping_pong, workgroup_size)?;
} else {
// Single pass for other effects only (SMAA, DoF)
self.dispatch_pass(ctx, BloomPhase::None, false, workgroup_size)?;
}
Ok(())
}
fn dispatch_pass(
&self,
ctx: &RenderContext,
phase: BloomPhase,
ping_pong: bool,
workgroup_size: (u32, u32),
) -> Result<()> {
let compute_pass = ctx.command_encoder.begin_compute_pass(Some(
&ComputePassDescriptor::new(Some("Effects Pass")).into(),
));
compute_pass.set_bind_group(0, self.bind_groups.get_bind_group(ping_pong)?, None)?;
if let Some(pipeline_key) = self.pipelines.get_bloom_pipeline(phase, ping_pong) {
compute_pass.set_pipeline(ctx.pipelines.compute.get(pipeline_key)?);
compute_pass.dispatch_workgroups(workgroup_size.0, Some(workgroup_size.1), Some(1));
}
compute_pass.end();
Ok(())
}
}