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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
//! `VoxelWorld` chunk streaming for DxContext: the init-time headroom growth
//! that seeds the chunk sub-allocators, then per-chunk add / remove / move
//! within that headroom.
use concinnity_core::gfx::mesh_payload::Vertex;
use concinnity_core::gfx::render_types::*;
use concinnity_core::render::backend::ChunkMesh;
use concinnity_core::render::draw_slot;
use concinnity_core::render::error;
use windows::Win32::Graphics::Direct3D12::*;
use super::super::com;
use super::super::context::*;
use super::super::texture::*;
// Byte-range sub-allocators for the headroom region appended to the shared
// vertex/index buffers by `setup_chunk_streaming` for streamed `VoxelWorld`
// chunks, disjoint from the build-time geometry and the mesh-streaming
// allocators. `draw.objects` slots vacated by removed chunks are recycled
// through the shared `DrawSlotAllocator` (`draw_slots`), so the draw list does
// not grow without bound as the camera roams an infinite world.
#[derive(Default)]
pub(in crate::directx) struct ChunkStreamState {
pub vtx_alloc: crate::suballoc::range_alloc::RangeAllocator,
pub idx_alloc: crate::suballoc::range_alloc::RangeAllocator,
}
impl DxContext {
// Grow the shared vertex/index buffers by a headroom region for streamed
// `VoxelWorld` chunks and seed the chunk sub-allocators with it. The chunk
// material's texture slots ride each chunk's cull record.
//
// Called once at init by `GraphicsSystem` when a `VoxelWorld` is present.
// The build-time geometry is copied verbatim into the start of the new
// (larger) DEFAULT-heap buffers; chunks are placed in the appended
// headroom by `add_chunk_mesh`. This runs before the first frame, so no
// in-flight command list references the replaced buffers.
pub(crate) fn setup_chunk_streaming(
&mut self,
chunk_vtx_bytes: usize,
chunk_idx_bytes: usize,
) -> error::RenderResult<()> {
self.wait_idle();
let old_v_len = self.scene.geometry.vertex_buffer_view.SizeInBytes as u64;
let old_i_len = self.scene.geometry.index_buffer_view.SizeInBytes as u64;
let new_v_len = old_v_len + chunk_vtx_bytes as u64;
let new_i_len = old_i_len + chunk_idx_bytes as u64;
// Buffers are created in COMMON; the CopyBufferRegion below implicitly
// promotes the destination COMMON -> COPY_DEST.
let new_vbuf = self.hw.alloc.alloc_buffer(
new_v_len,
D3D12_HEAP_TYPE_DEFAULT,
D3D12_RESOURCE_STATE_COMMON,
)?;
let new_ibuf = self.hw.alloc.alloc_buffer(
new_i_len,
D3D12_HEAP_TYPE_DEFAULT,
D3D12_RESOURCE_STATE_COMMON,
)?;
// Copy the build-time geometry into the start of the grown buffers so
// every existing draw's offsets stay valid.
// SAFETY: the command list is in the recording state, and every resource, descriptor and
// slice these commands name is live for the call.
one_shot_submit(&self.hw.device, &self.hw.command_queue, |cmd| unsafe {
let v_src = transition_barrier(
&self.scene.geometry.vertex_buffer,
D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER,
D3D12_RESOURCE_STATE_COPY_SOURCE,
);
let i_src = transition_barrier(
&self.scene.geometry.index_buffer,
D3D12_RESOURCE_STATE_INDEX_BUFFER,
D3D12_RESOURCE_STATE_COPY_SOURCE,
);
cmd.ResourceBarrier(&[v_src, i_src]);
cmd.CopyBufferRegion(
&*new_vbuf,
0,
&*self.scene.geometry.vertex_buffer,
0,
old_v_len,
);
cmd.CopyBufferRegion(
&*new_ibuf,
0,
&*self.scene.geometry.index_buffer,
0,
old_i_len,
);
let v_dst = transition_barrier(
&new_vbuf,
D3D12_RESOURCE_STATE_COPY_DEST,
D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER,
);
let i_dst = transition_barrier(
&new_ibuf,
D3D12_RESOURCE_STATE_COPY_DEST,
D3D12_RESOURCE_STATE_INDEX_BUFFER,
);
cmd.ResourceBarrier(&[v_dst, i_dst]);
})?;
self.scene.geometry.vertex_buffer_view = D3D12_VERTEX_BUFFER_VIEW {
BufferLocation: com::gpu_va(&new_vbuf),
SizeInBytes: new_v_len as u32,
StrideInBytes: std::mem::size_of::<Vertex>() as u32,
};
self.scene.geometry.index_buffer_view = D3D12_INDEX_BUFFER_VIEW {
BufferLocation: com::gpu_va(&new_ibuf),
SizeInBytes: new_i_len as u32,
// Static IB is u32 (matches the `Format` chosen in init/mod.rs).
Format: windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_R32_UINT,
};
self.scene.geometry.vertex_buffer = new_vbuf;
self.scene.geometry.index_buffer = new_ibuf;
// Seed the chunk allocators with the appended headroom. retire_frame 0:
// nothing has been drawn, so the space is reusable immediately.
self.chunk_stream
.vtx_alloc
.free(old_v_len, chunk_vtx_bytes as u64, 0);
self.chunk_stream
.idx_alloc
.free(old_i_len, chunk_idx_bytes as u64, 0);
Ok(())
}
// Place one streamed chunk's geometry in the chunk headroom region and
// write its `DrawObject` at the engine-allocated destination slot.
//
// The chunk is non-cullable and joins the `draw.always` set: the streaming
// window already bounds the resident chunk count. Indices stay
// mesh-relative (0-based) and the draw passes the vertex region's base as
// `base_vertex`, so a chunk placed past the 65 535-vertex `u16` index
// range still renders. `frame` reclaims retired deferred frees first.
// `wait_idle` runs before the geometry copy so the whole-resource
// COPY_DEST transition races no in-flight command list.
pub(crate) fn add_chunk_mesh(
&mut self,
mesh: ChunkMesh<'_>,
dst: draw_slot::SlotAlloc,
) -> error::RenderResult<()> {
let ChunkMesh {
verts: vertices,
idxs: indices,
model,
texture_slot,
normal_map_slot,
material,
frame,
} = mesh;
if vertices.is_empty() || indices.is_empty() {
return Err(error::RenderError::Other(
"add_chunk_mesh: empty chunk geometry".into(),
));
}
self.chunk_stream.vtx_alloc.reclaim(frame);
self.chunk_stream.idx_alloc.reclaim(frame);
let v_len = std::mem::size_of_val(vertices);
// Static IB is u32; chunk indices come in as u16 and get widened on
// write. Size the allocation against the u32 stride.
let i_len = indices.len() * std::mem::size_of::<u32>();
let (v_off, i_off) = crate::suballoc::geometry::place_mesh(
&mut self.chunk_stream.vtx_alloc,
&mut self.chunk_stream.idx_alloc,
v_len,
i_len,
|| "add_chunk_mesh".to_string(),
)?;
self.wait_idle();
// Vertices and indices both copy verbatim: the indices stay 0-based and
// the draw fixes them up with `base_vertex`.
let vert_bytes = bytemuck::cast_slice(vertices);
self.write_geometry_region(
&self.scene.geometry.vertex_buffer,
D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER,
v_off as u64,
vert_bytes,
)?;
// Chunk indices stay mesh-relative; the draw fixes them up with
// `base_vertex`. Widen u16 → u32 to match the static IB's stride.
let widened: Vec<u32> = indices.iter().map(|&i| u32::from(i)).collect();
let idx_bytes = bytemuck::cast_slice(&widened);
self.write_geometry_region(
&self.scene.geometry.index_buffer,
D3D12_RESOURCE_STATE_INDEX_BUFFER,
i_off as u64,
idx_bytes,
)?;
// v_off is a multiple of size_of::<Vertex>() (the headroom start and
// every alloc are), so the base is an exact vertex index.
let base_vertex = (v_off / std::mem::size_of::<Vertex>()) as i32;
let obj = DrawObject {
vertex_offset: v_off,
vertex_count: vertices.len(),
index_offset: i_off / std::mem::size_of::<u32>(),
index_count: indices.len(),
base_vertex,
geometry_generation: 0,
model,
texture_slot,
normal_map_slot,
material,
visible: true,
resident: true,
// Non-cullable: degenerate AABB disables frustum/distance culling.
bb_min: [f32::NAN; 3],
bb_max: [f32::NAN; 3],
cull_distance: 0.0,
// Streamed chunks always render at the build-time mesh; no LOD.
lod_alternates: Vec::new(),
// Streamed chunks render through the world default program.
shader_bucket: 0,
};
// Write at the engine-allocated destination slot.
let draw_idx = match dst {
draw_slot::SlotAlloc::Reuse(slot) => {
self.draw.objects[slot] = obj;
slot
}
draw_slot::SlotAlloc::Append(slot) => {
debug_assert_eq!(
slot,
self.draw.objects.len(),
"appended draw slot must match the draw-object count"
);
self.draw.objects.push(obj);
self.model_history.borrow_mut().reoccupy_draw(slot);
slot
}
};
// The slot's model-history entry belongs to whatever held it before, so
// a chunk that streams in reprojects through its own transform for one
// frame rather than ghosting from the previous occupant's.
self.model_history.borrow_mut().reoccupy_draw(draw_idx);
// A new resident chunk changes the RT-relevant draw set; the next RT
// update folds it into the BVH (building just this chunk's BLAS).
self.rt.topology_dirty = true;
Ok(())
}
// Free a streamed chunk's geometry region and retire its `DrawObject`
// slot for reuse.
//
// `retire_frame` is `current_frame + frames_in_flight` so an in-flight
// submission never has the freed region overwritten by a later
// `add_chunk_mesh`. The region is not zeroed: a non-resident draw is
// skipped everywhere and an `alloc` hands back exactly `size` bytes that
// `add_chunk_mesh` fully overwrites.
pub(crate) fn remove_chunk_mesh(
&mut self,
draw_idx: usize,
retire_frame: u64,
) -> error::RenderResult<()> {
let region = draw_slot::retire_chunk_slot(&mut self.draw.objects, draw_idx)
.map_err(error::RenderError::Other)?;
self.chunk_stream
.vtx_alloc
.free(region.vertex_offset, region.vertex_bytes, retire_frame);
self.chunk_stream
.idx_alloc
.free(region.index_offset, region.index_bytes, retire_frame);
// The removed chunk leaves the RT-relevant draw set; the next RT update
// drops its BLAS (deferred-freed once in-flight traces retire).
self.rt.topology_dirty = true;
Ok(())
}
pub(crate) fn set_chunk_model(
&mut self,
draw_idx: usize,
model: [[f32; 4]; 4],
) -> error::RenderResult<()> {
draw_slot::set_chunk_model(&mut self.draw.objects, draw_idx, model)
.map_err(error::RenderError::Other)
}
}