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(_) => log::info!("[Surtr] Received AI generated material"),
118 Err(e) => log::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 log::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 log::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;
685 let normal = [0.0, 0.0, 1.0];
686 let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
687 x: -10000.0,
688 y: -10000.0,
689 width: 20000.0,
690 height: 20000.0,
691 });
692 let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
693
694 let tex_index = texture_id.unwrap_or(0);
695
696 self.vertices.push(Vertex {
697 position: [x1, y1, z],
698 normal,
699 uv: [uv_rect.x, uv_rect.y],
700 color,
701 material_id: final_material_id,
702 radius,
703 slice,
704 logical: [0.0, 0.0],
705 size: [rect.width, rect.height],
706 clip,
707 tex_index,
708 });
709 self.vertices.push(Vertex {
710 position: [x2, y1, z],
711 normal,
712 uv: [uv_rect.x + uv_rect.width, uv_rect.y],
713 color,
714 material_id: final_material_id,
715 radius,
716 slice,
717 logical: [rect.width, 0.0],
718 size: [rect.width, rect.height],
719 clip,
720 tex_index,
721 });
722 self.vertices.push(Vertex {
723 position: [x2, y2, z],
724 normal,
725 uv: [uv_rect.x + uv_rect.width, uv_rect.y + uv_rect.height],
726 color,
727 material_id: final_material_id,
728 radius,
729 slice,
730 logical: [rect.width, rect.height],
731 size: [rect.width, rect.height],
732 clip,
733 tex_index,
734 });
735 self.vertices.push(Vertex {
736 position: [x1, y2, z],
737 normal,
738 uv: [uv_rect.x, uv_rect.y + uv_rect.height],
739 color,
740 material_id: final_material_id,
741 radius,
742 slice,
743 logical: [0.0, rect.height],
744 size: [rect.width, rect.height],
745 clip,
746 tex_index,
747 });
748
749 self.indices.extend_from_slice(&[
750 base_idx,
751 base_idx + 1,
752 base_idx + 2,
753 base_idx,
754 base_idx + 2,
755 base_idx + 3,
756 ]);
757
758 if let Some(call) = self.draw_calls.last_mut() {
759 call.index_count += 6;
760 }
761 }
762
763 pub fn end_frame(&mut self, mut encoder: wgpu::CommandEncoder) {
772 struct ActiveFrameResources {
773 surface_texture: Option<wgpu::SurfaceTexture>,
774 target_view: wgpu::TextureView,
775 scene_texture: wgpu::TextureView,
776 scene_msaa_texture: wgpu::TextureView,
777 depth_texture_view: wgpu::TextureView,
778 blur_env_bind_group_a: wgpu::BindGroup,
779 blur_env_bind_group_b: wgpu::BindGroup,
780 bloom_env_bind_group_a: wgpu::BindGroup,
781 bloom_env_bind_group_b: wgpu::BindGroup,
782 }
783
784 let res = if let Some(window_id) = self.current_window {
785 let Some(ctx) = self.surfaces.get(&window_id) else {
786 log::error!("[GPU] Missing surface context for end_frame");
787 return;
788 };
789 let frame = match ctx.surface.get_current_texture() {
790 wgpu::CurrentSurfaceTexture::Success(t) => t,
791 wgpu::CurrentSurfaceTexture::Suboptimal(t) => {
792 ctx.surface.configure(&self.device, &ctx.config);
793 t
794 }
795 other => {
796 log::warn!(
797 "[GPU] Surface texture acquisition failed ({:?}), reconfiguring surface",
798 other
799 );
800 ctx.surface.configure(&self.device, &ctx.config);
801 match ctx.surface.get_current_texture() {
803 wgpu::CurrentSurfaceTexture::Success(t) => t,
804 wgpu::CurrentSurfaceTexture::Suboptimal(t) => {
805 ctx.surface.configure(&self.device, &ctx.config);
806 t
807 }
808 retry_failed => {
809 log::error!(
810 "[GPU] Surface texture retry also failed ({:?}), skipping frame",
811 retry_failed
812 );
813 self.queue.submit(std::iter::once(encoder.finish()));
814 return;
815 }
816 }
817 }
818 };
819 let view = frame
820 .texture
821 .create_view(&wgpu::TextureViewDescriptor::default());
822
823 ActiveFrameResources {
824 surface_texture: Some(frame),
825 target_view: view,
826 scene_texture: ctx.scene_texture.clone(),
827 scene_msaa_texture: ctx.scene_msaa_texture.clone(),
828 depth_texture_view: ctx.depth_texture_view.clone(),
829 blur_env_bind_group_a: ctx.blur_env_bind_group_a.clone(),
830 blur_env_bind_group_b: ctx.blur_env_bind_group_b.clone(),
831 bloom_env_bind_group_a: ctx.bloom_env_bind_group_a.clone(),
832 bloom_env_bind_group_b: ctx.bloom_env_bind_group_b.clone(),
833 }
834 } else {
835 let Some(ctx) = self.headless_context.as_ref() else {
836 log::error!("[GPU] No headless context for end_frame");
837 return;
838 };
839
840 ActiveFrameResources {
841 surface_texture: None,
842 target_view: ctx.output_view.clone(),
843 scene_texture: ctx.scene_texture.clone(),
844 scene_msaa_texture: ctx.scene_msaa_texture.clone(),
845 depth_texture_view: ctx.depth_texture_view.clone(),
846 blur_env_bind_group_a: ctx.blur_env_bind_group_a.clone(),
847 blur_env_bind_group_b: ctx.blur_env_bind_group_b.clone(),
848 bloom_env_bind_group_a: ctx.bloom_env_bind_group_a.clone(),
849 bloom_env_bind_group_b: ctx.bloom_env_bind_group_b.clone(),
850 }
851 };
852
853 if !self.frame_rendered && (!self.vertices.is_empty() || !self.indices.is_empty()) {
856 log::debug!(
857 "[GPU] Auto-flushing staging belt in end_frame (render_frame was not called)"
858 );
859 let mut staging_encoder =
860 self.device
861 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
862 label: Some("Surtr Auto-Flush Staging Encoder"),
863 });
864 if !self.vertices.is_empty() {
865 let v_bytes = bytemuck::cast_slice(&self.vertices);
866 self.staging_belt
867 .write_buffer(
868 &mut staging_encoder,
869 &self.geometry_buffers.vertex_buffer,
870 0,
871 wgpu::BufferSize::new(v_bytes.len() as u64).unwrap(),
872 )
873 .copy_from_slice(v_bytes);
874 }
875 if !self.indices.is_empty() {
876 let i_bytes = bytemuck::cast_slice(&self.indices);
877 self.staging_belt
878 .write_buffer(
879 &mut staging_encoder,
880 &self.geometry_buffers.index_buffer,
881 0,
882 wgpu::BufferSize::new(i_bytes.len() as u64).unwrap(),
883 )
884 .copy_from_slice(i_bytes);
885 }
886 if !self.instance_data.is_empty() {
887 let inst_bytes = bytemuck::cast_slice(&self.instance_data);
888 self.staging_belt
889 .write_buffer(
890 &mut staging_encoder,
891 &self.geometry_buffers.instance_buffer,
892 0,
893 wgpu::BufferSize::new(inst_bytes.len() as u64).unwrap(),
894 )
895 .copy_from_slice(inst_bytes);
896 }
897 self.staging_belt.finish();
898 self.staging_command_buffers.push(staging_encoder.finish());
899 }
900
901 let has_glass = self
903 .draw_calls
904 .iter()
905 .any(|c| matches!(c.material, cvkg_core::DrawMaterial::Glass { .. }));
906 let has_bloom = self.bloom_enabled;
907 let has_accessibility =
908 self.color_blind_mode != crate::color_blindness::ColorBlindMode::Normal;
909
910 let (blur_id, bloom_id) = if let Some(window_id) = self.current_window {
920 let ctx = self.surfaces.get(&window_id).unwrap();
921 (ctx.blur_tex_a, ctx.bloom_tex_a)
922 } else {
923 let ctx = self.headless_context.as_ref().unwrap();
924 (ctx.blur_tex_a, ctx.bloom_tex_a)
925 };
926 self.registry
927 .alias(crate::kvasir::nodes::RES_BLUR_A, blur_id);
928 self.registry
929 .alias(crate::kvasir::nodes::RES_BLOOM_A, bloom_id);
930 self.registry
931 .alias_view(crate::kvasir::nodes::RES_SCENE, res.scene_texture.clone());
932 self.registry.alias_view(
933 crate::kvasir::nodes::RES_SCENE_MSAA,
934 res.scene_msaa_texture.clone(),
935 );
936
937 let scale = self.current_scale_factor();
938 let scale_bits = scale.to_bits();
939 let active_offscreens_count = self.active_offscreens.len();
940 let portal_regions_count = self.portal_regions.len();
941 let width = self.current_width();
942 let height = self.current_height();
943 let has_volumetric = self.volumetric_enabled;
944
945 let mut offscreen_hash: u64 = 0;
947 for offscreen in &self.active_offscreens {
948 offscreen_hash = offscreen_hash.wrapping_add(
949 offscreen.target_id.wrapping_mul(31)
950 ^ (offscreen.blend_mode as u64).wrapping_mul(17),
951 );
952 }
953 let mut portal_hash: u64 = 0;
954 for region in &self.portal_regions {
955 portal_hash = portal_hash.wrapping_add(
956 (region.x.to_bits() as u64)
957 .wrapping_mul(7)
958 .wrapping_add((region.y.to_bits() as u64).wrapping_mul(13))
959 .wrapping_add((region.width.to_bits() as u64).wrapping_mul(19))
960 .wrapping_add((region.height.to_bits() as u64).wrapping_mul(23)),
961 );
962 }
963
964 let use_cache = if let Some(ref cached) = self.cached_graph_plan {
965 cached.matches(
966 has_glass,
967 has_bloom,
968 has_accessibility,
969 has_volumetric,
970 active_offscreens_count,
971 offscreen_hash,
972 portal_regions_count,
973 portal_hash,
974 width,
975 height,
976 scale_bits,
977 self.material_compilation_hash,
978 )
979 } else {
980 false
981 };
982
983 if !use_cache {
984 let render_graph = crate::kvasir::nodes::build_render_graph(
985 &crate::kvasir::nodes::RenderGraphConfig {
986 has_glass,
987 has_bloom,
988 has_accessibility,
989 has_volumetric,
990 active_offscreens: &self.active_offscreens,
991 portal_regions: &self.portal_regions.iter().cloned().collect::<Vec<_>>(),
992 width,
993 height,
994 scale,
995 },
996 );
997 let planner = crate::kvasir::planner::ExecutionPlanner::new(&render_graph);
998 let compiled_plan = match planner.compile() {
999 Ok(plan) => plan,
1000 Err(e) => {
1001 log::error!(
1002 "[Kvasir] Render graph compilation failed ({}), skipping render passes",
1003 e
1004 );
1005 if let Some(surface_texture) = res.surface_texture {
1007 surface_texture.present();
1008 log::info!("[Surtr] Frame presented (graph compilation fallback)");
1009 }
1010 return;
1011 }
1012 };
1013
1014 self.cached_graph_plan = Some(crate::kvasir::graph_cache::CachedGraphPlan {
1016 has_glass,
1017 has_bloom,
1018 has_accessibility,
1019 has_volumetric,
1020 active_offscreens_count,
1021 offscreen_content_hash: offscreen_hash,
1022 portal_regions_count,
1023 portal_content_hash: portal_hash,
1024 width,
1025 height,
1026 scale_bits,
1027 material_compilation_hash: self.material_compilation_hash,
1028 graph: render_graph,
1029 plan: compiled_plan,
1030 });
1031 }
1032
1033 let cached = self.cached_graph_plan.as_ref().unwrap();
1034 let frame_start = self.last_frame_start;
1035 let budget_ms = self.frame_budget.target_ms;
1036 let allow_degradation = self.frame_budget.allow_degradation;
1037
1038 for &node_key in &cached.plan {
1039 if allow_degradation && budget_ms > 0.0 {
1053 let elapsed_ms = frame_start.elapsed().as_secs_f32() * 1000.0;
1054 if elapsed_ms > budget_ms
1055 && let Some(node) = cached.graph.node(node_key)
1056 {
1057 match node.pass_id() {
1058 crate::kvasir::nodes::PassId::BloomExtract
1059 | crate::kvasir::nodes::PassId::BloomBlur
1060 | crate::kvasir::nodes::PassId::Volumetric => {
1061 log::trace!(
1062 "[Kvasir] Skipping {} (over budget: {:.1}ms > {:.1}ms)",
1063 node.label(),
1064 elapsed_ms,
1065 budget_ms
1066 );
1067 continue;
1068 }
1069 _ => {} }
1072 }
1073 }
1074 if let Some(node) = cached.graph.node(node_key) {
1075 log::trace!("[Kvasir] Executing node: {}", node.label());
1076 let mut ctx = crate::kvasir::node::ExecutionContext {
1077 device: &self.device,
1078 queue: &self.queue,
1079 encoder: &mut encoder,
1080 registry: &self.registry,
1081 renderer: self,
1082 target_view: &res.target_view,
1083 depth_view: &res.depth_texture_view,
1084 blur_env_bind_group_a: &res.blur_env_bind_group_a,
1085 blur_env_bind_group_b: &res.blur_env_bind_group_b,
1086 bloom_env_bind_group_a: &res.bloom_env_bind_group_a,
1087 bloom_env_bind_group_b: &res.bloom_env_bind_group_b,
1088 scale_factor: scale,
1089 };
1090 node.execute(&mut ctx);
1091 }
1092 }
1093
1094 if !self.particles.staging.is_empty() || self.particles.count > 0 {
1098 if !self.particles.staging.is_empty() {
1100 let write_start = self.particles.write_head as usize;
1101 let write_count = self.particles.staging.len();
1102 let max = MAX_PARTICLES;
1103
1104 let effective_count = write_count.min(max);
1113 let drop_count = write_count - effective_count;
1114
1115 let first_chunk = (max - write_start).min(effective_count);
1117 let bytes = bytemuck::cast_slice(
1118 &self.particles.staging[drop_count..drop_count + first_chunk],
1119 );
1120 self.queue.write_buffer(
1121 &self.particle_buffer,
1122 (write_start * std::mem::size_of::<crate::types::GpuParticle>()) as u64,
1123 bytes,
1124 );
1125 if first_chunk < effective_count {
1126 let remaining = effective_count - first_chunk;
1127 let bytes2 = bytemuck::cast_slice(
1128 &self.particles.staging
1129 [drop_count + first_chunk..drop_count + first_chunk + remaining],
1130 );
1131 self.queue.write_buffer(&self.particle_buffer, 0, bytes2);
1132 self.particles.write_head = remaining as u32;
1133 } else {
1134 self.particles.write_head = ((write_start + effective_count) % max) as u32;
1135 }
1136 self.particles.count =
1137 (self.particles.count as usize + effective_count).min(max) as u32;
1138 self.particles.staging.clear();
1139
1140 self.particle_render_bind_group = None;
1142 }
1143
1144 let dt = self.current_scene.delta_time;
1146 let uniforms = crate::types::ParticleUniforms { dt, _pad: [0.0; 7] };
1147 self.queue.write_buffer(
1148 &self.particle_uniform_buffer,
1149 0,
1150 bytemuck::bytes_of(&uniforms),
1151 );
1152
1153 let compute_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1154 label: Some("Particle Compute BG"),
1155 layout: &self.particle_compute_bgl,
1156 entries: &[
1157 wgpu::BindGroupEntry {
1158 binding: 0,
1159 resource: self.particle_buffer.as_entire_binding(),
1160 },
1161 wgpu::BindGroupEntry {
1162 binding: 1,
1163 resource: self.particle_uniform_buffer.as_entire_binding(),
1164 },
1165 ],
1166 });
1167
1168 let mut compute_encoder =
1169 self.device
1170 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1171 label: Some("Particle Compute Encoder"),
1172 });
1173 {
1174 let mut cpass = compute_encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1175 label: Some("Particle Integration"),
1176 ..Default::default()
1177 });
1178 cpass.set_pipeline(&self.particle_compute_pipeline);
1179 cpass.set_bind_group(0, &compute_bind_group, &[]);
1180 let workgroups = self.particles.count.div_ceil(64).max(1);
1181 cpass.dispatch_workgroups(workgroups, 1, 1);
1182 }
1183 self.staging_command_buffers.push(compute_encoder.finish());
1184 }
1185
1186 if self.particles.count > 0 && self.particles.last_compact.elapsed().as_secs_f32() > 2.0 {
1188 self.particles.last_compact = std::time::Instant::now();
1189 let read_size = (self.particles.count as usize
1191 * std::mem::size_of::<crate::types::GpuParticle>())
1192 as u64;
1193 let staging_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
1194 label: Some("Particle Compact Staging"),
1195 size: read_size,
1196 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1197 mapped_at_creation: false,
1198 });
1199 let mut compact_encoder =
1200 self.device
1201 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1202 label: Some("Particle Compact Copy"),
1203 });
1204 compact_encoder.copy_buffer_to_buffer(
1205 &self.particle_buffer,
1206 0,
1207 &staging_buf,
1208 0,
1209 read_size,
1210 );
1211 self.staging_command_buffers.push(compact_encoder.finish());
1212 }
1217
1218 if self.particles.count > 0 {
1222 if self.particle_render_bind_group.is_none() {
1224 self.particle_render_bind_group =
1225 Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1226 label: Some("Particle Render BG"),
1227 layout: &self.particle_render_bgl,
1228 entries: &[wgpu::BindGroupEntry {
1229 binding: 0,
1230 resource: self.particle_buffer.as_entire_binding(),
1231 }],
1232 }));
1233 }
1234 if let Some(bg) = &self.particle_render_bind_group {
1235 let mut render_encoder =
1236 self.device
1237 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1238 label: Some("Particle Render Encoder"),
1239 });
1240 {
1241 let mut rpass = render_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1242 label: Some("Particle Render"),
1243 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1244 view: &res.target_view,
1245 resolve_target: None,
1246 ops: wgpu::Operations {
1247 load: wgpu::LoadOp::Load,
1248 store: wgpu::StoreOp::Store,
1249 },
1250 depth_slice: None,
1251 })],
1252 depth_stencil_attachment: None,
1253 timestamp_writes: None,
1254 occlusion_query_set: None,
1255 multiview_mask: None,
1256 });
1257 rpass.set_pipeline(&self.particle_render_pipeline);
1258 rpass.set_bind_group(0, bg, &[]);
1259 rpass.draw(0..self.particles.count, 0..1);
1260 }
1261 self.staging_command_buffers.push(render_encoder.finish());
1262 }
1263 }
1264
1265 self.staging_command_buffers.push(encoder.finish());
1270
1271 if let (Some(q), Some(b), Some(rb)) = (
1273 &self.skuld_queries,
1274 &self.skuld_buffer,
1275 &self.skuld_read_buffer,
1276 ) {
1277 let mut resolve_encoder =
1278 self.device
1279 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1280 label: Some("Skuld Resolve Encoder"),
1281 });
1282 resolve_encoder.resolve_query_set(q, 0..2, b, 0);
1283 resolve_encoder.copy_buffer_to_buffer(b, 0, rb, 0, 16);
1284 self.staging_command_buffers.push(resolve_encoder.finish());
1285 }
1286
1287 let cmds = std::mem::take(&mut self.staging_command_buffers);
1288 self.queue.submit(cmds);
1289 self.telemetry.frame_time_ms = self.last_frame_start.elapsed().as_secs_f32() * 1000.0;
1290 self.update_vram_telemetry();
1291
1292 self.registry.evict_frame_resources();
1295
1296 if let Some(f) = res.surface_texture {
1297 f.present();
1298 log::info!("[Surtr] Frame presented");
1299 }
1300 }
1301
1302 pub fn submit_buckets(&mut self, buckets: &cvkg_compositor::CommandBuckets) {
1311 let mut active_offscreens = Vec::new();
1313 let mut current_target_id = None;
1314
1315 let mut sorted_scene: Vec<_> = buckets.scene_commands.iter().collect();
1317 sorted_scene.sort_by_key(|cmd| match cmd {
1318 cvkg_compositor::engine::RenderCommand::Draw(routed) => {
1319 (routed.z_index as i64, routed.draw_order as i64)
1320 }
1321 _ => (0, 0),
1322 });
1323
1324 for cmd in sorted_scene {
1325 match cmd {
1326 cvkg_compositor::engine::RenderCommand::Draw(routed) => {
1327 self.set_material(cvkg_core::DrawMaterial::Opaque);
1328 self.submit_routed(routed, current_target_id);
1329 }
1330 cvkg_compositor::engine::RenderCommand::PushOffscreen {
1331 source_layer,
1332 material,
1333 bounds,
1334 } => {
1335 current_target_id = Some(source_layer.0);
1336
1337 let width = (bounds.width).max(1.0) as u32;
1339 let height = (bounds.height).max(1.0) as u32;
1340 self.registry
1341 .allocate_offscreen(&self.device, source_layer.0, [width, height]);
1342
1343 if let cvkg_compositor::Material::ShaderEffect {
1344 effect_name,
1345 params_json: _,
1346 ..
1347 } = material
1348 {
1349 active_offscreens.push(crate::types::OffscreenEffectConfig {
1350 target_id: source_layer.0,
1351 effect: effect_name.clone(),
1352 blend_mode: 0, effect_args: [0.0; 16], });
1355 }
1356 }
1357 cvkg_compositor::engine::RenderCommand::PopOffscreen => {
1358 current_target_id = None;
1359 }
1360 }
1361 }
1362 self.active_offscreens = active_offscreens;
1363
1364 let mut sorted_glass: Vec<_> = buckets.glass_commands.iter().collect();
1366 sorted_glass.sort_by_key(|cmd| match cmd {
1367 cvkg_compositor::engine::RenderCommand::Draw(routed) => {
1368 (routed.z_index as i64, routed.draw_order as i64)
1369 }
1370 _ => (0, 0),
1371 });
1372 for cmd in sorted_glass {
1373 if let cvkg_compositor::engine::RenderCommand::Draw(routed) = cmd {
1374 self.set_material(Self::convert_compositor_material(&routed.material));
1375 self.submit_routed(routed, None);
1376 }
1377 }
1378
1379 let mut sorted_overlay: Vec<_> = buckets.overlay_commands.iter().collect();
1381 sorted_overlay.sort_by_key(|cmd| match cmd {
1382 cvkg_compositor::engine::RenderCommand::Draw(routed) => {
1383 (routed.z_index as i64, routed.draw_order as i64)
1384 }
1385 _ => (0, 0),
1386 });
1387 for cmd in sorted_overlay {
1388 if let cvkg_compositor::engine::RenderCommand::Draw(routed) = cmd {
1389 self.set_material(cvkg_core::DrawMaterial::TopUI);
1390 self.submit_routed(routed, None);
1391 }
1392 }
1393 }
1394
1395 pub(crate) fn submit_routed(
1397 &mut self,
1398 routed: &cvkg_compositor::RoutedDrawCommand,
1399 target_id: Option<u64>,
1400 ) {
1401 let cmd = &routed.command;
1402 if cmd.index_count == 0 {
1403 return;
1404 }
1405 let material = Self::convert_compositor_material(&routed.material);
1406 self.draw_calls.push(DrawCall {
1407 texture_id: cmd.texture_id,
1408 scissor_rect: cmd.scissor_rect,
1409 index_start: cmd.index_start,
1410 index_count: cmd.index_count,
1411 instance_count: 1,
1412 material,
1413 target_id,
1414 instance_start: cmd.instance_id,
1415 draw_order: 0,
1416 });
1417 }
1418
1419 pub(crate) fn apply_opacity(&self, mut color: [f32; 4]) -> [f32; 4] {
1421 if let Some(&alpha) = self.opacity_stack.last() {
1422 color[3] *= alpha;
1423 }
1424 color
1425 }
1426
1427 pub(crate) fn resolve_material(material_id: u32) -> cvkg_core::DrawMaterial {
1430 Self::resolve_material_with_context(material_id, &cvkg_core::DrawMaterial::Opaque)
1431 }
1432
1433 pub(crate) fn resolve_material_with_context(
1437 material_id: u32,
1438 current: &cvkg_core::DrawMaterial,
1439 ) -> cvkg_core::DrawMaterial {
1440 use crate::renderer::material_id::*;
1441
1442 if matches!(current, cvkg_core::DrawMaterial::TopUI) && material_id != GLASS {
1445 return cvkg_core::DrawMaterial::TopUI;
1446 }
1447
1448 if let cvkg_core::DrawMaterial::Blend { mode } = current
1450 && material_id == 0
1451 {
1452 return cvkg_core::DrawMaterial::Blend { mode: *mode };
1453 }
1454
1455 match material_id {
1456 GLASS => {
1457 if let cvkg_core::DrawMaterial::Glass {
1458 blur_radius,
1459 ior_override,
1460 glass_intensity,
1461 } = current
1462 {
1463 cvkg_core::DrawMaterial::Glass {
1464 blur_radius: *blur_radius,
1465 ior_override: *ior_override,
1466 glass_intensity: *glass_intensity,
1467 }
1468 } else {
1469 cvkg_core::DrawMaterial::Glass {
1470 blur_radius: 20.0,
1471 ior_override: 0.0,
1472 glass_intensity: 1.0,
1473 }
1474 }
1475 }
1476 TOP_UI => cvkg_core::DrawMaterial::TopUI,
1477 BLEND_START..=BLEND_END => cvkg_core::DrawMaterial::Blend {
1478 mode: (material_id - 7),
1479 },
1480 _ => cvkg_core::DrawMaterial::Opaque,
1481 }
1482 }
1483
1484 pub(crate) fn convert_compositor_material(
1487 mat: &cvkg_compositor::Material,
1488 ) -> cvkg_core::DrawMaterial {
1489 match mat {
1490 cvkg_compositor::Material::Glass { blur_radius, .. } => {
1491 cvkg_core::DrawMaterial::Glass {
1492 blur_radius: *blur_radius,
1493 ior_override: 0.0,
1494 glass_intensity: 1.0,
1495 }
1496 }
1497 cvkg_compositor::Material::Overlay => cvkg_core::DrawMaterial::TopUI,
1498 cvkg_compositor::Material::Multiply => cvkg_core::DrawMaterial::Blend { mode: 1 },
1499 cvkg_compositor::Material::Screen => cvkg_core::DrawMaterial::Blend { mode: 2 },
1500 cvkg_compositor::Material::BlendOverlay => cvkg_core::DrawMaterial::Blend { mode: 3 },
1501 cvkg_compositor::Material::Darken => cvkg_core::DrawMaterial::Blend { mode: 4 },
1502 cvkg_compositor::Material::Lighten => cvkg_core::DrawMaterial::Blend { mode: 5 },
1503 cvkg_compositor::Material::ColorDodge => cvkg_core::DrawMaterial::Blend { mode: 6 },
1504 cvkg_compositor::Material::ColorBurn => cvkg_core::DrawMaterial::Blend { mode: 7 },
1505 cvkg_compositor::Material::HardLight => cvkg_core::DrawMaterial::Blend { mode: 8 },
1506 cvkg_compositor::Material::SoftLight => cvkg_core::DrawMaterial::Blend { mode: 9 },
1507 cvkg_compositor::Material::Difference => cvkg_core::DrawMaterial::Blend { mode: 10 },
1508 cvkg_compositor::Material::Exclusion => cvkg_core::DrawMaterial::Blend { mode: 11 },
1509 cvkg_compositor::Material::Hue => cvkg_core::DrawMaterial::Blend { mode: 12 },
1510 cvkg_compositor::Material::Saturation => cvkg_core::DrawMaterial::Blend { mode: 13 },
1511 cvkg_compositor::Material::Color => cvkg_core::DrawMaterial::Blend { mode: 14 },
1512 cvkg_compositor::Material::Luminosity => cvkg_core::DrawMaterial::Blend { mode: 15 },
1513 cvkg_compositor::Material::Opaque => cvkg_core::DrawMaterial::Opaque,
1514 _ => cvkg_core::DrawMaterial::Opaque,
1515 }
1516 }
1517
1518 pub(crate) fn position_vertices(
1520 vertices: &mut [Vertex],
1521 view_box: Rect,
1522 rect: Rect,
1523 material_id: u32,
1524 clip: [f32; 4],
1525 snap: impl Fn(f32) -> f32,
1526 ) {
1527 for v in vertices.iter_mut() {
1528 let rel_x = (v.position[0] - view_box.x) / view_box.width;
1529 let rel_y = (v.position[1] - view_box.y) / view_box.height;
1530 v.position[0] = snap(rect.x + rel_x * rect.width);
1531 v.position[1] = snap(rect.y + rel_y * rect.height);
1532 v.position[2] = 0.0; v.logical = [v.position[0], v.position[1]];
1534 v.clip = clip;
1535 v.material_id = material_id;
1536 }
1537 }
1538
1539 pub(crate) fn emit_draw_call(
1541 renderer: &mut GpuRenderer,
1542 material: cvkg_core::DrawMaterial,
1543 texture_id: Option<u32>,
1544 scissor_rect: Rect,
1545 index_count: u32,
1546 base_vertex: u32,
1547 ) {
1548 let draw_order = renderer.current_draw_order;
1549 let (translation, scale_transform, rotation, _, _) = renderer.current_transform();
1550 let current_instance_data = InstanceData {
1551 translation,
1552 scale: scale_transform,
1553 rotation,
1554 blur_radius: 0.0,
1555 ior_override: 0.0,
1556 glass_intensity: 1.0,
1557 };
1558 let last_call = renderer.draw_calls.last();
1561 let needs_new_call = renderer.draw_calls.is_empty()
1562 || renderer.current_texture_id != texture_id
1563 || last_call.unwrap().scissor_rect != renderer.clip_stack.last().copied()
1564 || last_call.unwrap().material != material
1565 || {
1566 let last_material = last_call.unwrap().material;
1567 matches!((material, last_material),
1568 (cvkg_core::DrawMaterial::Glass { blur_radius: a, ior_override: b, glass_intensity: c },
1569 cvkg_core::DrawMaterial::Glass { blur_radius: d, ior_override: e, glass_intensity: f })
1570 if a != d || b != e || c != f)
1571 };
1572
1573 if needs_new_call {
1574 renderer.current_texture_id = texture_id;
1575 renderer.instance_data.push(current_instance_data);
1576 renderer.draw_calls.push(DrawCall {
1577 target_id: None,
1578 texture_id,
1579 scissor_rect: renderer.clip_stack.last().copied(),
1580 index_start: (renderer.indices.len() - index_count as usize) as u32,
1581 index_count,
1582 instance_count: 1,
1583 material,
1584 instance_start: (renderer.instance_data.len() - 1) as u32,
1585 draw_order: 0,
1586 });
1587 } else {
1588 renderer.instance_data.push(current_instance_data);
1590 if let Some(call) = renderer.draw_calls.last_mut() {
1591 call.instance_count += 1;
1592 }
1593 }
1594 }
1595
1596 pub async fn capture_frame(&self) -> Result<Vec<u8>, String> {
1598 let ctx = self
1599 .headless_context
1600 .as_ref()
1601 .ok_or("Headless context required for capture")?;
1602
1603 let u32_size = std::mem::size_of::<u32>() as u32;
1604 let width = ctx.width;
1605 let height = ctx.height;
1606 let bytes_per_row = width * u32_size;
1607 let padding = (256 - (bytes_per_row % 256)) % 256;
1608 let padded_bytes_per_row = bytes_per_row + padding;
1609
1610 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
1611 label: Some("Capture Buffer"),
1612 size: (padded_bytes_per_row as u64 * height as u64),
1613 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1614 mapped_at_creation: false,
1615 });
1616
1617 let mut encoder = self
1618 .device
1619 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1620 label: Some("Capture Encoder"),
1621 });
1622
1623 encoder.copy_texture_to_buffer(
1624 wgpu::TexelCopyTextureInfo {
1625 texture: &ctx.output_texture,
1626 mip_level: 0,
1627 origin: wgpu::Origin3d::ZERO,
1628 aspect: wgpu::TextureAspect::All,
1629 },
1630 wgpu::TexelCopyBufferInfo {
1631 buffer: &output_buffer,
1632 layout: wgpu::TexelCopyBufferLayout {
1633 offset: 0,
1634 bytes_per_row: Some(padded_bytes_per_row),
1635 rows_per_image: Some(height),
1636 },
1637 },
1638 wgpu::Extent3d {
1639 width,
1640 height,
1641 depth_or_array_layers: 1,
1642 },
1643 );
1644
1645 self.queue.submit(Some(encoder.finish()));
1646
1647 let buffer_slice = output_buffer.slice(..);
1648 let (sender, receiver) = futures::channel::oneshot::channel();
1649 buffer_slice.map_async(wgpu::MapMode::Read, move |v| {
1650 let _ = sender.send(v);
1651 });
1652
1653 let _ = self.device.poll(wgpu::PollType::Wait {
1654 submission_index: None,
1655 timeout: None,
1656 });
1657
1658 if let Ok(Ok(_)) = receiver.await {
1659 let data = buffer_slice.get_mapped_range();
1660 let mut result = Vec::with_capacity((width * height * 4) as usize);
1661
1662 for y in 0..height {
1663 let start = (y * padded_bytes_per_row) as usize;
1664 let end = start + bytes_per_row as usize;
1665 result.extend_from_slice(&data[start..end]);
1666 }
1667
1668 log::trace!(
1669 "[GPU] capture_frame: data len={}, first 4 bytes={:?}",
1670 data.len(),
1671 &data[0..4.min(data.len())]
1672 );
1673
1674 drop(data);
1675 output_buffer.unmap();
1676 Ok(result)
1677 } else {
1678 Err("Failed to capture frame".to_string())
1679 }
1680 }
1681
1682 fn hash_gradient_stops(stops: &[[f32; 4]]) -> u64 {
1685 use std::hash::{Hash, Hasher};
1686 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1687 for stop in stops {
1688 for v in stop {
1689 v.to_bits().hash(&mut hasher);
1690 }
1691 }
1692 hasher.finish()
1693 }
1694
1695 #[allow(clippy::collapsible_if)]
1699 pub(crate) fn upload_gradient_stops(&mut self, stops: &[[f32; 4]]) {
1700 if stops.is_empty() {
1701 return;
1702 }
1703
1704 let hash = Self::hash_gradient_stops(stops);
1705
1706 if hash == self.gradient_stops_hash {
1708 if let Some((_, _, bg)) = self.gradient_texture_cache.get(&hash) {
1709 self.gradient_bind_group = bg.clone();
1710 return;
1711 }
1712 }
1713
1714 if let Some((_, view, bg)) = self.gradient_texture_cache.get(&hash) {
1716 self.gradient_stop_texture = view.texture().clone();
1717 self.gradient_stop_texture_view = view.clone();
1718 self.gradient_bind_group = bg.clone();
1719 self.gradient_stops_hash = hash;
1720 return;
1721 }
1722
1723 let max_stops = 32u32;
1725 let num_stops = stops.len().min(max_stops as usize) as u32;
1726
1727 let mut data = vec![0u8; (max_stops as usize) * 4];
1729 for (i, stop) in stops.iter().enumerate().take(max_stops as usize) {
1730 let r = (stop[0].clamp(0.0, 1.0) * 255.0).round() as u8;
1732 let g = (stop[1].clamp(0.0, 1.0) * 255.0).round() as u8;
1733 let b = (stop[2].clamp(0.0, 1.0) * 255.0).round() as u8;
1734 let a = (stop[3].clamp(0.0, 1.0) * 255.0).round() as u8;
1735 #[allow(clippy::identity_op)]
1738 {
1739 data[i * 4 + 0] = r;
1740 data[i * 4 + 1] = g;
1741 data[i * 4 + 2] = b;
1742 data[i * 4 + 3] = a;
1743 }
1744 }
1745
1746 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
1748 label: Some("Gradient Stops Texture"),
1749 size: wgpu::Extent3d {
1750 width: max_stops,
1751 height: 1,
1752 depth_or_array_layers: 1,
1753 },
1754 mip_level_count: 1,
1755 sample_count: 1,
1756 dimension: wgpu::TextureDimension::D2,
1757 format: wgpu::TextureFormat::Rgba8Unorm,
1758 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1759 view_formats: &[],
1760 });
1761
1762 self.queue.write_texture(
1763 wgpu::TexelCopyTextureInfo {
1764 texture: &texture,
1765 mip_level: 0,
1766 origin: wgpu::Origin3d::ZERO,
1767 aspect: wgpu::TextureAspect::All,
1768 },
1769 &data,
1770 wgpu::TexelCopyBufferLayout {
1771 offset: 0,
1772 bytes_per_row: Some(max_stops * 4),
1773 rows_per_image: Some(1),
1774 },
1775 wgpu::Extent3d {
1776 width: max_stops,
1777 height: 1,
1778 depth_or_array_layers: 1,
1779 },
1780 );
1781
1782 let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1783
1784 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1785 layout: &self.gradient_bind_group_layout,
1786 entries: &[
1787 wgpu::BindGroupEntry {
1788 binding: 0,
1789 resource: wgpu::BindingResource::TextureView(&texture_view),
1790 },
1791 wgpu::BindGroupEntry {
1792 binding: 1,
1793 resource: wgpu::BindingResource::Sampler(&self.dummy_sampler),
1794 },
1795 ],
1796 label: Some("Gradient Bind Group"),
1797 });
1798
1799 self.gradient_stops_hash = hash;
1801 self.gradient_stop_texture = texture.clone();
1802 self.gradient_stop_texture_view = texture_view.clone();
1803 self.gradient_bind_group = bind_group.clone();
1804 self.gradient_texture_cache
1805 .insert(hash, (texture, texture_view, bind_group));
1806 }
1807
1808 pub fn draw_gradient_multi(
1814 &mut self,
1815 rect: Rect,
1816 stops: &[[f32; 4]],
1817 angle: f32,
1818 is_radial: bool,
1819 ) {
1820 if stops.is_empty() {
1821 return;
1822 }
1823
1824 self.upload_gradient_stops(stops);
1826
1827 let num_stops = stops.len().min(32) as f32;
1828 let material_id = if is_radial { 31u32 } else { 30u32 };
1829
1830 let white = [1.0f32, 1.0, 1.0, 1.0];
1832
1833 let slice = [angle, num_stops, 0.0, 1.0];
1835
1836 self.fill_rect_with_full_params_and_slice(
1837 rect,
1838 white,
1839 material_id,
1840 None,
1841 0.0,
1842 Rect {
1843 x: 0.0,
1844 y: 0.0,
1845 width: 1.0,
1846 height: 1.0,
1847 },
1848 slice,
1849 [0.0, 0.0],
1850 );
1851 }
1852}