1use 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 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 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 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 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 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 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 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 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 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 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_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
221fn 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
243fn 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 None
276}
277
278fn main() {
279 logging::init();
280
281 run_app(|ctx| {
282 let tier_override = parse_tier();
283
284 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 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 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 self.ensure_depth_buffer(phys_width, phys_height);
404
405 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 let window = self.windows.get_mut(&window_id).unwrap();
428 let Some(frame) = window.begin_frame() else {
429 return; };
431
432 {
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) .label("batched_example_pass")
440 .build();
441 self.renderer.render(pass.wgpu_pass());
442 }
443 }
445}