1use super::GpuRenderer;
2use super::context_helpers::create_surface_context;
3use crate::types::{DrawCall, MAX_PARTICLES};
4use crate::vertex::{InstanceData, Vertex};
5use cvkg_core::{Rect, Renderer};
6use std::sync::Arc;
7
8impl GpuRenderer {
9 pub fn begin_frame_headless(&mut self) -> wgpu::CommandEncoder {
11 self.current_window = None;
12 self.compositor_index_cursor = self.indices.len() as u32;
13 self.reset_frame_state();
14
15 self.staging_belt.recall();
17
18 let ctx = self
19 .headless_context
20 .as_ref()
21 .expect("Headless context not initialized");
22 let time = self.start_time.elapsed().as_secs_f32();
23 let logical_w = ctx.width as f32 / ctx.scale_factor;
24 let logical_h = ctx.height as f32 / ctx.scale_factor;
25 let dt = time - self.current_scene.time;
26 self.current_scene.time = time;
27 self.current_scene.delta_time = dt;
28 self.current_scene.resolution = [logical_w, logical_h];
29 self.current_scene.scale_factor = ctx.scale_factor;
30 self.current_scene.proj =
31 glam::Mat4::orthographic_lh(0.0, logical_w, logical_h, 0.0, -1000.0, 1000.0);
32
33 self.queue.write_buffer(
34 &self.scene_buffer,
35 0,
36 bytemuck::bytes_of(&self.current_scene),
37 );
38
39 self.device
40 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
41 label: Some("Surtr Headless Command Encoder"),
42 })
43 }
44
45 fn reset_frame_state(&mut self) {
48 self.vertices.clear();
49 self.indices.clear();
50 self.instance_data.clear();
51 self.draw_calls.clear();
52 self.svg.clear_filter_batches();
53 self.shared_elements.clear();
54 self.current_texture_id = None;
55 self.current_panel_id = None;
56 self.panel_stack.clear();
57 self.world_space_panels.clear();
58 self.opacity_stack.clear();
59 self.opacity_stack.push(1.0);
60 self.clip_stack.clear();
61 self.slice_stack.clear();
62 self.transform_stack.clear();
63 self.portal_regions.clear();
64 self.hologram_instances.clear();
65 self.current_z = 0.0;
66 self.vnode_stack.clear();
67 self.event_handlers.clear();
68 let current_time = self.current_time();
72 let resolution = [self.current_width() as f32, self.current_height() as f32];
73 let time_uniform: [f32; 4] = [
74 current_time,
75 resolution[0],
76 resolution[1],
77 0.0, ];
79 self.queue.write_buffer(
80 &self.volumetric_uniform_buffer,
81 0,
82 bytemuck::cast_slice(&time_uniform),
83 );
84 self.frame_generation += 1;
86 const MAX_MEMO_AGE: u64 = 1000;
88 if self.frame_generation > MAX_MEMO_AGE {
89 let cutoff = self.frame_generation - MAX_MEMO_AGE;
90 self.memo_cache.retain(|_, entry| entry.frame_gen >= cutoff);
91 }
92 self.last_frame_start = std::time::Instant::now();
93 self.telemetry.draw_calls = 0;
94 self.telemetry.vertices = 0;
95 }
96
97 pub fn begin_frame(&mut self, window_id: winit::window::WindowId) -> wgpu::CommandEncoder {
99 self.begin_frame_internal(window_id, true)
100 }
101
102 pub fn begin_frame_reuse(
105 &mut self,
106 window_id: winit::window::WindowId,
107 ) -> wgpu::CommandEncoder {
108 self.begin_frame_internal(window_id, false)
109 }
110
111 fn begin_frame_internal(
112 &mut self,
113 window_id: winit::window::WindowId,
114 reset_state: bool,
115 ) -> wgpu::CommandEncoder {
116 if let Some(rx) = &self.ai_material_rx {
118 while let Ok(res) = rx.try_recv() {
119 match res {
120 Ok(_) => tracing::info!("[Surtr] Received AI generated material"),
121 Err(e) => tracing::warn!("[Surtr] AI material generation error: {:?}", e),
122 }
123 }
124 }
125
126 self.staging_belt.recall();
130 self.current_window = Some(window_id);
131 if reset_state {
132 self.reset_frame_state();
133 }
134
135 let ctx = self
136 .surfaces
137 .get(&window_id)
138 .expect("Window not registered");
139 let time = self.start_time.elapsed().as_secs_f32();
140 let logical_w = ctx.config.width as f32 / ctx.scale_factor;
141 let logical_h = ctx.config.height as f32 / ctx.scale_factor;
142 let dt = time - self.current_scene.time;
143 self.current_scene.time = time;
144 self.current_scene.delta_time = dt;
145 self.current_scene.resolution = [logical_w, logical_h];
146 self.current_scene.scale_factor = ctx.scale_factor;
147 self.current_scene.proj =
148 glam::Mat4::orthographic_lh(0.0, logical_w, logical_h, 0.0, -1000.0, 1000.0);
149
150 self.queue.write_buffer(
151 &self.scene_buffer,
152 0,
153 bytemuck::bytes_of(&self.current_scene),
154 );
155
156 self.device
157 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
158 label: Some("Surtr Command Encoder"),
159 })
160 }
161
162 pub fn register_window(&mut self, window: Arc<winit::window::Window>) {
164 let size = window.inner_size();
165 let surface = self
166 .instance
167 .create_surface(window.clone())
168 .expect("Failed to create surface");
169 let caps = surface.get_capabilities(&self.adapter);
170 let format = caps.formats[0];
171
172 let present_mode = if caps.present_modes.contains(&wgpu::PresentMode::Mailbox) {
174 wgpu::PresentMode::Mailbox
175 } else {
176 tracing::warn!("[GPU] Mailbox not supported, falling back to Fifo (V-Sync)");
177 wgpu::PresentMode::Fifo
178 };
179
180 let alpha_mode = if caps
181 .alpha_modes
182 .contains(&wgpu::CompositeAlphaMode::PostMultiplied)
183 {
184 wgpu::CompositeAlphaMode::PostMultiplied
185 } else if caps
186 .alpha_modes
187 .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
188 {
189 wgpu::CompositeAlphaMode::PreMultiplied
190 } else {
191 caps.alpha_modes[0]
192 };
193
194 tracing::info!(
195 "[GPU] Configuring surface: {}x{} | {:?} | {:?}",
196 size.width,
197 size.height,
198 present_mode,
199 alpha_mode
200 );
201
202 let config = wgpu::SurfaceConfiguration {
203 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
204 format,
205 width: size.width,
206 height: size.height,
207 present_mode,
208 alpha_mode,
209 view_formats: vec![],
210 desired_maximum_frame_latency: 1,
211 };
212 surface.configure(&self.device, &config);
213
214 let ctx = create_surface_context(
215 &self.device,
216 surface,
217 config,
218 &self.env_bind_group_layout,
219 &self.texture_bind_group_layout,
220 window.scale_factor() as f32,
221 self.quality_level.msaa_sample_count(),
222 &mut self.registry,
223 );
224
225 self.surfaces.insert(window.id(), ctx);
226 }
227
228 pub(crate) fn shatter_internal(
229 &mut self,
230 rect: Rect,
231 pieces: u32,
232 force: f32,
233 color: [f32; 4],
234 material_id: u32,
235 ) {
236 let count = (pieces as f32).sqrt().ceil() as u32;
238 let dw = rect.width / count as f32;
239 let dh = rect.height / count as f32;
240
241 let c = self.apply_opacity(color);
242
243 let cx = rect.x + rect.width * 0.5;
244 let cy = rect.y + rect.height * 0.5;
245
246 for y in 0..count {
247 for x in 0..count {
248 let init_x = rect.x + x as f32 * dw;
249 let init_y = rect.y + y as f32 * dh;
250
251 let dx = (init_x + dw * 0.5) - cx;
253 let dy = (init_y + dh * 0.5) - cy;
254 let dist = (dx * dx + dy * dy).sqrt().max(1.0);
255
256 let nx = dx / dist;
258 let ny = dy / dist;
259
260 let hash =
262 ((x as f32 * 12.9898 + y as f32 * 78.233).sin().fract() * 43_758.547).fract();
263 let hash2 =
264 ((x as f32 * 37.11 + y as f32 * 149.87).sin().fract() * 23_412.19).fract();
265
266 let speed_var = 0.5 + hash * 1.5;
267 let angle = ny.atan2(nx) + (hash2 - 0.5) * 0.6;
268 let disp_x = angle.cos() * force * 50.0 * speed_var;
269 let disp_y = angle.sin() * force * 50.0 * speed_var;
270
271 let gravity = force * force * 20.0;
273
274 let scale_factor = (1.0 - (force / 6.0).min(1.0)).max(0.0);
277 let shard_w = dw * scale_factor;
278 let shard_h = dh * scale_factor;
279
280 let displaced_x = init_x + disp_x + (dw - shard_w) * 0.5;
281 let displaced_y = init_y + disp_y + gravity + (dh - shard_h) * 0.5;
282
283 let shard_rect = Rect {
284 x: displaced_x,
285 y: displaced_y,
286 width: shard_w,
287 height: shard_h,
288 };
289
290 let uv = Rect {
291 x: x as f32 / count as f32,
292 y: y as f32 / count as f32,
293 width: 1.0 / count as f32,
294 height: 1.0 / count as f32,
295 };
296
297 self.fill_rect_with_full_params(shard_rect, c, material_id, None, force, uv);
298 }
299 }
300 }
301
302 pub(crate) fn recursive_bolt(
303 &mut self,
304 from: [f32; 2],
305 to: [f32; 2],
306 depth: u32,
307 color: [f32; 4],
308 ) {
309 if depth == 0 {
310 self.draw_lightning_segment(from, to, color);
311 return;
312 }
313
314 let mid_x = (from[0] + to[0]) * 0.5;
315 let mid_y = (from[1] + to[1]) * 0.5;
316
317 let dx = to[0] - from[0];
318 let dy = to[1] - from[1];
319 let len = (dx * dx + dy * dy).sqrt();
320
321 if len < 1e-4 {
322 return;
323 }
324
325 let offset_scale = len * 0.15;
327 let seed = (from[0] * 12.9898 + from[1] * 78.233 + (depth as f32) * 37.11)
328 .sin()
329 .fract();
330 let offset_x = -dy / len * (seed - 0.5) * offset_scale;
331 let offset_y = dx / len * (seed - 0.5) * offset_scale;
332
333 let mid = [mid_x + offset_x, mid_y + offset_y];
334
335 self.recursive_bolt(from, mid, depth - 1, color);
336 self.recursive_bolt(mid, to, depth - 1, color);
337
338 if depth > 2 && seed > 0.8 {
340 let branch_to = [
341 mid[0] + offset_x * 2.0 + (seed * 100.0).sin() * 50.0,
342 mid[1] + offset_y * 2.0 + (seed * 100.0).cos() * 50.0,
343 ];
344 self.recursive_bolt(mid, branch_to, depth - 2, color);
345 }
346 }
347
348 pub(crate) fn draw_lightning_segment(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
349 let dx = to[0] - from[0];
350 let dy = to[1] - from[1];
351 let len = (dx * dx + dy * dy).sqrt();
352 if len < 0.001 {
353 return;
354 }
355
356 let glow_width = 32.0;
357 let core_width = 4.0;
358 let c = self.apply_opacity(color);
359
360 let gnx = -dy / len * glow_width * 0.5;
362 let gny = dx / len * glow_width * 0.5;
363 let gp1 = [from[0] + gnx, from[1] + gny];
364 let gp2 = [to[0] + gnx, to[1] + gny];
365 let gp3 = [to[0] - gnx, to[1] - gny];
366 let gp4 = [from[0] - gnx, from[1] - gny];
367 self.push_oriented_quad(
368 [gp1, gp2, gp3, gp4],
369 c,
370 9,
371 Rect {
372 x: 0.0,
373 y: 0.0,
374 width: 1.0,
375 height: 1.0,
376 },
377 );
378
379 let cnx = -dy / len * core_width * 0.5;
381 let cny = dx / len * core_width * 0.5;
382 let cp1 = [from[0] + cnx, from[1] + cny];
383 let cp2 = [to[0] + cnx, to[1] + cny];
384 let cp3 = [to[0] - cnx, to[1] - cny];
385 let cp4 = [from[0] - cnx, from[1] - cny];
386 self.push_oriented_quad(
387 [cp1, cp2, cp3, cp4],
388 [1.0, 1.0, 1.0, c[3]],
389 0,
390 Rect {
391 x: 0.0,
392 y: 0.0,
393 width: 1.0,
394 height: 1.0,
395 },
396 );
397 }
398
399 pub(crate) fn push_oriented_quad(
400 &mut self,
401 points: [[f32; 2]; 4],
402 color: [f32; 4],
403 material_id: u32,
404 uv_rect: Rect,
405 ) {
406 let scissor = self.clip_stack.last().copied();
407 let texture_id = None; let (translation, scale_transform, rotation, _, _) = self.current_transform();
410 let current_instance_data = InstanceData {
411 translation,
412 scale: scale_transform,
413 rotation,
414 blur_radius: 0.0,
415 ior_override: 0.0,
416 glass_intensity: 1.0,
417 };
418
419 let material =
422 Self::resolve_material_with_context(material_id, &self.current_draw_material);
423 let final_material_id = match material {
424 cvkg_core::DrawMaterial::Opaque => material_id,
425 cvkg_core::DrawMaterial::TopUI => crate::renderer::material_id::TOP_UI,
426 cvkg_core::DrawMaterial::Glass { .. } => crate::renderer::material_id::GLASS,
427 cvkg_core::DrawMaterial::Blend { mode } => 7 + mode,
428 };
429
430 let last_call = self.draw_calls.last();
431 let needs_new_call = self.draw_calls.is_empty()
432 || self.current_texture_id != texture_id
433 || last_call.unwrap().scissor_rect != scissor
434 || last_call.unwrap().panel_id != self.current_panel_id
435 || last_call.unwrap().material != material
436 || {
437 let last_material = last_call.unwrap().material;
438 matches!((material, last_material),
439 (cvkg_core::DrawMaterial::Glass { blur_radius: a, ior_override: b, glass_intensity: c },
440 cvkg_core::DrawMaterial::Glass { blur_radius: d, ior_override: e, glass_intensity: f })
441 if a != d || b != e || c != f)
442 };
443
444 if needs_new_call {
445 self.current_texture_id = texture_id;
446 self.instance_data.push(current_instance_data);
447 self.draw_calls.push(DrawCall {
448 target_id: None,
449 panel_id: self.current_panel_id,
450 texture_id,
451 scissor_rect: scissor,
452 index_start: self.indices.len() as u32,
453 index_count: 0,
454 instance_count: 1,
455 material,
456 instance_start: (self.instance_data.len() - 1) as u32,
457 draw_order: 0,
458 });
459 } else {
460 self.instance_data.push(current_instance_data);
462 if let Some(call) = self.draw_calls.last_mut() {
463 call.instance_count += 1;
464 }
465 }
466
467 let uvs = [
468 [uv_rect.x, uv_rect.y],
469 [uv_rect.x + uv_rect.width, uv_rect.y],
470 [uv_rect.x + uv_rect.width, uv_rect.y + uv_rect.height],
471 [uv_rect.x, uv_rect.y + uv_rect.height],
472 ];
473
474 let rect = Rect {
475 x: points[0][0],
476 y: points[0][1],
477 width: 1.0,
478 height: 1.0,
479 };
480
481 for i in 0..4 {
482 let px = points[i][0];
483 let py = points[i][1];
484
485 self.vertices.push(Vertex {
486 position: [px, py, 0.0],
487 normal: [0.0, 0.0, 1.0],
488 uv: uvs[i],
489 color,
490 material_id: final_material_id,
491 radius: 0.0,
492 slice: [0.0, 0.0, 0.0, 1.0],
493 logical: [px - rect.x, py - rect.y],
494 size: [rect.width, rect.height],
495 clip: [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY],
496 tex_index: 0,
497 });
498 }
499
500 let base = self.vertices.len() as u32 - 4;
502 self.indices
503 .extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
504
505 if let Some(call) = self.draw_calls.last_mut() {
506 call.index_count += 6;
507 }
508 }
509
510 pub(crate) fn get_texture_id(&mut self, name: &str) -> Option<u32> {
511 self.texture_registry.get(name).copied()
512 }
513
514 pub fn fill_rect_with_mode(
516 &mut self,
517 rect: Rect,
518 color: [f32; 4],
519 material_id: u32,
520 texture_id: Option<u32>,
521 ) {
522 self.fill_rect_with_full_params(
523 rect,
524 color,
525 material_id,
526 texture_id,
527 0.0,
528 Rect {
529 x: 0.0,
530 y: 0.0,
531 width: 1.0,
532 height: 1.0,
533 },
534 );
535 }
536
537 pub(crate) fn fill_rect_with_full_params(
538 &mut self,
539 rect: Rect,
540 color: [f32; 4],
541 material_id: u32,
542 texture_id: Option<u32>,
543 radius: f32,
544 uv_rect: Rect,
545 ) {
546 if let Some(shadow) = self.shadow_stack.last().copied()
548 && shadow.color[3] > 0.001
549 {
550 let shadow_rect = Rect {
551 x: rect.x + shadow._offset[0],
552 y: rect.y + shadow._offset[1],
553 width: rect.width,
554 height: rect.height,
555 };
556 Renderer::draw_drop_shadow(
557 self,
558 shadow_rect,
559 radius,
560 shadow.color,
561 shadow.radius,
562 0.0, );
564 }
565
566 let slice = self
567 .slice_stack
568 .last()
569 .copied()
570 .map(|(a, o)| [a, o, 1.0, 1.0])
571 .unwrap_or([0.0, 0.0, 0.0, 1.0]);
572 self.fill_rect_with_full_params_and_slice(
573 rect,
574 color,
575 material_id,
576 texture_id,
577 radius,
578 uv_rect,
579 slice,
580 [0.0, 0.0],
581 );
582 }
583
584 #[allow(clippy::too_many_arguments)]
585 pub(crate) fn fill_rect_with_full_params_and_slice(
586 &mut self,
587 mut rect: Rect,
588 color: [f32; 4],
589 material_id: u32,
590 texture_id: Option<u32>,
591 radius: f32,
592 uv_rect: Rect,
593 slice: [f32; 4],
594 _glyph_time: [f32; 2],
595 ) {
596 if material_id != crate::renderer::material_id::GLASS {
599 let scale = self.current_scale_factor();
600 let snap = |v: f32| (v * scale).round() / scale;
601 rect.x = snap(rect.x);
602 rect.y = snap(rect.y);
603 rect.width = snap(rect.width);
604 rect.height = snap(rect.height);
605 }
606
607 let scissor = self.clip_stack.last().copied();
608
609 let material =
610 Self::resolve_material_with_context(material_id, &self.current_draw_material);
611 let final_material_id = match material {
612 cvkg_core::DrawMaterial::Opaque => material_id,
613 cvkg_core::DrawMaterial::TopUI => crate::renderer::material_id::TOP_UI,
614 cvkg_core::DrawMaterial::Glass { .. } => crate::renderer::material_id::GLASS,
615 cvkg_core::DrawMaterial::Blend { mode } => 7 + mode,
616 };
617
618 let (translation, scale_transform, rotation, _, _) = self.current_transform();
619 let (blur_radius, ior_override, glass_intensity) = if let cvkg_core::DrawMaterial::Glass {
620 blur_radius,
621 ior_override,
622 glass_intensity,
623 } = material
624 {
625 (blur_radius, ior_override, glass_intensity)
626 } else {
627 (0.0, 0.0, 1.0)
628 };
629
630 let current_instance_data = InstanceData {
631 translation,
632 scale: scale_transform,
633 rotation,
634 blur_radius,
635 ior_override,
636 glass_intensity,
637 };
638
639 let last_call = self.draw_calls.last();
646 let needs_new_call = self.draw_calls.is_empty()
647 || last_call.unwrap().scissor_rect != scissor
648 || last_call.unwrap().material != material
649 || last_call.unwrap().texture_id != self.current_texture_id
650 || last_call.unwrap().panel_id != self.current_panel_id
651 || {
652 let last_material = last_call.unwrap().material;
654 matches!((material, last_material),
655 (cvkg_core::DrawMaterial::Glass { blur_radius: a, ior_override: b, glass_intensity: c },
656 cvkg_core::DrawMaterial::Glass { blur_radius: d, ior_override: e, glass_intensity: f })
657 if a != d || b != e || c != f)
658 };
659
660 if needs_new_call {
661 self.current_texture_id = Some(0); self.instance_data.push(current_instance_data);
663 self.draw_calls.push(DrawCall {
664 target_id: None,
665 panel_id: self.current_panel_id,
666 texture_id: self.current_texture_id,
667 scissor_rect: scissor,
668 index_start: self.indices.len() as u32,
669 index_count: 0,
670 instance_count: 1,
671 material,
672 instance_start: (self.instance_data.len() - 1) as u32,
673 draw_order: 0,
674 });
675 } else {
676 self.instance_data.push(current_instance_data);
678 if let Some(call) = self.draw_calls.last_mut() {
679 call.instance_count += 1;
680 }
681 }
682
683 let scale = self.current_scale_factor();
684 let snap = |v: f32| (v * scale).round() / scale;
685
686 let base_idx = self.vertices.len() as u32;
687 let x1 = snap(rect.x);
688 let y1 = snap(rect.y);
689 let x2 = snap(rect.x + rect.width);
690 let y2 = snap(rect.y + rect.height);
691 let z = -self.current_z;
693 let normal = [0.0, 0.0, 1.0];
694 let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
695 x: -10000.0,
696 y: -10000.0,
697 width: 20000.0,
698 height: 20000.0,
699 });
700 let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
701
702 let tex_index = texture_id.unwrap_or(0);
703
704 self.vertices.push(Vertex {
705 position: [x1, y1, z],
706 normal,
707 uv: [uv_rect.x, uv_rect.y],
708 color,
709 material_id: final_material_id,
710 radius,
711 slice,
712 logical: [0.0, 0.0],
713 size: [rect.width, rect.height],
714 clip,
715 tex_index,
716 });
717 self.vertices.push(Vertex {
718 position: [x2, y1, z],
719 normal,
720 uv: [uv_rect.x + uv_rect.width, uv_rect.y],
721 color,
722 material_id: final_material_id,
723 radius,
724 slice,
725 logical: [rect.width, 0.0],
726 size: [rect.width, rect.height],
727 clip,
728 tex_index,
729 });
730 self.vertices.push(Vertex {
731 position: [x2, y2, z],
732 normal,
733 uv: [uv_rect.x + uv_rect.width, uv_rect.y + uv_rect.height],
734 color,
735 material_id: final_material_id,
736 radius,
737 slice,
738 logical: [rect.width, rect.height],
739 size: [rect.width, rect.height],
740 clip,
741 tex_index,
742 });
743 self.vertices.push(Vertex {
744 position: [x1, y2, z],
745 normal,
746 uv: [uv_rect.x, uv_rect.y + uv_rect.height],
747 color,
748 material_id: final_material_id,
749 radius,
750 slice,
751 logical: [0.0, rect.height],
752 size: [rect.width, rect.height],
753 clip,
754 tex_index,
755 });
756
757 self.indices.extend_from_slice(&[
758 base_idx,
759 base_idx + 1,
760 base_idx + 2,
761 base_idx,
762 base_idx + 2,
763 base_idx + 3,
764 ]);
765
766 if let Some(call) = self.draw_calls.last_mut() {
767 call.index_count += 6;
768 }
769 }
770
771 pub fn end_frame(&mut self, mut encoder: wgpu::CommandEncoder) {
780 struct ActiveFrameResources {
781 surface_texture: Option<wgpu::SurfaceTexture>,
782 target_view: wgpu::TextureView,
783 scene_texture: wgpu::TextureView,
784 scene_msaa_texture: wgpu::TextureView,
785 depth_texture_view: wgpu::TextureView,
786 blur_env_bind_group_a: wgpu::BindGroup,
787 blur_env_bind_group_b: wgpu::BindGroup,
788 bloom_env_bind_group_a: wgpu::BindGroup,
789 bloom_env_bind_group_b: wgpu::BindGroup,
790 }
791
792 let res = if let Some(window_id) = self.current_window {
793 let Some(ctx) = self.surfaces.get(&window_id) else {
794 tracing::error!("[GPU] Missing surface context for end_frame");
795 return;
796 };
797 let frame = match ctx.surface.get_current_texture() {
798 wgpu::CurrentSurfaceTexture::Success(t) => t,
799 wgpu::CurrentSurfaceTexture::Suboptimal(t) => {
800 ctx.surface.configure(&self.device, &ctx.config);
801 t
802 }
803 other => {
804 tracing::warn!(
805 "[GPU] Surface texture acquisition failed ({:?}), reconfiguring surface",
806 other
807 );
808 ctx.surface.configure(&self.device, &ctx.config);
809 match ctx.surface.get_current_texture() {
811 wgpu::CurrentSurfaceTexture::Success(t) => t,
812 wgpu::CurrentSurfaceTexture::Suboptimal(t) => {
813 ctx.surface.configure(&self.device, &ctx.config);
814 t
815 }
816 retry_failed => {
817 tracing::error!(
818 "[GPU] Surface texture retry also failed ({:?}), skipping frame",
819 retry_failed
820 );
821 self.queue.submit(std::iter::once(encoder.finish()));
822 return;
823 }
824 }
825 }
826 };
827 let view = frame
828 .texture
829 .create_view(&wgpu::TextureViewDescriptor::default());
830
831 ActiveFrameResources {
832 surface_texture: Some(frame),
833 target_view: view,
834 scene_texture: ctx.scene_texture.clone(),
835 scene_msaa_texture: ctx.scene_msaa_texture.clone(),
836 depth_texture_view: ctx.depth_texture_view.clone(),
837 blur_env_bind_group_a: ctx.blur_env_bind_group_a.clone(),
838 blur_env_bind_group_b: ctx.blur_env_bind_group_b.clone(),
839 bloom_env_bind_group_a: ctx.bloom_env_bind_group_a.clone(),
840 bloom_env_bind_group_b: ctx.bloom_env_bind_group_b.clone(),
841 }
842 } else {
843 let Some(ctx) = self.headless_context.as_ref() else {
844 tracing::error!("[GPU] No headless context for end_frame");
845 return;
846 };
847
848 ActiveFrameResources {
849 surface_texture: None,
850 target_view: ctx.output_view.clone(),
851 scene_texture: ctx.scene_texture.clone(),
852 scene_msaa_texture: ctx.scene_msaa_texture.clone(),
853 depth_texture_view: ctx.depth_texture_view.clone(),
854 blur_env_bind_group_a: ctx.blur_env_bind_group_a.clone(),
855 blur_env_bind_group_b: ctx.blur_env_bind_group_b.clone(),
856 bloom_env_bind_group_a: ctx.bloom_env_bind_group_a.clone(),
857 bloom_env_bind_group_b: ctx.bloom_env_bind_group_b.clone(),
858 }
859 };
860
861 if !self.frame_rendered && (!self.vertices.is_empty() || !self.indices.is_empty()) {
864 tracing::debug!(
865 "[GPU] Auto-flushing staging belt in end_frame (render_frame was not called)"
866 );
867 let mut staging_encoder =
868 self.device
869 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
870 label: Some("Surtr Auto-Flush Staging Encoder"),
871 });
872 if !self.vertices.is_empty() {
873 let v_bytes = bytemuck::cast_slice(&self.vertices);
874 self.staging_belt
875 .write_buffer(
876 &mut staging_encoder,
877 &self.geometry_buffers.vertex_buffer,
878 0,
879 wgpu::BufferSize::new(v_bytes.len() as u64).unwrap(),
880 )
881 .copy_from_slice(v_bytes);
882 }
883 if !self.indices.is_empty() {
884 let i_bytes = bytemuck::cast_slice(&self.indices);
885 self.staging_belt
886 .write_buffer(
887 &mut staging_encoder,
888 &self.geometry_buffers.index_buffer,
889 0,
890 wgpu::BufferSize::new(i_bytes.len() as u64).unwrap(),
891 )
892 .copy_from_slice(i_bytes);
893 }
894 if !self.instance_data.is_empty() {
895 let inst_bytes = bytemuck::cast_slice(&self.instance_data);
896 self.staging_belt
897 .write_buffer(
898 &mut staging_encoder,
899 &self.geometry_buffers.instance_buffer,
900 0,
901 wgpu::BufferSize::new(inst_bytes.len() as u64).unwrap(),
902 )
903 .copy_from_slice(inst_bytes);
904 }
905 self.staging_belt.finish();
906 self.staging_command_buffers.push(staging_encoder.finish());
907 }
908
909 let has_glass = self
911 .draw_calls
912 .iter()
913 .any(|c| matches!(c.material, cvkg_core::DrawMaterial::Glass { .. }));
914 let has_bloom = self.bloom_enabled;
915 let has_accessibility =
916 self.color_blind_mode != crate::color_blindness::ColorBlindMode::Normal;
917
918 let (blur_id, bloom_id) = if let Some(window_id) = self.current_window {
928 let ctx = self.surfaces.get(&window_id).unwrap();
929 (ctx.blur_tex_a, ctx.bloom_tex_a)
930 } else {
931 let ctx = self.headless_context.as_ref().unwrap();
932 (ctx.blur_tex_a, ctx.bloom_tex_a)
933 };
934 self.registry
935 .alias(crate::kvasir::nodes::RES_BLUR_A, blur_id);
936 self.registry
937 .alias(crate::kvasir::nodes::RES_BLOOM_A, bloom_id);
938 self.registry
939 .alias_view(crate::kvasir::nodes::RES_SCENE, res.scene_texture.clone());
940 self.registry.alias_view(
941 crate::kvasir::nodes::RES_SCENE_MSAA,
942 res.scene_msaa_texture.clone(),
943 );
944
945 let scale = self.current_scale_factor();
946 let scale_bits = scale.to_bits();
947 let active_offscreens_count = self.active_offscreens.len();
948 let portal_regions_count = self.portal_regions.len();
949 let width = self.current_width();
950 let height = self.current_height();
951 let has_volumetric = self.volumetric_enabled;
952
953 let mut offscreen_hash: u64 = 0;
955 for offscreen in &self.active_offscreens {
956 offscreen_hash = offscreen_hash.wrapping_add(
957 offscreen.target_id.wrapping_mul(31)
958 ^ (offscreen.blend_mode as u64).wrapping_mul(17),
959 );
960 }
961 let mut portal_hash: u64 = 0;
962 for region in &self.portal_regions {
963 portal_hash = portal_hash.wrapping_add(
964 (region.x.to_bits() as u64)
965 .wrapping_mul(7)
966 .wrapping_add((region.y.to_bits() as u64).wrapping_mul(13))
967 .wrapping_add((region.width.to_bits() as u64).wrapping_mul(19))
968 .wrapping_add((region.height.to_bits() as u64).wrapping_mul(23)),
969 );
970 }
971
972 let use_cache = if let Some(ref cached) = self.cached_graph_plan {
973 cached.matches(
974 has_glass,
975 has_bloom,
976 has_accessibility,
977 has_volumetric,
978 active_offscreens_count,
979 offscreen_hash,
980 portal_regions_count,
981 portal_hash,
982 width,
983 height,
984 scale_bits,
985 self.material_compilation_hash,
986 )
987 } else {
988 false
989 };
990
991 for (id, panel) in &self.world_space_panels {
992 let width = (panel.world_size.0 * panel.pixels_per_unit).max(1.0) as u32;
993 let height = (panel.world_size.1 * panel.pixels_per_unit).max(1.0) as u32;
994 self.registry.allocate_offscreen(&self.device, *id, [width, height]);
995 }
996
997 if !use_cache {
998 let render_graph = crate::kvasir::nodes::build_render_graph(
999 &crate::kvasir::nodes::RenderGraphConfig {
1000 has_glass,
1001 has_bloom,
1002 has_accessibility,
1003 has_volumetric,
1004 active_offscreens: &self.active_offscreens,
1005 portal_regions: &self.portal_regions.iter().cloned().collect::<Vec<_>>(),
1006 world_space_panels: &self.world_space_panels,
1007 width,
1008 height,
1009 scale,
1010 directional_light: None,
1011 mesh_instances_3d: Vec::new(),
1012 scene_radius: 100.0,
1013 },
1014 );
1015 let planner = crate::kvasir::planner::ExecutionPlanner::new(&render_graph);
1016 let compiled_plan = match planner.compile() {
1017 Ok(plan) => plan,
1018 Err(e) => {
1019 tracing::error!(
1020 "[Kvasir] Render graph compilation failed ({}), skipping render passes",
1021 e
1022 );
1023 if let Some(surface_texture) = res.surface_texture {
1025 surface_texture.present();
1026 tracing::info!("[Surtr] Frame presented (graph compilation fallback)");
1027 }
1028 return;
1029 }
1030 };
1031
1032 self.cached_graph_plan = Some(crate::kvasir::graph_cache::CachedGraphPlan {
1034 has_glass,
1035 has_bloom,
1036 has_accessibility,
1037 has_volumetric,
1038 active_offscreens_count,
1039 offscreen_content_hash: offscreen_hash,
1040 portal_regions_count,
1041 portal_content_hash: portal_hash,
1042 width,
1043 height,
1044 scale_bits,
1045 material_compilation_hash: self.material_compilation_hash,
1046 graph: render_graph,
1047 plan: compiled_plan,
1048 });
1049 }
1050
1051 let cached = self.cached_graph_plan.as_ref().unwrap();
1052 let frame_start = self.last_frame_start;
1053 let budget_ms = self.frame_budget.target_ms;
1054 let allow_degradation = self.frame_budget.allow_degradation;
1055
1056 for &node_key in &cached.plan {
1057 if allow_degradation && budget_ms > 0.0 {
1071 let elapsed_ms = frame_start.elapsed().as_secs_f32() * 1000.0;
1072 if elapsed_ms > budget_ms
1073 && let Some(node) = cached.graph.node(node_key)
1074 {
1075 match node.pass_id() {
1076 crate::kvasir::nodes::PassId::BloomExtract
1077 | crate::kvasir::nodes::PassId::BloomBlur
1078 | crate::kvasir::nodes::PassId::Volumetric => {
1079 tracing::trace!(
1080 "[Kvasir] Skipping {} (over budget: {:.1}ms > {:.1}ms)",
1081 node.label(),
1082 elapsed_ms,
1083 budget_ms
1084 );
1085 continue;
1086 }
1087 _ => {} }
1090 }
1091 }
1092 if let Some(node) = cached.graph.node(node_key) {
1093 tracing::trace!("[Kvasir] Executing node: {}", node.label());
1094 let mut ctx = crate::kvasir::node::ExecutionContext {
1095 device: &self.device,
1096 queue: &self.queue,
1097 encoder: &mut encoder,
1098 registry: &self.registry,
1099 renderer: self,
1100 target_view: &res.target_view,
1101 depth_view: &res.depth_texture_view,
1102 blur_env_bind_group_a: &res.blur_env_bind_group_a,
1103 blur_env_bind_group_b: &res.blur_env_bind_group_b,
1104 bloom_env_bind_group_a: &res.bloom_env_bind_group_a,
1105 bloom_env_bind_group_b: &res.bloom_env_bind_group_b,
1106 scale_factor: scale,
1107 };
1108 node.execute(&mut ctx);
1109 }
1110 }
1111
1112 if !self.particles.staging.is_empty() || self.particles.count > 0 {
1116 if !self.particles.staging.is_empty() {
1118 let write_start = self.particles.write_head as usize;
1119 let write_count = self.particles.staging.len();
1120 let max = MAX_PARTICLES;
1121
1122 let effective_count = write_count.min(max);
1131 let drop_count = write_count - effective_count;
1132
1133 let first_chunk = (max - write_start).min(effective_count);
1135 let bytes = bytemuck::cast_slice(
1136 &self.particles.staging[drop_count..drop_count + first_chunk],
1137 );
1138 self.queue.write_buffer(
1139 &self.particle_buffer,
1140 (write_start * std::mem::size_of::<crate::types::GpuParticle>()) as u64,
1141 bytes,
1142 );
1143 if first_chunk < effective_count {
1144 let remaining = effective_count - first_chunk;
1145 let bytes2 = bytemuck::cast_slice(
1146 &self.particles.staging
1147 [drop_count + first_chunk..drop_count + first_chunk + remaining],
1148 );
1149 self.queue.write_buffer(&self.particle_buffer, 0, bytes2);
1150 self.particles.write_head = remaining as u32;
1151 } else {
1152 self.particles.write_head = ((write_start + effective_count) % max) as u32;
1153 }
1154 self.particles.count =
1155 (self.particles.count as usize + effective_count).min(max) as u32;
1156 self.particles.staging.clear();
1157
1158 self.particle_render_bind_group = None;
1160 }
1161
1162 let dt = self.current_scene.delta_time;
1164 let uniforms = crate::types::ParticleUniforms { dt, _pad: [0.0; 7] };
1165 self.queue.write_buffer(
1166 &self.particle_uniform_buffer,
1167 0,
1168 bytemuck::bytes_of(&uniforms),
1169 );
1170
1171 let compute_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1172 label: Some("Particle Compute BG"),
1173 layout: &self.particle_compute_bgl,
1174 entries: &[
1175 wgpu::BindGroupEntry {
1176 binding: 0,
1177 resource: self.particle_buffer.as_entire_binding(),
1178 },
1179 wgpu::BindGroupEntry {
1180 binding: 1,
1181 resource: self.particle_uniform_buffer.as_entire_binding(),
1182 },
1183 ],
1184 });
1185
1186 let mut compute_encoder =
1187 self.device
1188 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1189 label: Some("Particle Compute Encoder"),
1190 });
1191 {
1192 let mut cpass = compute_encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1193 label: Some("Particle Integration"),
1194 ..Default::default()
1195 });
1196 cpass.set_pipeline(&self.particle_compute_pipeline);
1197 cpass.set_bind_group(0, &compute_bind_group, &[]);
1198 let workgroups = self.particles.count.div_ceil(64).max(1);
1199 cpass.dispatch_workgroups(workgroups, 1, 1);
1200 }
1201 self.staging_command_buffers.push(compute_encoder.finish());
1202 }
1203
1204 if self.particles.count > 0 && self.particles.last_compact.elapsed().as_secs_f32() > 2.0 {
1206 self.particles.last_compact = std::time::Instant::now();
1207 let read_size = (self.particles.count as usize
1209 * std::mem::size_of::<crate::types::GpuParticle>())
1210 as u64;
1211 let staging_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
1212 label: Some("Particle Compact Staging"),
1213 size: read_size,
1214 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1215 mapped_at_creation: false,
1216 });
1217 let mut compact_encoder =
1218 self.device
1219 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1220 label: Some("Particle Compact Copy"),
1221 });
1222 compact_encoder.copy_buffer_to_buffer(
1223 &self.particle_buffer,
1224 0,
1225 &staging_buf,
1226 0,
1227 read_size,
1228 );
1229 self.staging_command_buffers.push(compact_encoder.finish());
1230 }
1235
1236 if self.particles.count > 0 {
1240 if self.particle_render_bind_group.is_none() {
1242 self.particle_render_bind_group =
1243 Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1244 label: Some("Particle Render BG"),
1245 layout: &self.particle_render_bgl,
1246 entries: &[wgpu::BindGroupEntry {
1247 binding: 0,
1248 resource: self.particle_buffer.as_entire_binding(),
1249 }],
1250 }));
1251 }
1252 if let Some(bg) = &self.particle_render_bind_group {
1253 let mut render_encoder =
1254 self.device
1255 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1256 label: Some("Particle Render Encoder"),
1257 });
1258 {
1259 let mut rpass = render_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1260 label: Some("Particle Render"),
1261 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1262 view: &res.target_view,
1263 resolve_target: None,
1264 ops: wgpu::Operations {
1265 load: wgpu::LoadOp::Load,
1266 store: wgpu::StoreOp::Store,
1267 },
1268 depth_slice: None,
1269 })],
1270 depth_stencil_attachment: None,
1271 timestamp_writes: None,
1272 occlusion_query_set: None,
1273 multiview_mask: None,
1274 });
1275 rpass.set_pipeline(&self.particle_render_pipeline);
1276 rpass.set_bind_group(0, bg, &[]);
1277 rpass.draw(0..self.particles.count, 0..1);
1278 }
1279 self.staging_command_buffers.push(render_encoder.finish());
1280 }
1281 }
1282
1283 self.staging_command_buffers.push(encoder.finish());
1288
1289 if let (Some(q), Some(b), Some(rb)) = (
1291 &self.skuld_queries,
1292 &self.skuld_buffer,
1293 &self.skuld_read_buffer,
1294 ) {
1295 let mut resolve_encoder =
1296 self.device
1297 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1298 label: Some("Skuld Resolve Encoder"),
1299 });
1300 resolve_encoder.resolve_query_set(q, 0..2, b, 0);
1301 resolve_encoder.copy_buffer_to_buffer(b, 0, rb, 0, 16);
1302 self.staging_command_buffers.push(resolve_encoder.finish());
1303 }
1304
1305 let cmds = std::mem::take(&mut self.staging_command_buffers);
1306 self.queue.submit(cmds);
1307 self.telemetry.frame_time_ms = self.last_frame_start.elapsed().as_secs_f32() * 1000.0;
1308 self.update_vram_telemetry();
1309
1310 self.registry.evict_frame_resources();
1313
1314 if let Some(f) = res.surface_texture {
1315 f.present();
1316 tracing::info!("[Surtr] Frame presented");
1317 }
1318 }
1319
1320 pub fn submit_buckets(&mut self, buckets: &cvkg_compositor::CommandBuckets) {
1329 let mut active_offscreens = Vec::new();
1331 let mut current_target_id = None;
1332
1333 let mut sorted_scene: Vec<_> = buckets.scene_commands.iter().collect();
1335 sorted_scene.sort_by_key(|cmd| match cmd {
1336 cvkg_compositor::engine::RenderCommand::Draw(routed) => {
1337 (routed.z_index as i64, routed.draw_order as i64)
1338 }
1339 _ => (0, 0),
1340 });
1341
1342 for cmd in sorted_scene {
1343 match cmd {
1344 cvkg_compositor::engine::RenderCommand::Draw(routed) => {
1345 self.set_material(cvkg_core::DrawMaterial::Opaque);
1346 self.submit_routed(routed, current_target_id);
1347 }
1348 cvkg_compositor::engine::RenderCommand::PushOffscreen {
1349 source_layer,
1350 material,
1351 bounds,
1352 } => {
1353 current_target_id = Some(source_layer.0);
1354
1355 let width = (bounds.width).max(1.0) as u32;
1357 let height = (bounds.height).max(1.0) as u32;
1358 self.registry
1359 .allocate_offscreen(&self.device, source_layer.0, [width, height]);
1360
1361 if let cvkg_compositor::Material::ShaderEffect {
1362 effect_name,
1363 params_json: _,
1364 ..
1365 } = material
1366 {
1367 active_offscreens.push(crate::types::OffscreenEffectConfig {
1368 target_id: source_layer.0,
1369 effect: effect_name.clone(),
1370 blend_mode: 0, effect_args: [0.0; 16], });
1373 }
1374 }
1375 cvkg_compositor::engine::RenderCommand::PopOffscreen => {
1376 current_target_id = None;
1377 }
1378 }
1379 }
1380 self.active_offscreens = active_offscreens;
1381
1382 let mut sorted_glass: Vec<_> = buckets.glass_commands.iter().collect();
1384 sorted_glass.sort_by_key(|cmd| match cmd {
1385 cvkg_compositor::engine::RenderCommand::Draw(routed) => {
1386 (routed.z_index as i64, routed.draw_order as i64)
1387 }
1388 _ => (0, 0),
1389 });
1390 for cmd in sorted_glass {
1391 if let cvkg_compositor::engine::RenderCommand::Draw(routed) = cmd {
1392 self.set_material(Self::convert_compositor_material(&routed.material));
1393 self.submit_routed(routed, None);
1394 }
1395 }
1396
1397 let mut sorted_overlay: Vec<_> = buckets.overlay_commands.iter().collect();
1399 sorted_overlay.sort_by_key(|cmd| match cmd {
1400 cvkg_compositor::engine::RenderCommand::Draw(routed) => {
1401 (routed.z_index as i64, routed.draw_order as i64)
1402 }
1403 _ => (0, 0),
1404 });
1405 for cmd in sorted_overlay {
1406 if let cvkg_compositor::engine::RenderCommand::Draw(routed) = cmd {
1407 self.set_material(cvkg_core::DrawMaterial::TopUI);
1408 self.submit_routed(routed, None);
1409 }
1410 }
1411 }
1412
1413 pub(crate) fn submit_routed(
1415 &mut self,
1416 routed: &cvkg_compositor::RoutedDrawCommand,
1417 target_id: Option<u64>,
1418 ) {
1419 let cmd = &routed.command;
1420 if cmd.index_count == 0 {
1421 return;
1422 }
1423 let material = Self::convert_compositor_material(&routed.material);
1424 self.draw_calls.push(DrawCall {
1425 texture_id: cmd.texture_id,
1426 scissor_rect: cmd.scissor_rect,
1427 index_start: cmd.index_start,
1428 index_count: cmd.index_count,
1429 instance_count: 1,
1430 material,
1431 target_id,
1432 panel_id: self.current_panel_id,
1433 instance_start: cmd.instance_id,
1434 draw_order: 0,
1435 });
1436 }
1437
1438 pub(crate) fn apply_opacity(&self, mut color: [f32; 4]) -> [f32; 4] {
1440 if let Some(&alpha) = self.opacity_stack.last() {
1441 color[3] *= alpha;
1442 }
1443 color
1444 }
1445
1446 pub(crate) fn resolve_material(material_id: u32) -> cvkg_core::DrawMaterial {
1449 Self::resolve_material_with_context(material_id, &cvkg_core::DrawMaterial::Opaque)
1450 }
1451
1452 pub(crate) fn resolve_material_with_context(
1456 material_id: u32,
1457 current: &cvkg_core::DrawMaterial,
1458 ) -> cvkg_core::DrawMaterial {
1459 use crate::renderer::material_id::*;
1460
1461 if matches!(current, cvkg_core::DrawMaterial::TopUI) && material_id != GLASS {
1464 return cvkg_core::DrawMaterial::TopUI;
1465 }
1466
1467 if let cvkg_core::DrawMaterial::Blend { mode } = current
1469 && material_id == 0
1470 {
1471 return cvkg_core::DrawMaterial::Blend { mode: *mode };
1472 }
1473
1474 match material_id {
1475 GLASS => {
1476 if let cvkg_core::DrawMaterial::Glass {
1477 blur_radius,
1478 ior_override,
1479 glass_intensity,
1480 } = current
1481 {
1482 cvkg_core::DrawMaterial::Glass {
1483 blur_radius: *blur_radius,
1484 ior_override: *ior_override,
1485 glass_intensity: *glass_intensity,
1486 }
1487 } else {
1488 cvkg_core::DrawMaterial::Glass {
1489 blur_radius: 20.0,
1490 ior_override: 0.0,
1491 glass_intensity: 1.0,
1492 }
1493 }
1494 }
1495 TOP_UI => cvkg_core::DrawMaterial::TopUI,
1496 BLEND_START..=BLEND_END => cvkg_core::DrawMaterial::Blend {
1497 mode: (material_id - 7),
1498 },
1499 _ => cvkg_core::DrawMaterial::Opaque,
1500 }
1501 }
1502
1503 pub(crate) fn convert_compositor_material(
1506 mat: &cvkg_compositor::Material,
1507 ) -> cvkg_core::DrawMaterial {
1508 match mat {
1509 cvkg_compositor::Material::Glass { blur_radius, .. } => {
1510 cvkg_core::DrawMaterial::Glass {
1511 blur_radius: *blur_radius,
1512 ior_override: 0.0,
1513 glass_intensity: 1.0,
1514 }
1515 }
1516 cvkg_compositor::Material::Overlay => cvkg_core::DrawMaterial::TopUI,
1517 cvkg_compositor::Material::Multiply => cvkg_core::DrawMaterial::Blend { mode: 1 },
1518 cvkg_compositor::Material::Screen => cvkg_core::DrawMaterial::Blend { mode: 2 },
1519 cvkg_compositor::Material::BlendOverlay => cvkg_core::DrawMaterial::Blend { mode: 3 },
1520 cvkg_compositor::Material::Darken => cvkg_core::DrawMaterial::Blend { mode: 4 },
1521 cvkg_compositor::Material::Lighten => cvkg_core::DrawMaterial::Blend { mode: 5 },
1522 cvkg_compositor::Material::ColorDodge => cvkg_core::DrawMaterial::Blend { mode: 6 },
1523 cvkg_compositor::Material::ColorBurn => cvkg_core::DrawMaterial::Blend { mode: 7 },
1524 cvkg_compositor::Material::HardLight => cvkg_core::DrawMaterial::Blend { mode: 8 },
1525 cvkg_compositor::Material::SoftLight => cvkg_core::DrawMaterial::Blend { mode: 9 },
1526 cvkg_compositor::Material::Difference => cvkg_core::DrawMaterial::Blend { mode: 10 },
1527 cvkg_compositor::Material::Exclusion => cvkg_core::DrawMaterial::Blend { mode: 11 },
1528 cvkg_compositor::Material::Hue => cvkg_core::DrawMaterial::Blend { mode: 12 },
1529 cvkg_compositor::Material::Saturation => cvkg_core::DrawMaterial::Blend { mode: 13 },
1530 cvkg_compositor::Material::Color => cvkg_core::DrawMaterial::Blend { mode: 14 },
1531 cvkg_compositor::Material::Luminosity => cvkg_core::DrawMaterial::Blend { mode: 15 },
1532 cvkg_compositor::Material::Opaque => cvkg_core::DrawMaterial::Opaque,
1533 _ => cvkg_core::DrawMaterial::Opaque,
1534 }
1535 }
1536
1537 pub(crate) fn position_vertices(
1539 vertices: &mut [Vertex],
1540 view_box: Rect,
1541 rect: Rect,
1542 material_id: u32,
1543 clip: [f32; 4],
1544 snap: impl Fn(f32) -> f32,
1545 ) {
1546 for v in vertices.iter_mut() {
1547 let rel_x = (v.position[0] - view_box.x) / view_box.width;
1548 let rel_y = (v.position[1] - view_box.y) / view_box.height;
1549 v.position[0] = snap(rect.x + rel_x * rect.width);
1550 v.position[1] = snap(rect.y + rel_y * rect.height);
1551 v.position[2] = 0.0; v.logical = [v.position[0], v.position[1]];
1553 v.clip = clip;
1554 v.material_id = material_id;
1555 }
1556 }
1557
1558 pub(crate) fn emit_draw_call(
1560 renderer: &mut GpuRenderer,
1561 material: cvkg_core::DrawMaterial,
1562 texture_id: Option<u32>,
1563 scissor_rect: Rect,
1564 index_count: u32,
1565 base_vertex: u32,
1566 ) {
1567 let draw_order = renderer.current_draw_order;
1568 let (translation, scale_transform, rotation, _, _) = renderer.current_transform();
1569 let current_instance_data = InstanceData {
1570 translation,
1571 scale: scale_transform,
1572 rotation,
1573 blur_radius: 0.0,
1574 ior_override: 0.0,
1575 glass_intensity: 1.0,
1576 };
1577 let last_call = renderer.draw_calls.last();
1580 let needs_new_call = renderer.draw_calls.is_empty()
1581 || renderer.current_texture_id != texture_id
1582 || last_call.unwrap().scissor_rect != renderer.clip_stack.last().copied()
1583 || last_call.unwrap().panel_id != renderer.current_panel_id
1584 || last_call.unwrap().material != material
1585 || {
1586 let last_material = last_call.unwrap().material;
1587 matches!((material, last_material),
1588 (cvkg_core::DrawMaterial::Glass { blur_radius: a, ior_override: b, glass_intensity: c },
1589 cvkg_core::DrawMaterial::Glass { blur_radius: d, ior_override: e, glass_intensity: f })
1590 if a != d || b != e || c != f)
1591 };
1592
1593 if needs_new_call {
1594 renderer.current_texture_id = texture_id;
1595 renderer.instance_data.push(current_instance_data);
1596 renderer.draw_calls.push(DrawCall {
1597 target_id: None,
1598 panel_id: renderer.current_panel_id,
1599 texture_id,
1600 scissor_rect: renderer.clip_stack.last().copied(),
1601 index_start: (renderer.indices.len() - index_count as usize) as u32,
1602 index_count,
1603 instance_count: 1,
1604 material,
1605 instance_start: (renderer.instance_data.len() - 1) as u32,
1606 draw_order: 0,
1607 });
1608 } else {
1609 renderer.instance_data.push(current_instance_data);
1611 if let Some(call) = renderer.draw_calls.last_mut() {
1612 call.instance_count += 1;
1613 }
1614 }
1615 }
1616
1617 pub async fn capture_frame(&self) -> Result<Vec<u8>, String> {
1619 let ctx = self
1620 .headless_context
1621 .as_ref()
1622 .ok_or("Headless context required for capture")?;
1623
1624 let u32_size = std::mem::size_of::<u32>() as u32;
1625 let width = ctx.width;
1626 let height = ctx.height;
1627 let bytes_per_row = width * u32_size;
1628 let padding = (256 - (bytes_per_row % 256)) % 256;
1629 let padded_bytes_per_row = bytes_per_row + padding;
1630
1631 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
1632 label: Some("Capture Buffer"),
1633 size: (padded_bytes_per_row as u64 * height as u64),
1634 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1635 mapped_at_creation: false,
1636 });
1637
1638 let mut encoder = self
1639 .device
1640 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1641 label: Some("Capture Encoder"),
1642 });
1643
1644 encoder.copy_texture_to_buffer(
1645 wgpu::TexelCopyTextureInfo {
1646 texture: &ctx.output_texture,
1647 mip_level: 0,
1648 origin: wgpu::Origin3d::ZERO,
1649 aspect: wgpu::TextureAspect::All,
1650 },
1651 wgpu::TexelCopyBufferInfo {
1652 buffer: &output_buffer,
1653 layout: wgpu::TexelCopyBufferLayout {
1654 offset: 0,
1655 bytes_per_row: Some(padded_bytes_per_row),
1656 rows_per_image: Some(height),
1657 },
1658 },
1659 wgpu::Extent3d {
1660 width,
1661 height,
1662 depth_or_array_layers: 1,
1663 },
1664 );
1665
1666 self.queue.submit(Some(encoder.finish()));
1667
1668 let buffer_slice = output_buffer.slice(..);
1669 let (sender, receiver) = futures::channel::oneshot::channel();
1670 buffer_slice.map_async(wgpu::MapMode::Read, move |v| {
1671 let _ = sender.send(v);
1672 });
1673
1674 let _ = self.device.poll(wgpu::PollType::Wait {
1675 submission_index: None,
1676 timeout: None,
1677 });
1678
1679 if let Ok(Ok(_)) = receiver.await {
1680 let data = buffer_slice.get_mapped_range();
1681 let mut result = Vec::with_capacity((width * height * 4) as usize);
1682
1683 for y in 0..height {
1684 let start = (y * padded_bytes_per_row) as usize;
1685 let end = start + bytes_per_row as usize;
1686 result.extend_from_slice(&data[start..end]);
1687 }
1688
1689 tracing::trace!(
1690 "[GPU] capture_frame: data len={}, first 4 bytes={:?}",
1691 data.len(),
1692 &data[0..4.min(data.len())]
1693 );
1694
1695 drop(data);
1696 output_buffer.unmap();
1697 Ok(result)
1698 } else {
1699 Err("Failed to capture frame".to_string())
1700 }
1701 }
1702
1703 fn hash_gradient_stops(stops: &[[f32; 4]]) -> u64 {
1706 use std::hash::{Hash, Hasher};
1707 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1708 for stop in stops {
1709 for v in stop {
1710 v.to_bits().hash(&mut hasher);
1711 }
1712 }
1713 hasher.finish()
1714 }
1715
1716 #[allow(clippy::collapsible_if)]
1720 pub(crate) fn upload_gradient_stops(&mut self, stops: &[[f32; 4]]) {
1721 if stops.is_empty() {
1722 return;
1723 }
1724
1725 let hash = Self::hash_gradient_stops(stops);
1726
1727 if hash == self.gradient_stops_hash {
1729 if let Some((_, _, bg)) = self.gradient_texture_cache.get(&hash) {
1730 self.gradient_bind_group = bg.clone();
1731 return;
1732 }
1733 }
1734
1735 if let Some((_, view, bg)) = self.gradient_texture_cache.get(&hash) {
1737 self.gradient_stop_texture = view.texture().clone();
1738 self.gradient_stop_texture_view = view.clone();
1739 self.gradient_bind_group = bg.clone();
1740 self.gradient_stops_hash = hash;
1741 return;
1742 }
1743
1744 let max_stops = 32u32;
1746 let num_stops = stops.len().min(max_stops as usize) as u32;
1747
1748 let mut data = vec![0u8; (max_stops as usize) * 4];
1750 for (i, stop) in stops.iter().enumerate().take(max_stops as usize) {
1751 let r = (stop[0].clamp(0.0, 1.0) * 255.0).round() as u8;
1753 let g = (stop[1].clamp(0.0, 1.0) * 255.0).round() as u8;
1754 let b = (stop[2].clamp(0.0, 1.0) * 255.0).round() as u8;
1755 let a = (stop[3].clamp(0.0, 1.0) * 255.0).round() as u8;
1756 #[allow(clippy::identity_op)]
1759 {
1760 data[i * 4 + 0] = r;
1761 data[i * 4 + 1] = g;
1762 data[i * 4 + 2] = b;
1763 data[i * 4 + 3] = a;
1764 }
1765 }
1766
1767 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
1769 label: Some("Gradient Stops Texture"),
1770 size: wgpu::Extent3d {
1771 width: max_stops,
1772 height: 1,
1773 depth_or_array_layers: 1,
1774 },
1775 mip_level_count: 1,
1776 sample_count: 1,
1777 dimension: wgpu::TextureDimension::D2,
1778 format: wgpu::TextureFormat::Rgba8Unorm,
1779 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1780 view_formats: &[],
1781 });
1782
1783 self.queue.write_texture(
1784 wgpu::TexelCopyTextureInfo {
1785 texture: &texture,
1786 mip_level: 0,
1787 origin: wgpu::Origin3d::ZERO,
1788 aspect: wgpu::TextureAspect::All,
1789 },
1790 &data,
1791 wgpu::TexelCopyBufferLayout {
1792 offset: 0,
1793 bytes_per_row: Some(max_stops * 4),
1794 rows_per_image: Some(1),
1795 },
1796 wgpu::Extent3d {
1797 width: max_stops,
1798 height: 1,
1799 depth_or_array_layers: 1,
1800 },
1801 );
1802
1803 let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1804
1805 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1806 layout: &self.gradient_bind_group_layout,
1807 entries: &[
1808 wgpu::BindGroupEntry {
1809 binding: 0,
1810 resource: wgpu::BindingResource::TextureView(&texture_view),
1811 },
1812 wgpu::BindGroupEntry {
1813 binding: 1,
1814 resource: wgpu::BindingResource::Sampler(&self.dummy_sampler),
1815 },
1816 ],
1817 label: Some("Gradient Bind Group"),
1818 });
1819
1820 self.gradient_stops_hash = hash;
1822 self.gradient_stop_texture = texture.clone();
1823 self.gradient_stop_texture_view = texture_view.clone();
1824 self.gradient_bind_group = bind_group.clone();
1825 self.gradient_texture_cache
1826 .insert(hash, (texture, texture_view, bind_group));
1827 }
1828
1829 pub fn draw_gradient_multi(
1835 &mut self,
1836 rect: Rect,
1837 stops: &[[f32; 4]],
1838 angle: f32,
1839 is_radial: bool,
1840 ) {
1841 if stops.is_empty() {
1842 return;
1843 }
1844
1845 self.upload_gradient_stops(stops);
1847
1848 let num_stops = stops.len().min(32) as f32;
1849 let material_id = if is_radial { 31u32 } else { 30u32 };
1850
1851 let white = [1.0f32, 1.0, 1.0, 1.0];
1853
1854 let slice = [angle, num_stops, 0.0, 1.0];
1856
1857 self.fill_rect_with_full_params_and_slice(
1858 rect,
1859 white,
1860 material_id,
1861 None,
1862 0.0,
1863 Rect {
1864 x: 0.0,
1865 y: 0.0,
1866 width: 1.0,
1867 height: 1.0,
1868 },
1869 slice,
1870 [0.0, 0.0],
1871 );
1872 }
1873}