@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,822 @@
1
+ //! Texture formats + render-target creation helpers.
2
+ //!
3
+ //! All `*_FORMAT` constants, the mip-count constants, and every
4
+ //! `create_*` helper that hands the Renderer a `(Texture, View)`
5
+ //! pair live here. Pure data/helpers — no Renderer state. Each
6
+ //! helper is `pub(super)` so only the surrounding `renderer::`
7
+ //! module can call it.
8
+
9
+ use wgpu;
10
+
11
+ // ============================================================
12
+ // Depth texture helper
13
+ // ============================================================
14
+
15
+ pub(super) const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
16
+ /// Linear HDR format for the offscreen render target. The scene + sky
17
+ /// + immediate-mode 3D passes write here in linear space; a final
18
+ /// composite pass tonemaps to the sRGB surface format.
19
+ pub(super) const HDR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
20
+
21
+ /// Number of bloom mip levels. 5 mips gives a long-tail glow that
22
+ /// covers ~32× the source pixel size. More mips = more haloing,
23
+ /// fewer = less coverage. Each mip is half the previous size.
24
+ pub(super) const BLOOM_MIP_COUNT: u32 = 5;
25
+
26
+ /// SSAO RT layout: R = GTAO occlusion (bilaterally blurred), G =
27
+ /// contact-shadow factor (passed through blur unchanged so the fine-
28
+ /// detail ray-march result survives). Rgba8Unorm because WebGPU
29
+ /// requires `rgba8unorm` for storage-texture writes by default —
30
+ /// the compute GTAO pass (SSAO_SHADER_WGSL) uses `textureStore`.
31
+ /// Extra two channels left 0; downstream samplers only read .r/.g,
32
+ /// so the only cost is 4 B/px vs 2 B/px at half-res
33
+ /// (~180 kB extra on a 1600×900 surface).
34
+ pub(super) const SSAO_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
35
+
36
+ /// Material G-buffer format. Rg8Unorm: R = metallic, G = roughness.
37
+ /// Written as a second color attachment in the HDR pass; SSR (and
38
+ /// any future deferred passes) reads it for per-pixel material info.
39
+ pub(super) const MATERIAL_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rg8Unorm;
40
+
41
+ /// Linear-depth Hi-Z pyramid format. R32Float (not R16Float) because
42
+ /// WebGPU only mandates r32-family formats for single-channel storage
43
+ /// textures. The pyramid stores *positive* view-space distance
44
+ /// (|view_z|) so compute GTAO skips per-sample linearization. Sky
45
+ /// pixels get `HIZ_SKY_Z` (10 000) and the downsample uses `min` so
46
+ /// any near-field geometry in a tile dominates surrounding sky.
47
+ pub(super) const HIZ_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R32Float;
48
+
49
+ /// Number of mip levels in the linear-depth Hi-Z pyramid. 5 covers
50
+ /// a 16-pixel-radius footprint at the coarsest mip — enough for
51
+ /// the 0.25 UV clamp SSAO uses (~100 px at half-res 400-wide).
52
+ /// One linearize pass plus `HIZ_MIP_COUNT - 1` downsample passes.
53
+ pub(super) const HIZ_MIP_COUNT: u32 = 5;
54
+
55
+ /// Velocity buffer format. Rg16Float: two-channel 16-bit float for
56
+ /// sub-pixel precision screen-space velocity. Written as a third
57
+ /// color attachment in the HDR pass; motion blur and TAA read it.
58
+ pub(super) const VELOCITY_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rg16Float;
59
+
60
+ pub(super) fn create_depth_texture(device: &wgpu::Device, width: u32, height: u32) -> (wgpu::Texture, wgpu::TextureView) {
61
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
62
+ label: Some("depth_texture"),
63
+ size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
64
+ mip_level_count: 1,
65
+ sample_count: 1,
66
+ dimension: wgpu::TextureDimension::D2,
67
+ format: DEPTH_FORMAT,
68
+ // SSAO samples this texture in a separate pass after the
69
+ // depth-write HDR pass — needs TEXTURE_BINDING in addition
70
+ // to RENDER_ATTACHMENT.
71
+ //
72
+ // COPY_SRC: Phase 4c snapshots this to a transient depth
73
+ // texture so translucent materials can sample it without
74
+ // aliasing the pass's own depth-stencil attachment.
75
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
76
+ | wgpu::TextureUsages::TEXTURE_BINDING
77
+ | wgpu::TextureUsages::COPY_SRC,
78
+ view_formats: &[],
79
+ });
80
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
81
+ (texture, view)
82
+ }
83
+
84
+ pub(super) fn create_hdr_rt(device: &wgpu::Device, width: u32, height: u32) -> (wgpu::Texture, wgpu::TextureView) {
85
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
86
+ label: Some("hdr_rt"),
87
+ size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
88
+ mip_level_count: 1,
89
+ sample_count: 1,
90
+ dimension: wgpu::TextureDimension::D2,
91
+ format: HDR_FORMAT,
92
+ // Phase 4b adds COPY_SRC so the translucent-pass scheduler
93
+ // can snapshot hdr_rt → a SceneColor transient via
94
+ // copy_texture_to_texture before refractive draws run.
95
+ //
96
+ // STORAGE_BINDING: the path-trace megakernel (PT-1) writes the
97
+ // traced scene colour directly into hdr_rt from compute.
98
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
99
+ | wgpu::TextureUsages::TEXTURE_BINDING
100
+ | wgpu::TextureUsages::COPY_SRC
101
+ | wgpu::TextureUsages::STORAGE_BINDING,
102
+ view_formats: &[],
103
+ });
104
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
105
+ (texture, view)
106
+ }
107
+
108
+ /// Create the two ping-pong 1×1 exposure textures. Single fragment
109
+ /// writes to one, composite samples the other, swap each frame.
110
+ pub(super) fn create_exposure_textures(device: &wgpu::Device) -> ([wgpu::Texture; 2], [wgpu::TextureView; 2]) {
111
+ let make = |label: &str| -> (wgpu::Texture, wgpu::TextureView) {
112
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
113
+ label: Some(label),
114
+ size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
115
+ mip_level_count: 1,
116
+ sample_count: 1,
117
+ dimension: wgpu::TextureDimension::D2,
118
+ // Rg16Float (was R16Float): .r = smoothed exposure, .g = the
119
+ // anchored AE target (flicker fix — see EXPOSURE_SHADER_WGSL).
120
+ format: wgpu::TextureFormat::Rg16Float,
121
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
122
+ | wgpu::TextureUsages::TEXTURE_BINDING,
123
+ view_formats: &[],
124
+ });
125
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
126
+ (texture, view)
127
+ };
128
+ let (a, av) = make("exposure_a");
129
+ let (b, bv) = make("exposure_b");
130
+ ([a, b], [av, bv])
131
+ }
132
+
133
+ /// Create the material G-buffer (Rg8Unorm, surface size).
134
+ pub(super) fn create_material_rt(device: &wgpu::Device, width: u32, height: u32) -> (wgpu::Texture, wgpu::TextureView) {
135
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
136
+ label: Some("material_rt"),
137
+ size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
138
+ mip_level_count: 1,
139
+ sample_count: 1,
140
+ dimension: wgpu::TextureDimension::D2,
141
+ format: MATERIAL_FORMAT,
142
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
143
+ | wgpu::TextureUsages::TEXTURE_BINDING,
144
+ view_formats: &[],
145
+ });
146
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
147
+ (texture, view)
148
+ }
149
+
150
+ /// Create the albedo G-buffer (Rgba8Unorm, surface size). Written by
151
+ /// the scene pass so post-passes can modulate bounce light by the
152
+ /// receiving surface's diffuse albedo (SSGI etc.).
153
+ pub(super) fn create_albedo_rt(device: &wgpu::Device, width: u32, height: u32) -> (wgpu::Texture, wgpu::TextureView) {
154
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
155
+ label: Some("albedo_rt"),
156
+ size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
157
+ mip_level_count: 1,
158
+ sample_count: 1,
159
+ dimension: wgpu::TextureDimension::D2,
160
+ format: wgpu::TextureFormat::Rgba8Unorm,
161
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
162
+ | wgpu::TextureUsages::TEXTURE_BINDING,
163
+ view_formats: &[],
164
+ });
165
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
166
+ (texture, view)
167
+ }
168
+
169
+ /// Create the composed HDR render target. Scene HDR + SSR + SSGI *
170
+ /// albedo + bloom + fog + sun shafts all merged into one texture by
171
+ /// the `scene_compose` pass. Both the TAA-on path (TAA consumes this
172
+ /// as its "current frame" input) and the TAA-off path (composite
173
+ /// reads it directly) read from the same buffer, so fog / shafts /
174
+ /// post-effects stay visible regardless of TAA state.
175
+ pub(super) fn create_composed_rt(device: &wgpu::Device, width: u32, height: u32) -> (wgpu::Texture, wgpu::TextureView) {
176
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
177
+ label: Some("composed_rt"),
178
+ size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
179
+ mip_level_count: 1,
180
+ sample_count: 1,
181
+ dimension: wgpu::TextureDimension::D2,
182
+ format: HDR_FORMAT,
183
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
184
+ | wgpu::TextureUsages::TEXTURE_BINDING,
185
+ view_formats: &[],
186
+ });
187
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
188
+ (texture, view)
189
+ }
190
+
191
+ /// Create the velocity render target (Rg16Float, surface size).
192
+ /// Per-pixel screen-space velocity for motion blur and TAA.
193
+ pub(super) fn create_velocity_rt(device: &wgpu::Device, width: u32, height: u32) -> (wgpu::Texture, wgpu::TextureView) {
194
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
195
+ label: Some("velocity_rt"),
196
+ size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
197
+ mip_level_count: 1,
198
+ sample_count: 1,
199
+ dimension: wgpu::TextureDimension::D2,
200
+ format: VELOCITY_FORMAT,
201
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
202
+ | wgpu::TextureUsages::TEXTURE_BINDING,
203
+ view_formats: &[],
204
+ });
205
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
206
+ (texture, view)
207
+ }
208
+
209
+ /// Create the SSR render target (quarter-res HDR). Stochastic SSR
210
+ /// traces one GGX-sampled ray per pixel per frame and relies on the
211
+ /// temporal denoiser to converge the cone over 4–8 frames; pushing
212
+ /// from half to quarter-res cuts the march + temporal-filter pixel
213
+ /// counts 4× and the bilinear upsample in compose is imperceptible
214
+ /// because the GGX cone + temporal blend is already wider than one
215
+ /// quarter-res texel.
216
+ pub(super) fn create_ssr_rt(device: &wgpu::Device, width: u32, height: u32) -> (wgpu::Texture, wgpu::TextureView) {
217
+ let w = (width / 4).max(1);
218
+ let h = (height / 4).max(1);
219
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
220
+ label: Some("ssr_rt"),
221
+ size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
222
+ mip_level_count: 1,
223
+ sample_count: 1,
224
+ dimension: wgpu::TextureDimension::D2,
225
+ format: HDR_FORMAT,
226
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
227
+ | wgpu::TextureUsages::TEXTURE_BINDING,
228
+ view_formats: &[],
229
+ });
230
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
231
+ (texture, view)
232
+ }
233
+
234
+ /// Create the SSR temporal history textures (ping-pong pair, same
235
+ /// format/size as ssr_rt — half-res HDR). The temporal denoiser
236
+ /// blends the noisy current-frame stochastic SSR into the
237
+ /// reprojected previous-frame history so 4–8 frames of accumulation
238
+ /// converge to a smooth reflection.
239
+ pub(super) fn create_ssr_history_textures(
240
+ device: &wgpu::Device, width: u32, height: u32,
241
+ ) -> ([wgpu::Texture; 2], [wgpu::TextureView; 2]) {
242
+ let w = (width / 4).max(1);
243
+ let h = (height / 4).max(1);
244
+ let make = || {
245
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
246
+ label: Some("ssr_history"),
247
+ size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
248
+ mip_level_count: 1,
249
+ sample_count: 1,
250
+ dimension: wgpu::TextureDimension::D2,
251
+ format: HDR_FORMAT,
252
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
253
+ | wgpu::TextureUsages::TEXTURE_BINDING,
254
+ view_formats: &[],
255
+ });
256
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
257
+ (texture, view)
258
+ };
259
+ let (t0, v0) = make();
260
+ let (t1, v1) = make();
261
+ ([t0, t1], [v0, v1])
262
+ }
263
+
264
+ /// Create the SSGI render target (half-res HDR — indirect diffuse bounce light).
265
+ /// Same half-res HDR strategy as SSR: keeps the per-pixel ray march cheap
266
+ /// while still providing enough color resolution for colored bounce light.
267
+ pub(super) fn create_ssgi_rt(device: &wgpu::Device, width: u32, height: u32) -> (wgpu::Texture, wgpu::TextureView) {
268
+ let w = (width / 2).max(1);
269
+ let h = (height / 2).max(1);
270
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
271
+ label: Some("ssgi_rt"),
272
+ size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
273
+ mip_level_count: 1,
274
+ sample_count: 1,
275
+ dimension: wgpu::TextureDimension::D2,
276
+ format: HDR_FORMAT,
277
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
278
+ | wgpu::TextureUsages::TEXTURE_BINDING,
279
+ view_formats: &[],
280
+ });
281
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
282
+ (texture, view)
283
+ }
284
+
285
+ /// Probe grid = ceil(half_w / 16) × ceil(half_h / 16). 50×29 on a
286
+ /// 800×450 half-res Sponza RT = 1450 probes, each holding an 8×8
287
+ /// octahedral atlas = 64 radiance samples (ticket 007a).
288
+ pub(super) const PROBE_TILE_SIZE: u32 = 16;
289
+ pub(super) const PROBE_OCT_SIZE: u32 = 8;
290
+ pub(super) const PROBE_OCT_TEXELS: u32 = PROBE_OCT_SIZE * PROBE_OCT_SIZE;
291
+
292
+ pub(super) fn probe_grid_dims(width: u32, height: u32) -> (u32, u32) {
293
+ let half_w = (width / 2).max(1);
294
+ let half_h = (height / 2).max(1);
295
+ let gw = (half_w + PROBE_TILE_SIZE - 1) / PROBE_TILE_SIZE;
296
+ let gh = (half_h + PROBE_TILE_SIZE - 1) / PROBE_TILE_SIZE;
297
+ (gw.max(1), gh.max(1))
298
+ }
299
+
300
+ /// 3D Rgba16Float texture with dimensions `(probe_grid_w, probe_grid_h,
301
+ /// 64)` — one voxel per probe × octahedral texel. Shared shape for the
302
+ /// trace output and the ping-pong history textures.
303
+ fn create_probe_3d_tex(
304
+ device: &wgpu::Device, label: &'static str, gw: u32, gh: u32,
305
+ ) -> (wgpu::Texture, wgpu::TextureView) {
306
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
307
+ label: Some(label),
308
+ size: wgpu::Extent3d { width: gw, height: gh, depth_or_array_layers: PROBE_OCT_TEXELS },
309
+ mip_level_count: 1,
310
+ sample_count: 1,
311
+ dimension: wgpu::TextureDimension::D3,
312
+ format: HDR_FORMAT,
313
+ usage: wgpu::TextureUsages::STORAGE_BINDING
314
+ | wgpu::TextureUsages::TEXTURE_BINDING,
315
+ view_formats: &[],
316
+ });
317
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
318
+ (texture, view)
319
+ }
320
+
321
+ /// Per-frame trace output. The trace pass writes into this, the temporal
322
+ /// pass reads it as the "current" input. Not ping-pong because its
323
+ /// contents are fully regenerated every frame.
324
+ pub(super) fn create_probe_trace_tex(
325
+ device: &wgpu::Device, width: u32, height: u32,
326
+ ) -> (wgpu::Texture, wgpu::TextureView) {
327
+ let (gw, gh) = probe_grid_dims(width, height);
328
+ create_probe_3d_tex(device, "probe_trace", gw, gh)
329
+ }
330
+
331
+ /// Ticket 014 V2/V5 — scene-wide SDF clipmap. 64³ R32Float covering
332
+ /// `SCENE_SDF_CLIPMAP_EXTENT` metres around the camera (V5). The
333
+ /// initial origin is the first-frame camera position, voxel-snapped.
334
+ /// Re-bakes when the camera has moved past
335
+ /// `SCENE_SDF_CLIPMAP_REBAKE_THRESHOLD` of the extent.
336
+ pub(super) const SCENE_SDF_CLIPMAP_RES: u32 = 64;
337
+ pub(super) const SCENE_SDF_CLIPMAP_EXTENT: f32 = 40.0;
338
+ /// V5 — voxel-size invalidation threshold, expressed as a fraction
339
+ /// of the full extent. 0.25 = rebake when the camera has moved more
340
+ /// than 10 m from the clipmap centre on a 40 m clipmap.
341
+ pub(super) const SCENE_SDF_CLIPMAP_REBAKE_THRESHOLD: f32 = 0.25;
342
+ /// Amortized-rebake fix: triangles are binned on the CPU into this many
343
+ /// cells per axis (must divide SCENE_SDF_CLIPMAP_RES) so each voxel only
344
+ /// tests triangles from its own pre-expanded cell instead of the whole
345
+ /// scene. One cell width (extent / cells = 2.5 m) is also the narrow-band
346
+ /// clamp the bake shader writes for empty cells.
347
+ pub(super) const SCENE_SDF_CLIPMAP_BIN_CELLS: u32 = 16;
348
+ /// Amortized-rebake fix: voxel Z-layers baked per frame into the staging
349
+ /// texture (must be a multiple of the 4-deep workgroup). 16 layers →
350
+ /// a full 64³ rebake spreads over 4 frames while traces keep sampling
351
+ /// the live clipmap.
352
+ pub(super) const SCENE_SDF_CLIPMAP_LAYERS_PER_FRAME: u32 = 16;
353
+
354
+ pub(super) fn create_scene_sdf_clipmap(
355
+ device: &wgpu::Device,
356
+ ) -> (wgpu::Texture, wgpu::TextureView) {
357
+ // COPY_DST: the amortized rebake bakes into a staging clone and
358
+ // copies over the live texture when the last slice lands.
359
+ create_scene_sdf_clipmap_tex(
360
+ device,
361
+ "scene_sdf_clipmap",
362
+ wgpu::TextureUsages::STORAGE_BINDING
363
+ | wgpu::TextureUsages::TEXTURE_BINDING
364
+ | wgpu::TextureUsages::COPY_DST,
365
+ )
366
+ }
367
+
368
+ /// Amortized-rebake fix: staging clone the sliced bake writes into while
369
+ /// the live clipmap keeps serving traces.
370
+ pub(super) fn create_scene_sdf_clipmap_staging(
371
+ device: &wgpu::Device,
372
+ ) -> (wgpu::Texture, wgpu::TextureView) {
373
+ create_scene_sdf_clipmap_tex(
374
+ device,
375
+ "scene_sdf_clipmap_staging",
376
+ wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::COPY_SRC,
377
+ )
378
+ }
379
+
380
+ fn create_scene_sdf_clipmap_tex(
381
+ device: &wgpu::Device,
382
+ label: &str,
383
+ usage: wgpu::TextureUsages,
384
+ ) -> (wgpu::Texture, wgpu::TextureView) {
385
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
386
+ label: Some(label),
387
+ size: wgpu::Extent3d {
388
+ width: SCENE_SDF_CLIPMAP_RES,
389
+ height: SCENE_SDF_CLIPMAP_RES,
390
+ depth_or_array_layers: SCENE_SDF_CLIPMAP_RES,
391
+ },
392
+ mip_level_count: 1,
393
+ sample_count: 1,
394
+ dimension: wgpu::TextureDimension::D3,
395
+ format: wgpu::TextureFormat::R32Float,
396
+ usage,
397
+ view_formats: &[],
398
+ });
399
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
400
+ (texture, view)
401
+ }
402
+
403
+ /// Ticket 014 V6/V10/V13 — World-Space Radiance Cache. 16³ grid of
404
+ /// octahedral probes per cascade, each an 8×8 slab of pre-integrated
405
+ /// distant lighting (padded to 10×10 in V10 for hardware bilinear).
406
+ /// V13 stacks `WSRC_CASCADE_COUNT` cascades in Z at a range of
407
+ /// extents so long-range bounces don't share one probe's coarse
408
+ /// cell with close bounces.
409
+ ///
410
+ /// Cascade extents: near 30 m / mid 120 m / far 500 m. 16³ probes
411
+ /// per cascade → cell sizes of 1.875 / 7.5 / 31.25 m. The near
412
+ /// cascade re-bakes often as the camera moves (1.875 × 0.25 ≈
413
+ /// 0.47 m threshold → 7.5 m rebake extent; inverted: cascades with
414
+ /// larger extents rebake less often).
415
+ ///
416
+ /// Atlas layout (rgba16float D3 at `(160, 160, 48)`):
417
+ /// probe `(gx, gy, gz)` in cascade `c ∈ [0, 3)`, padded octel
418
+ /// `(ox_pad, oy_pad) ∈ [0, 10)²` → texel
419
+ /// `(gx * 10 + ox_pad, gy * 10 + oy_pad, c * 16 + gz)`
420
+ ///
421
+ /// Borders are octahedrally-wrapped at bake time (V11) so the
422
+ /// sampler's bilinear tap stays smooth across the silhouette.
423
+ pub(super) const WSRC_GRID_RES: u32 = 16;
424
+ pub(super) const WSRC_OCT_PAD: u32 = 1;
425
+ pub(super) const WSRC_OCT_PADDED: u32 = PROBE_OCT_SIZE + 2 * WSRC_OCT_PAD;
426
+ pub(super) const WSRC_CASCADE_COUNT: u32 = 3;
427
+ pub(super) const WSRC_CASCADE_EXTENTS: [f32; 3] = [30.0, 120.0, 500.0];
428
+ pub(super) const WSRC_REBAKE_THRESHOLD: f32 = 0.25;
429
+
430
+ pub(super) fn create_wsrc_atlas(
431
+ device: &wgpu::Device,
432
+ ) -> (wgpu::Texture, wgpu::TextureView) {
433
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
434
+ label: Some("wsrc_atlas"),
435
+ size: wgpu::Extent3d {
436
+ width: WSRC_GRID_RES * WSRC_OCT_PADDED,
437
+ height: WSRC_GRID_RES * WSRC_OCT_PADDED,
438
+ depth_or_array_layers: WSRC_GRID_RES * WSRC_CASCADE_COUNT,
439
+ },
440
+ mip_level_count: 1,
441
+ sample_count: 1,
442
+ dimension: wgpu::TextureDimension::D3,
443
+ format: HDR_FORMAT,
444
+ usage: wgpu::TextureUsages::STORAGE_BINDING
445
+ | wgpu::TextureUsages::TEXTURE_BINDING,
446
+ view_formats: &[],
447
+ });
448
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
449
+ (texture, view)
450
+ }
451
+
452
+ /// Ticket 014 — per-mesh unsigned distance field. V1 is a fixed 32³
453
+ /// R32Float 3D texture per mesh (128 KB each). R32 instead of R16 so
454
+ /// `WriteOnly` storage binding is accepted on the core wgpu feature
455
+ /// set (R16Float storage-write would need an optional feature). Used
456
+ /// later by the SW probe trace for sphere-marching when HW ray query
457
+ /// isn't available. UDF (not SDF) — the march advances by the scalar
458
+ /// distance regardless of sign, which side-steps the inside/outside
459
+ /// problem on open-surface Sponza meshes (walls, banners, capitals).
460
+ pub(super) const MESH_SDF_RES: u32 = 32;
461
+
462
+ pub(super) fn create_mesh_sdf_texture(
463
+ device: &wgpu::Device,
464
+ label: &'static str,
465
+ ) -> (wgpu::Texture, wgpu::TextureView) {
466
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
467
+ label: Some(label),
468
+ size: wgpu::Extent3d {
469
+ width: MESH_SDF_RES,
470
+ height: MESH_SDF_RES,
471
+ depth_or_array_layers: MESH_SDF_RES,
472
+ },
473
+ mip_level_count: 1,
474
+ sample_count: 1,
475
+ dimension: wgpu::TextureDimension::D3,
476
+ format: wgpu::TextureFormat::R32Float,
477
+ // COPY_DST is needed by ticket 022's disk cache so a cached
478
+ // SDF can be uploaded via queue.write_texture instead of
479
+ // re-baked. COPY_SRC is needed by the same ticket's readback
480
+ // path on cache miss. Both have zero runtime cost when the
481
+ // cache isn't used (just usage-flag bits at allocation).
482
+ usage: wgpu::TextureUsages::STORAGE_BINDING
483
+ | wgpu::TextureUsages::TEXTURE_BINDING
484
+ | wgpu::TextureUsages::COPY_SRC
485
+ | wgpu::TextureUsages::COPY_DST,
486
+ view_formats: &[],
487
+ });
488
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
489
+ (texture, view)
490
+ }
491
+
492
+ /// Ticket 013 — Mesh Cards. Shared 2D atlases sampled by the HW probe
493
+ /// trace at hit. V2 stores 6 signed-axis slots per mesh (±X ±Y ±Z)
494
+ /// at 64×64 each; 4096² atlas ⇒ 64×64 = 4096 slots ⇒ 682 meshes at
495
+ /// full 6-axis capture (Sponza's ~405 fits comfortably).
496
+ ///
497
+ /// Two atlases are kept in lockstep:
498
+ /// - `mesh_card_albedo_atlas` — baked once per mesh at load.
499
+ /// - `mesh_card_radiance_atlas` — written every frame by the card-
500
+ /// lighting compute pass (albedo × sun × NdotL + sky × NdotUp).
501
+ /// The HW trace samples radiance directly at hit, amortising shading
502
+ /// cost across all rays that land in the same card texel.
503
+ pub(super) const CARD_ATLAS_SIZE: u32 = 4096;
504
+ pub(super) const CARD_SLOT_SIZE: u32 = 64;
505
+ pub(super) const CARD_SLOTS_PER_ROW: u32 = CARD_ATLAS_SIZE / CARD_SLOT_SIZE;
506
+ pub(super) const CARD_MAX_SLOTS: u32 = CARD_SLOTS_PER_ROW * CARD_SLOTS_PER_ROW;
507
+ /// V2: 6 directed axes per mesh (+X, -X, +Y, -Y, +Z, -Z).
508
+ pub(super) const CARD_AXES_PER_MESH: u32 = 6;
509
+
510
+ /// Create the mesh-card albedo atlas. `RENDER_ATTACHMENT` for capture,
511
+ /// `TEXTURE_BINDING` for both the card-lighting compute input and a
512
+ /// direct HW-trace fallback.
513
+ pub(super) fn create_mesh_card_atlas(
514
+ device: &wgpu::Device,
515
+ ) -> (wgpu::Texture, wgpu::TextureView) {
516
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
517
+ label: Some("mesh_card_albedo_atlas"),
518
+ size: wgpu::Extent3d {
519
+ width: CARD_ATLAS_SIZE,
520
+ height: CARD_ATLAS_SIZE,
521
+ depth_or_array_layers: 1,
522
+ },
523
+ mip_level_count: 1,
524
+ sample_count: 1,
525
+ dimension: wgpu::TextureDimension::D2,
526
+ format: wgpu::TextureFormat::Rgba8UnormSrgb,
527
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
528
+ | wgpu::TextureUsages::TEXTURE_BINDING,
529
+ view_formats: &[],
530
+ });
531
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
532
+ (texture, view)
533
+ }
534
+
535
+ /// Ticket 013 V3 — emissive atlas. Same shape as the albedo atlas but
536
+ /// carries material emissive per card texel. Read by the card-lighting
537
+ /// pass to add as-is to the radiance output.
538
+ pub(super) fn create_mesh_card_emissive_atlas(
539
+ device: &wgpu::Device,
540
+ ) -> (wgpu::Texture, wgpu::TextureView) {
541
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
542
+ label: Some("mesh_card_emissive_atlas"),
543
+ size: wgpu::Extent3d {
544
+ width: CARD_ATLAS_SIZE,
545
+ height: CARD_ATLAS_SIZE,
546
+ depth_or_array_layers: 1,
547
+ },
548
+ mip_level_count: 1,
549
+ sample_count: 1,
550
+ dimension: wgpu::TextureDimension::D2,
551
+ format: wgpu::TextureFormat::Rgba8UnormSrgb,
552
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
553
+ | wgpu::TextureUsages::TEXTURE_BINDING,
554
+ view_formats: &[],
555
+ });
556
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
557
+ (texture, view)
558
+ }
559
+
560
+ /// Create the mesh-card radiance atlas. Written every frame by the
561
+ /// card-lighting compute pass; sampled at hit by the HW probe trace.
562
+ /// Rgba16Float so we can carry multiplicatively-composed sun + sky
563
+ /// without banding. `STORAGE_BINDING` for the compute write,
564
+ /// `TEXTURE_BINDING` for the trace sample.
565
+ pub(super) fn create_mesh_card_radiance_atlas(
566
+ device: &wgpu::Device,
567
+ ) -> (wgpu::Texture, wgpu::TextureView) {
568
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
569
+ label: Some("mesh_card_radiance_atlas"),
570
+ size: wgpu::Extent3d {
571
+ width: CARD_ATLAS_SIZE,
572
+ height: CARD_ATLAS_SIZE,
573
+ depth_or_array_layers: 1,
574
+ },
575
+ mip_level_count: 1,
576
+ sample_count: 1,
577
+ dimension: wgpu::TextureDimension::D2,
578
+ format: HDR_FORMAT,
579
+ usage: wgpu::TextureUsages::STORAGE_BINDING
580
+ | wgpu::TextureUsages::TEXTURE_BINDING,
581
+ view_formats: &[],
582
+ });
583
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
584
+ (texture, view)
585
+ }
586
+
587
+ /// Ping-pong probe-radiance history. Each frame the temporal pass
588
+ /// reads `[prev_idx]` (last frame's blended history), blends it with
589
+ /// the fresh trace, and writes to `[write_idx]`. The resolve pass then
590
+ /// samples `[write_idx]`. Separate textures — the temporal pass cannot
591
+ /// bind the same view as both sampled input and storage-write output.
592
+ pub(super) fn create_probe_history_textures(
593
+ device: &wgpu::Device, width: u32, height: u32,
594
+ ) -> ([wgpu::Texture; 2], [wgpu::TextureView; 2]) {
595
+ let (gw, gh) = probe_grid_dims(width, height);
596
+ let (t0, v0) = create_probe_3d_tex(device, "probe_history_0", gw, gh);
597
+ let (t1, v1) = create_probe_3d_tex(device, "probe_history_1", gw, gh);
598
+ ([t0, t1], [v0, v1])
599
+ }
600
+
601
+ /// Create the DoF render target (full-res HDR, same format as TAA output).
602
+ /// DoF reads the TAA output + depth, writes the blurred result here.
603
+ /// Composite then reads this instead of the TAA output.
604
+ pub(super) fn create_dof_rt(device: &wgpu::Device, width: u32, height: u32) -> (wgpu::Texture, wgpu::TextureView) {
605
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
606
+ label: Some("dof_rt"),
607
+ size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
608
+ mip_level_count: 1,
609
+ sample_count: 1,
610
+ dimension: wgpu::TextureDimension::D2,
611
+ format: HDR_FORMAT,
612
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
613
+ | wgpu::TextureUsages::TEXTURE_BINDING,
614
+ view_formats: &[],
615
+ });
616
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
617
+ (texture, view)
618
+ }
619
+
620
+ /// Create the SSS render target (full-res HDR, same format as DoF/motion-blur).
621
+ pub(super) fn create_sss_rt(device: &wgpu::Device, width: u32, height: u32) -> (wgpu::Texture, wgpu::TextureView) {
622
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
623
+ label: Some("sss_rt"),
624
+ size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
625
+ mip_level_count: 1,
626
+ sample_count: 1,
627
+ dimension: wgpu::TextureDimension::D2,
628
+ format: HDR_FORMAT,
629
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
630
+ | wgpu::TextureUsages::TEXTURE_BINDING,
631
+ view_formats: &[],
632
+ });
633
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
634
+ (texture, view)
635
+ }
636
+
637
+ /// Halton low-discrepancy sequence (base `b`, index `i`, 1-based).
638
+ /// Returns a value in [0, 1). Used to generate sub-pixel jitter
639
+ /// offsets that are well-distributed across the pixel — the TAA
640
+ /// accumulation effectively integrates over those sample points
641
+ /// to produce a stably anti-aliased image.
642
+ pub(super) fn halton(mut i: u32, b: u32) -> f32 {
643
+ let mut f = 1.0_f32;
644
+ let mut r = 0.0_f32;
645
+ while i > 0 {
646
+ f /= b as f32;
647
+ r += f * (i % b) as f32;
648
+ i /= b;
649
+ }
650
+ r
651
+ }
652
+
653
+ /// Create the two TAA history textures (HDR format, surface size).
654
+ pub(super) fn create_taa_textures(device: &wgpu::Device, width: u32, height: u32) -> ([wgpu::Texture; 2], [wgpu::TextureView; 2]) {
655
+ let make = |label: &str| -> (wgpu::Texture, wgpu::TextureView) {
656
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
657
+ label: Some(label),
658
+ size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
659
+ mip_level_count: 1,
660
+ sample_count: 1,
661
+ dimension: wgpu::TextureDimension::D2,
662
+ format: HDR_FORMAT,
663
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
664
+ | wgpu::TextureUsages::TEXTURE_BINDING,
665
+ view_formats: &[],
666
+ });
667
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
668
+ (texture, view)
669
+ };
670
+ let (a, av) = make("taa_a");
671
+ let (b, bv) = make("taa_b");
672
+ ([a, b], [av, bv])
673
+ }
674
+
675
+ /// Create the SSAO render target. Written by the compute GTAO pass
676
+ /// (via `STORAGE_BINDING`) and sampled by the bilateral blur +
677
+ /// downstream passes.
678
+ pub(super) fn create_ssao_rt(device: &wgpu::Device, width: u32, height: u32) -> (wgpu::Texture, wgpu::TextureView) {
679
+ let w = (width / 2).max(1);
680
+ let h = (height / 2).max(1);
681
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
682
+ label: Some("ssao_rt"),
683
+ size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
684
+ mip_level_count: 1,
685
+ sample_count: 1,
686
+ dimension: wgpu::TextureDimension::D2,
687
+ format: SSAO_FORMAT,
688
+ usage: wgpu::TextureUsages::STORAGE_BINDING
689
+ | wgpu::TextureUsages::TEXTURE_BINDING,
690
+ view_formats: &[],
691
+ });
692
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
693
+ (texture, view)
694
+ }
695
+
696
+ /// Ping-pong half-res SSAO history textures (temporal accumulation).
697
+ /// Same size/format as `ssao_rt`. The compute pass reads the previous
698
+ /// frame's history via `TEXTURE_BINDING` and writes the blended
699
+ /// current-frame result back via `STORAGE_BINDING`. Downstream the
700
+ /// bilateral blur samples the current-frame ssao_rt as before —
701
+ /// history is only used as an input to the GTAO compute.
702
+ pub(super) fn create_ssao_history_textures(
703
+ device: &wgpu::Device,
704
+ width: u32,
705
+ height: u32,
706
+ ) -> ([wgpu::Texture; 2], [wgpu::TextureView; 2]) {
707
+ let w = (width / 2).max(1);
708
+ let h = (height / 2).max(1);
709
+ let make = |label: &str| -> (wgpu::Texture, wgpu::TextureView) {
710
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
711
+ label: Some(label),
712
+ size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
713
+ mip_level_count: 1,
714
+ sample_count: 1,
715
+ dimension: wgpu::TextureDimension::D2,
716
+ format: SSAO_FORMAT,
717
+ usage: wgpu::TextureUsages::STORAGE_BINDING
718
+ | wgpu::TextureUsages::TEXTURE_BINDING,
719
+ view_formats: &[],
720
+ });
721
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
722
+ (texture, view)
723
+ };
724
+ let (a, av) = make("ssao_history_a");
725
+ let (b, bv) = make("ssao_history_b");
726
+ ([a, b], [av, bv])
727
+ }
728
+
729
+ /// Create the SSAO bilateral-blur render target (same format/size as ssao_rt).
730
+ pub(super) fn create_ssao_blur_rt(device: &wgpu::Device, width: u32, height: u32) -> (wgpu::Texture, wgpu::TextureView) {
731
+ let w = (width / 2).max(1);
732
+ let h = (height / 2).max(1);
733
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
734
+ label: Some("ssao_blur_rt"),
735
+ size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
736
+ mip_level_count: 1,
737
+ sample_count: 1,
738
+ dimension: wgpu::TextureDimension::D2,
739
+ format: SSAO_FORMAT,
740
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
741
+ | wgpu::TextureUsages::TEXTURE_BINDING,
742
+ view_formats: &[],
743
+ });
744
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
745
+ (texture, view)
746
+ }
747
+
748
+ /// Build the linear-depth Hi-Z pyramid as `HIZ_MIP_COUNT` separate
749
+ /// single-mip textures. One multi-mip texture is cheaper on paper
750
+ /// but Metal's per-subresource state tracking trips when wgpu
751
+ /// writes one mip and samples another in the same encoder — the
752
+ /// bloom chain has the same issue and uses this same layout.
753
+ pub(super) fn create_linear_depth_hiz_chain(
754
+ device: &wgpu::Device,
755
+ width: u32,
756
+ height: u32,
757
+ ) -> (Vec<wgpu::Texture>, Vec<wgpu::TextureView>) {
758
+ let mut textures = Vec::with_capacity(HIZ_MIP_COUNT as usize);
759
+ let mut views = Vec::with_capacity(HIZ_MIP_COUNT as usize);
760
+ for i in 0..HIZ_MIP_COUNT {
761
+ let w = ((width / 2) >> i).max(1);
762
+ let h = ((height / 2) >> i).max(1);
763
+ let tex = device.create_texture(&wgpu::TextureDescriptor {
764
+ label: Some("linear_depth_hiz_mip"),
765
+ size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
766
+ mip_level_count: 1,
767
+ sample_count: 1,
768
+ dimension: wgpu::TextureDimension::D2,
769
+ format: HIZ_FORMAT,
770
+ usage: wgpu::TextureUsages::STORAGE_BINDING
771
+ | wgpu::TextureUsages::TEXTURE_BINDING,
772
+ view_formats: &[],
773
+ });
774
+ let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
775
+ textures.push(tex);
776
+ views.push(view);
777
+ }
778
+ (textures, views)
779
+ }
780
+
781
+ /// Create the bloom mip-chain texture + per-mip render views + a
782
+ /// full-chain view for sampling. Mip 0 starts at surface/2 size and
783
+ /// each subsequent mip halves down to ~surface/2^N. Caller is
784
+ /// responsible for deciding N (usually BLOOM_MIP_COUNT). At least
785
+ /// 1×1 is enforced per mip.
786
+ /// Build the bloom chain as N separate single-mip textures rather
787
+ /// than one multi-mip texture. Multi-mip textures with one mip
788
+ /// bound as render target while another mip is sampled in the
789
+ /// same encoder trips wgpu/Metal's per-subresource state tracking
790
+ /// — symptoms include large black bars in the sampled output. N
791
+ /// separate textures sidestep the problem entirely (each pass's
792
+ /// read/write hits a distinct texture). `bloom_full_view` is a
793
+ /// view onto mip 0's texture, kept for backward compatibility.
794
+ pub(super) fn create_bloom_chain(
795
+ device: &wgpu::Device,
796
+ width: u32,
797
+ height: u32,
798
+ mip_count: u32,
799
+ ) -> (Vec<wgpu::Texture>, Vec<wgpu::TextureView>, wgpu::TextureView) {
800
+ let mut textures = Vec::with_capacity(mip_count as usize);
801
+ let mut views = Vec::with_capacity(mip_count as usize);
802
+ for i in 0..mip_count {
803
+ let w = ((width / 2) >> i).max(1);
804
+ let h = ((height / 2) >> i).max(1);
805
+ let tex = device.create_texture(&wgpu::TextureDescriptor {
806
+ label: Some("bloom_mip_tex"),
807
+ size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
808
+ mip_level_count: 1,
809
+ sample_count: 1,
810
+ dimension: wgpu::TextureDimension::D2,
811
+ format: HDR_FORMAT,
812
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
813
+ | wgpu::TextureUsages::TEXTURE_BINDING,
814
+ view_formats: &[],
815
+ });
816
+ let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
817
+ textures.push(tex);
818
+ views.push(view);
819
+ }
820
+ let full_view = textures[0].create_view(&wgpu::TextureViewDescriptor::default());
821
+ (textures, views, full_view)
822
+ }