1use crate::kvasir::node::{ExecutionContext, KvasirNode};
2use crate::kvasir::nodes::{PassId, RES_BLUR_A, RES_SCENE};
3use crate::kvasir::resource::ResourceId;
4use crate::renderer::GpuRenderer;
5
6pub struct BackdropCopyNode {
7 pub inputs: Vec<ResourceId>,
8 pub outputs: Vec<ResourceId>,
9}
10
11impl BackdropCopyNode {
12 pub fn new() -> Self {
13 Self {
14 inputs: vec![RES_SCENE],
15 outputs: vec![RES_BLUR_A],
16 }
17 }
18}
19
20impl KvasirNode for BackdropCopyNode {
21 fn label(&self) -> &'static str {
22 "Backdrop Copy"
23 }
24
25 fn inputs(&self) -> &[ResourceId] {
26 &self.inputs
27 }
28
29 fn outputs(&self) -> &[ResourceId] {
30 &self.outputs
31 }
32
33 fn pass_id(&self) -> PassId {
34 PassId::BackdropCopy
35 }
36
37 fn execute(&self, ctx: &mut ExecutionContext) {
38 let target_texture = match ctx.registry.get_texture(RES_BLUR_A) {
39 Some(v) => v,
40 None => {
41 log::error!("Missing texture for {}", stringify!(RES_BLUR_A));
42 return;
43 }
44 };
45 let target_view = {
46 let mut cache = GpuRenderer::lock_or_clear_cache(&ctx.renderer.texture_view_cache);
47 cache
48 .entry((RES_BLUR_A, 0))
49 .or_insert_with(|| {
50 target_texture.create_view(&wgpu::TextureViewDescriptor {
51 label: Some("backdrop_copy_mip0"),
52 base_mip_level: 0,
53 mip_level_count: Some(1),
54 ..Default::default()
55 })
56 })
57 .clone()
58 };
59
60 let mut p = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
61 label: Some("Surtr Backdrop Copy"),
62 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
63 view: &target_view,
64 resolve_target: None,
65 ops: wgpu::Operations {
66 load: wgpu::LoadOp::Clear(wgpu::Color {
67 r: 0.0,
68 g: 0.0,
69 b: 0.0,
70 a: 0.0,
71 }),
72 store: wgpu::StoreOp::Store,
73 },
74 depth_slice: None,
75 })],
76 ..Default::default()
77 });
78
79 p.set_pipeline(&ctx.renderer.copy_pipeline);
80
81 let scene_view = match ctx.registry.get_texture_view(RES_SCENE) {
82 Some(v) => v,
83 None => {
84 log::error!("Missing texture view for {}", stringify!(RES_SCENE));
85 return;
86 }
87 };
88 let source_bind_group = {
89 let mut cache = GpuRenderer::lock_or_clear_cache(&ctx.renderer.bind_group_cache);
90 cache
91 .entry((RES_SCENE, 0, false))
92 .or_insert_with(|| {
93 ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
94 label: Some("backdrop_copy_bg"),
95 layout: &ctx.renderer.texture_bind_group_layout,
96 entries: &[
97 wgpu::BindGroupEntry {
98 binding: 0,
99 resource: wgpu::BindingResource::TextureViewArray(&vec![
100 &scene_view;
101 32
102 ]),
103 },
104 wgpu::BindGroupEntry {
105 binding: 1,
106 resource: wgpu::BindingResource::Sampler(
107 &ctx.renderer.dummy_sampler,
108 ),
109 },
110 ],
111 })
112 })
113 .clone()
114 };
115
116 p.set_bind_group(0, &source_bind_group, &[]);
117 p.set_bind_group(1, &ctx.renderer.dummy_env_bind_group, &[]);
118 p.set_bind_group(2, &ctx.renderer.berserker_bind_group, &[]);
119 p.draw(0..3, 0..1);
120 }
121}
122
123pub struct BackdropBlurNode {
124 pub inputs: Vec<ResourceId>,
125 pub outputs: Vec<ResourceId>,
126 pub width: u32,
127 pub height: u32,
128}
129
130impl BackdropBlurNode {
131 pub fn new(width: u32, height: u32) -> Self {
132 Self {
133 inputs: vec![RES_BLUR_A],
134 outputs: vec![RES_BLUR_A],
135 width,
136 height,
137 }
138 }
139}
140
141impl KvasirNode for BackdropBlurNode {
142 fn label(&self) -> &'static str {
143 "Backdrop Blur"
144 }
145
146 fn inputs(&self) -> &[ResourceId] {
147 &self.inputs
148 }
149
150 fn outputs(&self) -> &[ResourceId] {
151 &self.outputs
152 }
153
154 fn pass_id(&self) -> PassId {
155 PassId::BackdropBlur
156 }
157
158 fn execute(&self, ctx: &mut ExecutionContext) {
159 let blur_tex = match ctx.registry.get_texture(RES_BLUR_A) {
160 Some(v) => v,
161 None => {
162 log::error!("Missing texture for {}", stringify!(RES_BLUR_A));
163 return;
164 }
165 };
166
167 let kawase_uniform = &ctx.renderer.kawase_uniform;
169
170 let num_mips = blur_tex.mip_level_count();
172 let effective_mips = (num_mips as usize).min(6);
177 if effective_mips < 2 {
178 return;
179 }
180
181 let mip_views: Vec<wgpu::TextureView> = (0..effective_mips)
182 .map(|mip| {
183 let mut cache = GpuRenderer::lock_or_clear_cache(&ctx.renderer.texture_view_cache);
184 cache
185 .entry((RES_BLUR_A, mip as u32))
186 .or_insert_with(|| {
187 blur_tex.create_view(&wgpu::TextureViewDescriptor {
188 label: Some(&format!("blur_mip_{}", mip)),
189 base_mip_level: mip as u32,
190 mip_level_count: Some(1),
191 ..Default::default()
192 })
193 })
194 .clone()
195 })
196 .collect();
197
198 let blur_width = self.width;
199 let blur_height = self.height;
200
201 let mip_scales: Vec<(f32, f32, f32)> = (0..effective_mips)
203 .map(|i| {
204 let div = (1u32 << i) as f32;
205 (
206 blur_width as f32 / div,
207 blur_height as f32 / div,
208 (i + 1) as f32,
209 )
210 })
211 .collect();
212
213 for mip in 1..effective_mips {
214 let kernel_width = mip_scales[mip].2;
215 let uniform_data: [f32; 8] = [
216 mip_scales[(mip - 1)].0,
217 mip_scales[(mip - 1)].1,
218 (mip - 1) as f32,
219 kernel_width,
220 0.0,
221 0.0,
222 0.0,
223 0.0,
224 ];
225 ctx.queue
226 .write_buffer(kawase_uniform, 0, bytemuck::cast_slice(&uniform_data[..8]));
227
228 let w = mip_scales[mip].0.max(1.0) as u32;
229 let h = mip_scales[mip].1.max(1.0) as u32;
230
231 let bg = ctx.get_or_create_bind_group(
233 (RES_BLUR_A, mip as u32, false),
234 &ctx.renderer.kawase_bind_group_layout,
235 &[
236 wgpu::BindGroupEntry {
237 binding: 0,
238 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
239 buffer: kawase_uniform,
240 offset: 0,
241 size: wgpu::BufferSize::new(32),
242 }),
243 },
244 wgpu::BindGroupEntry {
245 binding: 1,
246 resource: wgpu::BindingResource::TextureView(&mip_views[(mip - 1)]),
247 },
248 wgpu::BindGroupEntry {
249 binding: 2,
250 resource: wgpu::BindingResource::Sampler(&ctx.renderer.sampler),
251 },
252 ],
253 Some("kawase_down"),
254 );
255
256 let mut p = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
257 label: Some(&format!("Kawase Down {}", mip)),
258 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
259 view: &mip_views[mip],
260 resolve_target: None,
261 ops: wgpu::Operations {
262 load: wgpu::LoadOp::Clear(wgpu::Color {
263 r: 0.0,
264 g: 0.0,
265 b: 0.0,
266 a: 0.0,
267 }),
268 store: wgpu::StoreOp::Store,
269 },
270 depth_slice: None,
271 })],
272 ..Default::default()
273 });
274 p.set_viewport(0.0, 0.0, w as f32, h as f32, 0.0, 1.0);
275 p.set_pipeline(&ctx.renderer.kawase_down_pipeline);
276 p.set_bind_group(0, Some(&bg), &[]);
277 p.draw(0..3, 0..1);
278 }
279
280 for mip in (1..effective_mips).rev() {
281 let kernel_width = mip_scales[mip].2;
282 let uniform_data: [f32; 8] = [
283 mip_scales[mip].0,
284 mip_scales[mip].1,
285 mip as f32,
286 kernel_width,
287 0.0,
288 0.0,
289 0.0,
290 0.0,
291 ];
292 ctx.queue
293 .write_buffer(kawase_uniform, 0, bytemuck::cast_slice(&uniform_data[..8]));
294
295 let w = mip_scales[(mip - 1)].0.max(1.0) as u32;
296 let h = mip_scales[(mip - 1)].1.max(1.0) as u32;
297
298 let bg = ctx.get_or_create_bind_group(
300 (RES_BLUR_A, mip as u32, true),
301 &ctx.renderer.kawase_bind_group_layout,
302 &[
303 wgpu::BindGroupEntry {
304 binding: 0,
305 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
306 buffer: kawase_uniform,
307 offset: 0,
308 size: wgpu::BufferSize::new(32),
309 }),
310 },
311 wgpu::BindGroupEntry {
312 binding: 1,
313 resource: wgpu::BindingResource::TextureView(&mip_views[mip]),
314 },
315 wgpu::BindGroupEntry {
316 binding: 2,
317 resource: wgpu::BindingResource::Sampler(&ctx.renderer.sampler),
318 },
319 ],
320 Some(&format!("kawase_up_bg_{}", mip)),
321 );
322
323 let mut p = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
324 label: Some(&format!("Kawase Up {}", mip)),
325 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
326 view: &mip_views[(mip - 1)],
327 resolve_target: None,
328 ops: wgpu::Operations {
329 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
330 store: wgpu::StoreOp::Store,
331 },
332 depth_slice: None,
333 })],
334 ..Default::default()
335 });
336 p.set_viewport(0.0, 0.0, w as f32, h as f32, 0.0, 1.0);
337 p.set_pipeline(&ctx.renderer.kawase_up_pipeline);
338 p.set_bind_group(0, Some(&bg), &[]);
339 p.draw(0..3, 0..1);
340 }
341
342 log::trace!(
343 "[Kvasir] backdrop_blur: Kawase pyramid ({}x{})",
344 blur_width,
345 blur_height
346 );
347 }
348}
349
350pub struct GlassNode {
351 pub inputs: Vec<ResourceId>,
352 pub outputs: Vec<ResourceId>,
353 pub scale: f32,
354}
355
356impl GlassNode {
357 pub fn new(scale: f32) -> Self {
358 Self {
359 inputs: vec![RES_SCENE, RES_BLUR_A],
360 outputs: vec![RES_SCENE],
361 scale,
362 }
363 }
364}
365
366impl KvasirNode for GlassNode {
367 fn label(&self) -> &'static str {
368 "Glass"
369 }
370
371 fn inputs(&self) -> &[ResourceId] {
372 &self.inputs
373 }
374
375 fn outputs(&self) -> &[ResourceId] {
376 &self.outputs
377 }
378
379 fn pass_id(&self) -> PassId {
380 PassId::Glass
381 }
382
383 fn execute(&self, ctx: &mut ExecutionContext) {
384 let rt_w = ctx.renderer.current_width() as i32;
385 let rt_h = ctx.renderer.current_height() as i32;
386
387 let scene_view = match ctx.registry.get_texture_view(RES_SCENE) {
388 Some(v) => v,
389 None => {
390 log::error!("Missing texture view for {}", stringify!(RES_SCENE));
391 return;
392 }
393 };
394 let msaa_view = match ctx
395 .registry
396 .get_texture_view(crate::kvasir::nodes::RES_SCENE_MSAA)
397 {
398 Some(v) => v,
399 None => {
400 log::error!(
401 "Missing texture view for {}",
402 stringify!(crate::kvasir::nodes::RES_SCENE_MSAA)
403 );
404 return;
405 }
406 };
407 let blur_view = match ctx.registry.get_texture_view(RES_BLUR_A) {
408 Some(v) => v,
409 None => {
410 log::error!("Missing texture view for {}", stringify!(RES_BLUR_A));
411 return;
412 }
413 };
414
415 let ctx_blur_env_bind_group_a = ctx.blur_env_bind_group_a;
416
417 let mut p = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
421 label: Some("Surtr P3 Liquid Glass"),
422 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
423 view: &scene_view,
424 resolve_target: None,
425 ops: wgpu::Operations {
426 load: wgpu::LoadOp::Load,
427 store: wgpu::StoreOp::Store,
428 },
429 depth_slice: None,
430 })],
431 depth_stencil_attachment: None,
432 ..Default::default()
433 });
434
435 p.set_pipeline(&ctx.renderer.glass_pipeline);
436 p.set_vertex_buffer(0, ctx.renderer.geometry_buffers.vertex_buffer.slice(..));
437 p.set_vertex_buffer(1, ctx.renderer.geometry_buffers.instance_buffer.slice(..));
438 p.set_index_buffer(
439 ctx.renderer.geometry_buffers.index_buffer.slice(..),
440 wgpu::IndexFormat::Uint32,
441 );
442 p.set_bind_group(0, &ctx.renderer.dummy_texture_bind_group, &[]);
443 p.set_bind_group(1, ctx_blur_env_bind_group_a, &[]);
444 p.set_bind_group(2, &ctx.renderer.berserker_bind_group, &[]);
445
446 let scale = self.scale;
447 for call in ctx
448 .renderer
449 .draw_calls
450 .iter()
451 .filter(|c| matches!(c.material, cvkg_core::DrawMaterial::Glass { .. }))
452 {
453 let mut portal_index = None;
457 if let Some(scissor) = call.scissor_rect {
458 let key = (
460 scissor.x.round() as i32,
461 scissor.y.round() as i32,
462 scissor.width.round() as i32,
463 scissor.height.round() as i32,
464 );
465 for (idx, region) in ctx.renderer.portal_regions.iter().enumerate() {
466 let rkey = (
467 region.x.round() as i32,
468 region.y.round() as i32,
469 region.width.round() as i32,
470 region.height.round() as i32,
471 );
472 if key == rkey {
473 portal_index = Some(idx);
474 break;
475 }
476 }
477 }
478
479 let env_bg = if let Some(portal_idx) = portal_index {
480 let portal_res_id = ResourceId(2000 + portal_idx as u32);
481 if let Some(portal_view) = ctx.registry.get_texture_view(portal_res_id) {
482 let cache_key = (portal_res_id, 0, false);
483 let mut cache = GpuRenderer::lock_or_clear_cache(&ctx.renderer.bind_group_cache);
484 let bg = cache.entry(cache_key).or_insert_with(|| {
485 ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
486 label: Some(&format!("portal_blur_env_bg_{}", portal_idx)),
487 layout: &ctx.renderer.env_bind_group_layout,
488 entries: &[
489 wgpu::BindGroupEntry {
490 binding: 0,
491 resource: wgpu::BindingResource::TextureView(&portal_view),
492 },
493 wgpu::BindGroupEntry {
494 binding: 1,
495 resource: wgpu::BindingResource::Sampler(&ctx.renderer.sampler),
496 },
497 ],
498 })
499 });
500 Some(bg.clone())
501 } else {
502 None
503 }
504 } else {
505 None
506 };
507
508 let active_env_bg = env_bg.as_ref().unwrap_or(ctx_blur_env_bind_group_a);
509 p.set_bind_group(1, active_env_bg, &[]);
510
511 let bg = if let Some(id) = call.texture_id {
512 if id == 0 {
513 &ctx.renderer.mega_heim_bind_group
514 } else {
515 ctx.renderer
516 .texture_bind_groups
517 .get(id as usize)
518 .unwrap_or(&ctx.renderer.dummy_texture_bind_group)
519 }
520 } else {
521 &ctx.renderer.dummy_texture_bind_group
522 };
523
524 p.set_bind_group(0, bg, &[]);
525
526 if let Some(rect) = call.scissor_rect
527 && rt_w > 0
528 && rt_h > 0
529 {
530 let x1 = (rect.x * scale).round() as i32;
531 let y1 = (rect.y * scale).round() as i32;
532 let x2 = ((rect.x + rect.width) * scale).round() as i32;
533 let y2 = ((rect.y + rect.height) * scale).round() as i32;
534 let w = (x2 - x1).clamp(0, rt_w);
535 let h = (y2 - y1).clamp(0, rt_h);
536 if w > 0 && h > 0 {
537 p.set_scissor_rect(x1 as u32, y1 as u32, w as u32, h as u32);
538 } else {
539 p.set_scissor_rect(0, 0, 0, 0);
540 }
541 } else {
542 p.set_scissor_rect(0, 0, rt_w as u32, rt_h as u32);
543 }
544 p.draw_indexed(
545 call.index_start..call.index_start + call.index_count,
546 0,
547 call.instance_start..call.instance_start + call.instance_count,
548 );
549 }
550 }
551}