1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
//! Ray-traced reflections: the scene acceleration structure, the reflection pass
//! that traces it, and the reflection composite the SSR resolve shares.
use ash::vk;
use concinnity_core::render::backend_init::{PostSettings, SceneData};
use concinnity_core::render::error::RenderResult;
use concinnity_core::render::retire_pool::RetirePool;
use super::InitGpu;
use super::effects::shared_post_device;
use crate::vulkan::context::{
VkCull, VkDescriptors, VkGeometry, VkRayTracing, VkSceneAssets, VkTargets,
};
use crate::vulkan::post::PostSupport;
use crate::vulkan::post::gbuffer::GbufferResources;
use crate::vulkan::post::reflection_composite::{
VkReflectionCompositePass, build_reflection_composite,
};
use crate::vulkan::post::rt_reflections::RtReflectionsResources;
use concinnity_core::render::post::reflection_path::ReflectionPath;
pub(super) struct RtInputs<'a> {
pub(super) world: &'a SceneData<'a>,
pub(super) geometry: &'a VkGeometry,
pub(super) scene: &'a VkSceneAssets,
pub(super) targets: &'a VkTargets,
pub(super) gbuffer: Option<&'a GbufferResources>,
pub(super) descriptors: &'a VkDescriptors,
pub(super) cull: &'a VkCull,
pub(super) post: &'a PostSettings,
pub(super) post_support: &'a PostSupport,
pub(super) rt_wanted: bool,
}
pub(super) struct RtResources {
pub(super) state: VkRayTracing,
pub(super) reflections: Option<RtReflectionsResources>,
pub(super) composite: Option<VkReflectionCompositePass>,
pub(super) seethrough_mesh_indices: Vec<usize>,
pub(super) has_seethrough_meshes: bool,
}
pub(super) fn build_rt_reflections(
gpu: &InitGpu<'_>,
inputs: RtInputs<'_>,
) -> RenderResult<RtResources> {
let InitGpu {
hw,
command_pool,
frames,
hot_reload,
} = *gpu;
let (device, alloc) = (&hw.device, &hw.alloc);
let RtInputs {
world,
geometry,
scene,
targets,
gbuffer,
descriptors,
cull,
post,
post_support,
rt_wanted,
} = inputs;
let (hdr_resolve_images, render_extent) = (&targets.hdr_resolve_images, targets.render_extent);
// Hardware ray-traced reflections: the inline-`rayQueryEXT` reflection pass
// + the scene acceleration structure it traces. Built only when the world
// requested it AND the device exposed the ray-query extensions
// (`rt_wanted`). Reuses the SSR pre-pass G-buffer (forced on earlier) for
// the per-pixel surface point + normal, and the bindless pool (when live)
// for textured hit shading. A shader compile failure leaves the pass `None`
// and the graph keeps `SsrResolve`. The pass outlives the acceleration
// structure: a scene with no resident geometry (or an AS build error) starts
// with none, and the first topology change that brings geometry seeds it.
// `ReflectionPath` settles which resolve takes the shared graph slot each
// frame.
//
// Layer 2 see-through glass is opt-in per `Material` (the `see_through`
// arg, which implies `transparent`): see-through only looks right when the
// space behind the glass is modeled. A material that is `transparent` but
// NOT `see_through` renders as Layer 1 (opaque, low roughness, scene
// reflections) = tinted reflective glass that hides the interior. This list
// drives the transparent-pass producer, the opaque-pass skip and the
// RT-BLAS exclude together.
let seethrough_mesh_indices: Vec<usize> = world
.draw_objects
.iter()
.enumerate()
.filter(|(_, o)| o.material.transparent != 0 && o.material.see_through != 0)
.map(|(i, _)| i)
.collect();
// Whether those meshes will be rerouted, decided here because the initial
// BLAS is built before the transparent pass that owns the mesh producer.
// It is the same predicate that producer is gated on. The one divergence
// is a mesh-shader compile failure, which leaves the producer absent (and
// logs): the meshes then render opaque but stay out of this BLAS until a
// topology refresh re-reads `seethrough_meshes_enabled` and puts them back.
let has_seethrough_meshes = !seethrough_mesh_indices.is_empty() && hw.rt_capable;
let rt_opt = if rt_wanted {
let hdr_views: Vec<vk::ImageView> = hdr_resolve_images.iter().map(|i| i.view).collect();
// RT reads the unified G-buffer pre-pass's per-frame normal+depth +
// roughness (built earlier whenever any consumer is on); `gbuffer` is
// `Some` here because RT forces the pre-pass on.
let gb = gbuffer
.as_ref()
.expect("RT forces the unified G-buffer pre-pass to exist");
let nd_views = gb.normal_depth_views();
let rough_views = gb.roughness_views();
match crate::vulkan::post::rt_reflections::RtReflectionsResources::new(
crate::vulkan::post::rt_reflections::RtBuild {
alloc,
device,
width: render_extent.width,
height: render_extent.height,
frames,
},
post.rt_reflections
.expect("rt_wanted implies rt_settings is Some"),
crate::vulkan::post::rt_reflections::RtStaticInputs {
vertex_buffer: geometry.vertex_buffer.buffer(),
index_buffer: geometry.index_buffer.buffer(),
hdr_resolve_views: &hdr_views,
gbuffer_views: &nd_views,
roughness_views: &rough_views,
},
crate::vulkan::post::rt_reflections::RtLayoutConfig {
bindless_set_layout: cull.bindless_set_layout.as_ref().map(|l| l.handle()),
global_set_layout: descriptors.global_set_layout.handle(),
pool_size: cull.bindless_pool_size,
hot_reload,
},
) {
Ok(rt) => Some(rt),
Err(e) => {
tracing::warn!("RT reflections pass build failed (falling back to SSR): {e}");
None
}
}
} else {
None
};
let rt_accel_opt = if rt_opt.is_some() {
match crate::vulkan::raytrace::build_rt_accel(
crate::vulkan::raytrace::RtDeviceCtx {
alloc,
instance: &hw.instance,
device,
pd: hw.physical_device,
},
command_pool,
hw.graphics_queue,
crate::vulkan::raytrace::RtSceneGeometry {
shared: crate::vulkan::raytrace::SharedGeometry::of(geometry),
draw_objects: &world.draw_objects,
clusters: &world.instanced_clusters,
albedo_count: scene.textures.len(),
exclude_seethrough: has_seethrough_meshes,
// Skinned meshes upload after init; the first dynamic frame
// adds them, or seeds a BVH for them when there is none.
skinned_present: false,
},
frames,
) {
Ok(Some(accel)) => Some(accel),
Ok(None) => {
tracing::info!(
"RT reflections requested but no resident triangle geometry to trace yet"
);
None
}
Err(e) => {
tracing::warn!("RT acceleration-structure build failed: {e}");
None
}
}
} else {
None
};
// Reflection composite: built whenever a resolve feeds it. Both resolves
// write radiance+weight into their output target; this blurs by roughness
// and composites over the scene into its own output, which then replaces
// the raw resolve output as the scene image every downstream pass samples.
let composite_opt =
if ReflectionPath::new(post.ssr.is_some(), rt_opt.is_some(), rt_accel_opt.is_some())
.composite
{
Some(build_reflection_composite(
&shared_post_device(gpu, post_support, scene, descriptors),
post.reflection_blur_scale,
render_extent,
)?)
} else {
None
};
Ok(RtResources {
state: VkRayTracing {
accel: rt_accel_opt,
retired: RetirePool::new(),
retire_tick: 0,
dynamic_mode: post.rt_dynamic,
skinned_geometry: post.rt_skinned_geometry,
update_streak: Default::default(),
skin: rt_opt
.is_some()
.then(|| crate::vulkan::raytrace::build_rt_skin(alloc, device, hot_reload))
.flatten(),
},
reflections: rt_opt,
composite: composite_opt,
seethrough_mesh_indices,
has_seethrough_meshes,
})
}