1use cvkg_core::Rect;
35use std::sync::Arc;
36
37pub use accesskit::{
40 ActionHandler, ActionRequest, ActivationHandler, DeactivationHandler,
41 Node, NodeId, Role, Tree, TreeId, TreeUpdate,
42};
43pub use accesskit_winit::Adapter as ShieldWallAdapter;
44
45
46#[repr(C)]
47#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
48pub struct Vertex {
49 pub position: [f32; 2],
50 pub uv: [f32; 2],
51 pub color: [f32; 4],
52 pub mode: u32,
53}
54
55#[derive(Debug, Clone)]
58struct DrawCall {
59 pub texture_name: Option<String>,
60 pub index_start: u32,
61 pub index_count: u32,
62}
63
64impl Vertex {
65 const ATTRIBUTES: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
66 0 => Float32x2,
67 1 => Float32x2,
68 2 => Float32x4,
69 3 => Uint32
70 ];
71
72 fn desc() -> wgpu::VertexBufferLayout<'static> {
73 wgpu::VertexBufferLayout {
74 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
75 step_mode: wgpu::VertexStepMode::Vertex,
76 attributes: &Self::ATTRIBUTES,
77 }
78 }
79}
80
81pub struct SurtrRenderer {
83 device: Arc<wgpu::Device>,
84 queue: Arc<wgpu::Queue>,
85 surface: wgpu::Surface<'static>,
86 config: wgpu::SurfaceConfiguration,
87 pipeline: wgpu::RenderPipeline,
88
89 #[allow(dead_code)]
91 bloom_extract_pipeline: wgpu::RenderPipeline,
92 blur_h_pipeline: wgpu::RenderPipeline,
93 blur_v_pipeline: wgpu::RenderPipeline,
94 composite_pipeline: wgpu::RenderPipeline,
95 blur_texture_a: wgpu::TextureView,
96 blur_texture_b: wgpu::TextureView,
97 blur_bind_group_a: wgpu::BindGroup,
98 blur_bind_group_b: wgpu::BindGroup,
99
100 #[allow(dead_code)]
102 font_system: cosmic_text::FontSystem,
103 #[allow(dead_code)]
104 swash_cache: cosmic_text::SwashCache,
105
106 dummy_bind_group: wgpu::BindGroup,
108 texture_bind_group_layout: wgpu::BindGroupLayout,
109 textures: std::collections::HashMap<String, wgpu::BindGroup>,
110
111 vertex_buffer: wgpu::Buffer,
113 index_buffer: wgpu::Buffer,
114 vertices: Vec<Vertex>,
115 indices: Vec<u16>,
116 draw_calls: Vec<DrawCall>,
117 current_texture_name: Option<String>,
118
119 opacity_stack: Vec<f32>,
121 clip_stack: Vec<Rect>,
123}
124
125const MAX_VERTICES: usize = 10000;
126const MAX_INDICES: usize = 15000;
127
128impl SurtrRenderer {
129 pub async fn forge(window: Arc<winit::window::Window>) -> Self {
131 let instance = wgpu::Instance::default();
132 let surface = instance.create_surface(window.clone()).unwrap();
133 let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
134 power_preference: wgpu::PowerPreference::HighPerformance,
135 compatible_surface: Some(&surface),
136 force_fallback_adapter: false,
137 }).await.expect("Failed to find a suitable GPU for Surtr");
138
139 let (device, queue) = adapter.request_device(
140 &wgpu::DeviceDescriptor {
141 label: Some("Surtr Forge"),
142 required_features: wgpu::Features::empty(),
143 required_limits: wgpu::Limits::default(),
144 },
145 None,
146 ).await.expect("Failed to create Surtr device");
147
148 let device = Arc::new(device);
149 let queue = Arc::new(queue);
150
151 let size = window.inner_size();
152 let config = surface.get_default_config(&adapter, size.width, size.height).unwrap();
153 surface.configure(&device, &config);
154
155 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
157 label: Some("Muspelheim Main Shader"),
158 source: wgpu::ShaderSource::Wgsl(include_str!("shaders.wgsl").into()),
159 });
160
161 let texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
163 entries: &[
164 wgpu::BindGroupLayoutEntry {
165 binding: 0,
166 visibility: wgpu::ShaderStages::FRAGMENT,
167 ty: wgpu::BindingType::Texture {
168 multisampled: false,
169 view_dimension: wgpu::TextureViewDimension::D2,
170 sample_type: wgpu::TextureSampleType::Float { filterable: true },
171 },
172 count: None,
173 },
174 wgpu::BindGroupLayoutEntry {
175 binding: 1,
176 visibility: wgpu::ShaderStages::FRAGMENT,
177 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
178 count: None,
179 },
180 ],
181 label: Some("Niflheim Texture Bind Group Layout"),
182 });
183
184 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
186 label: Some("Surtr Pipeline Layout"),
187 bind_group_layouts: &[&texture_bind_group_layout],
188 push_constant_ranges: &[],
189 });
190
191 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
192 label: Some("Surtr Main Pipeline"),
193 layout: Some(&pipeline_layout),
194 vertex: wgpu::VertexState {
195 module: &shader,
196 entry_point: "vs_main",
197 buffers: &[Vertex::desc()],
198 },
199 fragment: Some(wgpu::FragmentState {
200 module: &shader,
201 entry_point: "fs_main",
202 targets: &[Some(wgpu::ColorTargetState {
203 format: config.format,
204 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
205 write_mask: wgpu::ColorWrites::ALL,
206 })],
207 }),
208 primitive: wgpu::PrimitiveState::default(),
209 depth_stencil: None,
210 multisample: wgpu::MultisampleState::default(),
211 multiview: None,
212 });
213
214 let bloom_extract_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
216 label: Some("Muspelheim Bloom Extract"),
217 layout: Some(&pipeline_layout),
218 vertex: wgpu::VertexState {
219 module: &shader,
220 entry_point: "vs_fullscreen",
221 buffers: &[],
222 },
223 fragment: Some(wgpu::FragmentState {
224 module: &shader,
225 entry_point: "fs_bloom_extract",
226 targets: &[Some(wgpu::ColorTargetState {
227 format: config.format,
228 blend: None,
229 write_mask: wgpu::ColorWrites::ALL,
230 })],
231 }),
232 primitive: wgpu::PrimitiveState::default(),
233 depth_stencil: None,
234 multisample: wgpu::MultisampleState::default(),
235 multiview: None,
236 });
237
238 let blur_h_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
240 label: Some("Muspelheim Horizontal Blur"),
241 layout: Some(&pipeline_layout),
242 vertex: wgpu::VertexState {
243 module: &shader,
244 entry_point: "vs_fullscreen",
245 buffers: &[],
246 },
247 fragment: Some(wgpu::FragmentState {
248 module: &shader,
249 entry_point: "fs_blur_h",
250 targets: &[Some(wgpu::ColorTargetState {
251 format: config.format,
252 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
253 write_mask: wgpu::ColorWrites::ALL,
254 })],
255 }),
256 primitive: wgpu::PrimitiveState::default(),
257 depth_stencil: None,
258 multisample: wgpu::MultisampleState::default(),
259 multiview: None,
260 });
261
262 let blur_v_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
263 label: Some("Muspelheim Vertical Blur"),
264 layout: Some(&pipeline_layout),
265 vertex: wgpu::VertexState {
266 module: &shader,
267 entry_point: "vs_fullscreen",
268 buffers: &[],
269 },
270 fragment: Some(wgpu::FragmentState {
271 module: &shader,
272 entry_point: "fs_blur_v",
273 targets: &[Some(wgpu::ColorTargetState {
274 format: config.format,
275 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
276 write_mask: wgpu::ColorWrites::ALL,
277 })],
278 }),
279 primitive: wgpu::PrimitiveState::default(),
280 depth_stencil: None,
281 multisample: wgpu::MultisampleState::default(),
282 multiview: None,
283 });
284
285 let composite_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
287 label: Some("Muspelheim Composite"),
288 layout: Some(&pipeline_layout),
289 vertex: wgpu::VertexState {
290 module: &shader,
291 entry_point: "vs_fullscreen",
292 buffers: &[],
293 },
294 fragment: Some(wgpu::FragmentState {
295 module: &shader,
296 entry_point: "fs_composite",
297 targets: &[Some(wgpu::ColorTargetState {
298 format: config.format,
299 blend: Some(wgpu::BlendState {
301 color: wgpu::BlendComponent {
302 src_factor: wgpu::BlendFactor::One,
303 dst_factor: wgpu::BlendFactor::One,
304 operation: wgpu::BlendOperation::Add,
305 },
306 alpha: wgpu::BlendComponent {
307 src_factor: wgpu::BlendFactor::One,
308 dst_factor: wgpu::BlendFactor::One,
309 operation: wgpu::BlendOperation::Add,
310 },
311 }),
312 write_mask: wgpu::ColorWrites::ALL,
313 })],
314 }),
315 primitive: wgpu::PrimitiveState::default(),
316 depth_stencil: None,
317 multisample: wgpu::MultisampleState::default(),
318 multiview: None,
319 });
320
321 let blur_tex_desc = wgpu::TextureDescriptor {
323 label: Some("Muspelheim Intermediate"),
324 size: wgpu::Extent3d {
325 width: config.width,
326 height: config.height,
327 depth_or_array_layers: 1,
328 },
329 mip_level_count: 1,
330 sample_count: 1,
331 dimension: wgpu::TextureDimension::D2,
332 format: config.format,
333 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
334 view_formats: &[],
335 };
336 let blur_texture_a_obj = device.create_texture(&blur_tex_desc);
337 let blur_texture_b_obj = device.create_texture(&blur_tex_desc);
338 let blur_texture_a = blur_texture_a_obj.create_view(&wgpu::TextureViewDescriptor::default());
339 let blur_texture_b = blur_texture_b_obj.create_view(&wgpu::TextureViewDescriptor::default());
340
341 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
342 address_mode_u: wgpu::AddressMode::ClampToEdge,
343 address_mode_v: wgpu::AddressMode::ClampToEdge,
344 mag_filter: wgpu::FilterMode::Linear,
345 ..Default::default()
346 });
347
348 let blur_bind_group_a = device.create_bind_group(&wgpu::BindGroupDescriptor {
349 layout: &texture_bind_group_layout,
350 entries: &[
351 wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&blur_texture_a) },
352 wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
353 ],
354 label: Some("Blur Bind Group A"),
355 });
356
357 let blur_bind_group_b = device.create_bind_group(&wgpu::BindGroupDescriptor {
358 layout: &texture_bind_group_layout,
359 entries: &[
360 wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&blur_texture_b) },
361 wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
362 ],
363 label: Some("Blur Bind Group B"),
364 });
365
366 let dummy_size = wgpu::Extent3d {
368 width: 1,
369 height: 1,
370 depth_or_array_layers: 1,
371 };
372 let dummy_texture = device.create_texture(&wgpu::TextureDescriptor {
373 label: Some("Niflheim Dummy Texture"),
374 size: dummy_size,
375 mip_level_count: 1,
376 sample_count: 1,
377 dimension: wgpu::TextureDimension::D2,
378 format: wgpu::TextureFormat::Rgba8UnormSrgb,
379 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
380 view_formats: &[],
381 });
382 queue.write_texture(
383 wgpu::ImageCopyTexture {
384 texture: &dummy_texture,
385 mip_level: 0,
386 origin: wgpu::Origin3d::ZERO,
387 aspect: wgpu::TextureAspect::All,
388 },
389 &[255, 255, 255, 255],
390 wgpu::ImageDataLayout {
391 offset: 0,
392 bytes_per_row: Some(4),
393 rows_per_image: Some(1),
394 },
395 dummy_size,
396 );
397
398 let dummy_view = dummy_texture.create_view(&wgpu::TextureViewDescriptor::default());
399 let dummy_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
400 address_mode_u: wgpu::AddressMode::ClampToEdge,
401 address_mode_v: wgpu::AddressMode::ClampToEdge,
402 address_mode_w: wgpu::AddressMode::ClampToEdge,
403 mag_filter: wgpu::FilterMode::Linear,
404 min_filter: wgpu::FilterMode::Nearest,
405 mipmap_filter: wgpu::FilterMode::Nearest,
406 ..Default::default()
407 });
408
409 let dummy_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
410 layout: &texture_bind_group_layout,
411 entries: &[
412 wgpu::BindGroupEntry {
413 binding: 0,
414 resource: wgpu::BindingResource::TextureView(&dummy_view),
415 },
416 wgpu::BindGroupEntry {
417 binding: 1,
418 resource: wgpu::BindingResource::Sampler(&dummy_sampler),
419 },
420 ],
421 label: Some("Niflheim Dummy Bind Group"),
422 });
423
424 let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
426 label: Some("Surtr Vertex Anvil"),
427 size: (MAX_VERTICES * std::mem::size_of::<Vertex>()) as u64,
428 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
429 mapped_at_creation: false,
430 });
431
432 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
433 label: Some("Surtr Index Anvil"),
434 size: (MAX_INDICES * std::mem::size_of::<u16>()) as u64,
435 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
436 mapped_at_creation: false,
437 });
438
439
440 Self {
441 device,
442 queue,
443 surface,
444 config,
445 pipeline,
446 bloom_extract_pipeline,
447 blur_h_pipeline,
448 blur_v_pipeline,
449 composite_pipeline,
450 blur_texture_a,
451 blur_texture_b,
452 blur_bind_group_a,
453 blur_bind_group_b,
454 font_system: cosmic_text::FontSystem::new(),
455 swash_cache: cosmic_text::SwashCache::new(),
456 dummy_bind_group,
457 texture_bind_group_layout,
458 textures: std::collections::HashMap::new(),
459 vertex_buffer,
460 index_buffer,
461 vertices: Vec::with_capacity(MAX_VERTICES),
462 indices: Vec::with_capacity(MAX_INDICES),
463 draw_calls: Vec::new(),
464 current_texture_name: None,
465 opacity_stack: Vec::new(),
466 clip_stack: Vec::new(),
467 }
468 }
469
470 pub fn begin_frame(&mut self) -> wgpu::CommandEncoder {
472 self.vertices.clear();
473 self.indices.clear();
474 self.draw_calls.clear();
475 self.current_texture_name = None;
476 self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
477 label: Some("Surtr's Flaming Sword"),
478 })
479 }
480
481 pub fn end_frame(&mut self, mut encoder: wgpu::CommandEncoder) {
483 self.queue.write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&self.vertices));
484 self.queue.write_buffer(&self.index_buffer, 0, bytemuck::cast_slice(&self.indices));
485
486 let frame = self.surface.get_current_texture()
487 .expect("Surtr: failed to acquire surface texture");
488 let screen = frame.texture.create_view(&wgpu::TextureViewDescriptor::default());
489
490 {
492 let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
493 label: Some("Surtr P1 Base"),
494 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
495 view: &screen,
496 resolve_target: None,
497 ops: wgpu::Operations {
498 load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }), store: wgpu::StoreOp::Store,
500 },
501 })],
502 depth_stencil_attachment: None,
503 occlusion_query_set: None,
504 timestamp_writes: None,
505 });
506 if !self.draw_calls.is_empty() {
507 p.set_pipeline(&self.pipeline);
508 p.set_vertex_buffer(0, self.vertex_buffer.slice(..));
509 p.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
510 for call in &self.draw_calls {
511 let bg = if let Some(name) = &call.texture_name {
512 self.textures.get(name).unwrap_or(&self.dummy_bind_group)
513 } else {
514 &self.dummy_bind_group
515 };
516 p.set_bind_group(0, bg, &[]);
517 p.draw_indexed(call.index_start..call.index_start + call.index_count, 0, 0..1);
518 }
519 }
520 }
521
522 {
524 let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
525 label: Some("Surtr P2 Bloom Src"),
526 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
527 view: &self.blur_texture_a,
528 resolve_target: None,
529 ops: wgpu::Operations {
530 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
531 store: wgpu::StoreOp::Store,
532 },
533 })],
534 depth_stencil_attachment: None,
535 occlusion_query_set: None,
536 timestamp_writes: None,
537 });
538 if !self.draw_calls.is_empty() {
539 p.set_pipeline(&self.pipeline);
540 p.set_vertex_buffer(0, self.vertex_buffer.slice(..));
541 p.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
542 for call in &self.draw_calls {
543 let bg = if let Some(name) = &call.texture_name {
544 self.textures.get(name).unwrap_or(&self.dummy_bind_group)
545 } else {
546 &self.dummy_bind_group
547 };
548 p.set_bind_group(0, bg, &[]);
549 p.draw_indexed(call.index_start..call.index_start + call.index_count, 0, 0..1);
550 }
551 }
552 }
553
554 let blur_iters: u32 = 6;
556 for i in 0..blur_iters {
557 {
558 let label = format!("Surtr Blur H iter {}", i);
559 let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
560 label: Some(&label),
561 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
562 view: &self.blur_texture_b,
563 resolve_target: None,
564 ops: wgpu::Operations {
565 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
566 store: wgpu::StoreOp::Store,
567 },
568 })],
569 depth_stencil_attachment: None,
570 occlusion_query_set: None,
571 timestamp_writes: None,
572 });
573 p.set_pipeline(&self.blur_h_pipeline);
574 p.set_bind_group(0, &self.blur_bind_group_a, &[]);
575 p.draw(0..3, 0..1);
576 }
577 {
578 let label = format!("Surtr Blur V iter {}", i);
579 let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
580 label: Some(&label),
581 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
582 view: &self.blur_texture_a,
583 resolve_target: None,
584 ops: wgpu::Operations {
585 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
586 store: wgpu::StoreOp::Store,
587 },
588 })],
589 depth_stencil_attachment: None,
590 occlusion_query_set: None,
591 timestamp_writes: None,
592 });
593 p.set_pipeline(&self.blur_v_pipeline);
594 p.set_bind_group(0, &self.blur_bind_group_b, &[]);
595 p.draw(0..3, 0..1);
596 }
597 }
598
599 {
601 let mut p = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
602 label: Some("Surtr P7 Composite"),
603 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
604 view: &screen,
605 resolve_target: None,
606 ops: wgpu::Operations {
607 load: wgpu::LoadOp::Load,
608 store: wgpu::StoreOp::Store,
609 },
610 })],
611 depth_stencil_attachment: None,
612 occlusion_query_set: None,
613 timestamp_writes: None,
614 });
615 p.set_pipeline(&self.composite_pipeline);
616 p.set_bind_group(0, &self.blur_bind_group_a, &[]);
617 p.draw(0..3, 0..1);
618 }
619
620 self.queue.submit(Some(encoder.finish()));
621 frame.present();
622 }
623}
624
625impl cvkg_core::Renderer for SurtrRenderer {
626 fn fill_rect(&mut self, rect: Rect, color: [f32; 4]) {
627 self.fill_rect_with_mode(rect, self.apply_opacity(color), 0, None);
628 }
629
630 fn fill_rounded_rect(&mut self, rect: Rect, _radius: f32, color: [f32; 4]) {
631 self.fill_rect_with_mode(rect, self.apply_opacity(color), 0, None);
634 }
635
636 fn fill_ellipse(&mut self, rect: Rect, color: [f32; 4]) {
637 self.fill_rect_with_mode(rect, self.apply_opacity(color), 0, None);
639 }
640
641 fn stroke_rect(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
642 let c = self.apply_opacity(color);
643 let hw = stroke_width;
644 self.fill_rect_with_mode(Rect { x: rect.x, y: rect.y, width: rect.width, height: hw }, c, 1, None);
646 self.fill_rect_with_mode(Rect { x: rect.x, y: rect.y + rect.height - hw, width: rect.width, height: hw }, c, 1, None);
647 self.fill_rect_with_mode(Rect { x: rect.x, y: rect.y, width: hw, height: rect.height }, c, 1, None);
648 self.fill_rect_with_mode(Rect { x: rect.x + rect.width - hw, y: rect.y, width: hw, height: rect.height }, c, 1, None);
649 }
650
651 fn stroke_rounded_rect(&mut self, rect: Rect, _radius: f32, color: [f32; 4], stroke_width: f32) {
652 self.stroke_rect(rect, color, stroke_width);
654 }
655
656 fn stroke_ellipse(&mut self, rect: Rect, color: [f32; 4], stroke_width: f32) {
657 self.stroke_rect(rect, color, stroke_width);
658 }
659
660 fn draw_line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, color: [f32; 4], stroke_width: f32) {
661 let dx = x2 - x1;
663 let dy = y2 - y1;
664 let len = (dx * dx + dy * dy).sqrt();
665 if len < 0.001 { return; }
666 let c = self.apply_opacity(color);
667 let min_x = x1.min(x2);
669 let min_y = y1.min(y2);
670 let w = (dx.abs()).max(stroke_width);
671 let h = (dy.abs()).max(stroke_width);
672 self.fill_rect_with_mode(Rect { x: min_x, y: min_y, width: w, height: h }, c, 1, None);
673 }
674
675 fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
676 let c = self.apply_opacity(color);
679 self.fill_rect_with_mode(Rect { x, y, width: size * 0.6 * text.len() as f32, height: size }, c, 0, None);
680 }
681
682 fn draw_texture(&mut self, texture_id: u32, rect: Rect) {
683 self.fill_rect_with_mode(rect, [1.0, 1.0, 1.0, 1.0], 2, Some(format!("tex_{}", texture_id)));
684 }
685
686 fn draw_image(&mut self, image_name: &str, rect: Rect) {
687 self.fill_rect_with_mode(rect, [1.0, 1.0, 1.0, 1.0], 2, Some(image_name.to_string()));
688 }
689
690 fn load_image(&mut self, name: &str, data: &[u8]) {
691 let img = image::load_from_memory(data).expect("Failed to load image").to_rgba8();
692 let (width, height) = img.dimensions();
693 let size = wgpu::Extent3d { width, height, depth_or_array_layers: 1 };
694 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
695 label: Some(name),
696 size,
697 mip_level_count: 1,
698 sample_count: 1,
699 dimension: wgpu::TextureDimension::D2,
700 format: wgpu::TextureFormat::Rgba8UnormSrgb,
701 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
702 view_formats: &[],
703 });
704 self.queue.write_texture(
705 wgpu::ImageCopyTexture {
706 texture: &texture,
707 mip_level: 0,
708 origin: wgpu::Origin3d::ZERO,
709 aspect: wgpu::TextureAspect::All,
710 },
711 &img,
712 wgpu::ImageDataLayout {
713 offset: 0,
714 bytes_per_row: Some(4 * width),
715 rows_per_image: Some(height),
716 },
717 size,
718 );
719 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
720 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
721 address_mode_u: wgpu::AddressMode::ClampToEdge,
722 address_mode_v: wgpu::AddressMode::ClampToEdge,
723 mag_filter: wgpu::FilterMode::Linear,
724 ..Default::default()
725 });
726 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
727 layout: &self.texture_bind_group_layout,
728 entries: &[
729 wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view) },
730 wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
731 ],
732 label: Some(name),
733 });
734 self.textures.insert(name.to_string(), bind_group);
735 }
736
737 fn push_clip_rect(&mut self, rect: Rect) {
738 self.clip_stack.push(rect);
740 }
741
742 fn pop_clip_rect(&mut self) {
743 self.clip_stack.pop();
744 }
745
746 fn push_opacity(&mut self, opacity: f32) {
747 let current = self.opacity_stack.last().copied().unwrap_or(1.0);
748 self.opacity_stack.push(current * opacity);
749 }
750
751 fn pop_opacity(&mut self) {
752 self.opacity_stack.pop();
753 }
754}
755
756impl cvkg_core::FrameRenderer<wgpu::CommandEncoder> for SurtrRenderer {
757 fn begin_frame(&mut self) -> wgpu::CommandEncoder {
758 self.begin_frame()
759 }
760
761 fn end_frame(&mut self, encoder: wgpu::CommandEncoder) {
762 self.end_frame(encoder)
763 }
764}
765
766impl SurtrRenderer {
767 fn apply_opacity(&self, mut color: [f32; 4]) -> [f32; 4] {
769 if let Some(&alpha) = self.opacity_stack.last() {
770 color[3] *= alpha;
771 }
772 color
773 }
774
775 pub fn fill_rect_with_mode(&mut self, rect: Rect, color: [f32; 4], mode: u32, texture_name: Option<String>) {
778 let needs_new_call = self.draw_calls.is_empty() || self.current_texture_name != texture_name;
780
781 if needs_new_call {
782 self.current_texture_name = texture_name.clone();
783 self.draw_calls.push(DrawCall {
784 texture_name,
785 index_start: self.indices.len() as u32,
786 index_count: 0,
787 });
788 }
789
790 let base_idx = self.vertices.len() as u16;
791
792 let half_w = self.config.width as f32 / 2.0;
794 let half_h = self.config.height as f32 / 2.0;
795
796 let x1 = (rect.x / half_w) - 1.0;
797 let y1 = 1.0 - (rect.y / half_h);
798 let x2 = ((rect.x + rect.width) / half_w) - 1.0;
799 let y2 = 1.0 - ((rect.y + rect.height) / half_h);
800
801 self.vertices.push(Vertex { position: [x1, y1], uv: [0.0, 0.0], color, mode });
802 self.vertices.push(Vertex { position: [x2, y1], uv: [1.0, 0.0], color, mode });
803 self.vertices.push(Vertex { position: [x2, y2], uv: [1.0, 1.0], color, mode });
804 self.vertices.push(Vertex { position: [x1, y2], uv: [0.0, 1.0], color, mode });
805
806 self.indices.extend_from_slice(&[
807 base_idx, base_idx + 1, base_idx + 2,
808 base_idx, base_idx + 2, base_idx + 3,
809 ]);
810
811 if let Some(call) = self.draw_calls.last_mut() {
813 call.index_count += 6;
814 }
815 }
816}