Skip to main content

batched_renderer/
batched_renderer.rs

1//! Batched renderer example demonstrating the unified instance rendering system.
2//!
3//! Renders various shapes using the `BatchRenderer2D`:
4//! - Solid colored quads at different depths
5//! - Rounded rectangles with SDF corners
6//! - Border-only outlines
7//! - Semi-transparent overlapping quads
8//! - Animated position and color changes
9//!
10//! Run with: `cargo run -p astrelis-render --example batched_renderer`
11//!
12//! Select render tier with `--tier`:
13//!   `cargo run -p astrelis-render --example batched_renderer -- --tier auto`
14//!   `cargo run -p astrelis-render --example batched_renderer -- --tier 1`      (or `direct`)
15//!   `cargo run -p astrelis-render --example batched_renderer -- --tier 2`      (or `indirect`)
16//!   `cargo run -p astrelis-render --example batched_renderer -- --tier 3`      (or `bindless`)
17
18use std::collections::HashMap;
19use std::sync::Arc;
20
21use astrelis_core::logging;
22use astrelis_render::batched::{
23    BatchRenderer2D, BestBatchCapability2D, BindlessBatchCapability2D, DirectBatchCapability2D,
24    DrawBatch2D, DrawType2D, IndirectBatchCapability2D, RenderTier, UnifiedInstance2D,
25    create_batch_renderer_2d,
26};
27use astrelis_render::{
28    Color, GraphicsContext, GraphicsContextDescriptor, RenderWindow, RenderWindowBuilder,
29};
30use astrelis_winit::WindowId;
31use astrelis_winit::app::run_app;
32use astrelis_winit::window::{WindowDescriptor, WinitPhysicalSize};
33
34const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
35
36struct App {
37    context: Arc<GraphicsContext>,
38    windows: HashMap<WindowId, RenderWindow>,
39    renderer: Box<dyn BatchRenderer2D>,
40    depth_texture: wgpu::Texture,
41    depth_view: wgpu::TextureView,
42    depth_width: u32,
43    depth_height: u32,
44    frame_count: u64,
45}
46
47impl App {
48    fn ensure_depth_buffer(&mut self, width: u32, height: u32) {
49        if self.depth_width == width && self.depth_height == height {
50            return;
51        }
52        let w = width.max(1);
53        let h = height.max(1);
54        let texture = self
55            .context
56            .device()
57            .create_texture(&wgpu::TextureDescriptor {
58                label: Some("example_depth"),
59                size: wgpu::Extent3d {
60                    width: w,
61                    height: h,
62                    depth_or_array_layers: 1,
63                },
64                mip_level_count: 1,
65                sample_count: 1,
66                dimension: wgpu::TextureDimension::D2,
67                format: DEPTH_FORMAT,
68                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
69                view_formats: &[],
70            });
71        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
72        self.depth_texture = texture;
73        self.depth_view = view;
74        self.depth_width = w;
75        self.depth_height = h;
76    }
77
78    /// Build an orthographic projection matrix for the given viewport size.
79    /// Maps (0,0) at top-left to (width, height) at bottom-right.
80    /// Z range: 0.0 (far) to 1.0 (near), matching GreaterEqual depth compare.
81    fn ortho_projection(width: f32, height: f32) -> [[f32; 4]; 4] {
82        [
83            [2.0 / width, 0.0, 0.0, 0.0],
84            [0.0, -2.0 / height, 0.0, 0.0],
85            [0.0, 0.0, 1.0, 0.0],
86            [-1.0, 1.0, 0.0, 1.0],
87        ]
88    }
89
90    /// Generate the demo instances for the current frame.
91    fn build_instances(&self, width: f32, height: f32) -> Vec<UnifiedInstance2D> {
92        let t = self.frame_count as f32 / 60.0;
93        let mut instances = Vec::new();
94
95        // --- Background panel (gray, full viewport, furthest back) ---
96        instances.push(UnifiedInstance2D {
97            position: [10.0, 10.0],
98            size: [width - 20.0, height - 20.0],
99            color: [0.15, 0.15, 0.18, 1.0],
100            border_radius: 12.0,
101            z_depth: 0.01,
102            draw_type: DrawType2D::Quad as u32,
103            ..Default::default()
104        });
105
106        // --- Grid of colored quads ---
107        let cols = 5;
108        let rows = 3;
109        let margin = 30.0;
110        let gap = 10.0;
111        let cell_w = (width - 2.0 * margin - (cols as f32 - 1.0) * gap) / cols as f32;
112        let cell_h = (height * 0.5 - margin - (rows as f32 - 1.0) * gap) / rows as f32;
113
114        for row in 0..rows {
115            for col in 0..cols {
116                let x = margin + col as f32 * (cell_w + gap);
117                let y = margin + row as f32 * (cell_h + gap);
118                let idx = row * cols + col;
119
120                // Hue-shift color based on grid position
121                let hue = (idx as f32 / (rows * cols) as f32) * 360.0;
122                let (r, g, b) = hsl_to_rgb(hue, 0.7, 0.55);
123
124                instances.push(UnifiedInstance2D {
125                    position: [x, y],
126                    size: [cell_w, cell_h],
127                    color: [r, g, b, 1.0],
128                    border_radius: 6.0,
129                    z_depth: 0.1 + idx as f32 * 0.001,
130                    draw_type: DrawType2D::Quad as u32,
131                    ..Default::default()
132                });
133            }
134        }
135
136        // --- Animated floating rounded rect ---
137        let float_x = width * 0.5 + (t * 0.8).sin() * width * 0.25 - 60.0;
138        let float_y = height * 0.35 + (t * 1.2).cos() * 30.0;
139        instances.push(UnifiedInstance2D {
140            position: [float_x, float_y],
141            size: [120.0, 50.0],
142            color: [1.0, 0.85, 0.2, 0.9],
143            border_radius: 25.0,
144            z_depth: 0.8,
145            draw_type: DrawType2D::Quad as u32,
146            ..Default::default()
147        });
148
149        // --- Border-only outlines (bottom area) ---
150        let outline_y = height * 0.6;
151        for i in 0..4 {
152            let x = margin + i as f32 * 140.0;
153            let thickness = 1.0 + i as f32;
154            let radius = 4.0 + i as f32 * 8.0;
155            instances.push(UnifiedInstance2D {
156                position: [x, outline_y],
157                size: [120.0, 80.0],
158                color: [0.4, 0.8, 1.0, 1.0],
159                border_radius: radius,
160                border_thickness: thickness,
161                z_depth: 0.5,
162                draw_type: DrawType2D::Quad as u32,
163                ..Default::default()
164            });
165        }
166
167        // --- Overlapping transparent quads (demonstrating depth + alpha) ---
168        let overlap_x = width * 0.5 - 100.0;
169        let overlap_y = height * 0.75;
170        let colors = [
171            [1.0, 0.3, 0.3, 0.6],
172            [0.3, 1.0, 0.3, 0.6],
173            [0.3, 0.3, 1.0, 0.6],
174        ];
175        for (i, color) in colors.iter().enumerate() {
176            let offset = i as f32 * 40.0;
177            instances.push(UnifiedInstance2D {
178                position: [overlap_x + offset, overlap_y + offset * 0.5],
179                size: [120.0, 80.0],
180                color: *color,
181                border_radius: 8.0,
182                z_depth: 0.6 + i as f32 * 0.05,
183                draw_type: DrawType2D::Quad as u32,
184                ..Default::default()
185            });
186        }
187
188        // --- Pulsing circle (via large border_radius) ---
189        let pulse = ((t * 2.0).sin() * 0.5 + 0.5) * 0.4 + 0.6;
190        let circle_size = 60.0 * pulse;
191        instances.push(UnifiedInstance2D {
192            position: [width - margin - circle_size, outline_y + 10.0],
193            size: [circle_size, circle_size],
194            color: [1.0, 0.5, 0.0, 0.95],
195            border_radius: circle_size * 0.5,
196            z_depth: 0.7,
197            draw_type: DrawType2D::Quad as u32,
198            ..Default::default()
199        });
200
201        // --- Small shader-clipped quad (demonstrating clip rect) ---
202        let clip_x = margin;
203        let clip_y = height * 0.75;
204        instances.push(UnifiedInstance2D {
205            position: [clip_x, clip_y],
206            size: [200.0, 60.0],
207            color: [0.9, 0.2, 0.7, 1.0],
208            border_radius: 4.0,
209            z_depth: 0.55,
210            draw_type: DrawType2D::Quad as u32,
211            // Clip to a smaller region to demonstrate shader clipping
212            clip_min: [clip_x + 20.0, clip_y + 10.0],
213            clip_max: [clip_x + 160.0, clip_y + 50.0],
214            ..Default::default()
215        });
216
217        instances
218    }
219}
220
221/// Simple HSL to RGB conversion.
222fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) {
223    let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
224    let h_prime = h / 60.0;
225    let x = c * (1.0 - (h_prime % 2.0 - 1.0).abs());
226    let (r1, g1, b1) = if h_prime < 1.0 {
227        (c, x, 0.0)
228    } else if h_prime < 2.0 {
229        (x, c, 0.0)
230    } else if h_prime < 3.0 {
231        (0.0, c, x)
232    } else if h_prime < 4.0 {
233        (0.0, x, c)
234    } else if h_prime < 5.0 {
235        (x, 0.0, c)
236    } else {
237        (c, 0.0, x)
238    };
239    let m = l - c * 0.5;
240    (r1 + m, g1 + m, b1 + m)
241}
242
243/// Parse the `--tier` CLI argument to select a render tier.
244///
245/// The three tiers represent increasing levels of GPU capability:
246/// - **Tier 1 (Direct):** One `draw()` call per texture group. Works on all hardware.
247/// - **Tier 2 (Indirect):** Uses `multi_draw_indirect()` per texture group, batching
248///   draw calls into GPU-side indirect buffers. Requires `MULTI_DRAW_INDIRECT`.
249/// - **Tier 3 (Bindless):** Single `multi_draw_indirect()` per frame using texture
250///   binding arrays. Requires `TEXTURE_BINDING_ARRAY` + `MULTI_DRAW_INDIRECT`.
251///
252/// Passing `--tier auto` (or omitting the flag) lets the engine choose the best
253/// tier supported by the current GPU.
254fn parse_tier() -> Option<RenderTier> {
255    let args: Vec<String> = std::env::args().collect();
256    for (i, arg) in args.iter().enumerate() {
257        if arg == "--tier"
258            && let Some(value) = args.get(i + 1)
259        {
260            return match value.as_str() {
261                "1" | "direct" => Some(RenderTier::Direct),
262                "2" | "indirect" => Some(RenderTier::Indirect),
263                "3" | "bindless" => Some(RenderTier::Bindless),
264                "auto" => None,
265                other => {
266                    eprintln!(
267                        "Unknown tier '{other}'. Options: 1|direct, 2|indirect, 3|bindless, auto"
268                    );
269                    std::process::exit(1);
270                }
271            };
272        }
273    }
274    // Default: auto-detect
275    None
276}
277
278fn main() {
279    logging::init();
280
281    run_app(|ctx| {
282        let tier_override = parse_tier();
283
284        // Use the capability API to configure GPU requirements.
285        // For auto-detect, request the best capability (graceful degradation).
286        // For a specific tier, require that tier's capability.
287        let descriptor = match tier_override {
288            None => GraphicsContextDescriptor::new().request_capability::<BestBatchCapability2D>(),
289            Some(RenderTier::Direct) => {
290                GraphicsContextDescriptor::new().require_capability::<DirectBatchCapability2D>()
291            }
292            Some(RenderTier::Indirect) => {
293                GraphicsContextDescriptor::new().require_capability::<IndirectBatchCapability2D>()
294            }
295            Some(RenderTier::Bindless) => {
296                GraphicsContextDescriptor::new().require_capability::<BindlessBatchCapability2D>()
297            }
298        };
299        let graphics_ctx =
300            pollster::block_on(GraphicsContext::new_owned_with_descriptor(descriptor))
301                .expect("Failed to create graphics context");
302
303        let window = ctx
304            .create_window(WindowDescriptor {
305                title: "Batched Renderer Example".to_string(),
306                size: Some(WinitPhysicalSize::new(800.0, 600.0)),
307                ..Default::default()
308            })
309            .expect("Failed to create window");
310
311        let surface_format = wgpu::TextureFormat::Bgra8UnormSrgb;
312
313        let renderable_window = RenderWindowBuilder::new()
314            .color_format(surface_format)
315            .with_depth_default()
316            .build(window, graphics_ctx.clone())
317            .expect("Failed to create render window");
318
319        let window_id = renderable_window.id();
320
321        let renderer =
322            create_batch_renderer_2d(graphics_ctx.clone(), surface_format, tier_override);
323
324        tracing::info!("Using render tier: {}", renderer.tier());
325
326        // Create initial depth buffer
327        let depth_texture = graphics_ctx
328            .device()
329            .create_texture(&wgpu::TextureDescriptor {
330                label: Some("example_depth"),
331                size: wgpu::Extent3d {
332                    width: 1,
333                    height: 1,
334                    depth_or_array_layers: 1,
335                },
336                mip_level_count: 1,
337                sample_count: 1,
338                dimension: wgpu::TextureDimension::D2,
339                format: DEPTH_FORMAT,
340                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
341                view_formats: &[],
342            });
343        let depth_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
344
345        let mut windows = HashMap::new();
346        windows.insert(window_id, renderable_window);
347
348        Box::new(App {
349            context: graphics_ctx,
350            windows,
351            renderer,
352            depth_texture,
353            depth_view,
354            depth_width: 1,
355            depth_height: 1,
356            frame_count: 0,
357        })
358    });
359}
360
361impl astrelis_winit::app::App for App {
362    fn update(
363        &mut self,
364        _ctx: &mut astrelis_winit::app::AppCtx,
365        _time: &astrelis_winit::FrameTime,
366    ) {
367        self.frame_count += 1;
368    }
369
370    fn render(
371        &mut self,
372        _ctx: &mut astrelis_winit::app::AppCtx,
373        window_id: WindowId,
374        events: &mut astrelis_winit::event::EventBatch,
375    ) {
376        // Handle resize and get dimensions (scoped to release window borrow)
377        let (phys_width, phys_height) = {
378            let Some(window) = self.windows.get_mut(&window_id) else {
379                return;
380            };
381
382            events.dispatch(|event| {
383                if let astrelis_winit::event::Event::WindowResized(size) = event {
384                    window.resized(*size);
385                    astrelis_winit::event::HandleStatus::consumed()
386                } else {
387                    astrelis_winit::event::HandleStatus::ignored()
388                }
389            });
390
391            let phys = window.physical_size();
392            (phys.width, phys.height)
393        };
394
395        let width = phys_width as f32;
396        let height = phys_height as f32;
397
398        if width < 1.0 || height < 1.0 {
399            return;
400        }
401
402        // Ensure depth buffer matches viewport
403        self.ensure_depth_buffer(phys_width, phys_height);
404
405        // Build instances and prepare GPU data
406        let instances = self.build_instances(width, height);
407        let batch = DrawBatch2D {
408            instances,
409            textures: vec![],
410            projection: Self::ortho_projection(width, height),
411        };
412        self.renderer.prepare(&batch);
413
414        let stats = self.renderer.stats();
415        if self.frame_count.is_multiple_of(120) {
416            tracing::info!(
417                "Frame {}: {} instances ({} opaque, {} transparent), {} draw calls",
418                self.frame_count,
419                stats.instance_count,
420                stats.opaque_count,
421                stats.transparent_count,
422                stats.draw_calls,
423            );
424        }
425
426        // Re-borrow window for rendering
427        let window = self.windows.get_mut(&window_id).unwrap();
428        let Some(frame) = window.begin_frame() else {
429            return; // Surface not available
430        };
431
432        // Create render pass with depth stencil attachment
433        {
434            let mut pass = frame
435                .render_pass()
436                .clear_color(Color::rgba(0.08, 0.08, 0.1, 1.0))
437                .depth_attachment(&self.depth_view)
438                .clear_depth(0.0) // 0.0 = far with GreaterEqual
439                .label("batched_example_pass")
440                .build();
441            self.renderer.render(pass.wgpu_pass());
442        }
443        // Frame auto-submits on drop
444    }
445}