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