1use super::GpuRenderer;
2use super::context_helpers::create_surface_context;
3use crate::types::{DrawCall, MAX_PARTICLES};
4use crate::vertex::{Vertex, InstanceData};
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 = [
70 self.current_width() as f32,
71 self.current_height() as f32,
72 ];
73 let time_uniform: [f32; 4] = [
74 current_time,
75 resolution[0],
76 resolution[1],
77 0.0, ];
79 self.queue.write_buffer(
80 &self.volumetric_uniform_buffer,
81 0,
82 bytemuck::cast_slice(&time_uniform),
83 );
84 self.frame_generation += 1;
86 const MAX_MEMO_AGE: u64 = 1000;
88 if self.frame_generation > MAX_MEMO_AGE {
89 let cutoff = self.frame_generation - MAX_MEMO_AGE;
90 self.memo_cache
91 .retain(|_, entry| entry.frame_gen >= cutoff);
92 }
93 self.last_frame_start = std::time::Instant::now();
94 self.telemetry.draw_calls = 0;
95 self.telemetry.vertices = 0;
96 }
97
98 pub fn begin_frame(&mut self, window_id: winit::window::WindowId) -> wgpu::CommandEncoder {
100 self.begin_frame_internal(window_id, true)
101 }
102
103 pub fn begin_frame_reuse(&mut self, window_id: winit::window::WindowId) -> wgpu::CommandEncoder {
106 self.begin_frame_internal(window_id, false)
107 }
108
109 fn begin_frame_internal(&mut self, window_id: winit::window::WindowId, reset_state: bool) -> wgpu::CommandEncoder {
110 if let Some(rx) = &self.ai_material_rx {
112 while let Ok(res) = rx.try_recv() {
113 match res {
114 Ok(_) => log::info!("[Surtr] Received AI generated material"),
115 Err(e) => log::warn!("[Surtr] AI material generation error: {:?}", e),
116 }
117 }
118 }
119
120 self.staging_belt.recall();
124 self.current_window = Some(window_id);
125 if reset_state {
126 self.reset_frame_state();
127 }
128
129 let ctx = self
130 .surfaces
131 .get(&window_id)
132 .expect("Window not registered");
133 let time = self.start_time.elapsed().as_secs_f32();
134 let logical_w = ctx.config.width as f32 / ctx.scale_factor;
135 let logical_h = ctx.config.height as f32 / ctx.scale_factor;
136 let dt = time - self.current_scene.time;
137 self.current_scene.time = time;
138 self.current_scene.delta_time = dt;
139 self.current_scene.resolution = [logical_w, logical_h];
140 self.current_scene.scale_factor = ctx.scale_factor;
141 self.current_scene.proj =
142 glam::Mat4::orthographic_lh(0.0, logical_w, logical_h, 0.0, -1000.0, 1000.0);
143
144 self.queue.write_buffer(
145 &self.scene_buffer,
146 0,
147 bytemuck::bytes_of(&self.current_scene),
148 );
149
150 self.device
151 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
152 label: Some("Surtr Command Encoder"),
153 })
154 }
155
156 pub fn register_window(&mut self, window: Arc<winit::window::Window>) {
158 let size = window.inner_size();
159 let surface = self
160 .instance
161 .create_surface(window.clone())
162 .expect("Failed to create surface");
163 let caps = surface.get_capabilities(&self.adapter);
164 let format = caps.formats[0];
165
166 let present_mode = if caps.present_modes.contains(&wgpu::PresentMode::Mailbox) {
168 wgpu::PresentMode::Mailbox
169 } else {
170 log::warn!("[GPU] Mailbox not supported, falling back to Fifo (V-Sync)");
171 wgpu::PresentMode::Fifo
172 };
173
174 let alpha_mode = if caps
175 .alpha_modes
176 .contains(&wgpu::CompositeAlphaMode::PostMultiplied)
177 {
178 wgpu::CompositeAlphaMode::PostMultiplied
179 } else if caps
180 .alpha_modes
181 .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
182 {
183 wgpu::CompositeAlphaMode::PreMultiplied
184 } else {
185 caps.alpha_modes[0]
186 };
187
188 log::info!(
189 "[GPU] Configuring surface: {}x{} | {:?} | {:?}",
190 size.width,
191 size.height,
192 present_mode,
193 alpha_mode
194 );
195
196 let config = wgpu::SurfaceConfiguration {
197 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
198 format,
199 width: size.width,
200 height: size.height,
201 present_mode,
202 alpha_mode,
203 view_formats: vec![],
204 desired_maximum_frame_latency: 1,
205 };
206 surface.configure(&self.device, &config);
207
208 let ctx = create_surface_context(
209 &self.device,
210 surface,
211 config,
212 &self.env_bind_group_layout,
213 &self.texture_bind_group_layout,
214 window.scale_factor() as f32,
215 self.quality_level.msaa_sample_count(),
216 &mut self.registry,
217 );
218
219 self.surfaces.insert(window.id(), ctx);
220 }
221
222 pub(crate) fn shatter_internal(
223 &mut self,
224 rect: Rect,
225 pieces: u32,
226 force: f32,
227 color: [f32; 4],
228 material_id: u32,
229 ) {
230 let count = (pieces as f32).sqrt().ceil() as u32;
232 let dw = rect.width / count as f32;
233 let dh = rect.height / count as f32;
234
235 let c = self.apply_opacity(color);
236
237 let cx = rect.x + rect.width * 0.5;
238 let cy = rect.y + rect.height * 0.5;
239
240 for y in 0..count {
241 for x in 0..count {
242 let init_x = rect.x + x as f32 * dw;
243 let init_y = rect.y + y as f32 * dh;
244
245 let dx = (init_x + dw * 0.5) - cx;
247 let dy = (init_y + dh * 0.5) - cy;
248 let dist = (dx * dx + dy * dy).sqrt().max(1.0);
249
250 let nx = dx / dist;
252 let ny = dy / dist;
253
254 let hash =
256 ((x as f32 * 12.9898 + y as f32 * 78.233).sin().fract() * 43_758.547).fract();
257 let hash2 =
258 ((x as f32 * 37.11 + y as f32 * 149.87).sin().fract() * 23_412.19).fract();
259
260 let speed_var = 0.5 + hash * 1.5;
261 let angle = ny.atan2(nx) + (hash2 - 0.5) * 0.6;
262 let disp_x = angle.cos() * force * 50.0 * speed_var;
263 let disp_y = angle.sin() * force * 50.0 * speed_var;
264
265 let gravity = force * force * 20.0;
267
268 let scale_factor = (1.0 - (force / 6.0).min(1.0)).max(0.0);
271 let shard_w = dw * scale_factor;
272 let shard_h = dh * scale_factor;
273
274 let displaced_x = init_x + disp_x + (dw - shard_w) * 0.5;
275 let displaced_y = init_y + disp_y + gravity + (dh - shard_h) * 0.5;
276
277 let shard_rect = Rect {
278 x: displaced_x,
279 y: displaced_y,
280 width: shard_w,
281 height: shard_h,
282 };
283
284 let uv = Rect {
285 x: x as f32 / count as f32,
286 y: y as f32 / count as f32,
287 width: 1.0 / count as f32,
288 height: 1.0 / count as f32,
289 };
290
291 self.fill_rect_with_full_params(shard_rect, c, material_id, None, force, uv);
292 }
293 }
294 }
295
296 pub(crate) fn recursive_bolt(
297 &mut self,
298 from: [f32; 2],
299 to: [f32; 2],
300 depth: u32,
301 color: [f32; 4],
302 ) {
303 if depth == 0 {
304 self.draw_lightning_segment(from, to, color);
305 return;
306 }
307
308 let mid_x = (from[0] + to[0]) * 0.5;
309 let mid_y = (from[1] + to[1]) * 0.5;
310
311 let dx = to[0] - from[0];
312 let dy = to[1] - from[1];
313 let len = (dx * dx + dy * dy).sqrt();
314
315 if len < 1e-4 {
316 return;
317 }
318
319 let offset_scale = len * 0.15;
321 let seed = (from[0] * 12.9898 + from[1] * 78.233 + (depth as f32) * 37.11)
322 .sin()
323 .fract();
324 let offset_x = -dy / len * (seed - 0.5) * offset_scale;
325 let offset_y = dx / len * (seed - 0.5) * offset_scale;
326
327 let mid = [mid_x + offset_x, mid_y + offset_y];
328
329 self.recursive_bolt(from, mid, depth - 1, color);
330 self.recursive_bolt(mid, to, depth - 1, color);
331
332 if depth > 2 && seed > 0.8 {
334 let branch_to = [
335 mid[0] + offset_x * 2.0 + (seed * 100.0).sin() * 50.0,
336 mid[1] + offset_y * 2.0 + (seed * 100.0).cos() * 50.0,
337 ];
338 self.recursive_bolt(mid, branch_to, depth - 2, color);
339 }
340 }
341
342 pub(crate) fn draw_lightning_segment(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
343 let dx = to[0] - from[0];
344 let dy = to[1] - from[1];
345 let len = (dx * dx + dy * dy).sqrt();
346 if len < 0.001 {
347 return;
348 }
349
350 let glow_width = 32.0;
351 let core_width = 4.0;
352 let c = self.apply_opacity(color);
353
354 let gnx = -dy / len * glow_width * 0.5;
356 let gny = dx / len * glow_width * 0.5;
357 let gp1 = [from[0] + gnx, from[1] + gny];
358 let gp2 = [to[0] + gnx, to[1] + gny];
359 let gp3 = [to[0] - gnx, to[1] - gny];
360 let gp4 = [from[0] - gnx, from[1] - gny];
361 self.push_oriented_quad(
362 [gp1, gp2, gp3, gp4],
363 c,
364 9,
365 Rect {
366 x: 0.0,
367 y: 0.0,
368 width: 1.0,
369 height: 1.0,
370 },
371 );
372
373 let cnx = -dy / len * core_width * 0.5;
375 let cny = dx / len * core_width * 0.5;
376 let cp1 = [from[0] + cnx, from[1] + cny];
377 let cp2 = [to[0] + cnx, to[1] + cny];
378 let cp3 = [to[0] - cnx, to[1] - cny];
379 let cp4 = [from[0] - cnx, from[1] - cny];
380 self.push_oriented_quad(
381 [cp1, cp2, cp3, cp4],
382 [1.0, 1.0, 1.0, c[3]],
383 0,
384 Rect {
385 x: 0.0,
386 y: 0.0,
387 width: 1.0,
388 height: 1.0,
389 },
390 );
391 }
392
393 pub(crate) fn push_oriented_quad(
394 &mut self,
395 points: [[f32; 2]; 4],
396 color: [f32; 4],
397 material_id: u32,
398 uv_rect: Rect,
399 ) {
400 let scissor = self.clip_stack.last().copied();
401 let texture_id = None; let (translation, scale_transform, rotation, _, _) = self.current_transform();
404 let current_instance_data = InstanceData {
405 translation,
406 scale: scale_transform,
407 rotation,
408 blur_radius: 0.0,
409 ior_override: 0.0,
410 glass_intensity: 1.0,
411 };
412
413 let last_call = self.draw_calls.last();
416 let needs_new_call = self.draw_calls.is_empty()
417 || self.current_texture_id != texture_id
418 || last_call.unwrap().scissor_rect != scissor
419 || last_call.unwrap().material != Self::resolve_material_with_context(material_id, &self.current_draw_material)
420 || {
421 let last_material = last_call.unwrap().material;
422 let current_material = Self::resolve_material_with_context(material_id, &self.current_draw_material);
423 matches!((current_material, last_material),
424 (cvkg_core::DrawMaterial::Glass { blur_radius: a, ior_override: b, glass_intensity: c },
425 cvkg_core::DrawMaterial::Glass { blur_radius: d, ior_override: e, glass_intensity: f })
426 if a != d || b != e || c != f)
427 };
428
429 if needs_new_call {
430 self.current_texture_id = texture_id;
431 self.instance_data.push(current_instance_data);
432 self.draw_calls.push(DrawCall {
433 target_id: None,
434 texture_id,
435 scissor_rect: scissor,
436 index_start: self.indices.len() as u32,
437 index_count: 0,
438 instance_count: 1,
439 material: Self::resolve_material_with_context(material_id, &self.current_draw_material),
440 instance_start: (self.instance_data.len() - 1) as u32,
441 draw_order: 0,
442 });
443 } else {
444 self.instance_data.push(current_instance_data);
446 if let Some(call) = self.draw_calls.last_mut() {
447 call.instance_count += 1;
448 }
449 }
450
451 let uvs = [
452 [uv_rect.x, uv_rect.y],
453 [uv_rect.x + uv_rect.width, uv_rect.y],
454 [uv_rect.x + uv_rect.width, uv_rect.y + uv_rect.height],
455 [uv_rect.x, uv_rect.y + uv_rect.height],
456 ];
457
458 let rect = Rect {
459 x: points[0][0],
460 y: points[0][1],
461 width: 1.0,
462 height: 1.0,
463 };
464
465 for i in 0..4 {
466 let px = points[i][0];
467 let py = points[i][1];
468
469 self.vertices.push(Vertex {
470 position: [px, py, 0.0],
471 normal: [0.0, 0.0, 1.0],
472 uv: uvs[i],
473 color,
474 material_id,
475 radius: 0.0,
476 slice: [0.0, 0.0, 0.0, 1.0],
477 logical: [px - rect.x, py - rect.y],
478 size: [rect.width, rect.height],
479 clip: [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY],
480 tex_index: 0,
481 });
482 }
483
484 let base = self.vertices.len() as u32 - 4;
486 self.indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
487
488 if let Some(call) = self.draw_calls.last_mut() {
489 call.index_count += 6;
490 }
491 }
492
493 pub(crate) fn get_texture_id(&mut self, name: &str) -> Option<u32> {
494 self.texture_registry.get(name).copied()
495 }
496
497 pub fn fill_rect_with_mode(
499 &mut self,
500 rect: Rect,
501 color: [f32; 4],
502 material_id: u32,
503 texture_id: Option<u32>,
504 ) {
505 self.fill_rect_with_full_params(
506 rect,
507 color,
508 material_id,
509 texture_id,
510 0.0,
511 Rect {
512 x: 0.0,
513 y: 0.0,
514 width: 1.0,
515 height: 1.0,
516 },
517 );
518 }
519
520 pub(crate) fn fill_rect_with_full_params(
521 &mut self,
522 rect: Rect,
523 color: [f32; 4],
524 material_id: u32,
525 texture_id: Option<u32>,
526 radius: f32,
527 uv_rect: Rect,
528 ) {
529 if let Some(shadow) = self.shadow_stack.last().copied()
531 && shadow.color[3] > 0.001
532 {
533 let shadow_rect = Rect {
534 x: rect.x + shadow._offset[0],
535 y: rect.y + shadow._offset[1],
536 width: rect.width,
537 height: rect.height,
538 };
539 Renderer::draw_drop_shadow(
540 self,
541 shadow_rect,
542 radius,
543 shadow.color,
544 shadow.radius,
545 0.0, );
547 }
548
549 let slice = self
550 .slice_stack
551 .last()
552 .copied()
553 .map(|(a, o)| [a, o, 1.0, 1.0])
554 .unwrap_or([0.0, 0.0, 0.0, 1.0]);
555 self.fill_rect_with_full_params_and_slice(
556 rect,
557 color,
558 material_id,
559 texture_id,
560 radius,
561 uv_rect,
562 slice,
563 [0.0, 0.0],
564 );
565 }
566
567 #[allow(clippy::too_many_arguments)]
568 pub(crate) fn fill_rect_with_full_params_and_slice(
569 &mut self,
570 mut rect: Rect,
571 color: [f32; 4],
572 material_id: u32,
573 texture_id: Option<u32>,
574 radius: f32,
575 uv_rect: Rect,
576 slice: [f32; 4],
577 _glyph_time: [f32; 2],
578 ) {
579 if material_id != crate::renderer::material_id::GLASS {
582 let scale = self.current_scale_factor();
583 let snap = |v: f32| (v * scale).round() / scale;
584 rect.x = snap(rect.x);
585 rect.y = snap(rect.y);
586 rect.width = snap(rect.width);
587 rect.height = snap(rect.height);
588 }
589
590 let scissor = self.clip_stack.last().copied();
591
592 let material = Self::resolve_material_with_context(material_id, &self.current_draw_material);
593
594 let (translation, scale_transform, rotation, _, _) = self.current_transform();
595 let (blur_radius, ior_override, glass_intensity) = if let cvkg_core::DrawMaterial::Glass {
596 blur_radius,
597 ior_override,
598 glass_intensity,
599 } = material
600 {
601 (blur_radius, ior_override, glass_intensity)
602 } else {
603 (0.0, 0.0, 1.0)
604 };
605
606 let current_instance_data = InstanceData {
607 translation,
608 scale: scale_transform,
609 rotation,
610 blur_radius,
611 ior_override,
612 glass_intensity,
613 };
614
615 let last_call = self.draw_calls.last();
622 let needs_new_call = self.draw_calls.is_empty()
623 || last_call.unwrap().scissor_rect != scissor
624 || last_call.unwrap().material != material
625 || last_call.unwrap().texture_id != self.current_texture_id
626 || {
627 let last_material = last_call.unwrap().material;
629 matches!((material, last_material),
630 (cvkg_core::DrawMaterial::Glass { blur_radius: a, ior_override: b, glass_intensity: c },
631 cvkg_core::DrawMaterial::Glass { blur_radius: d, ior_override: e, glass_intensity: f })
632 if a != d || b != e || c != f)
633 };
634
635 if needs_new_call {
636 self.current_texture_id = Some(0); self.instance_data.push(current_instance_data);
638 self.draw_calls.push(DrawCall {
639 target_id: None,
640 texture_id: self.current_texture_id,
641 scissor_rect: scissor,
642 index_start: self.indices.len() as u32,
643 index_count: 0,
644 instance_count: 1,
645 material,
646 instance_start: (self.instance_data.len() - 1) as u32,
647 draw_order: 0,
648 });
649 } else {
650 self.instance_data.push(current_instance_data);
652 if let Some(call) = self.draw_calls.last_mut() {
653 call.instance_count += 1;
654 }
655 }
656
657 let scale = self.current_scale_factor();
658 let snap = |v: f32| (v * scale).round() / scale;
659
660 let base_idx = self.vertices.len() as u32;
661 let x1 = snap(rect.x);
662 let y1 = snap(rect.y);
663 let x2 = snap(rect.x + rect.width);
664 let y2 = snap(rect.y + rect.height);
665 let z = self.current_z;
666 let normal = [0.0, 0.0, 1.0];
667 let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
668 x: -10000.0,
669 y: -10000.0,
670 width: 20000.0,
671 height: 20000.0,
672 });
673 let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
674
675 let tex_index = texture_id.unwrap_or(0);
676
677 self.vertices.push(Vertex {
678 position: [x1, y1, z],
679 normal,
680 uv: [uv_rect.x, uv_rect.y],
681 color,
682 material_id,
683 radius,
684 slice,
685 logical: [0.0, 0.0],
686 size: [rect.width, rect.height],
687 clip,
688 tex_index,
689 });
690 self.vertices.push(Vertex {
691 position: [x2, y1, z],
692 normal,
693 uv: [uv_rect.x + uv_rect.width, uv_rect.y],
694 color,
695 material_id,
696 radius,
697 slice,
698 logical: [rect.width, 0.0],
699 size: [rect.width, rect.height],
700 clip,
701 tex_index,
702 });
703 self.vertices.push(Vertex {
704 position: [x2, y2, z],
705 normal,
706 uv: [uv_rect.x + uv_rect.width, uv_rect.y + uv_rect.height],
707 color,
708 material_id,
709 radius,
710 slice,
711 logical: [rect.width, rect.height],
712 size: [rect.width, rect.height],
713 clip,
714 tex_index,
715 });
716 self.vertices.push(Vertex {
717 position: [x1, y2, z],
718 normal,
719 uv: [uv_rect.x, uv_rect.y + uv_rect.height],
720 color,
721 material_id,
722 radius,
723 slice,
724 logical: [0.0, rect.height],
725 size: [rect.width, rect.height],
726 clip,
727 tex_index,
728 });
729
730 self.indices.extend_from_slice(&[
731 base_idx,
732 base_idx + 1,
733 base_idx + 2,
734 base_idx,
735 base_idx + 2,
736 base_idx + 3,
737 ]);
738
739 if let Some(call) = self.draw_calls.last_mut() {
740 call.index_count += 6;
741 }
742 }
743
744 pub fn end_frame(&mut self, mut encoder: wgpu::CommandEncoder) {
753 struct ActiveFrameResources {
754 surface_texture: Option<wgpu::SurfaceTexture>,
755 target_view: wgpu::TextureView,
756 scene_texture: wgpu::TextureView,
757 scene_msaa_texture: wgpu::TextureView,
758 depth_texture_view: wgpu::TextureView,
759 blur_env_bind_group_a: wgpu::BindGroup,
760 blur_env_bind_group_b: wgpu::BindGroup,
761 bloom_env_bind_group_a: wgpu::BindGroup,
762 bloom_env_bind_group_b: wgpu::BindGroup,
763 }
764
765 let res = if let Some(window_id) = self.current_window {
766 let Some(ctx) = self.surfaces.get(&window_id) else {
767 log::error!("[GPU] Missing surface context for end_frame");
768 return;
769 };
770 let frame = match ctx.surface.get_current_texture() {
771 wgpu::CurrentSurfaceTexture::Success(t) => t,
772 wgpu::CurrentSurfaceTexture::Suboptimal(t) => {
773 ctx.surface.configure(&self.device, &ctx.config);
774 t
775 }
776 other => {
777 log::warn!(
778 "[GPU] Surface texture acquisition failed ({:?}), reconfiguring surface",
779 other
780 );
781 ctx.surface.configure(&self.device, &ctx.config);
782 match ctx.surface.get_current_texture() {
784 wgpu::CurrentSurfaceTexture::Success(t) => t,
785 wgpu::CurrentSurfaceTexture::Suboptimal(t) => {
786 ctx.surface.configure(&self.device, &ctx.config);
787 t
788 }
789 retry_failed => {
790 log::error!(
791 "[GPU] Surface texture retry also failed ({:?}), skipping frame",
792 retry_failed
793 );
794 self.queue.submit(std::iter::once(encoder.finish()));
795 return;
796 }
797 }
798 }
799 };
800 let view = frame
801 .texture
802 .create_view(&wgpu::TextureViewDescriptor::default());
803
804 ActiveFrameResources {
805 surface_texture: Some(frame),
806 target_view: view,
807 scene_texture: ctx.scene_texture.clone(),
808 scene_msaa_texture: ctx.scene_msaa_texture.clone(),
809 depth_texture_view: ctx.depth_texture_view.clone(),
810 blur_env_bind_group_a: ctx.blur_env_bind_group_a.clone(),
811 blur_env_bind_group_b: ctx.blur_env_bind_group_b.clone(),
812 bloom_env_bind_group_a: ctx.bloom_env_bind_group_a.clone(),
813 bloom_env_bind_group_b: ctx.bloom_env_bind_group_b.clone(),
814 }
815 } else {
816 let Some(ctx) = self.headless_context.as_ref() else {
817 log::error!("[GPU] No headless context for end_frame");
818 return;
819 };
820
821 ActiveFrameResources {
822 surface_texture: None,
823 target_view: ctx.output_view.clone(),
824 scene_texture: ctx.scene_texture.clone(),
825 scene_msaa_texture: ctx.scene_msaa_texture.clone(),
826 depth_texture_view: ctx.depth_texture_view.clone(),
827 blur_env_bind_group_a: ctx.blur_env_bind_group_a.clone(),
828 blur_env_bind_group_b: ctx.blur_env_bind_group_b.clone(),
829 bloom_env_bind_group_a: ctx.bloom_env_bind_group_a.clone(),
830 bloom_env_bind_group_b: ctx.bloom_env_bind_group_b.clone(),
831 }
832 };
833
834 if !self.frame_rendered && (!self.vertices.is_empty() || !self.indices.is_empty()) {
837 log::debug!("[GPU] Auto-flushing staging belt in end_frame (render_frame was not called)");
838 let mut staging_encoder =
839 self.device
840 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
841 label: Some("Surtr Auto-Flush Staging Encoder"),
842 });
843 if !self.vertices.is_empty() {
844 let v_bytes = bytemuck::cast_slice(&self.vertices);
845 self.staging_belt
846 .write_buffer(
847 &mut staging_encoder,
848 &self.geometry_buffers.vertex_buffer,
849 0,
850 wgpu::BufferSize::new(v_bytes.len() as u64).unwrap(),
851 )
852 .copy_from_slice(v_bytes);
853 }
854 if !self.indices.is_empty() {
855 let i_bytes = bytemuck::cast_slice(&self.indices);
856 self.staging_belt
857 .write_buffer(
858 &mut staging_encoder,
859 &self.geometry_buffers.index_buffer,
860 0,
861 wgpu::BufferSize::new(i_bytes.len() as u64).unwrap(),
862 )
863 .copy_from_slice(i_bytes);
864 }
865 if !self.instance_data.is_empty() {
866 let inst_bytes = bytemuck::cast_slice(&self.instance_data);
867 self.staging_belt
868 .write_buffer(
869 &mut staging_encoder,
870 &self.geometry_buffers.instance_buffer,
871 0,
872 wgpu::BufferSize::new(inst_bytes.len() as u64).unwrap(),
873 )
874 .copy_from_slice(inst_bytes);
875 }
876 self.staging_belt.finish();
877 self.staging_command_buffers.push(staging_encoder.finish());
878 }
879
880 let has_glass = self
882 .draw_calls
883 .iter()
884 .any(|c| matches!(c.material, cvkg_core::DrawMaterial::Glass { .. }));
885 let has_bloom = self.bloom_enabled;
886 let has_accessibility =
887 self.color_blind_mode != crate::color_blindness::ColorBlindMode::Normal;
888
889 let (blur_id, bloom_id) = if let Some(window_id) = self.current_window {
899 let ctx = self.surfaces.get(&window_id).unwrap();
900 (ctx.blur_tex_a, ctx.bloom_tex_a)
901 } else {
902 let ctx = self.headless_context.as_ref().unwrap();
903 (ctx.blur_tex_a, ctx.bloom_tex_a)
904 };
905 self.registry.alias(crate::kvasir::nodes::RES_BLUR_A, blur_id);
906 self.registry.alias(crate::kvasir::nodes::RES_BLOOM_A, bloom_id);
907 self.registry
908 .alias_view(crate::kvasir::nodes::RES_SCENE, res.scene_texture.clone());
909 self.registry.alias_view(
910 crate::kvasir::nodes::RES_SCENE_MSAA,
911 res.scene_msaa_texture.clone(),
912 );
913
914 let scale = self.current_scale_factor();
915 let scale_bits = scale.to_bits();
916 let active_offscreens_count = self.active_offscreens.len();
917 let portal_regions_count = self.portal_regions.len();
918 let width = self.current_width();
919 let height = self.current_height();
920 let has_volumetric = self.volumetric_enabled;
921
922 let mut offscreen_hash: u64 = 0;
924 for offscreen in &self.active_offscreens {
925 offscreen_hash = offscreen_hash.wrapping_add(
926 offscreen.target_id.wrapping_mul(31)
927 ^ (offscreen.blend_mode as u64).wrapping_mul(17)
928 );
929 }
930 let mut portal_hash: u64 = 0;
931 for region in &self.portal_regions {
932 portal_hash = portal_hash.wrapping_add(
933 (region.x.to_bits() as u64).wrapping_mul(7)
934 .wrapping_add((region.y.to_bits() as u64).wrapping_mul(13))
935 .wrapping_add((region.width.to_bits() as u64).wrapping_mul(19))
936 .wrapping_add((region.height.to_bits() as u64).wrapping_mul(23))
937 );
938 }
939
940 let use_cache = if let Some(ref cached) = self.cached_graph_plan {
941 cached.matches(
942 has_glass,
943 has_bloom,
944 has_accessibility,
945 has_volumetric,
946 active_offscreens_count,
947 offscreen_hash,
948 portal_regions_count,
949 portal_hash,
950 width,
951 height,
952 scale_bits,
953 self.material_compilation_hash,
954 )
955 } else {
956 false
957 };
958
959 if !use_cache {
960 let render_graph = crate::kvasir::nodes::build_render_graph(&crate::kvasir::nodes::RenderGraphConfig {
961 has_glass,
962 has_bloom,
963 has_accessibility,
964 has_volumetric,
965 active_offscreens: &self.active_offscreens,
966 portal_regions: &self.portal_regions.iter().cloned().collect::<Vec<_>>(),
967 width,
968 height,
969 scale,
970 });
971 let planner = crate::kvasir::planner::ExecutionPlanner::new(&render_graph);
972 let compiled_plan = match planner.compile() {
973 Ok(plan) => plan,
974 Err(e) => {
975 log::error!(
976 "[Kvasir] Render graph compilation failed ({}), skipping render passes",
977 e
978 );
979 if let Some(surface_texture) = res.surface_texture {
981 surface_texture.present();
982 }
983 return;
984 }
985 };
986
987 self.cached_graph_plan = Some(crate::kvasir::graph_cache::CachedGraphPlan {
989 has_glass,
990 has_bloom,
991 has_accessibility,
992 has_volumetric,
993 active_offscreens_count,
994 offscreen_content_hash: offscreen_hash,
995 portal_regions_count,
996 portal_content_hash: portal_hash,
997 width,
998 height,
999 scale_bits,
1000 material_compilation_hash: self.material_compilation_hash,
1001 graph: render_graph,
1002 plan: compiled_plan,
1003 });
1004 }
1005
1006 let cached = self.cached_graph_plan.as_ref().unwrap();
1007 let frame_start = self.last_frame_start;
1008 let budget_ms = self.frame_budget.target_ms;
1009 let allow_degradation = self.frame_budget.allow_degradation;
1010
1011 for &node_key in &cached.plan {
1012 if allow_degradation && budget_ms > 0.0 {
1026 let elapsed_ms = frame_start.elapsed().as_secs_f32() * 1000.0;
1027 if elapsed_ms > budget_ms {
1028 if let Some(node) = cached.graph.node(node_key) {
1029 match node.pass_id() {
1030 crate::kvasir::nodes::PassId::BloomExtract
1031 | crate::kvasir::nodes::PassId::BloomBlur
1032 | crate::kvasir::nodes::PassId::Volumetric => {
1033 log::trace!(
1034 "[Kvasir] Skipping {} (over budget: {:.1}ms > {:.1}ms)",
1035 node.label(),
1036 elapsed_ms,
1037 budget_ms
1038 );
1039 continue;
1040 }
1041 _ => {} }
1044 }
1045 }
1046 }
1047 if let Some(node) = cached.graph.node(node_key) {
1048 log::trace!("[Kvasir] Executing node: {}", node.label());
1049 let mut ctx = crate::kvasir::node::ExecutionContext {
1050 device: &self.device,
1051 queue: &self.queue,
1052 encoder: &mut encoder,
1053 registry: &self.registry,
1054 renderer: self,
1055 target_view: &res.target_view,
1056 depth_view: &res.depth_texture_view,
1057 blur_env_bind_group_a: &res.blur_env_bind_group_a,
1058 blur_env_bind_group_b: &res.blur_env_bind_group_b,
1059 bloom_env_bind_group_a: &res.bloom_env_bind_group_a,
1060 bloom_env_bind_group_b: &res.bloom_env_bind_group_b,
1061 scale_factor: scale,
1062 };
1063 node.execute(&mut ctx);
1064 }
1065 }
1066
1067 if !self.particles.staging.is_empty() || self.particles.count > 0 {
1071 if !self.particles.staging.is_empty() {
1073 let write_start = self.particles.write_head as usize;
1074 let write_count = self.particles.staging.len();
1075 let max = MAX_PARTICLES;
1076
1077 let effective_count = write_count.min(max);
1086 let drop_count = write_count - effective_count;
1087
1088 let first_chunk = (max - write_start).min(effective_count);
1090 let bytes = bytemuck::cast_slice(&self.particles.staging[drop_count..drop_count + first_chunk]);
1091 self.queue.write_buffer(
1092 &self.particle_buffer,
1093 (write_start * std::mem::size_of::<crate::types::GpuParticle>()) as u64,
1094 bytes,
1095 );
1096 if first_chunk < effective_count {
1097 let remaining = effective_count - first_chunk;
1098 let bytes2 = bytemuck::cast_slice(&self.particles.staging[drop_count + first_chunk..drop_count + first_chunk + remaining]);
1099 self.queue.write_buffer(
1100 &self.particle_buffer,
1101 0,
1102 bytes2,
1103 );
1104 self.particles.write_head = remaining as u32;
1105 } else {
1106 self.particles.write_head =
1107 ((write_start + effective_count) % max) as u32;
1108 }
1109 self.particles.count = (self.particles.count as usize + effective_count)
1110 .min(max) as u32;
1111 self.particles.staging.clear();
1112
1113 self.particle_render_bind_group = None;
1115 }
1116
1117 let dt = self.current_scene.delta_time;
1119 let uniforms = crate::types::ParticleUniforms { dt, _pad: [0.0; 7] };
1120 self.queue.write_buffer(
1121 &self.particle_uniform_buffer,
1122 0,
1123 bytemuck::bytes_of(&uniforms),
1124 );
1125
1126 let compute_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1127 label: Some("Particle Compute BG"),
1128 layout: &self.particle_compute_bgl,
1129 entries: &[
1130 wgpu::BindGroupEntry {
1131 binding: 0,
1132 resource: self.particle_buffer.as_entire_binding(),
1133 },
1134 wgpu::BindGroupEntry {
1135 binding: 1,
1136 resource: self.particle_uniform_buffer.as_entire_binding(),
1137 },
1138 ],
1139 });
1140
1141 let mut compute_encoder =
1142 self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1143 label: Some("Particle Compute Encoder"),
1144 });
1145 {
1146 let mut cpass = compute_encoder.begin_compute_pass(
1147 &wgpu::ComputePassDescriptor {
1148 label: Some("Particle Integration"),
1149 ..Default::default()
1150 },
1151 );
1152 cpass.set_pipeline(&self.particle_compute_pipeline);
1153 cpass.set_bind_group(0, &compute_bind_group, &[]);
1154 let workgroups = ((self.particles.count + 63) / 64).max(1);
1155 cpass.dispatch_workgroups(workgroups, 1, 1);
1156 }
1157 self.staging_command_buffers.push(compute_encoder.finish());
1158 }
1159
1160 if self.particles.count > 0
1162 && self.particles.last_compact.elapsed().as_secs_f32() > 2.0
1163 {
1164 self.particles.last_compact = std::time::Instant::now();
1165 let read_size =
1167 (self.particles.count as usize * std::mem::size_of::<crate::types::GpuParticle>())
1168 as u64;
1169 let staging_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
1170 label: Some("Particle Compact Staging"),
1171 size: read_size,
1172 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1173 mapped_at_creation: false,
1174 });
1175 let mut compact_encoder =
1176 self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1177 label: Some("Particle Compact Copy"),
1178 });
1179 compact_encoder.copy_buffer_to_buffer(
1180 &self.particle_buffer,
1181 0,
1182 &staging_buf,
1183 0,
1184 read_size,
1185 );
1186 self.staging_command_buffers.push(compact_encoder.finish());
1187 }
1192
1193 if self.particles.count > 0 {
1197 if self.particle_render_bind_group.is_none() {
1199 self.particle_render_bind_group =
1200 Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1201 label: Some("Particle Render BG"),
1202 layout: &self.particle_render_bgl,
1203 entries: &[wgpu::BindGroupEntry {
1204 binding: 0,
1205 resource: self.particle_buffer.as_entire_binding(),
1206 }],
1207 }));
1208 }
1209 if let Some(bg) = &self.particle_render_bind_group {
1210 let mut render_encoder =
1211 self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1212 label: Some("Particle Render Encoder"),
1213 });
1214 {
1215 let mut rpass = render_encoder.begin_render_pass(
1216 &wgpu::RenderPassDescriptor {
1217 label: Some("Particle Render"),
1218 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1219 view: &res.target_view,
1220 resolve_target: None,
1221 ops: wgpu::Operations {
1222 load: wgpu::LoadOp::Load,
1223 store: wgpu::StoreOp::Store,
1224 },
1225 depth_slice: None,
1226 })],
1227 depth_stencil_attachment: None,
1228 timestamp_writes: None,
1229 occlusion_query_set: None,
1230 multiview_mask: None,
1231 },
1232 );
1233 rpass.set_pipeline(&self.particle_render_pipeline);
1234 rpass.set_bind_group(0, bg, &[]);
1235 rpass.draw(0..self.particles.count, 0..1);
1236 }
1237 self.staging_command_buffers.push(render_encoder.finish());
1238 }
1239 }
1240
1241 self.staging_command_buffers.push(encoder.finish());
1246
1247 if let (Some(q), Some(b), Some(rb)) = (
1249 &self.skuld_queries,
1250 &self.skuld_buffer,
1251 &self.skuld_read_buffer,
1252 ) {
1253 let mut resolve_encoder =
1254 self.device
1255 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1256 label: Some("Skuld Resolve Encoder"),
1257 });
1258 resolve_encoder.resolve_query_set(q, 0..2, b, 0);
1259 resolve_encoder.copy_buffer_to_buffer(b, 0, rb, 0, 16);
1260 self.staging_command_buffers.push(resolve_encoder.finish());
1261 }
1262
1263 let cmds = std::mem::take(&mut self.staging_command_buffers);
1264 self.queue.submit(cmds);
1265 self.telemetry.frame_time_ms = self.last_frame_start.elapsed().as_secs_f32() * 1000.0;
1266 self.update_vram_telemetry();
1267
1268 self.registry.evict_frame_resources();
1271
1272 if let Some(f) = res.surface_texture {
1273 f.present();
1274 }
1275 }
1276
1277 pub fn submit_buckets(&mut self, buckets: &cvkg_compositor::CommandBuckets) {
1286 let mut active_offscreens = Vec::new();
1288 let mut current_target_id = None;
1289
1290 let mut sorted_scene: Vec<_> = buckets.scene_commands.iter().collect();
1292 sorted_scene.sort_by_key(|cmd| {
1293 match cmd {
1294 cvkg_compositor::engine::RenderCommand::Draw(routed) => {
1295 (routed.z_index as i64, routed.draw_order as i64)
1296 }
1297 _ => (0, 0),
1298 }
1299 });
1300
1301 for cmd in sorted_scene {
1302 match cmd {
1303 cvkg_compositor::engine::RenderCommand::Draw(routed) => {
1304 self.set_material(cvkg_core::DrawMaterial::Opaque);
1305 self.submit_routed(routed, current_target_id);
1306 }
1307 cvkg_compositor::engine::RenderCommand::PushOffscreen {
1308 source_layer,
1309 material,
1310 bounds,
1311 } => {
1312 current_target_id = Some(source_layer.0);
1313
1314 let width = (bounds.width).max(1.0) as u32;
1316 let height = (bounds.height).max(1.0) as u32;
1317 self.registry
1318 .allocate_offscreen(&self.device, source_layer.0, [width, height]);
1319
1320 if let cvkg_compositor::Material::ShaderEffect {
1321 effect_name,
1322 params_json: _,
1323 ..
1324 } = material
1325 {
1326 active_offscreens.push(crate::types::OffscreenEffectConfig {
1327 target_id: source_layer.0,
1328 effect: effect_name.clone(),
1329 blend_mode: 0, effect_args: [0.0; 16], });
1332 }
1333 }
1334 cvkg_compositor::engine::RenderCommand::PopOffscreen => {
1335 current_target_id = None;
1336 }
1337 }
1338 }
1339 self.active_offscreens = active_offscreens;
1340
1341 let mut sorted_glass: Vec<_> = buckets.glass_commands.iter().collect();
1343 sorted_glass.sort_by_key(|cmd| match cmd {
1344 cvkg_compositor::engine::RenderCommand::Draw(routed) => {
1345 (routed.z_index as i64, routed.draw_order as i64)
1346 }
1347 _ => (0, 0),
1348 });
1349 for cmd in sorted_glass {
1350 if let cvkg_compositor::engine::RenderCommand::Draw(routed) = cmd {
1351 self.set_material(Self::convert_compositor_material(&routed.material));
1352 self.submit_routed(routed, None);
1353 }
1354 }
1355
1356 let mut sorted_overlay: Vec<_> = buckets.overlay_commands.iter().collect();
1358 sorted_overlay.sort_by_key(|cmd| match cmd {
1359 cvkg_compositor::engine::RenderCommand::Draw(routed) => {
1360 (routed.z_index as i64, routed.draw_order as i64)
1361 }
1362 _ => (0, 0),
1363 });
1364 for cmd in sorted_overlay {
1365 if let cvkg_compositor::engine::RenderCommand::Draw(routed) = cmd {
1366 self.set_material(cvkg_core::DrawMaterial::TopUI);
1367 self.submit_routed(routed, None);
1368 }
1369 }
1370 }
1371
1372 pub(crate) fn submit_routed(
1374 &mut self,
1375 routed: &cvkg_compositor::RoutedDrawCommand,
1376 target_id: Option<u64>,
1377 ) {
1378 let cmd = &routed.command;
1379 if cmd.index_count == 0 {
1380 return;
1381 }
1382 let material = Self::convert_compositor_material(&routed.material);
1383 self.draw_calls.push(DrawCall {
1384 texture_id: cmd.texture_id,
1385 scissor_rect: cmd.scissor_rect,
1386 index_start: cmd.index_start,
1387 index_count: cmd.index_count,
1388 instance_count: 1,
1389 material,
1390 target_id,
1391 instance_start: cmd.instance_id,
1392 draw_order: 0,
1393 });
1394 }
1395
1396 pub(crate) fn apply_opacity(&self, mut color: [f32; 4]) -> [f32; 4] {
1398 if let Some(&alpha) = self.opacity_stack.last() {
1399 color[3] *= alpha;
1400 }
1401 color
1402 }
1403
1404 pub(crate) fn resolve_material(material_id: u32) -> cvkg_core::DrawMaterial {
1407 Self::resolve_material_with_context(material_id, &cvkg_core::DrawMaterial::Opaque)
1408 }
1409
1410 pub(crate) fn resolve_material_with_context(
1414 material_id: u32,
1415 current: &cvkg_core::DrawMaterial,
1416 ) -> cvkg_core::DrawMaterial {
1417 use crate::renderer::material_id::*;
1418
1419 if matches!(current, cvkg_core::DrawMaterial::TopUI) && material_id != GLASS {
1422 return cvkg_core::DrawMaterial::TopUI;
1423 }
1424
1425 match material_id {
1426 GLASS => {
1427 if let cvkg_core::DrawMaterial::Glass {
1428 blur_radius,
1429 ior_override,
1430 glass_intensity,
1431 } = current
1432 {
1433 cvkg_core::DrawMaterial::Glass {
1434 blur_radius: *blur_radius,
1435 ior_override: *ior_override,
1436 glass_intensity: *glass_intensity,
1437 }
1438 } else {
1439 cvkg_core::DrawMaterial::Glass {
1440 blur_radius: 20.0,
1441 ior_override: 0.0,
1442 glass_intensity: 1.0,
1443 }
1444 }
1445 }
1446 TOP_UI => cvkg_core::DrawMaterial::TopUI,
1447 BLEND_START..=BLEND_END => cvkg_core::DrawMaterial::Blend {
1448 mode: (material_id - 7) as u32,
1449 },
1450 _ => cvkg_core::DrawMaterial::Opaque,
1451 }
1452 }
1453
1454 pub(crate) fn convert_compositor_material(mat: &cvkg_compositor::Material) -> cvkg_core::DrawMaterial {
1457 match mat {
1458 cvkg_compositor::Material::Glass { blur_radius, .. } => {
1459 cvkg_core::DrawMaterial::Glass {
1460 blur_radius: *blur_radius,
1461 ior_override: 0.0,
1462 glass_intensity: 1.0,
1463 }
1464 }
1465 cvkg_compositor::Material::Overlay => cvkg_core::DrawMaterial::TopUI,
1466 cvkg_compositor::Material::Multiply => cvkg_core::DrawMaterial::Blend { mode: 1 },
1467 cvkg_compositor::Material::Screen => cvkg_core::DrawMaterial::Blend { mode: 2 },
1468 cvkg_compositor::Material::BlendOverlay => cvkg_core::DrawMaterial::Blend { mode: 3 },
1469 cvkg_compositor::Material::Darken => cvkg_core::DrawMaterial::Blend { mode: 4 },
1470 cvkg_compositor::Material::Lighten => cvkg_core::DrawMaterial::Blend { mode: 5 },
1471 cvkg_compositor::Material::ColorDodge => cvkg_core::DrawMaterial::Blend { mode: 6 },
1472 cvkg_compositor::Material::ColorBurn => cvkg_core::DrawMaterial::Blend { mode: 7 },
1473 cvkg_compositor::Material::HardLight => cvkg_core::DrawMaterial::Blend { mode: 8 },
1474 cvkg_compositor::Material::SoftLight => cvkg_core::DrawMaterial::Blend { mode: 9 },
1475 cvkg_compositor::Material::Difference => cvkg_core::DrawMaterial::Blend { mode: 10 },
1476 cvkg_compositor::Material::Exclusion => cvkg_core::DrawMaterial::Blend { mode: 11 },
1477 cvkg_compositor::Material::Hue => cvkg_core::DrawMaterial::Blend { mode: 12 },
1478 cvkg_compositor::Material::Saturation => cvkg_core::DrawMaterial::Blend { mode: 13 },
1479 cvkg_compositor::Material::Color => cvkg_core::DrawMaterial::Blend { mode: 14 },
1480 cvkg_compositor::Material::Luminosity => cvkg_core::DrawMaterial::Blend { mode: 15 },
1481 cvkg_compositor::Material::Opaque => cvkg_core::DrawMaterial::Opaque,
1482 _ => cvkg_core::DrawMaterial::Opaque,
1483 }
1484 }
1485
1486 pub(crate) fn position_vertices(
1488 vertices: &mut [Vertex],
1489 view_box: Rect,
1490 rect: Rect,
1491 material_id: u32,
1492 clip: [f32; 4],
1493 snap: impl Fn(f32) -> f32,
1494 ) {
1495 for v in vertices.iter_mut() {
1496 let rel_x = (v.position[0] - view_box.x) / view_box.width;
1497 let rel_y = (v.position[1] - view_box.y) / view_box.height;
1498 v.position[0] = snap(rect.x + rel_x * rect.width);
1499 v.position[1] = snap(rect.y + rel_y * rect.height);
1500 v.position[2] = 0.0; v.logical = [v.position[0], v.position[1]];
1502 v.clip = clip;
1503 v.material_id = material_id;
1504 }
1505 }
1506
1507 pub(crate) fn emit_draw_call(
1509 renderer: &mut GpuRenderer,
1510 material: cvkg_core::DrawMaterial,
1511 texture_id: Option<u32>,
1512 scissor_rect: Rect,
1513 index_count: u32,
1514 base_vertex: u32,
1515 ) {
1516 let draw_order = renderer.current_draw_order;
1517 let (translation, scale_transform, rotation, _, _) = renderer.current_transform();
1518 let current_instance_data = InstanceData {
1519 translation,
1520 scale: scale_transform,
1521 rotation,
1522 blur_radius: 0.0,
1523 ior_override: 0.0,
1524 glass_intensity: 1.0,
1525 };
1526 let last_call = renderer.draw_calls.last();
1529 let needs_new_call = renderer.draw_calls.is_empty()
1530 || renderer.current_texture_id != texture_id
1531 || last_call.unwrap().scissor_rect != renderer.clip_stack.last().copied()
1532 || last_call.unwrap().material != material
1533 || {
1534 let last_material = last_call.unwrap().material;
1535 matches!((material, last_material),
1536 (cvkg_core::DrawMaterial::Glass { blur_radius: a, ior_override: b, glass_intensity: c },
1537 cvkg_core::DrawMaterial::Glass { blur_radius: d, ior_override: e, glass_intensity: f })
1538 if a != d || b != e || c != f)
1539 };
1540
1541 if needs_new_call {
1542 renderer.current_texture_id = texture_id;
1543 renderer.instance_data.push(current_instance_data);
1544 renderer.draw_calls.push(DrawCall {
1545 target_id: None,
1546 texture_id,
1547 scissor_rect: renderer.clip_stack.last().copied(),
1548 index_start: (renderer.indices.len() - index_count as usize) as u32,
1549 index_count,
1550 instance_count: 1,
1551 material,
1552 instance_start: (renderer.instance_data.len() - 1) as u32,
1553 draw_order: 0,
1554 });
1555 } else {
1556 renderer.instance_data.push(current_instance_data);
1558 if let Some(call) = renderer.draw_calls.last_mut() {
1559 call.instance_count += 1;
1560 }
1561 }
1562 }
1563
1564 pub async fn capture_frame(&self) -> Result<Vec<u8>, String> {
1566 let ctx = self
1567 .headless_context
1568 .as_ref()
1569 .ok_or("Headless context required for capture")?;
1570
1571 let u32_size = std::mem::size_of::<u32>() as u32;
1572 let width = ctx.width;
1573 let height = ctx.height;
1574 let bytes_per_row = width * u32_size;
1575 let padding = (256 - (bytes_per_row % 256)) % 256;
1576 let padded_bytes_per_row = bytes_per_row + padding;
1577
1578 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
1579 label: Some("Capture Buffer"),
1580 size: (padded_bytes_per_row as u64 * height as u64),
1581 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1582 mapped_at_creation: false,
1583 });
1584
1585 let mut encoder = self
1586 .device
1587 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1588 label: Some("Capture Encoder"),
1589 });
1590
1591 encoder.copy_texture_to_buffer(
1592 wgpu::TexelCopyTextureInfo {
1593 texture: &ctx.output_texture,
1594 mip_level: 0,
1595 origin: wgpu::Origin3d::ZERO,
1596 aspect: wgpu::TextureAspect::All,
1597 },
1598 wgpu::TexelCopyBufferInfo {
1599 buffer: &output_buffer,
1600 layout: wgpu::TexelCopyBufferLayout {
1601 offset: 0,
1602 bytes_per_row: Some(padded_bytes_per_row),
1603 rows_per_image: Some(height),
1604 },
1605 },
1606 wgpu::Extent3d {
1607 width,
1608 height,
1609 depth_or_array_layers: 1,
1610 },
1611 );
1612
1613 self.queue.submit(Some(encoder.finish()));
1614
1615 let buffer_slice = output_buffer.slice(..);
1616 let (sender, receiver) = futures::channel::oneshot::channel();
1617 buffer_slice.map_async(wgpu::MapMode::Read, move |v| {
1618 let _ = sender.send(v);
1619 });
1620
1621 let _ = self.device.poll(wgpu::PollType::Wait {
1622 submission_index: None,
1623 timeout: None,
1624 });
1625
1626 if let Ok(Ok(_)) = receiver.await {
1627 let data = buffer_slice.get_mapped_range();
1628 let mut result = Vec::with_capacity((width * height * 4) as usize);
1629
1630 for y in 0..height {
1631 let start = (y * padded_bytes_per_row) as usize;
1632 let end = start + bytes_per_row as usize;
1633 result.extend_from_slice(&data[start..end]);
1634 }
1635
1636 log::trace!(
1637 "[GPU] capture_frame: data len={}, first 4 bytes={:?}",
1638 data.len(),
1639 &data[0..4.min(data.len())]
1640 );
1641
1642 drop(data);
1643 output_buffer.unmap();
1644 Ok(result)
1645 } else {
1646 Err("Failed to capture frame".to_string())
1647 }
1648 }
1649}