Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-renderer/src/batch.rs
1//! Render batching system for efficient GPU rendering
2 
3use crate::text::TextRenderer;
4use crate::vertex::Vertex;
5use std::collections::HashMap;
6use std::ops::Range;
7use strato_core::types::{Color, Rect, Transform};
8 
9use strato_core::text::TextAlign;
10 
11/// Draw command types
12#[derive(Debug, Clone)]
13pub enum DrawCommand {
14 /// Draw a filled rectangle
15 Rect {
16 rect: Rect,
17 color: Color,
18 transform: Transform,
19 index_range: Range<u32>,
20 },
21 /// Draw a rectangle with rounded corners
22 RoundedRect {
23 rect: Rect,
24 color: Color,
25 radius: f32,
26 transform: Transform,
27 },
28 /// Draw text
29 Text {
30 text: String,
31 position: (f32, f32),
32 color: Color,
33 font_size: f32,
34 letter_spacing: f32,
35 align: TextAlign,
36 },
37 /// Draw an image
38 Image {
39 id: u64,
40 data: std::sync::Arc<Vec<u8>>,
41 width: u32,
42 height: u32,
43 rect: Rect,
44 color: Color,
45 },
46 /// Draw a textured quad
47 TexturedQuad {
48 rect: Rect,
49 texture_id: u32,
50 uv_rect: Rect,
51 color: Color,
52 transform: Transform,
53 index_range: Range<u32>,
54 },
55 /// Draw a circle
56 Circle {
57 center: (f32, f32),
58 radius: f32,
59 color: Color,
60 segments: u32,
61 transform: Transform,
62 index_range: Range<u32>,
63 },
64 /// Draw a line
65 Line {
66 start: (f32, f32),
67 end: (f32, f32),
68 color: Color,
69 thickness: f32,
70 index_range: Range<u32>,
71 },
72 /// Push a clipping rectangle
73 PushClip(Rect),
74 /// Pop the last clipping rectangle
75 PopClip,
76}
77 
78/// Render batch for collecting draw commands
79pub struct RenderBatch {
80 pub vertices: Vec<Vertex>,
81 pub indices: Vec<u16>,
82 pub commands: Vec<DrawCommand>,
83 pub overlay_commands: Vec<DrawCommand>,
84 vertex_count: u16,
85 texture_atlas: HashMap<u32, TextureInfo>,
86 text_renderer: TextRenderer,
87}
88 
89/// Texture information for batching
90#[derive(Debug, Clone)]
91#[allow(dead_code)] // Fields are used for texture management but not in simplified implementation
92pub struct TextureInfo {
93 pub width: u32,
94 pub height: u32,
95 pub format: wgpu::TextureFormat,
96}
97 
98impl RenderBatch {
99 /// Create a new render batch
100 pub fn new() -> Self {
101 Self {
102 vertices: Vec::with_capacity(1024),
103 indices: Vec::with_capacity(1536),
104 commands: Vec::new(),
105 overlay_commands: Vec::new(),
106 vertex_count: 0,
107 texture_atlas: HashMap::new(),
108 text_renderer: TextRenderer::new(),
109 }
110 }
111 
112 /// Clear the batch
113 pub fn clear(&mut self) {
114 self.vertices.clear();
115 self.indices.clear();
116 self.commands.clear();
117 self.overlay_commands.clear();
118 self.vertex_count = 0;
119 }
120 
121 /// Get the number of draw commands in the batch
122 pub fn command_count(&self) -> usize {
123 self.commands.len() + self.overlay_commands.len()
124 }
125 
126 /// Add a rectangle to the batch
127 pub fn add_rect(&mut self, rect: Rect, color: Color, transform: Transform) {
128 let start_index = self.indices.len() as u32;
129 self.batch_rect(rect, color, transform);
130 let end_index = self.indices.len() as u32;
131 
132 let command = DrawCommand::Rect {
133 rect,
134 color,
135 transform,
136 index_range: start_index..end_index,
137 };
138 self.commands.push(command);
139 }
140 
141 /// Push a clipping rectangle
142 pub fn push_clip(&mut self, rect: Rect) {
143 self.commands.push(DrawCommand::PushClip(rect));
144 }
145 
146 /// Pop the last clipping rectangle
147 pub fn pop_clip(&mut self) {
148 self.commands.push(DrawCommand::PopClip);
149 }
150 
151 /// Add a rounded rectangle to the batch
152 pub fn add_rounded_rect(
153 &mut self,
154 rect: Rect,
155 color: Color,
156 radius: f32,
157 transform: Transform,
158 ) {
159 let command = DrawCommand::RoundedRect {
160 rect,
161 color,
162 radius,
163 transform,
164 };
165 self.commands.push(command);
166 }
167 
168 /// Add text to the batch
169 pub fn add_text(
170 &mut self,
171 text: String,
172 position: (f32, f32),
173 color: Color,
174 font_size: f32,
175 letter_spacing: f32,
176 ) {
177 self.add_text_aligned(
178 text,
179 position,
180 color,
181 font_size,
182 letter_spacing,
183 TextAlign::Left,
184 );
185 }
186 
187 /// Add aligned text to the batch
188 pub fn add_text_aligned(
189 &mut self,
190 text: String,
191 position: (f32, f32),
192 color: Color,
193 font_size: f32,
194 letter_spacing: f32,
195 align: TextAlign,
196 ) {
197 let command = DrawCommand::Text {
198 text: text.clone(),
199 position,
200 color,
201 font_size,
202 letter_spacing,
203 align,
204 };
205 self.commands.push(command);
206 }
207 
208 /// Add a rectangle to the overlay layer (drawn last)
209 pub fn add_overlay_rect(&mut self, rect: Rect, color: Color, transform: Transform) {
210 let command = DrawCommand::Rect {
211 rect,
212 color,
213 transform,
214 index_range: 0..0,
215 };
216 self.overlay_commands.push(command);
217 }
218 
219 /// Add aligned text to the overlay layer (drawn last)
220 pub fn add_overlay_text_aligned(
221 &mut self,
222 text: String,
223 position: (f32, f32),
224 color: Color,
225 font_size: f32,
226 letter_spacing: f32,
227 align: TextAlign,
228 ) {
229 let command = DrawCommand::Text {
230 text: text.clone(),
231 position,
232 color,
233 font_size,
234 letter_spacing,
235 align,
236 };
237 self.overlay_commands.push(command);
238 }
239 
240 /// Add an image to the batch
241 pub fn add_image(
242 &mut self,
243 id: u64,
244 data: std::sync::Arc<Vec<u8>>,
245 width: u32,
246 height: u32,
247 rect: Rect,
248 color: Color,
249 ) {
250 let command = DrawCommand::Image {
251 id,
252 data,
253 width,
254 height,
255 rect,
256 color,
257 };
258 self.commands.push(command);
259 // We can't batch vertices yet because we don't know UVs until upload
260 }
261 
262 /// Add a textured quad to the batch
263 pub fn add_textured_quad(
264 &mut self,
265 rect: Rect,
266 texture_id: u32,
267 uv_rect: Rect,
268 color: Color,
269 transform: Transform,
270 ) {
271 let start_index = self.indices.len() as u32;
272 self.batch_textured_quad(rect, uv_rect, color, transform);
273 let end_index = self.indices.len() as u32;
274 
275 let command = DrawCommand::TexturedQuad {
276 rect,
277 texture_id,
278 uv_rect,
279 color,
280 transform,
281 index_range: start_index..end_index,
282 };
283 self.commands.push(command);
284 }
285 
286 /// Add a circle to the batch
287 pub fn add_circle(
288 &mut self,
289 center: (f32, f32),
290 radius: f32,
291 color: Color,
292 segments: u32,
293 transform: Transform,
294 ) {
295 let start_index = self.indices.len() as u32;
296 self.batch_circle(center, radius, color, segments, transform);
297 let end_index = self.indices.len() as u32;
298 
299 let command = DrawCommand::Circle {
300 center,
301 radius,
302 color,
303 segments,
304 transform,
305 index_range: start_index..end_index,
306 };
307 self.commands.push(command);
308 }
309 
310 /// Add a line to the batch
311 pub fn add_line(&mut self, start: (f32, f32), end: (f32, f32), color: Color, thickness: f32) {
312 let start_index = self.indices.len() as u32;
313 self.batch_line(start, end, color, thickness);
314 let end_index = self.indices.len() as u32;
315 
316 let command = DrawCommand::Line {
317 start,
318 end,
319 color,
320 thickness,
321 index_range: start_index..end_index,
322 };
323 self.commands.push(command);
324 }
325 
326 /// Add raw vertices and indices to the batch
327 pub fn add_vertices(&mut self, vertices: &[Vertex], indices: &[u16]) {
328 let vertex_offset = self.vertices.len() as u16;
329 
330 // Add vertices
331 self.vertices.extend_from_slice(vertices);
332 
333 // Add indices with offset
334 for &index in indices {
335 self.indices.push(vertex_offset + index);
336 }
337 
338 self.vertex_count += vertices.len() as u16;
339 }
340 
341 /// Batch text with real GPU glyph rendering (requires TextureManager access)
342 ///
343 /// This is the full implementation that renders actual glyphs from the font atlas
344 #[allow(dead_code)]
345 fn batch_text_gpu(
346 &mut self,
347 texture_mgr: &mut crate::gpu::TextureManager,
348 queue: &wgpu::Queue,
349 text: &str,
350 position: (f32, f32),
351 color: Color,
352 font_size: u32,
353 ) {
354 let (mut x, y) = position;
355 let color_arr = [color.r, color.g, color.b, color.a];
356 
357 for ch in text.chars() {
358 if let Some(glyph) = texture_mgr.get_or_cache_glyph(queue, ch, font_size) {
359 // Calculate glyph position with bearing
360 let glyph_x = x + glyph.metrics.bearing_x as f32;
361 let glyph_y = y - glyph.metrics.bearing_y as f32;
362 
363 // Glyph dimensions
364 let w = glyph.metrics.width as f32;
365 let h = glyph.metrics.height as f32;
366 
367 // UV coordinates from atlas
368 let (u0, v0, u1, v1) = glyph.uv_rect;
369 
370 // Create textured quad for this glyph
371 let base_idx = self.vertex_count;
372 
373 // Add 4 vertices for the quad
374 self.vertices
375 .push(Vertex::textured([glyph_x, glyph_y], [u0, v0], color_arr));
376 self.vertices.push(Vertex::textured(
377 [glyph_x + w, glyph_y],
378 [u1, v0],
379 color_arr,
380 ));
381 self.vertices.push(Vertex::textured(
382 [glyph_x + w, glyph_y + h],
383 [u1, v1],
384 color_arr,
385 ));
386 self.vertices.push(Vertex::textured(
387 [glyph_x, glyph_y + h],
388 [u0, v1],
389 color_arr,
390 ));
391 
392 // Add 2 triangles (6 indices)
393 self.indices.push(base_idx);
394 self.indices.push(base_idx + 1);
395 self.indices.push(base_idx + 2);
396 
397 self.indices.push(base_idx);
398 self.indices.push(base_idx + 2);
399 self.indices.push(base_idx + 3);
400 
401 self.vertex_count += 4;
402 
403 // Advance to next character position
404 x += glyph.metrics.advance;
405 } else {
406 // Fallback: advance by half font size if glyph unavailable
407 x += font_size as f32 * 0.5;
408 }
409 }
410 }
411 
412 /// Batch a rectangle into vertices and indices
413 fn batch_rect(&mut self, rect: Rect, color: Color, transform: Transform) {
414 let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
415 
416 // Apply transform to vertices
417 let positions = [
418 self.apply_transform([x, y], transform),
419 self.apply_transform([x + w, y], transform),
420 self.apply_transform([x + w, y + h], transform),
421 self.apply_transform([x, y + h], transform),
422 ];
423 
424 // Create vertices with all UV coords at (0,0) for solid color rendering
425 let vertices = [
426 Vertex {
427 position: positions[0],
428 uv: [0.0, 0.0], // Solid color - no texture
429 color: [color.r, color.g, color.b, color.a],
430 params: [0.0, 0.0, 0.0, 0.0],
431 flags: 0,
432 },
433 Vertex {
434 position: positions[1],
435 uv: [0.0, 0.0], // Solid color - no texture
436 color: [color.r, color.g, color.b, color.a],
437 params: [0.0, 0.0, 0.0, 0.0],
438 flags: 0,
439 },
440 Vertex {
441 position: positions[2],
442 uv: [0.0, 0.0], // Solid color - no texture
443 color: [color.r, color.g, color.b, color.a],
444 params: [0.0, 0.0, 0.0, 0.0],
445 flags: 0,
446 },
447 Vertex {
448 position: positions[3],
449 uv: [0.0, 0.0], // Solid color - no texture
450 color: [color.r, color.g, color.b, color.a],
451 params: [0.0, 0.0, 0.0, 0.0],
452 flags: 0,
453 },
454 ];
455 
456 // Add vertices
457 self.vertices.extend_from_slice(&vertices);
458 
459 // Add indices for two triangles
460 let base = self.vertex_count;
461 self.indices
462 .extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
463 
464 self.vertex_count += 4;
465 }
466 
467 /// Batch a textured quad
468 fn batch_textured_quad(
469 &mut self,
470 rect: Rect,
471 uv_rect: Rect,
472 color: Color,
473 transform: Transform,
474 ) {
475 let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
476 let (u, v, uw, vh) = (uv_rect.x, uv_rect.y, uv_rect.width, uv_rect.height);
477 
478 // Apply transform to vertices
479 let positions = [
480 self.apply_transform([x, y], transform),
481 self.apply_transform([x + w, y], transform),
482 self.apply_transform([x + w, y + h], transform),
483 self.apply_transform([x, y + h], transform),
484 ];
485 
486 // Create vertices with UV coordinates
487 let vertices = [
488 Vertex {
489 position: positions[0],
490 uv: [u, v],
491 color: [color.r, color.g, color.b, color.a],
492 params: [0.0, 0.0, 0.0, 0.0],
493 flags: 0,
494 },
495 Vertex {
496 position: positions[1],
497 uv: [u + uw, v],
498 color: [color.r, color.g, color.b, color.a],
499 params: [0.0, 0.0, 0.0, 0.0],
500 flags: 0,
501 },
502 Vertex {
503 position: positions[2],
504 uv: [u + uw, v + vh],
505 color: [color.r, color.g, color.b, color.a],
506 params: [0.0, 0.0, 0.0, 0.0],
507 flags: 0,
508 },
509 Vertex {
510 position: positions[3],
511 uv: [u, v + vh],
512 color: [color.r, color.g, color.b, color.a],
513 params: [0.0, 0.0, 0.0, 0.0],
514 flags: 0,
515 },
516 ];
517 
518 self.vertices.extend_from_slice(&vertices);
519 
520 let base = self.vertex_count;
521 self.indices
522 .extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
523 
524 self.vertex_count += 4;
525 }
526 
527 /// Batch a circle
528 fn batch_circle(
529 &mut self,
530 center: (f32, f32),
531 radius: f32,
532 color: Color,
533 segments: u32,
534 transform: Transform,
535 ) {
536 let (cx, cy) = center;
537 
538 // Center vertex
539 self.vertices.push(Vertex {
540 position: self.apply_transform([cx, cy], transform),
541 uv: [0.5, 0.5],
542 color: [color.r, color.g, color.b, color.a],
543 params: [0.0, 0.0, 0.0, 0.0],
544 flags: 0,
545 });
546 
547 let center_index = self.vertex_count;
548 self.vertex_count += 1;
549 
550 // Generate circle vertices
551 for i in 0..=segments {
552 let angle = (i as f32 / segments as f32) * 2.0 * std::f32::consts::PI;
553 let x = cx + radius * angle.cos();
554 let y = cy + radius * angle.sin();
555 
556 self.vertices.push(Vertex {
557 position: self.apply_transform([x, y], transform),
558 uv: [0.5 + 0.5 * angle.cos(), 0.5 + 0.5 * angle.sin()],
559 color: [color.r, color.g, color.b, color.a],
560 params: [0.0, 0.0, 0.0, 0.0],
561 flags: 0,
562 });
563 
564 if i > 0 {
565 self.indices.extend_from_slice(&[
566 center_index,
567 self.vertex_count - 1,
568 self.vertex_count,
569 ]);
570 }
571 
572 self.vertex_count += 1;
573 }
574 }
575 
576 /// Batch a line as a rectangle
577 fn batch_line(&mut self, start: (f32, f32), end: (f32, f32), color: Color, thickness: f32) {
578 let (x1, y1) = start;
579 let (x2, y2) = end;
580 
581 // Calculate line direction and perpendicular
582 let dx = x2 - x1;
583 let dy = y2 - y1;
584 let length = (dx * dx + dy * dy).sqrt();
585 
586 if length == 0.0 {
587 return;
588 }
589 
590 let nx = -dy / length * thickness * 0.5;
591 let ny = dx / length * thickness * 0.5;
592 
593 // Create line vertices
594 let vertices = [
595 Vertex {
596 position: [x1 + nx, y1 + ny],
597 uv: [0.0, 0.0],
598 color: [color.r, color.g, color.b, color.a],
599 params: [0.0, 0.0, 0.0, 0.0],
600 flags: 0,
601 },
602 Vertex {
603 position: [x2 + nx, y2 + ny],
604 uv: [1.0, 0.0],
605 color: [color.r, color.g, color.b, color.a],
606 params: [0.0, 0.0, 0.0, 0.0],
607 flags: 0,
608 },
609 Vertex {
610 position: [x2 - nx, y2 - ny],
611 uv: [1.0, 1.0],
612 color: [color.r, color.g, color.b, color.a],
613 params: [0.0, 0.0, 0.0, 0.0],
614 flags: 0,
615 },
616 Vertex {
617 position: [x1 - nx, y1 - ny],
618 uv: [0.0, 1.0],
619 color: [color.r, color.g, color.b, color.a],
620 params: [0.0, 0.0, 0.0, 0.0],
621 flags: 0,
622 },
623 ];
624 
625 self.vertices.extend_from_slice(&vertices);
626 
627 let base = self.vertex_count;
628 self.indices
629 .extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
630 
631 self.vertex_count += 4;
632 }
633 
634 /// Apply transform to a position
635 fn apply_transform(&self, pos: [f32; 2], transform: Transform) -> [f32; 2] {
636 // Transform uses a matrix internally, so we need to transform the point
637 let point = strato_core::types::Point::new(pos[0], pos[1]);
638 let transformed = transform.transform_point(point);
639 [transformed.x, transformed.y]
640 }
641 
642 /// Get the number of draw calls
643 pub fn draw_call_count(&self) -> usize {
644 self.commands.len()
645 }
646 
647 /// Get the number of vertices
648 pub fn vertex_count(&self) -> usize {
649 self.vertices.len()
650 }
651 
652 /// Get the number of triangles
653 pub fn triangle_count(&self) -> usize {
654 self.indices.len() / 3
655 }
656 
657 /// Register a texture in the atlas
658 pub fn register_texture(
659 &mut self,
660 id: u32,
661 width: u32,
662 height: u32,
663 format: wgpu::TextureFormat,
664 ) {
665 self.texture_atlas.insert(
666 id,
667 TextureInfo {
668 width,
669 height,
670 format,
671 },
672 );
673 }
674 
675 /// Get texture info
676 pub fn get_texture_info(&self, id: u32) -> Option<&TextureInfo> {
677 self.texture_atlas.get(&id)
678 }
679}
680 
681impl Default for RenderBatch {
682 fn default() -> Self {
683 Self::new()
684 }
685}
686 
687#[cfg(test)]
688mod tests {
689 use super::*;
690 use glam::Vec2;
691 use strato_core::types::Color;
692 
693 #[test]
694 fn test_batch_rect() {
695 let mut batch = RenderBatch::new();
696 let rect = Rect::new(10.0, 20.0, 100.0, 50.0);
697 let color = Color::rgba(1.0, 0.0, 0.0, 1.0);
698 let transform = Transform::default();
699 
700 batch.add_rect(rect, color, transform);
701 
702 assert_eq!(batch.vertex_count(), 4);
703 assert_eq!(batch.triangle_count(), 2);
704 assert_eq!(batch.draw_call_count(), 1);
705 }
706 
707 #[test]
708 fn test_batch_circle() {
709 let mut batch = RenderBatch::new();
710 let center = (50.0, 50.0);
711 let radius = 25.0;
712 let color = Color::rgba(0.0, 1.0, 0.0, 1.0);
713 let segments = 16;
714 
715 let segments = 16;
716 let transform = Transform::default();
717 
718 batch.add_circle(center, radius, color, segments, transform);
719 
720 assert_eq!(batch.vertex_count(), segments as usize + 2); // center + perimeter + closing
721 assert_eq!(batch.draw_call_count(), 1);
722 }
723 
724 #[test]
725 fn test_clear_batch() {
726 let mut batch = RenderBatch::new();
727 let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
728 let color = Color::WHITE;
729 let transform = Transform::default();
730 
731 batch.add_rect(rect, color, transform);
732 assert!(!batch.vertices.is_empty());
733 
734 batch.clear();
735 assert!(batch.vertices.is_empty());
736 assert!(batch.indices.is_empty());
737 assert_eq!(batch.draw_call_count(), 0);
738 }
739}
740