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
// src/directx/draw_iter.rs
//
// Shared scene-traversal helpers for the geometry passes. The main pass and
// every geometry pre-pass (SSR, SSAO, velocity) walk the same visible set
// with the same frustum/distance cull and the same per-object LOD pick, then
// issue a draw. Only the per-draw GPU state (pipeline, root constants,
// descriptor bindings, draw args) differs between passes.
//
// These helpers own the common skeleton (the gate + LOD pick + cluster cull +
// bucket iteration) and hand each pass a closure for its per-draw specifics,
// so the cull/LOD logic lives in one place and every pass selects the same
// LOD slice and the same surviving instances (the pre-pass G-buffers stay
// pixel-aligned with what the main pass rasterized).
use crate::gfx::render_types::{DrawObject, InstancedCluster, SkinnedDrawObject};
use super::com;
use super::context::{DxContext, InstanceBucketLayout};
impl DxContext {
// Walk the visible build-time / runtime static draw objects, gating on
// `visible && resident` and on the see-through reroute, picking the
// camera-distance LOD slice, then invoking `emit` with the object, its draw
// index, and the chosen LOD's `(index_offset, index_count)`. The caller binds
// the pipeline + shared root state once before calling; `emit` sets only the
// per-draw state (material/model constants, object SRV table) and issues the
// draw.
pub(in crate::directx) fn draw_static_objects<F>(
&self,
visible: &[u32],
cam_pos: [f32; 3],
mut emit: F,
) where
F: FnMut(&DrawObject, usize, usize, usize),
{
// See-through glass meshes (Layer 2) draw in the transparent pass when the
// RT path is live, so skip them here and in every pre-pass that shares
// this walk: leaving one in would both paint it opaque and stamp its depth
// over the scene the refraction tap reads. A no-op on non-RT worlds and
// worlds with no see-through material. The GPU-driven path takes the same
// decision through `build_draw_args_buffer`'s ENABLED clear.
let skip_seethrough = self.mesh_glass_active();
for &draw_idx in visible {
let i = draw_idx as usize;
let Some(obj) = self.draw.objects.get(i) else {
continue;
};
if !obj.visible || !obj.resident {
continue;
}
if skip_seethrough && obj.material.see_through != 0 {
continue;
}
let d = crate::gfx::lod::camera_distance(obj, cam_pos);
let (index_offset, index_count) = obj.active_lod(d);
emit(obj, i, index_offset, index_count);
}
}
// Walk the resident streamed-chunk draw objects -- the build-time-geometry
// tail past `draw.n_objects` that are NOT runtime clones -- invoking `emit` with the
// chunk's reserve index `k` (0-based, into `[chunk_record_base() + k]`) and the
// `DrawObject`. Chunk geometry already lives in the shared VB/IB, so chunks
// fold into the static+instance prefix indirect draw as plain records (with
// their own `base_vertex` + flat-pool material). Runtime clones (in
// `clone.slot_by_draw_idx`) are skipped -- they keep the legacy per-object
// path. Non-resident slots are skipped too: chunks and clones now share the
// draw-slot free list, so a retired clone leaves a non-resident gap in this
// tail (no longer in `slot_by_draw_idx`); counting those gaps toward `k` could
// push a live chunk past `draw.n_chunk` and silently drop it. Only resident chunks
// consume a reserve index, bounded by the streaming window (<= `draw.n_chunk`).
// Returns the number of chunk records emitted, so the caller can disable the
// unused reserve tail.
pub(in crate::directx) fn for_each_chunk_record<F>(&self, mut emit: F) -> usize
where
F: FnMut(usize, &DrawObject),
{
if self.draw.n_chunk == 0 {
return 0;
}
let mut k = 0;
for (i, obj) in self
.draw
.objects
.iter()
.enumerate()
.skip(self.draw.n_objects)
{
if self.clone.slot_by_draw_idx.contains_key(&i) {
continue; // runtime clone -> legacy per-object path
}
if !obj.resident {
continue; // retired chunk / clone gap -- not a live chunk
}
if k >= self.draw.n_chunk {
break;
}
emit(k, obj);
k += 1;
}
k
}
// Walk the skinned draw objects, gating on `visible` (skinned meshes are
// not stream-evicted, so there is no `resident` field) and picking the
// LOD slice by distance to the model-matrix translation, then invoke
// `emit` with the object, its index, and the chosen LOD slice.
pub(in crate::directx) fn draw_skinned_objects<F>(&self, cam_pos: [f32; 3], mut emit: F)
where
F: FnMut(&SkinnedDrawObject, usize, usize, usize),
{
for (i, obj) in self.skinned.draw_objects.iter().enumerate() {
if !obj.visible {
continue;
}
let d = crate::gfx::lod::skinned_camera_distance(obj, cam_pos);
let (index_offset, index_count) = obj.active_lod(d);
emit(obj, i, index_offset, index_count);
}
}
// Walk the instanced clusters: frustum + distance cull each cluster, read
// the shared per-frame LOD bucket layout (filled by `build_instance_upload`
// at the top of the frame), and for every surviving bucket invoke
// `per_bucket` with the bucket and the cluster's instance-matrix upload
// GPU virtual address. `per_cluster` runs once per surviving cluster
// (before its buckets) so the caller can bind cluster-wide state
// (material constants + the cluster's albedo/normal SRV table).
//
// All instanced geometry passes (main + SSR / SSAO / velocity pre-passes)
// share this skeleton; they differ only in which root slot `per_bucket`
// bumps for the per-bucket instance SRV and what `per_cluster` binds.
pub(in crate::directx) fn draw_instanced_clusters<C, B>(
&self,
frame_idx: usize,
frustum: &crate::gfx::frustum::Frustum,
cam_pos: [f32; 3],
mut per_cluster: C,
mut per_bucket: B,
) where
C: FnMut(usize, &InstancedCluster),
B: FnMut(&InstanceBucketLayout, u64),
{
for (cluster_idx, cluster) in self.instanced.clusters.iter().enumerate() {
if cluster.instances.is_empty() {
continue;
}
// Cluster-wide frustum + distance cull.
if cluster.cullable() {
if !frustum.intersects_aabb(cluster.cluster_bb_min, cluster.cluster_bb_max) {
continue;
}
if cluster.cull_distance > 0.0 {
let d2 = crate::gfx::frustum::aabb_distance_sq(
cam_pos,
cluster.cluster_bb_min,
cluster.cluster_bb_max,
);
if d2 > cluster.cull_distance * cluster.cull_distance {
continue;
}
}
}
// Per-cluster LOD bucket layout that `build_instance_upload` filled
// into this frame's upload buffer. Held across the bucket loop so
// every bucket reads a consistent partition; no draw closure
// touches `instance_bucket_layouts`, so the read lock is safe.
let buckets_borrow = self.instanced.bucket_layouts.read().unwrap();
let Some(buckets) = buckets_borrow.get(cluster_idx) else {
continue;
};
if buckets.is_empty() {
continue;
}
let inst_buf = &self.instanced.upload_buffers[frame_idx][cluster_idx];
let inst_gva_base = com::gpu_va(inst_buf);
per_cluster(cluster_idx, cluster);
for bucket in buckets.iter() {
per_bucket(bucket, inst_gva_base);
}
}
}
}