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
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
// Runtime residency of the material-referenced world shader pipelines.
//
// Init builds a pipeline for every world Shader whose payload it decoded and
// leaves a `None` in `world_pipelines` for each one it deferred (a Shader owned
// by a scene other than the start scene). The streaming pump calls in here as
// those scenes pin and unpin, handing over a pipeline its worker already built.
//
// The bucket regions of the GPU-culled command buffer are issued here too: the
// cull kernel wrote every record's command into exactly one region, so each
// region is one `cmd_draw_indexed_indirect` under that bucket's pipeline.
// Mirrors `metal/world_shaders.rs` and `directx/world_shaders.rs`.
use ash::vk;
use concinnity_core::render::backend::{PipelineBuilder, PipelineSwap, PreparedPipelines};
use concinnity_core::render::backend_init::WorldShader;
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::world_pipelines::{check_rebuild, replace_bucket};
use super::context::VkContext;
use super::pipeline::{
BindlessSpv, BucketPipelineTargets, BucketPipelines, PrepassTargets, build_bucket_pipeline,
build_bucket_prepass, build_world_prepass, build_world_shader_pipeline,
};
use crate::vulkan::pipeline_builder::{VkPipelineBuilder, world_shader_for};
use std::sync::Arc;
impl VkContext {
// Install one shader bucket's main-pass and pre-pass pipelines: `prepared`
// when they were built for this context's targets, else ones built here.
// Replaces whatever the bucket currently holds, so a re-pin after an
// eviction installs cleanly.
pub(in crate::vulkan) fn install_world_shader(
&mut self,
bucket: u32,
programs: &concinnity_core::components::ShaderPrograms,
prepared: Option<PreparedPipelines>,
) -> RenderResult<()> {
let pipelines = self.bucket_pipelines(bucket, programs, prepared, false)?;
// The displaced pipelines drop into the device's retire queue, which
// holds them until every frame in flight that recorded against them
// retires.
self.cull.world_pipelines.install(bucket, pipelines)?;
Ok(())
}
// One shader bucket's pipelines to replace a bucket that has a pre-pass
// when `live_prepass`: `prepared` when it keeps what the live bucket draws
// with, else ones built here. Refused when the build would drop a live
// pre-pass, so the caller keeps the live pair.
fn bucket_pipelines(
&self,
bucket: u32,
programs: &concinnity_core::components::ShaderPrograms,
prepared: Option<PreparedPipelines>,
live_prepass: bool,
) -> RenderResult<BucketPipelines> {
self.cull.world_pipelines.slot(bucket)?;
let targets = self.bucket_pipeline_targets().ok_or_else(|| {
RenderError::Other("shader buckets need the bindless main pass".to_string())
})?;
replace_bucket(
bucket as usize,
live_prepass,
world_shader_for(prepared, &self.pipeline_gate, targets),
|p: &BucketPipelines| p.prepass.is_some(),
|| build_world_shader_pipeline(&self.hw.device, targets, bucket as usize, programs),
)
}
// Rebuild one world Shader's pipelines from hot-reloaded programs, or swap
// in `prepared` when they were built for this context's targets. Bucket 0
// is the main pass's and the pre-pass's own pair; another bucket is rebuilt
// only while installed. Both halves are built before the old pipelines
// retire, so a failed build of either leaves the live ones bound.
pub(in crate::vulkan) fn update_world_shader(
&mut self,
bucket: u32,
programs: &concinnity_core::components::ShaderPrograms,
prepared: Option<PreparedPipelines>,
) -> RenderResult<PipelineSwap> {
if bucket == 0 {
let prepared = self
.bucket_pipeline_targets()
.and_then(|targets| world_shader_for(prepared, &self.pipeline_gate, targets));
self.update_default_world_shader(programs, prepared)?;
return Ok(PipelineSwap::Swapped);
}
self.cull.world_pipelines.slot(bucket)?;
if !self.cull.world_pipelines.resident(bucket as usize) {
return Ok(PipelineSwap::NotResident);
}
let live_prepass = (self.cull.world_pipelines.get(bucket as usize))
.is_some_and(|live| live.prepass.is_some());
let pipelines = self.bucket_pipelines(bucket, programs, prepared, live_prepass)?;
self.cull.world_pipelines.install(bucket, pipelines)?;
Ok(PipelineSwap::Swapped)
}
// What every bucket's pipelines are built against here, or `None` when the
// GPU-driven main pass is not live. The pre-pass's half is there only once
// the world has a G-buffer.
pub(in crate::vulkan) fn bucket_pipeline_targets(&self) -> Option<BucketPipelineTargets> {
let layout = self.cull.bindless_pipeline_layout.as_ref()?;
let prepass = match (self.cull.prepass_layout.as_ref(), self.gbuffer.as_ref()) {
(Some(prepass), Some(gb)) => Some(PrepassTargets {
render_pass: gb.prepass_render_pass.handle(),
layout: prepass.pipeline_layout.handle(),
}),
_ => None,
};
Some(BucketPipelineTargets {
render_pass: self.targets.main_render_pass.handle(),
layout: layout.handle(),
prepass,
msaa_samples: self.targets.msaa_samples,
swapchain_format: self.swapchain.format,
hot_reload: self.hot_reload.enabled,
template_generation: self.hot_reload.generation,
})
}
// Build the pre-pass pipeline each bucket is missing while a G-buffer
// exists. A pipeline that fails to build leaves its bucket without
// G-buffer draws, never without shading. A G-buffer stays for the
// context's life once built, and a failed rebuild empties only its
// targets, never the render pass these pipelines build against.
pub(in crate::vulkan) fn sync_prepass_pipelines(&mut self) {
let Some((targets, prepass)) = self
.bucket_pipeline_targets()
.and_then(|targets| Some((targets, targets.prepass?)))
else {
return;
};
let device = self.hw.device.clone();
let engine = &self.cull.bindless_main_spv;
let build =
|bucket: usize, programs: Option<&concinnity_core::components::ShaderPrograms>| {
match programs {
Some(programs) => {
build_world_prepass(&device, prepass, bucket, programs, targets.hot_reload)
}
None => build_bucket_prepass(
&device,
prepass,
bucket,
(&engine.prepass_vert, &engine.prepass_frag),
),
}
};
if self.cull.prepass_pipeline.is_none() {
let fresh = build(0, self.world_shader.as_ref());
self.cull.prepass_pipeline = fresh;
}
let buckets: Vec<usize> = self.cull.world_pipelines.resident_buckets().collect();
for bucket in buckets {
let Some(pipelines) = self.cull.world_pipelines.get(bucket) else {
continue;
};
if pipelines.prepass.is_some() {
continue;
}
let fresh = build(bucket, pipelines.programs.as_ref());
if let Some(pipelines) = self.cull.world_pipelines.get_mut(bucket) {
pipelines.prepass = fresh;
}
}
}
// Every resident material bucket's pipelines rebuilt from the current
// templates, `engine` standing in for a bucket with no Shader of its own.
// Fails as a whole when any bucket's main pipeline fails or its rebuild
// would drop a live pre-pass, so the caller keeps every live pair.
pub(in crate::vulkan) fn rebuild_world_buckets(
&self,
engine: &BindlessSpv,
) -> RenderResult<Vec<(usize, BucketPipelines)>> {
let Some(targets) = self.bucket_pipeline_targets() else {
return Ok(Vec::new());
};
let mut rebuilt = Vec::new();
for bucket in self.cull.world_pipelines.resident_buckets() {
let Some(live) = self.cull.world_pipelines.get(bucket) else {
continue;
};
let shader = WorldShader {
programs: live.programs.as_ref(),
deferred: false,
};
let pipelines =
build_bucket_pipeline(&self.hw.device, targets, bucket, shader, engine)?;
check_rebuild(bucket, live.prepass.is_some(), pipelines.prepass.is_some())?;
rebuilt.push((bucket, pipelines));
}
Ok(rebuilt)
}
// A builder for this context's world Shader and volume pipelines, for a
// streaming or hot-reload worker.
pub(in crate::vulkan) fn pipeline_builder(&self) -> Arc<dyn PipelineBuilder> {
Arc::new(VkPipelineBuilder {
device: self.hw.device.clone(),
gate: self.pipeline_gate.clone(),
world: self.bucket_pipeline_targets(),
volumes: self.volume_pipeline_targets(),
})
}
// Release one bucket's pipelines. They drop into the device's retire queue,
// which destroys them only once every frame in flight that recorded against
// them has retired.
pub(in crate::vulkan) fn evict_world_shader(&mut self, bucket: u32) {
self.cull.world_pipelines.evict(bucket);
}
// Issue the bucket 1.. regions of `indirect`, each under its own material
// shader's pipeline. Bucket 0's region is issued by the caller (it runs under
// the pipeline the pass already bound), so this covers only the
// material-referenced shaders. `draw_count` is the record prefix each region
// draws, matching bucket 0's. Returns the number of indirect draws issued, and
// leaves the last bucket's pipeline bound.
pub(in crate::vulkan) fn draw_bucket_regions(
&self,
cmd: vk::CommandBuffer,
indirect: vk::Buffer,
draw_count: u32,
) -> u32 {
self.for_each_resident_bucket(|bucket| {
let Some(pipelines) = self.cull.world_pipelines.get(bucket) else {
return;
};
// Every bucket shares the bindless layout, so the Wireframe twin
// stands in for each one while that view mode is on.
let pipeline = self.wireframe_or(&pipelines.main, self.wireframe.bindless.as_ref());
// SAFETY: `cmd` is a command buffer in the recording state, and every handle and slice
// these commands name is live for the call.
unsafe {
self.hw.device.cmd_bind_pipeline(
cmd,
vk::PipelineBindPoint::GRAPHICS,
pipeline.handle(),
);
}
self.draw_bucket_region(cmd, indirect, draw_count, bucket);
})
}
// Issue the bucket 1.. regions of `indirect`, each under its shader's
// G-buffer pre-pass pipeline, so a world Shader's vertex hook places its
// depth and motion as it places its shading. A non-resident bucket is
// skipped here as in the main pass, so the pre-pass never lays down depth
// and motion for geometry the color pass omits, and so is one whose
// pre-pass pipeline failed to build. Returns the regions issued, and leaves
// the last bucket's pipeline bound.
pub(in crate::vulkan) fn draw_prepass_bucket_regions(
&self,
cmd: vk::CommandBuffer,
indirect: vk::Buffer,
draw_count: u32,
) -> u32 {
let mut issued = 0;
for bucket in self.cull.world_pipelines.resident_buckets() {
let Some(prepass) = self
.cull
.world_pipelines
.get(bucket)
.and_then(|pipelines| pipelines.prepass.as_ref())
else {
continue;
};
// SAFETY: `cmd` is a command buffer in the recording state, and every handle and slice
// these commands name is live for the call.
unsafe {
self.hw.device.cmd_bind_pipeline(
cmd,
vk::PipelineBindPoint::GRAPHICS,
prepass.handle(),
);
}
self.draw_bucket_region(cmd, indirect, draw_count, bucket);
issued += 1;
}
issued
}
// Run `f` for every bucket past the default whose Shader is resident,
// returning how many ran. A bucket whose scene has not pinned yet has no
// pipeline: skip it until warmup builds one rather than drawing it with the
// wrong program.
fn for_each_resident_bucket(&self, mut f: impl FnMut(usize)) -> u32 {
let mut issued = 0;
for bucket in self.cull.world_pipelines.resident_buckets() {
f(bucket);
issued += 1;
}
issued
}
fn draw_bucket_region(
&self,
cmd: vk::CommandBuffer,
indirect: vk::Buffer,
draw_count: u32,
bucket: usize,
) {
// SAFETY: `cmd` is a command buffer in the recording state, and every handle and slice
// these commands name is live for the call.
unsafe {
self.hw.device.cmd_draw_indexed_indirect(
cmd,
indirect,
self.bucket_region_offset(bucket),
draw_count,
super::cull::INDIRECT_COMMAND_STRIDE,
);
}
}
}