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