1use std::collections::HashMap;
19use std::sync::Arc;
20
21use astrelis_core::logging;
22use astrelis_render::batched::{
23 create_batch_renderer_2d, BatchRenderer2D, BestBatchCapability2D, BindlessBatchCapability2D,
24 DirectBatchCapability2D, DrawBatch2D, DrawType2D, IndirectBatchCapability2D, RenderTier,
25 UnifiedInstance2D,
26};
27use astrelis_render::{
28 GraphicsContext, GraphicsContextDescriptor, RenderableWindow, WindowContextDescriptor,
29};
30use astrelis_winit::app::run_app;
31use astrelis_winit::window::{WindowBackend, WindowDescriptor, WinitPhysicalSize};
32use astrelis_winit::WindowId;
33
34const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
35
36struct App {
37 context: Arc<GraphicsContext>,
38 windows: HashMap<WindowId, RenderableWindow>,
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.context.device().create_texture(&wgpu::TextureDescriptor {
55 label: Some("example_depth"),
56 size: wgpu::Extent3d {
57 width: w,
58 height: h,
59 depth_or_array_layers: 1,
60 },
61 mip_level_count: 1,
62 sample_count: 1,
63 dimension: wgpu::TextureDimension::D2,
64 format: DEPTH_FORMAT,
65 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
66 view_formats: &[],
67 });
68 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
69 self.depth_texture = texture;
70 self.depth_view = view;
71 self.depth_width = w;
72 self.depth_height = h;
73 }
74
75 fn ortho_projection(width: f32, height: f32) -> [[f32; 4]; 4] {
79 [
80 [2.0 / width, 0.0, 0.0, 0.0],
81 [0.0, -2.0 / height, 0.0, 0.0],
82 [0.0, 0.0, 1.0, 0.0],
83 [-1.0, 1.0, 0.0, 1.0],
84 ]
85 }
86
87 fn build_instances(&self, width: f32, height: f32) -> Vec<UnifiedInstance2D> {
89 let t = self.frame_count as f32 / 60.0;
90 let mut instances = Vec::new();
91
92 instances.push(UnifiedInstance2D {
94 position: [10.0, 10.0],
95 size: [width - 20.0, height - 20.0],
96 color: [0.15, 0.15, 0.18, 1.0],
97 border_radius: 12.0,
98 z_depth: 0.01,
99 draw_type: DrawType2D::Quad as u32,
100 ..Default::default()
101 });
102
103 let cols = 5;
105 let rows = 3;
106 let margin = 30.0;
107 let gap = 10.0;
108 let cell_w = (width - 2.0 * margin - (cols as f32 - 1.0) * gap) / cols as f32;
109 let cell_h = (height * 0.5 - margin - (rows as f32 - 1.0) * gap) / rows as f32;
110
111 for row in 0..rows {
112 for col in 0..cols {
113 let x = margin + col as f32 * (cell_w + gap);
114 let y = margin + row as f32 * (cell_h + gap);
115 let idx = row * cols + col;
116
117 let hue = (idx as f32 / (rows * cols) as f32) * 360.0;
119 let (r, g, b) = hsl_to_rgb(hue, 0.7, 0.55);
120
121 instances.push(UnifiedInstance2D {
122 position: [x, y],
123 size: [cell_w, cell_h],
124 color: [r, g, b, 1.0],
125 border_radius: 6.0,
126 z_depth: 0.1 + idx as f32 * 0.001,
127 draw_type: DrawType2D::Quad as u32,
128 ..Default::default()
129 });
130 }
131 }
132
133 let float_x = width * 0.5 + (t * 0.8).sin() * width * 0.25 - 60.0;
135 let float_y = height * 0.35 + (t * 1.2).cos() * 30.0;
136 instances.push(UnifiedInstance2D {
137 position: [float_x, float_y],
138 size: [120.0, 50.0],
139 color: [1.0, 0.85, 0.2, 0.9],
140 border_radius: 25.0,
141 z_depth: 0.8,
142 draw_type: DrawType2D::Quad as u32,
143 ..Default::default()
144 });
145
146 let outline_y = height * 0.6;
148 for i in 0..4 {
149 let x = margin + i as f32 * 140.0;
150 let thickness = 1.0 + i as f32;
151 let radius = 4.0 + i as f32 * 8.0;
152 instances.push(UnifiedInstance2D {
153 position: [x, outline_y],
154 size: [120.0, 80.0],
155 color: [0.4, 0.8, 1.0, 1.0],
156 border_radius: radius,
157 border_thickness: thickness,
158 z_depth: 0.5,
159 draw_type: DrawType2D::Quad as u32,
160 ..Default::default()
161 });
162 }
163
164 let overlap_x = width * 0.5 - 100.0;
166 let overlap_y = height * 0.75;
167 let colors = [
168 [1.0, 0.3, 0.3, 0.6],
169 [0.3, 1.0, 0.3, 0.6],
170 [0.3, 0.3, 1.0, 0.6],
171 ];
172 for (i, color) in colors.iter().enumerate() {
173 let offset = i as f32 * 40.0;
174 instances.push(UnifiedInstance2D {
175 position: [overlap_x + offset, overlap_y + offset * 0.5],
176 size: [120.0, 80.0],
177 color: *color,
178 border_radius: 8.0,
179 z_depth: 0.6 + i as f32 * 0.05,
180 draw_type: DrawType2D::Quad as u32,
181 ..Default::default()
182 });
183 }
184
185 let pulse = ((t * 2.0).sin() * 0.5 + 0.5) * 0.4 + 0.6;
187 let circle_size = 60.0 * pulse;
188 instances.push(UnifiedInstance2D {
189 position: [width - margin - circle_size, outline_y + 10.0],
190 size: [circle_size, circle_size],
191 color: [1.0, 0.5, 0.0, 0.95],
192 border_radius: circle_size * 0.5,
193 z_depth: 0.7,
194 draw_type: DrawType2D::Quad as u32,
195 ..Default::default()
196 });
197
198 let clip_x = margin;
200 let clip_y = height * 0.75;
201 instances.push(UnifiedInstance2D {
202 position: [clip_x, clip_y],
203 size: [200.0, 60.0],
204 color: [0.9, 0.2, 0.7, 1.0],
205 border_radius: 4.0,
206 z_depth: 0.55,
207 draw_type: DrawType2D::Quad as u32,
208 clip_min: [clip_x + 20.0, clip_y + 10.0],
210 clip_max: [clip_x + 160.0, clip_y + 50.0],
211 ..Default::default()
212 });
213
214 instances
215 }
216}
217
218fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) {
220 let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
221 let h_prime = h / 60.0;
222 let x = c * (1.0 - (h_prime % 2.0 - 1.0).abs());
223 let (r1, g1, b1) = if h_prime < 1.0 {
224 (c, x, 0.0)
225 } else if h_prime < 2.0 {
226 (x, c, 0.0)
227 } else if h_prime < 3.0 {
228 (0.0, c, x)
229 } else if h_prime < 4.0 {
230 (0.0, x, c)
231 } else if h_prime < 5.0 {
232 (x, 0.0, c)
233 } else {
234 (c, 0.0, x)
235 };
236 let m = l - c * 0.5;
237 (r1 + m, g1 + m, b1 + m)
238}
239
240fn parse_tier() -> Option<RenderTier> {
252 let args: Vec<String> = std::env::args().collect();
253 for (i, arg) in args.iter().enumerate() {
254 if arg == "--tier" {
255 if let Some(value) = args.get(i + 1) {
256 return match value.as_str() {
257 "1" | "direct" => Some(RenderTier::Direct),
258 "2" | "indirect" => Some(RenderTier::Indirect),
259 "3" | "bindless" => Some(RenderTier::Bindless),
260 "auto" => None,
261 other => {
262 eprintln!(
263 "Unknown tier '{other}'. Options: 1|direct, 2|indirect, 3|bindless, auto"
264 );
265 std::process::exit(1);
266 }
267 };
268 }
269 }
270 }
271 None
273}
274
275fn main() {
276 logging::init();
277
278 run_app(|ctx| {
279 let tier_override = parse_tier();
280
281 let descriptor = match tier_override {
285 None => GraphicsContextDescriptor::new()
286 .request_capability::<BestBatchCapability2D>(),
287 Some(RenderTier::Direct) => GraphicsContextDescriptor::new()
288 .require_capability::<DirectBatchCapability2D>(),
289 Some(RenderTier::Indirect) => GraphicsContextDescriptor::new()
290 .require_capability::<IndirectBatchCapability2D>(),
291 Some(RenderTier::Bindless) => GraphicsContextDescriptor::new()
292 .require_capability::<BindlessBatchCapability2D>(),
293 };
294 let graphics_ctx =
295 pollster::block_on(GraphicsContext::new_owned_with_descriptor(descriptor))
296 .expect("Failed to create graphics context");
297
298 let window = ctx
299 .create_window(WindowDescriptor {
300 title: "Batched Renderer Example".to_string(),
301 size: Some(WinitPhysicalSize::new(800.0, 600.0)),
302 ..Default::default()
303 })
304 .expect("Failed to create window");
305
306 let surface_format = wgpu::TextureFormat::Bgra8UnormSrgb;
307
308 let renderable_window = RenderableWindow::new_with_descriptor(
309 window,
310 graphics_ctx.clone(),
311 WindowContextDescriptor {
312 format: Some(surface_format),
313 ..Default::default()
314 },
315 )
316 .expect("Failed to create renderable window");
317
318 let window_id = renderable_window.id();
319
320 let renderer = create_batch_renderer_2d(
321 graphics_ctx.clone(),
322 surface_format,
323 tier_override,
324 );
325
326 tracing::info!("Using render tier: {}", renderer.tier());
327
328 let depth_texture = graphics_ctx
330 .device()
331 .create_texture(&wgpu::TextureDescriptor {
332 label: Some("example_depth"),
333 size: wgpu::Extent3d {
334 width: 1,
335 height: 1,
336 depth_or_array_layers: 1,
337 },
338 mip_level_count: 1,
339 sample_count: 1,
340 dimension: wgpu::TextureDimension::D2,
341 format: DEPTH_FORMAT,
342 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
343 view_formats: &[],
344 });
345 let depth_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
346
347 let mut windows = HashMap::new();
348 windows.insert(window_id, renderable_window);
349
350 Box::new(App {
351 context: graphics_ctx,
352 windows,
353 renderer,
354 depth_texture,
355 depth_view,
356 depth_width: 1,
357 depth_height: 1,
358 frame_count: 0,
359 })
360 });
361}
362
363impl astrelis_winit::app::App for App {
364 fn update(
365 &mut self,
366 _ctx: &mut astrelis_winit::app::AppCtx,
367 _time: &astrelis_winit::FrameTime,
368 ) {
369 self.frame_count += 1;
370 }
371
372 fn render(
373 &mut self,
374 _ctx: &mut astrelis_winit::app::AppCtx,
375 window_id: WindowId,
376 events: &mut astrelis_winit::event::EventBatch,
377 ) {
378 let (phys_width, phys_height) = {
380 let Some(window) = self.windows.get_mut(&window_id) else {
381 return;
382 };
383
384 events.dispatch(|event| {
385 if let astrelis_winit::event::Event::WindowResized(size) = event {
386 window.resized(*size);
387 astrelis_winit::event::HandleStatus::consumed()
388 } else {
389 astrelis_winit::event::HandleStatus::ignored()
390 }
391 });
392
393 let phys = window.physical_size();
394 (phys.width, phys.height)
395 };
396
397 let width = phys_width as f32;
398 let height = phys_height as f32;
399
400 if width < 1.0 || height < 1.0 {
401 return;
402 }
403
404 self.ensure_depth_buffer(phys_width, phys_height);
406
407 let instances = self.build_instances(width, height);
409 let batch = DrawBatch2D {
410 instances,
411 textures: vec![],
412 projection: Self::ortho_projection(width, height),
413 };
414 self.renderer.prepare(&batch);
415
416 let stats = self.renderer.stats();
417 if self.frame_count % 120 == 0 {
418 tracing::info!(
419 "Frame {}: {} instances ({} opaque, {} transparent), {} draw calls",
420 self.frame_count,
421 stats.instance_count,
422 stats.opaque_count,
423 stats.transparent_count,
424 stats.draw_calls,
425 );
426 }
427
428 let window = self.windows.get_mut(&window_id).unwrap();
430 let mut frame = window.begin_drawing();
431
432 frame.with_pass(
434 astrelis_render::RenderPassBuilder::new()
435 .label("batched_example_pass")
436 .target(astrelis_render::RenderTarget::Surface)
437 .clear_color(astrelis_render::Color::rgba(0.08, 0.08, 0.1, 1.0))
438 .clear_depth(0.0) .depth_stencil_attachment(
440 &self.depth_view,
441 Some(wgpu::Operations {
442 load: wgpu::LoadOp::Clear(0.0),
443 store: wgpu::StoreOp::Store,
444 }),
445 None,
446 ),
447 |pass| {
448 self.renderer.render(pass.wgpu_pass());
449 },
450 );
451
452 frame.finish();
453 }
454}