@bornengine/engine 0.4.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +231 -0
  3. package/native/android/Cargo.lock +1848 -0
  4. package/native/android/Cargo.toml +24 -0
  5. package/native/android/src/lib.rs +702 -0
  6. package/native/ios/Cargo.lock +1690 -0
  7. package/native/ios/Cargo.toml +32 -0
  8. package/native/ios/src/lib.rs +1267 -0
  9. package/native/linux/Cargo.lock +3279 -0
  10. package/native/linux/Cargo.toml +29 -0
  11. package/native/linux/src/lib.rs +1331 -0
  12. package/native/macos/Cargo.lock +3310 -0
  13. package/native/macos/Cargo.toml +46 -0
  14. package/native/macos/src/lib.rs +1302 -0
  15. package/native/shared/Cargo.lock +1899 -0
  16. package/native/shared/Cargo.toml +62 -0
  17. package/native/shared/assets/default_font.ttf +0 -0
  18. package/native/shared/build.rs +270 -0
  19. package/native/shared/shaders/common/clouds.wgsl +122 -0
  20. package/native/shared/shaders/common/fog.wgsl +16 -0
  21. package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
  22. package/native/shared/shaders/common/imposter.wgsl +112 -0
  23. package/native/shared/shaders/common/pbr.wgsl +186 -0
  24. package/native/shared/shaders/common/shadows.wgsl +186 -0
  25. package/native/shared/shaders/common/sky.wgsl +8 -0
  26. package/native/shared/shaders/common/tonemap.wgsl +25 -0
  27. package/native/shared/shaders/impulse_field.wgsl +57 -0
  28. package/native/shared/shaders/material_abi.wgsl +383 -0
  29. package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
  30. package/native/shared/src/anim_mixer.rs +61 -0
  31. package/native/shared/src/attach.rs +263 -0
  32. package/native/shared/src/audio/decode.rs +123 -0
  33. package/native/shared/src/audio/mod.rs +863 -0
  34. package/native/shared/src/audio/render.rs +892 -0
  35. package/native/shared/src/audio/spsc.rs +156 -0
  36. package/native/shared/src/audio/stream.rs +226 -0
  37. package/native/shared/src/custom_shaders.rs +104 -0
  38. package/native/shared/src/decals.rs +245 -0
  39. package/native/shared/src/drs.rs +211 -0
  40. package/native/shared/src/engine.rs +261 -0
  41. package/native/shared/src/ffi.rs +116 -0
  42. package/native/shared/src/ffi_core/assets.rs +388 -0
  43. package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
  44. package/native/shared/src/ffi_core/draw.rs +334 -0
  45. package/native/shared/src/ffi_core/game_loop.rs +577 -0
  46. package/native/shared/src/ffi_core/input.rs +234 -0
  47. package/native/shared/src/ffi_core/mod.rs +127 -0
  48. package/native/shared/src/ffi_core/models.rs +1154 -0
  49. package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
  50. package/native/shared/src/ffi_core/scene.rs +626 -0
  51. package/native/shared/src/ffi_core/vfx.rs +212 -0
  52. package/native/shared/src/ffi_core/visual.rs +691 -0
  53. package/native/shared/src/frame_callbacks.rs +122 -0
  54. package/native/shared/src/geometry.rs +236 -0
  55. package/native/shared/src/handles.rs +182 -0
  56. package/native/shared/src/input.rs +448 -0
  57. package/native/shared/src/jolt_sys.rs +822 -0
  58. package/native/shared/src/lib.rs +55 -0
  59. package/native/shared/src/models.rs +1093 -0
  60. package/native/shared/src/models_gltf.rs +1280 -0
  61. package/native/shared/src/particles.rs +391 -0
  62. package/native/shared/src/physics_jolt.rs +1908 -0
  63. package/native/shared/src/picking.rs +298 -0
  64. package/native/shared/src/postfx.rs +345 -0
  65. package/native/shared/src/profiler.rs +492 -0
  66. package/native/shared/src/ragdoll.rs +474 -0
  67. package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
  68. package/native/shared/src/renderer/brdf_lut.rs +154 -0
  69. package/native/shared/src/renderer/draw2d.rs +143 -0
  70. package/native/shared/src/renderer/formats.rs +822 -0
  71. package/native/shared/src/renderer/froxel.rs +421 -0
  72. package/native/shared/src/renderer/gi_bake.rs +653 -0
  73. package/native/shared/src/renderer/graph.rs +462 -0
  74. package/native/shared/src/renderer/hiz.rs +269 -0
  75. package/native/shared/src/renderer/hot_reload.rs +390 -0
  76. package/native/shared/src/renderer/impulse_field.rs +456 -0
  77. package/native/shared/src/renderer/lighting.rs +154 -0
  78. package/native/shared/src/renderer/material_instancing.rs +171 -0
  79. package/native/shared/src/renderer/material_pipeline.rs +700 -0
  80. package/native/shared/src/renderer/material_system.rs +1996 -0
  81. package/native/shared/src/renderer/material_system_tests.rs +601 -0
  82. package/native/shared/src/renderer/material_system_wasm.rs +41 -0
  83. package/native/shared/src/renderer/mod.rs +12556 -0
  84. package/native/shared/src/renderer/model_draw.rs +641 -0
  85. package/native/shared/src/renderer/occlusion.rs +429 -0
  86. package/native/shared/src/renderer/planar_pass.rs +593 -0
  87. package/native/shared/src/renderer/planar_reflection.rs +499 -0
  88. package/native/shared/src/renderer/post_pass.rs +249 -0
  89. package/native/shared/src/renderer/postfx_chain.rs +728 -0
  90. package/native/shared/src/renderer/pt_pass.rs +577 -0
  91. package/native/shared/src/renderer/scene_pass.rs +607 -0
  92. package/native/shared/src/renderer/shader_include.rs +205 -0
  93. package/native/shared/src/renderer/shader_library.rs +135 -0
  94. package/native/shared/src/renderer/shaders/ao.rs +570 -0
  95. package/native/shared/src/renderer/shaders/core.rs +1243 -0
  96. package/native/shared/src/renderer/shaders/env.rs +907 -0
  97. package/native/shared/src/renderer/shaders/gi.rs +810 -0
  98. package/native/shared/src/renderer/shaders/mod.rs +19 -0
  99. package/native/shared/src/renderer/shaders/post.rs +1558 -0
  100. package/native/shared/src/renderer/shaders/pt.rs +1859 -0
  101. package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
  102. package/native/shared/src/renderer/shadow_pass.rs +731 -0
  103. package/native/shared/src/renderer/ssgi_pass.rs +392 -0
  104. package/native/shared/src/renderer/ssr_pass.rs +188 -0
  105. package/native/shared/src/renderer/texture_store.rs +473 -0
  106. package/native/shared/src/renderer/transient.rs +591 -0
  107. package/native/shared/src/renderer/types.rs +941 -0
  108. package/native/shared/src/renderer/util.rs +152 -0
  109. package/native/shared/src/scene.rs +1362 -0
  110. package/native/shared/src/sdf_cache.rs +274 -0
  111. package/native/shared/src/shadows.rs +1036 -0
  112. package/native/shared/src/staging.rs +102 -0
  113. package/native/shared/src/string_header.rs +266 -0
  114. package/native/shared/src/text_renderer.rs +502 -0
  115. package/native/shared/src/textures.rs +197 -0
  116. package/native/tvos/Cargo.lock +1693 -0
  117. package/native/tvos/Cargo.toml +36 -0
  118. package/native/tvos/metal-patched/Cargo.toml +178 -0
  119. package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
  120. package/native/tvos/metal-patched/LICENSE-MIT +25 -0
  121. package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
  122. package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
  123. package/native/tvos/metal-patched/src/argument.rs +366 -0
  124. package/native/tvos/metal-patched/src/blitpass.rs +102 -0
  125. package/native/tvos/metal-patched/src/buffer.rs +71 -0
  126. package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
  127. package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
  128. package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
  129. package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
  130. package/native/tvos/metal-patched/src/computepass.rs +107 -0
  131. package/native/tvos/metal-patched/src/constants.rs +152 -0
  132. package/native/tvos/metal-patched/src/counters.rs +119 -0
  133. package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
  134. package/native/tvos/metal-patched/src/device.rs +2134 -0
  135. package/native/tvos/metal-patched/src/drawable.rs +39 -0
  136. package/native/tvos/metal-patched/src/encoder.rs +2041 -0
  137. package/native/tvos/metal-patched/src/heap.rs +281 -0
  138. package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
  139. package/native/tvos/metal-patched/src/lib.rs +657 -0
  140. package/native/tvos/metal-patched/src/library.rs +902 -0
  141. package/native/tvos/metal-patched/src/mps.rs +575 -0
  142. package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
  143. package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
  144. package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
  145. package/native/tvos/metal-patched/src/renderpass.rs +443 -0
  146. package/native/tvos/metal-patched/src/resource.rs +182 -0
  147. package/native/tvos/metal-patched/src/sampler.rs +165 -0
  148. package/native/tvos/metal-patched/src/sync.rs +178 -0
  149. package/native/tvos/metal-patched/src/texture.rs +352 -0
  150. package/native/tvos/metal-patched/src/types.rs +90 -0
  151. package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
  152. package/native/tvos/src/audio_backend.rs +197 -0
  153. package/native/tvos/src/lib.rs +1891 -0
  154. package/native/visionos/Cargo.lock +1693 -0
  155. package/native/visionos/Cargo.toml +40 -0
  156. package/native/visionos/src/audio_backend.rs +197 -0
  157. package/native/visionos/src/lib.rs +1887 -0
  158. package/native/watchos/Cargo.lock +16 -0
  159. package/native/watchos/Cargo.toml +19 -0
  160. package/native/watchos/shaders/bloom_postfx.metal +99 -0
  161. package/native/watchos/src/BloomWatchApp.swift +1267 -0
  162. package/native/watchos/src/BloomWatchAudio.swift +179 -0
  163. package/native/watchos/src/audio.rs +55 -0
  164. package/native/watchos/src/draw_list.rs +229 -0
  165. package/native/watchos/src/ffi_stubs.rs +915 -0
  166. package/native/watchos/src/ffi_stubs_manual.rs +35 -0
  167. package/native/watchos/src/lib.rs +1124 -0
  168. package/native/watchos/src/models.rs +746 -0
  169. package/native/watchos/src/postfx.rs +95 -0
  170. package/native/watchos/src/scene.rs +534 -0
  171. package/native/watchos/src/textures.rs +184 -0
  172. package/native/web/Cargo.lock +1657 -0
  173. package/native/web/Cargo.toml +43 -0
  174. package/native/web/bloom_glue.js +695 -0
  175. package/native/web/build.sh +131 -0
  176. package/native/web/index.html +35 -0
  177. package/native/web/jolt_bridge.js +1519 -0
  178. package/native/web/src/input_ffi.rs +286 -0
  179. package/native/web/src/lib.rs +1796 -0
  180. package/native/web/src/material_ffi.rs +710 -0
  181. package/native/web/src/parity_ffi.rs +343 -0
  182. package/native/web/src/physics_ffi.rs +643 -0
  183. package/native/web/src/ragdoll_ffi.rs +250 -0
  184. package/native/web/src/render_settings.rs +98 -0
  185. package/native/windows/Cargo.lock +1815 -0
  186. package/native/windows/Cargo.toml +68 -0
  187. package/native/windows/src/lib.rs +1486 -0
  188. package/package.json +4279 -0
  189. package/src/audio/index.ts +315 -0
  190. package/src/core/colors.ts +63 -0
  191. package/src/core/index.ts +1206 -0
  192. package/src/core/keys.ts +63 -0
  193. package/src/core/types.ts +104 -0
  194. package/src/index.ts +171 -0
  195. package/src/math/index.ts +516 -0
  196. package/src/mobile/index.ts +294 -0
  197. package/src/models/index.ts +1258 -0
  198. package/src/physics/index.ts +1134 -0
  199. package/src/scene/index.ts +698 -0
  200. package/src/shapes/index.ts +120 -0
  201. package/src/text/index.ts +48 -0
  202. package/src/textures/index.ts +187 -0
  203. package/src/vfx/index.ts +191 -0
  204. package/src/world/index.ts +24 -0
  205. package/src/world/loader.ts +423 -0
  206. package/src/world/prefab.ts +217 -0
  207. package/src/world/render.ts +172 -0
  208. package/src/world/saver.ts +108 -0
  209. package/src/world/serialize.ts +301 -0
  210. package/src/world/terrain.ts +355 -0
  211. package/src/world/types.ts +160 -0
  212. package/src/world/validate.ts +319 -0
  213. package/src/world/version.ts +114 -0
@@ -0,0 +1,728 @@
1
+ //! Post-FX chain passes split from end_frame_with_scene (2000-line file
2
+ //! policy + render-graph migration prep). Starts with the bloom chain;
3
+ //! the rest of the tail (compose/upscale/TAA/DoF/blur/SSS) migrates here
4
+ //! cluster by cluster.
5
+
6
+ use super::*;
7
+ use wgpu::util::DeviceExt;
8
+
9
+ impl Renderer {
10
+ /// Bloom: progressive downsample (Karis-thresholded first tap)
11
+ /// followed by additive upsample back up the chain. No-op (clears
12
+ /// nothing) when disabled — compose skips the bloom sample.
13
+ pub(super) fn record_bloom_chain(
14
+ &mut self,
15
+ encoder: &mut wgpu::CommandEncoder,
16
+ profiler: &mut crate::profiler::Profiler,
17
+ surf_w: u32,
18
+ surf_h: u32,
19
+ ) {
20
+ // ============================================================
21
+ // Bloom: progressive downsample (Karis-thresholded first tap)
22
+ // followed by additive upsample back up the chain.
23
+ // ============================================================
24
+ if self.bloom_enabled {
25
+ let mip_dims: Vec<(u32, u32)> = (0..BLOOM_MIP_COUNT)
26
+ .map(|i| (
27
+ ((surf_w / 2) >> i).max(1),
28
+ ((surf_h / 2) >> i).max(1),
29
+ ))
30
+ .collect();
31
+
32
+ // Build per-pass bind groups + uniform writes. Each downsample
33
+ // reads the previous mip (or hdr_rt for the first) and writes
34
+ // to the current mip. Each upsample reads mip i+1 and blends
35
+ // additively into mip i.
36
+ let bloom_filter_radius = 1.0_f32; // upsample tent radius
37
+
38
+ // Downsample chain: mip 0 reads HDR, mips 1..N read previous mip.
39
+ for i in 0..BLOOM_MIP_COUNT as usize {
40
+ let (src_view, src_w, src_h, threshold_pass) = if i == 0 {
41
+ (&self.hdr_rt_view, surf_w as f32, surf_h as f32, true)
42
+ } else {
43
+ let prev = &self.bloom_mip_views[i - 1];
44
+ let (pw, ph) = mip_dims[i - 1];
45
+ (prev, pw as f32, ph as f32, false)
46
+ };
47
+
48
+ // Threshold in pre-exposure HDR units. The 2.5 reference was tuned
49
+ // at exposure 1.0 (Sponza), so scale it inversely with the game's
50
+ // manual exposure to keep bloom triggering at the same DISPLAY
51
+ // brightness regardless of exposure. Auto-exposure keeps the legacy
52
+ // constant — 2.5 was tuned against auto-normalised HDR originally.
53
+ let thr = if self.auto_exposure {
54
+ 2.5
55
+ } else {
56
+ 2.5 / self.manual_exposure.max(0.05)
57
+ };
58
+ let bp = BloomParams {
59
+ params: [1.0 / src_w, 1.0 / src_h, bloom_filter_radius, thr],
60
+ };
61
+ // Per-pass uniform buffer. A single shared buffer written with
62
+ // queue.write_buffer once per pass does NOT work here: all writes
63
+ // land before the encoder executes at submit, so every pass in the
64
+ // chain read the LAST write (wrong texel sizes for every mip, and
65
+ // it would clobber the threshold above).
66
+ let ub = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
67
+ label: Some("bloom_downsample_params"),
68
+ contents: bytemuck::bytes_of(&bp),
69
+ usage: wgpu::BufferUsages::UNIFORM,
70
+ });
71
+
72
+ let bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
73
+ label: Some("bloom_downsample_bg"),
74
+ layout: &self.bloom_layout,
75
+ entries: &[
76
+ wgpu::BindGroupEntry { binding: 0, resource: ub.as_entire_binding() },
77
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(src_view) },
78
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&self.composite_sampler) },
79
+ ],
80
+ });
81
+
82
+ let bloom_ts = profiler.pass_timestamp_writes("bloom_pass");
83
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
84
+ label: Some("bloom_downsample_pass"),
85
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
86
+ view: &self.bloom_mip_views[i],
87
+ resolve_target: None,
88
+ depth_slice: None,
89
+ ops: wgpu::Operations {
90
+ load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
91
+ store: wgpu::StoreOp::Store,
92
+ },
93
+ })],
94
+ depth_stencil_attachment: None,
95
+ timestamp_writes: bloom_ts,
96
+ occlusion_query_set: None,
97
+ multiview_mask: None,
98
+ });
99
+ let pl = if threshold_pass {
100
+ &self.bloom_pipeline_threshold_downsample
101
+ } else {
102
+ &self.bloom_pipeline_downsample
103
+ };
104
+ pass.set_pipeline(pl);
105
+ // Force the viewport to this mip's actual size — wgpu's
106
+ // auto-viewport derives from the surface config, not the
107
+ // mip-view attachment, so without this the bloom pass
108
+ // writes into a fraction of the mip and leaves the rest
109
+ // uninitialized.
110
+ let (mw, mh) = mip_dims[i];
111
+ pass.set_viewport(0.0, 0.0, mw as f32, mh as f32, 0.0, 1.0);
112
+ pass.set_bind_group(0, &bg, &[]);
113
+ pass.draw(0..3, 0..1);
114
+ }
115
+
116
+ // Upsample chain: blend mip i+1 additively into mip i for
117
+ // i = N-2..0. Final mip 0 ends up with the full bloom result.
118
+ for i in (0..(BLOOM_MIP_COUNT as usize - 1)).rev() {
119
+ let src_view = &self.bloom_mip_views[i + 1];
120
+ let (sw, sh) = mip_dims[i + 1];
121
+
122
+ let bp = BloomParams {
123
+ params: [1.0 / sw as f32, 1.0 / sh as f32, bloom_filter_radius, 0.0],
124
+ };
125
+ // Per-pass uniform buffer — see the downsample loop note: a shared
126
+ // buffer + write_buffer per pass aliases to the LAST write at submit.
127
+ let ub = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
128
+ label: Some("bloom_upsample_params"),
129
+ contents: bytemuck::bytes_of(&bp),
130
+ usage: wgpu::BufferUsages::UNIFORM,
131
+ });
132
+
133
+ let bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
134
+ label: Some("bloom_upsample_bg"),
135
+ layout: &self.bloom_layout,
136
+ entries: &[
137
+ wgpu::BindGroupEntry { binding: 0, resource: ub.as_entire_binding() },
138
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(src_view) },
139
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&self.composite_sampler) },
140
+ ],
141
+ });
142
+
143
+ let bloom_up_ts = profiler.pass_timestamp_writes("bloom_pass");
144
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
145
+ label: Some("bloom_upsample_pass"),
146
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
147
+ view: &self.bloom_mip_views[i],
148
+ resolve_target: None,
149
+ depth_slice: None,
150
+ ops: wgpu::Operations {
151
+ // Load — additive blend on top of what
152
+ // downsample wrote.
153
+ load: wgpu::LoadOp::Load,
154
+ store: wgpu::StoreOp::Store,
155
+ },
156
+ })],
157
+ depth_stencil_attachment: None,
158
+ timestamp_writes: bloom_up_ts,
159
+ occlusion_query_set: None,
160
+ multiview_mask: None,
161
+ });
162
+ pass.set_pipeline(&self.bloom_pipeline_upsample);
163
+ // Same viewport fix as the downsample loop above — without
164
+ // this the upsample tents only cover a sub-region of the
165
+ // destination mip.
166
+ let (mw, mh) = mip_dims[i];
167
+ pass.set_viewport(0.0, 0.0, mw as f32, mh as f32, 0.0, 1.0);
168
+ pass.set_bind_group(0, &bg, &[]);
169
+ pass.draw(0..3, 0..1);
170
+ }
171
+ } // end if self.bloom_enabled
172
+ }
173
+ }
174
+
175
+ impl Renderer {
176
+ /// Scene compose: merge HDR + SSR + SSGI*albedo + bloom + fog + sun
177
+ /// shafts into composed_rt. Runs unconditionally so the TAA-on path
178
+ /// (TAA consumes this) and the TAA-off path (composite consumes it)
179
+ /// see the same atmospherics.
180
+ pub(super) fn record_scene_compose(
181
+ &mut self,
182
+ encoder: &mut wgpu::CommandEncoder,
183
+ ) {
184
+ // Composite input views (were locals in end_frame_with_scene).
185
+ // PT-1: while path tracing, the SSR passes are skipped entirely,
186
+ // so history is stale — route compose to ssr_rt, which the march
187
+ // else-branch keeps cleared to transparent black.
188
+ let ssr_composite_view = if self.ssr_enabled && !self.pt_owns_frame() {
189
+ &self.ssr_history_views[self.ssr_history_idx]
190
+ } else {
191
+ &self.ssr_rt_view
192
+ };
193
+ let ssgi_composite_view = &self.ssgi_rt_view;
194
+ // ============================================================
195
+ // Scene-compose pass: merge HDR + SSR + SSGI*albedo + bloom
196
+ // + fog + sun shafts into composed_rt. Runs unconditionally
197
+ // so both the TAA-on path (TAA consumes this) and the
198
+ // TAA-off path (composite consumes this) get the same
199
+ // atmospherics + post-effects.
200
+ // ============================================================
201
+ let inv_vp_current = self.current_inv_vp_matrix;
202
+ // Sun shaft screen-space position. Project a point far along
203
+ // the sun direction through the current VP. If behind the
204
+ // camera (clip.w ≤ 0), the sun is off-screen → disable.
205
+ let sun_dir = self.lighting_uniforms.light_dir;
206
+ let sun_world = [sun_dir[0] * 1000.0, sun_dir[1] * 1000.0, sun_dir[2] * 1000.0, 1.0];
207
+ let clip = mat4_mul_vec4(&self.current_vp_matrix, &sun_world);
208
+ let (sun_uv, shaft_strength_eff) = if clip[3] > 0.0 {
209
+ let ndc_x = clip[0] / clip[3];
210
+ let ndc_y = clip[1] / clip[3];
211
+ let u = ndc_x * 0.5 + 0.5;
212
+ let v = 1.0 - (ndc_y * 0.5 + 0.5);
213
+ // Allow off-screen suns to still cast shafts that streak
214
+ // in from the edge — clamp to a small margin beyond ±[0,1]
215
+ // rather than disabling outright.
216
+ let off = u < -1.0 || u > 2.0 || v < -1.0 || v > 2.0;
217
+ if off { ([0.0, 0.0], 0.0) } else { ([u, v], self.sun_shaft_strength) }
218
+ } else {
219
+ ([0.0, 0.0], 0.0)
220
+ };
221
+ // When bloom_enabled is false we skip the downsample/upsample
222
+ // chain entirely; forcing the composite's bloom multiplier to
223
+ // 0 here means stale bloom_mip_views[0] contents contribute
224
+ // nothing visually.
225
+ let effective_bloom_intensity = if self.bloom_enabled { self.bloom_intensity } else { 0.0 };
226
+ let cp = SceneComposeParams {
227
+ // misc.y = procedural-sky aerial-perspective on/off flag.
228
+ // The scene_compose shader reads this to decide between
229
+ // the legacy 16-step fog march and the V2 3D LUT sample.
230
+ misc: [
231
+ effective_bloom_intensity,
232
+ if self.procedural_sky_enabled { 1.0 } else { 0.0 },
233
+ AERIAL_MAX_DIST_KM,
234
+ 0.0,
235
+ ],
236
+ inv_vp: inv_vp_current,
237
+ fog_color_density: [
238
+ self.fog_color[0], self.fog_color[1], self.fog_color[2], self.fog_density,
239
+ ],
240
+ fog_params: [self.fog_height_ref, self.fog_height_falloff, 0.0, 0.0],
241
+ sun_shaft_uv_strength: [
242
+ sun_uv[0], sun_uv[1], shaft_strength_eff, self.sun_shaft_decay,
243
+ ],
244
+ sun_shaft_color: [
245
+ self.sun_shaft_color[0], self.sun_shaft_color[1], self.sun_shaft_color[2], 0.0,
246
+ ],
247
+ };
248
+ self.queue.write_buffer(&self.scene_compose_uniform_buffer, 0, bytemuck::bytes_of(&cp));
249
+ {
250
+ let bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
251
+ label: Some("scene_compose_bg"),
252
+ layout: &self.scene_compose_layout,
253
+ entries: &[
254
+ wgpu::BindGroupEntry { binding: 0, resource: self.scene_compose_uniform_buffer.as_entire_binding() },
255
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&self.hdr_rt_view) },
256
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&self.composite_sampler) },
257
+ wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(ssr_composite_view) },
258
+ wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::Sampler(&self.composite_sampler) },
259
+ wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::TextureView(ssgi_composite_view) },
260
+ wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::Sampler(&self.composite_sampler) },
261
+ wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::TextureView(&self.bloom_mip_views[0]) },
262
+ wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::Sampler(&self.composite_sampler) },
263
+ wgpu::BindGroupEntry { binding: 9, resource: wgpu::BindingResource::TextureView(&self.albedo_rt_view) },
264
+ wgpu::BindGroupEntry { binding: 10, resource: wgpu::BindingResource::Sampler(&self.composite_sampler) },
265
+ wgpu::BindGroupEntry { binding: 11, resource: wgpu::BindingResource::TextureView(&self.depth_view) },
266
+ wgpu::BindGroupEntry { binding: 12, resource: wgpu::BindingResource::Sampler(&self.ssao_depth_sampler) },
267
+ // EN-005 V2 — always bound; shader gates use on `misc.y`.
268
+ wgpu::BindGroupEntry { binding: 13, resource: wgpu::BindingResource::TextureView(&self.aerial_perspective_view) },
269
+ wgpu::BindGroupEntry { binding: 14, resource: wgpu::BindingResource::Sampler(&self.aerial_perspective_sampler) },
270
+ ],
271
+ });
272
+ // NOTE: GPU timestamp deliberately not requested on this pass.
273
+ // Empirically (sponza, Metal) the reported delta was ~249 ms
274
+ // for what should be a sub-millisecond fullscreen pass. Likely
275
+ // the end-of-pass write is synchronized to a later barrier
276
+ // and includes idle time. CPU-side timing via the enclosing
277
+ // `post_fx` phase captures the cost adequately.
278
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
279
+ label: Some("scene_compose_pass"),
280
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
281
+ view: &self.composed_rt_view,
282
+ resolve_target: None,
283
+ depth_slice: None,
284
+ ops: wgpu::Operations {
285
+ load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
286
+ store: wgpu::StoreOp::Store,
287
+ },
288
+ })],
289
+ depth_stencil_attachment: None,
290
+ timestamp_writes: None,
291
+ occlusion_query_set: None,
292
+ multiview_mask: None,
293
+ });
294
+ pass.set_pipeline(&self.scene_compose_pipeline);
295
+ pass.set_bind_group(0, &bg, &[]);
296
+ pass.draw(0..3, 0..1);
297
+ }
298
+
299
+ }
300
+ }
301
+
302
+ impl Renderer {
303
+ /// Post-FX tail: upscale (when sub-res and TAA is off), TAA, DoF,
304
+ /// motion blur, SSS, and CAS — each stage reads the output of the
305
+ /// last enabled stage before it. The internal `pre_*_view`
306
+ /// selections encode that chain; `composite_source_view` re-derives
307
+ /// the final link for the composite pass.
308
+ pub(super) fn record_postfx_tail(
309
+ &mut self,
310
+ encoder: &mut wgpu::CommandEncoder,
311
+ profiler: &mut crate::profiler::Profiler,
312
+ ) {
313
+ // ============================================================
314
+ // Upscale pass: render-res composed_rt → full-surface upscale_rt.
315
+ // Engages only when render_scale < 1.0 AND TAA is off — when
316
+ // TAA runs it does its own Catmull-Rom upscale. Downstream
317
+ // post-FX (DoF/MB/SSS/composite) read upscale_rt instead of
318
+ // hdr_rt in this case so the chain operates at full surface
319
+ // resolution.
320
+ // ============================================================
321
+ if self.render_scale < 0.999 && !self.taa_enabled {
322
+ let up = UpscaleParams {
323
+ params: [self.upscale_mode as f32, 0.0, 0.0, 0.0],
324
+ };
325
+ self.queue.write_buffer(&self.upscale_uniform_buffer, 0, bytemuck::bytes_of(&up));
326
+ let bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
327
+ label: Some("upscale_bg"),
328
+ layout: &self.upscale_layout,
329
+ entries: &[
330
+ wgpu::BindGroupEntry { binding: 0, resource: self.upscale_uniform_buffer.as_entire_binding() },
331
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&self.composed_rt_view) },
332
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&self.composite_sampler) },
333
+ ],
334
+ });
335
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
336
+ label: Some("upscale_pass"),
337
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
338
+ view: &self.upscale_rt_view,
339
+ resolve_target: None,
340
+ depth_slice: None,
341
+ ops: wgpu::Operations {
342
+ load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
343
+ store: wgpu::StoreOp::Store,
344
+ },
345
+ })],
346
+ depth_stencil_attachment: None,
347
+ timestamp_writes: None,
348
+ occlusion_query_set: None,
349
+ multiview_mask: None,
350
+ });
351
+ pass.set_pipeline(&self.upscale_pipeline);
352
+ pass.set_bind_group(0, &bg, &[]);
353
+ pass.draw(0..3, 0..1);
354
+ }
355
+
356
+ // ============================================================
357
+ // TAA pass: reprojection + neighborhood clamp on composed_rt.
358
+ // Skipped when TAA is off — composite reads composed_rt
359
+ // directly and gets the same composed / fog / shafts output.
360
+ // ============================================================
361
+ let taa_dst_idx = self.taa_current_idx;
362
+ let taa_src_idx = 1 - self.taa_current_idx;
363
+
364
+ if self.taa_enabled {
365
+ // Effective temporal window scales with per-pixel sample
366
+ // density (~render_scale²). At 0.5 → 0.0625 (~16-frame
367
+ // window, close to the prior 0.05/20-frame); at 1.0 →
368
+ // 0.10 (~10-frame), matching native TAA.
369
+ let s2 = self.render_scale * self.render_scale;
370
+ let steady = 0.05 + 0.05 * s2;
371
+ let alpha = if self.taa_frame_index < 4 { 1.0 } else { steady };
372
+ // yz = the current jitter as a composed-UV offset. Content shifts by
373
+ // -jitter_ndc through the GL-convention perspective divide (w = -z),
374
+ // so the rendered position of a feature is uv + (-0.5*jx, +0.5*jy)
375
+ // (v axis flips). Empirically arbitrated by the variance rig: the
376
+ // wrong sign DOUBLES the effective jitter and the numbers scream.
377
+ let tp = TaaParams {
378
+ params: [alpha,
379
+ -0.5 * self.current_jitter_ndc[0],
380
+ 0.5 * self.current_jitter_ndc[1],
381
+ 0.0],
382
+ inv_vp: self.current_inv_vp_matrix,
383
+ prev_vp: self.prev_vp_matrix,
384
+ };
385
+ self.queue.write_buffer(&self.taa_uniform_buffer, 0, bytemuck::bytes_of(&tp));
386
+
387
+ let bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
388
+ label: Some("taa_bg"),
389
+ layout: &self.taa_layout,
390
+ entries: &[
391
+ wgpu::BindGroupEntry { binding: 0, resource: self.taa_uniform_buffer.as_entire_binding() },
392
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&self.composed_rt_view) },
393
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&self.composite_sampler) },
394
+ wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&self.taa_views[taa_src_idx]) },
395
+ wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::Sampler(&self.composite_sampler) },
396
+ wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::TextureView(&self.depth_view) },
397
+ wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::Sampler(&self.ssao_depth_sampler) },
398
+ wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::TextureView(&self.velocity_rt_view) },
399
+ wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::Sampler(&self.composite_sampler) },
400
+ ],
401
+ });
402
+ let taa_ts = profiler.pass_timestamp_writes("taa_pass");
403
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
404
+ label: Some("taa_pass"),
405
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
406
+ view: &self.taa_views[taa_dst_idx],
407
+ resolve_target: None,
408
+ depth_slice: None,
409
+ ops: wgpu::Operations {
410
+ load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
411
+ store: wgpu::StoreOp::Store,
412
+ },
413
+ })],
414
+ depth_stencil_attachment: None,
415
+ timestamp_writes: taa_ts,
416
+ occlusion_query_set: None,
417
+ multiview_mask: None,
418
+ });
419
+ pass.set_pipeline(&self.taa_pipeline);
420
+ pass.set_bind_group(0, &bg, &[]);
421
+ pass.draw(0..3, 0..1);
422
+ }
423
+
424
+ // ============================================================
425
+ // DoF pass: variable-radius Poisson disc blur driven by CoC
426
+ // Reads TAA output / upscale_rt / hdr_rt + depth → dof_rt
427
+ // ============================================================
428
+ let pre_dof_view = if self.taa_enabled {
429
+ &self.taa_views[taa_dst_idx]
430
+ } else if self.render_scale < 0.999 {
431
+ &self.upscale_rt_view
432
+ } else {
433
+ &self.hdr_rt_view
434
+ };
435
+
436
+ if self.dof_enabled && self.dof_aperture > 0.0 {
437
+ let inv_proj = self.current_inv_proj_matrix;
438
+ let dp = DofParams {
439
+ params: [self.dof_focus_distance, self.dof_aperture, self.dof_max_blur, 0.0],
440
+ inv_proj,
441
+ };
442
+ self.queue.write_buffer(&self.dof_uniform_buffer, 0, bytemuck::bytes_of(&dp));
443
+
444
+ let bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
445
+ label: Some("dof_bg"),
446
+ layout: &self.dof_layout,
447
+ entries: &[
448
+ wgpu::BindGroupEntry { binding: 0, resource: self.dof_uniform_buffer.as_entire_binding() },
449
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(pre_dof_view) },
450
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&self.composite_sampler) },
451
+ wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&self.depth_view) },
452
+ wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::Sampler(&self.ssao_depth_sampler) },
453
+ ],
454
+ });
455
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
456
+ label: Some("dof_pass"),
457
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
458
+ view: &self.dof_rt_view,
459
+ resolve_target: None,
460
+ depth_slice: None,
461
+ ops: wgpu::Operations {
462
+ load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
463
+ store: wgpu::StoreOp::Store,
464
+ },
465
+ })],
466
+ depth_stencil_attachment: None,
467
+ timestamp_writes: None,
468
+ occlusion_query_set: None,
469
+ multiview_mask: None,
470
+ });
471
+ pass.set_pipeline(&self.dof_pipeline);
472
+ pass.set_bind_group(0, &bg, &[]);
473
+ pass.draw(0..3, 0..1);
474
+ }
475
+
476
+ // ============================================================
477
+ // Motion blur pass: 8-tap directional blur along velocity
478
+ // Reads upstream color + velocity_rt → motion_blur_rt
479
+ // ============================================================
480
+ let pre_mblur_view = if self.dof_enabled && self.dof_aperture > 0.0 {
481
+ &self.dof_rt_view
482
+ } else if self.taa_enabled {
483
+ &self.taa_views[taa_dst_idx]
484
+ } else if self.render_scale < 0.999 {
485
+ &self.upscale_rt_view
486
+ } else {
487
+ &self.hdr_rt_view
488
+ };
489
+
490
+ if self.motion_blur_enabled && self.motion_blur_strength > 0.0 {
491
+ let mbp = MotionBlurParams {
492
+ params: [self.motion_blur_strength, self.motion_blur_max_blur, 0.0, 0.0],
493
+ };
494
+ self.queue.write_buffer(&self.motion_blur_uniform_buffer, 0, bytemuck::bytes_of(&mbp));
495
+
496
+ let bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
497
+ label: Some("motion_blur_bg"),
498
+ layout: &self.motion_blur_layout,
499
+ entries: &[
500
+ wgpu::BindGroupEntry { binding: 0, resource: self.motion_blur_uniform_buffer.as_entire_binding() },
501
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(pre_mblur_view) },
502
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&self.composite_sampler) },
503
+ wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&self.velocity_rt_view) },
504
+ wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::Sampler(&self.composite_sampler) },
505
+ ],
506
+ });
507
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
508
+ label: Some("motion_blur_pass"),
509
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
510
+ view: &self.motion_blur_rt_view,
511
+ resolve_target: None,
512
+ depth_slice: None,
513
+ ops: wgpu::Operations {
514
+ load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
515
+ store: wgpu::StoreOp::Store,
516
+ },
517
+ })],
518
+ depth_stencil_attachment: None,
519
+ timestamp_writes: None,
520
+ occlusion_query_set: None,
521
+ multiview_mask: None,
522
+ });
523
+ pass.set_pipeline(&self.motion_blur_pipeline);
524
+ pass.set_bind_group(0, &bg, &[]);
525
+ pass.draw(0..3, 0..1);
526
+ }
527
+
528
+ // ============================================================
529
+ // SSS pass: chromatic disc blur (skin / wax / leaves)
530
+ // Reads upstream color + depth → sss_rt.
531
+ // Runs after motion blur so it applies to the fully composited
532
+ // motion state, not to individual geometry.
533
+ // ============================================================
534
+ let pre_sss_view = if self.motion_blur_enabled && self.motion_blur_strength > 0.0 {
535
+ &self.motion_blur_rt_view
536
+ } else if self.dof_enabled && self.dof_aperture > 0.0 {
537
+ &self.dof_rt_view
538
+ } else if self.taa_enabled {
539
+ &self.taa_views[taa_dst_idx]
540
+ } else if self.render_scale < 0.999 {
541
+ &self.upscale_rt_view
542
+ } else {
543
+ &self.hdr_rt_view
544
+ };
545
+
546
+ if self.sss_enabled && self.sss_strength > 0.0 {
547
+ let sp = SssParams {
548
+ params: [self.sss_strength, self.sss_width, 500.0, 0.0],
549
+ };
550
+ self.queue.write_buffer(&self.sss_uniform_buffer, 0, bytemuck::bytes_of(&sp));
551
+
552
+ let bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
553
+ label: Some("sss_bg"),
554
+ layout: &self.sss_layout,
555
+ entries: &[
556
+ wgpu::BindGroupEntry { binding: 0, resource: self.sss_uniform_buffer.as_entire_binding() },
557
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(pre_sss_view) },
558
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&self.composite_sampler) },
559
+ wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&self.depth_view) },
560
+ wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::Sampler(&self.ssao_depth_sampler) },
561
+ ],
562
+ });
563
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
564
+ label: Some("sss_pass"),
565
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
566
+ view: &self.sss_rt_view,
567
+ resolve_target: None,
568
+ depth_slice: None,
569
+ ops: wgpu::Operations {
570
+ load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
571
+ store: wgpu::StoreOp::Store,
572
+ },
573
+ })],
574
+ depth_stencil_attachment: None,
575
+ timestamp_writes: None,
576
+ occlusion_query_set: None,
577
+ multiview_mask: None,
578
+ });
579
+ pass.set_pipeline(&self.sss_pipeline);
580
+ pass.set_bind_group(0, &bg, &[]);
581
+ pass.draw(0..3, 0..1);
582
+ }
583
+
584
+ // ============================================================
585
+ // RCAS sharpen pass: contrast-adaptive 5-tap cross. Reads the
586
+ // same texture composite would otherwise sample (sss/mb/dof/
587
+ // taa/upscale/composed) and writes cas_rt. Off by default —
588
+ // gated on cas_strength > 0.
589
+ // ============================================================
590
+ let cas_input_view: &wgpu::TextureView = if self.sss_enabled && self.sss_strength > 0.0 {
591
+ &self.sss_rt_view
592
+ } else if self.motion_blur_enabled && self.motion_blur_strength > 0.0 {
593
+ &self.motion_blur_rt_view
594
+ } else if self.dof_enabled && self.dof_aperture > 0.0 {
595
+ &self.dof_rt_view
596
+ } else if self.taa_enabled {
597
+ &self.taa_views[taa_dst_idx]
598
+ } else if self.render_scale < 0.999 {
599
+ &self.upscale_rt_view
600
+ } else {
601
+ // TAA off, native res: composed_rt is already full-surface
602
+ // and carries SSR / SSGI / bloom / fog / shafts.
603
+ &self.composed_rt_view
604
+ };
605
+
606
+ if self.cas_strength > 0.0 {
607
+ let cp = RcasParams {
608
+ params: [self.cas_strength, 0.0, 0.0, 0.0],
609
+ };
610
+ self.queue.write_buffer(&self.cas_uniform_buffer, 0, bytemuck::bytes_of(&cp));
611
+ let bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
612
+ label: Some("cas_bg"),
613
+ layout: &self.cas_layout,
614
+ entries: &[
615
+ wgpu::BindGroupEntry { binding: 0, resource: self.cas_uniform_buffer.as_entire_binding() },
616
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(cas_input_view) },
617
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&self.composite_sampler) },
618
+ ],
619
+ });
620
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
621
+ label: Some("cas_pass"),
622
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
623
+ view: &self.cas_rt_view,
624
+ resolve_target: None,
625
+ depth_slice: None,
626
+ ops: wgpu::Operations {
627
+ load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
628
+ store: wgpu::StoreOp::Store,
629
+ },
630
+ })],
631
+ depth_stencil_attachment: None,
632
+ timestamp_writes: None,
633
+ occlusion_query_set: None,
634
+ multiview_mask: None,
635
+ });
636
+ pass.set_pipeline(&self.cas_pipeline);
637
+ pass.set_bind_group(0, &bg, &[]);
638
+ pass.draw(0..3, 0..1);
639
+ }
640
+
641
+ }
642
+
643
+ /// The view the composite pass reads: the output of the LAST enabled
644
+ /// stage in chain order CAS > SSS > motion blur > DoF > TAA >
645
+ /// upscale > raw HDR. Must mirror the `pre_*_view` cascade in
646
+ /// record_postfx_tail.
647
+ pub(super) fn composite_source_view(&self) -> &wgpu::TextureView {
648
+ if self.cas_strength > 0.0 {
649
+ &self.cas_rt_view
650
+ } else if self.sss_enabled && self.sss_strength > 0.0 {
651
+ &self.sss_rt_view
652
+ } else if self.motion_blur_enabled && self.motion_blur_strength > 0.0 {
653
+ &self.motion_blur_rt_view
654
+ } else if self.dof_enabled && self.dof_aperture > 0.0 {
655
+ &self.dof_rt_view
656
+ } else if self.taa_enabled {
657
+ &self.taa_views[self.taa_current_idx]
658
+ } else if self.render_scale < 0.999 {
659
+ &self.upscale_rt_view
660
+ } else {
661
+ &self.hdr_rt_view
662
+ }
663
+ }
664
+ }
665
+
666
+ impl Renderer {
667
+ /// Auto-exposure measure + adapt pass into the dst slot of the
668
+ /// ping-pong exposure texture. No-op when auto_exposure is off (the
669
+ /// composite keeps reading the stale texture, which manual_exposure
670
+ /// bypasses). The caller owns the src/dst indices because the
671
+ /// composite binds the same dst view.
672
+ pub(super) fn record_auto_exposure(
673
+ &mut self,
674
+ encoder: &mut wgpu::CommandEncoder,
675
+ exposure_src_idx: usize,
676
+ exposure_dst_idx: usize,
677
+ ) {
678
+ // The luminance source is whatever the composite will read.
679
+ let composite_src_view = self.composite_source_view();
680
+ if self.auto_exposure {
681
+ let ep = ExposureParams {
682
+ params: [
683
+ self.auto_exposure_key,
684
+ self.auto_exposure_rate,
685
+ // Wide clamp — without SSGI, Sponza's shadowed
686
+ // corridors have ~7× less average luma than its
687
+ // sunlit courtyard, so exposure needs to span
688
+ // the same range to keep perceived brightness
689
+ // stable across rotations.
690
+ 0.1,
691
+ 10.0,
692
+ ],
693
+ };
694
+ self.queue.write_buffer(&self.exposure_uniform_buffer, 0, bytemuck::bytes_of(&ep));
695
+
696
+ let bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
697
+ label: Some("exposure_bg"),
698
+ layout: &self.exposure_layout,
699
+ entries: &[
700
+ wgpu::BindGroupEntry { binding: 0, resource: self.exposure_uniform_buffer.as_entire_binding() },
701
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(composite_src_view) },
702
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&self.composite_sampler) },
703
+ wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&self.exposure_views[exposure_src_idx]) },
704
+ wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::Sampler(&self.composite_sampler) },
705
+ ],
706
+ });
707
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
708
+ label: Some("exposure_pass"),
709
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
710
+ view: &self.exposure_views[exposure_dst_idx],
711
+ resolve_target: None,
712
+ depth_slice: None,
713
+ ops: wgpu::Operations {
714
+ load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
715
+ store: wgpu::StoreOp::Store,
716
+ },
717
+ })],
718
+ depth_stencil_attachment: None,
719
+ timestamp_writes: None,
720
+ occlusion_query_set: None,
721
+ multiview_mask: None,
722
+ });
723
+ pass.set_pipeline(&self.exposure_pipeline);
724
+ pass.set_bind_group(0, &bg, &[]);
725
+ pass.draw(0..3, 0..1);
726
+ }
727
+ }
728
+ }