Skip to main content

renderer_api/
renderer_api.rs

1//! Renderer helper API and two-pass offscreen rendering example.
2//!
3//! Demonstrates:
4//! - Using the `Renderer` helper for resource creation (shaders, buffers, textures)
5//! - Rendering to an offscreen `Framebuffer` (pass 1) and blitting it to the surface (pass 2)
6//! - `BlendMode::Alpha` (standard source-over blending for scene geometry)
7//!   vs `BlendMode::PremultipliedAlpha` (for compositing a pre-rendered framebuffer)
8
9use astrelis_core::logging;
10use astrelis_render::{
11    BlendMode, Color, Framebuffer, GraphicsContext, RenderTarget, RenderWindow,
12    RenderWindowBuilder, Renderer, wgpu,
13};
14use astrelis_winit::{
15    FrameTime, WindowId,
16    app::{App, AppCtx, run_app},
17    event::EventBatch,
18    window::{WindowDescriptor, WinitPhysicalSize},
19};
20use std::sync::Arc;
21
22struct RendererApp {
23    _context: Arc<GraphicsContext>,
24    _renderer: Renderer,
25    window: RenderWindow,
26    window_id: WindowId,
27    pipeline: wgpu::RenderPipeline,
28    bind_group: wgpu::BindGroup,
29    vertex_buffer: wgpu::Buffer,
30    // Offscreen framebuffer for demonstrating framebuffer rendering
31    offscreen_fb: Framebuffer,
32    blit_pipeline: wgpu::RenderPipeline,
33    blit_bind_group: wgpu::BindGroup,
34    time: f32,
35}
36
37fn main() {
38    logging::init();
39
40    run_app(|ctx| {
41        let graphics_ctx =
42            GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
43        let renderer = Renderer::new(graphics_ctx.clone());
44
45        let window = ctx
46            .create_window(WindowDescriptor {
47                title: "Renderer API Example".to_string(),
48                size: Some(WinitPhysicalSize::new(800.0, 600.0)),
49                ..Default::default()
50            })
51            .expect("Failed to create window");
52
53        let window = RenderWindowBuilder::new()
54            .color_format(wgpu::TextureFormat::Bgra8UnormSrgb)
55            .with_depth_default()
56            .build(window, graphics_ctx.clone())
57            .expect("Failed to create render window");
58
59        let window_id = window.id();
60
61        // Create shader using Renderer API
62        let shader = renderer.create_shader(Some("Color Shader"), SHADER_SOURCE);
63
64        // Create texture using Renderer helper
65        let texture_data = create_gradient_texture();
66        let texture = renderer.create_texture_2d(
67            Some("Gradient Texture"),
68            256,
69            256,
70            wgpu::TextureFormat::Rgba8UnormSrgb,
71            wgpu::TextureUsages::TEXTURE_BINDING,
72            &texture_data,
73        );
74
75        let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
76        let sampler = renderer.create_linear_sampler(Some("Linear Sampler"));
77
78        // Create bind group using Renderer API
79        let bind_group_layout = renderer.create_bind_group_layout(
80            Some("Texture Bind Group Layout"),
81            &[
82                wgpu::BindGroupLayoutEntry {
83                    binding: 0,
84                    visibility: wgpu::ShaderStages::FRAGMENT,
85                    ty: wgpu::BindingType::Texture {
86                        multisampled: false,
87                        view_dimension: wgpu::TextureViewDimension::D2,
88                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
89                    },
90                    count: None,
91                },
92                wgpu::BindGroupLayoutEntry {
93                    binding: 1,
94                    visibility: wgpu::ShaderStages::FRAGMENT,
95                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
96                    count: None,
97                },
98            ],
99        );
100
101        let bind_group = renderer.create_bind_group(
102            Some("Texture Bind Group"),
103            &bind_group_layout,
104            &[
105                wgpu::BindGroupEntry {
106                    binding: 0,
107                    resource: wgpu::BindingResource::TextureView(&texture_view),
108                },
109                wgpu::BindGroupEntry {
110                    binding: 1,
111                    resource: wgpu::BindingResource::Sampler(&sampler),
112                },
113            ],
114        );
115
116        let pipeline_layout = renderer.create_pipeline_layout(
117            Some("Render Pipeline Layout"),
118            &[&bind_group_layout],
119            &[],
120        );
121
122        // Create pipeline using Renderer API with BlendMode
123        let pipeline = renderer.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
124            label: Some("Render Pipeline"),
125            layout: Some(&pipeline_layout),
126            vertex: wgpu::VertexState {
127                module: &shader,
128                entry_point: Some("vs_main"),
129                buffers: &[wgpu::VertexBufferLayout {
130                    // 4 floats × 4 bytes = 16 bytes per vertex (2×f32 pos + 2×f32 UV)
131                    array_stride: 4 * 4,
132                    step_mode: wgpu::VertexStepMode::Vertex,
133                    attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2],
134                }],
135                compilation_options: wgpu::PipelineCompilationOptions::default(),
136            },
137            fragment: Some(wgpu::FragmentState {
138                module: &shader,
139                entry_point: Some("fs_main"),
140                // Use BlendMode for transparent rendering
141                targets: &[Some(
142                    BlendMode::Alpha.to_color_target_state(wgpu::TextureFormat::Rgba8UnormSrgb),
143                )],
144                compilation_options: wgpu::PipelineCompilationOptions::default(),
145            }),
146            primitive: wgpu::PrimitiveState {
147                topology: wgpu::PrimitiveTopology::TriangleList,
148                strip_index_format: None,
149                front_face: wgpu::FrontFace::Ccw,
150                cull_mode: Some(wgpu::Face::Back),
151                polygon_mode: wgpu::PolygonMode::Fill,
152                unclipped_depth: false,
153                conservative: false,
154            },
155            depth_stencil: None,
156            multisample: wgpu::MultisampleState {
157                count: 1,
158                mask: !0,
159                alpha_to_coverage_enabled: false,
160            },
161            multiview: None,
162            cache: None,
163        });
164
165        #[rustfmt::skip]
166        let vertices: &[f32] = &[
167            -0.8, -0.8,  0.0, 1.0,
168             0.8, -0.8,  1.0, 1.0,
169             0.8,  0.8,  1.0, 0.0,
170            -0.8, -0.8,  0.0, 1.0,
171             0.8,  0.8,  1.0, 0.0,
172            -0.8,  0.8,  0.0, 0.0,
173        ];
174
175        // Create vertex buffer using Renderer helper
176        let vertex_buffer = renderer.create_vertex_buffer(Some("Vertex Buffer"), vertices);
177
178        // Create offscreen framebuffer using the new Framebuffer abstraction
179        let offscreen_fb = Framebuffer::builder(400, 300)
180            .format(wgpu::TextureFormat::Rgba8UnormSrgb)
181            .label("Offscreen FB")
182            .build(&graphics_ctx);
183
184        // Create blit shader and pipeline for rendering framebuffer to surface
185        let blit_shader = renderer.create_shader(Some("Blit Shader"), BLIT_SHADER_SOURCE);
186
187        let blit_bind_group_layout = renderer.create_bind_group_layout(
188            Some("Blit Bind Group Layout"),
189            &[
190                wgpu::BindGroupLayoutEntry {
191                    binding: 0,
192                    visibility: wgpu::ShaderStages::FRAGMENT,
193                    ty: wgpu::BindingType::Texture {
194                        multisampled: false,
195                        view_dimension: wgpu::TextureViewDimension::D2,
196                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
197                    },
198                    count: None,
199                },
200                wgpu::BindGroupLayoutEntry {
201                    binding: 1,
202                    visibility: wgpu::ShaderStages::FRAGMENT,
203                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
204                    count: None,
205                },
206            ],
207        );
208
209        let blit_bind_group = renderer.create_bind_group(
210            Some("Blit Bind Group"),
211            &blit_bind_group_layout,
212            &[
213                wgpu::BindGroupEntry {
214                    binding: 0,
215                    resource: wgpu::BindingResource::TextureView(offscreen_fb.color_view()),
216                },
217                wgpu::BindGroupEntry {
218                    binding: 1,
219                    resource: wgpu::BindingResource::Sampler(&sampler),
220                },
221            ],
222        );
223
224        let blit_pipeline_layout = renderer.create_pipeline_layout(
225            Some("Blit Pipeline Layout"),
226            &[&blit_bind_group_layout],
227            &[],
228        );
229
230        let blit_pipeline = renderer.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
231            label: Some("Blit Pipeline"),
232            layout: Some(&blit_pipeline_layout),
233            vertex: wgpu::VertexState {
234                module: &blit_shader,
235                entry_point: Some("vs_main"),
236                buffers: &[],
237                compilation_options: wgpu::PipelineCompilationOptions::default(),
238            },
239            fragment: Some(wgpu::FragmentState {
240                module: &blit_shader,
241                entry_point: Some("fs_main"),
242                // Use PremultipliedAlpha for framebuffer blitting
243                targets: &[Some(
244                    BlendMode::PremultipliedAlpha
245                        .to_color_target_state(wgpu::TextureFormat::Bgra8UnormSrgb),
246                )],
247                compilation_options: wgpu::PipelineCompilationOptions::default(),
248            }),
249            primitive: wgpu::PrimitiveState {
250                topology: wgpu::PrimitiveTopology::TriangleList,
251                ..Default::default()
252            },
253            depth_stencil: None,
254            multisample: wgpu::MultisampleState::default(),
255            multiview: None,
256            cache: None,
257        });
258
259        tracing::info!("Renderer initialized successfully");
260        tracing::info!("Device: {:?}", renderer.context().info());
261
262        Box::new(RendererApp {
263            _context: graphics_ctx,
264            _renderer: renderer,
265            window,
266            window_id,
267            pipeline,
268            bind_group,
269            vertex_buffer,
270            offscreen_fb,
271            blit_pipeline,
272            blit_bind_group,
273            time: 0.0,
274        })
275    });
276}
277
278impl App for RendererApp {
279    fn update(&mut self, _ctx: &mut AppCtx, _time: &FrameTime) {
280        // Global logic - update animation time
281        self.time += 0.016;
282    }
283
284    fn render(&mut self, _ctx: &mut AppCtx, window_id: WindowId, events: &mut EventBatch) {
285        if window_id != self.window_id {
286            return;
287        }
288
289        // Handle window-specific resize events
290        events.dispatch(|event| {
291            if let astrelis_winit::event::Event::WindowResized(size) = event {
292                self.window.resized(*size);
293                astrelis_winit::event::HandleStatus::consumed()
294            } else {
295                astrelis_winit::event::HandleStatus::ignored()
296            }
297        });
298
299        let Some(frame) = self.window.begin_frame() else {
300            return; // Surface not available
301        };
302
303        // Pass 1: Render to offscreen framebuffer
304        {
305            let mut pass = frame
306                .render_pass()
307                .target(RenderTarget::Framebuffer(&self.offscreen_fb))
308                .clear_color(Color::rgb(0.2, 0.1, 0.3))
309                .label("offscreen_pass")
310                .build();
311            pass.set_pipeline(&self.pipeline);
312            pass.set_bind_group(0, &self.bind_group, &[]);
313            pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
314            pass.draw(0..6, 0..1);
315        }
316
317        // Pass 2: Blit framebuffer to surface
318        {
319            let mut pass = frame
320                .render_pass()
321                .clear_color(Color::rgb(0.1, 0.2, 0.3))
322                .label("blit_pass")
323                .build();
324            pass.set_pipeline(&self.blit_pipeline);
325            pass.set_bind_group(0, &self.blit_bind_group, &[]);
326            // Draw fullscreen triangle
327            pass.draw(0..3, 0..1);
328        }
329        // Frame auto-submits on drop
330    }
331}
332
333fn create_gradient_texture() -> Vec<u8> {
334    let mut texture_data = vec![0u8; (256 * 256 * 4) as usize];
335    for y in 0..256 {
336        for x in 0..256 {
337            let idx = ((y * 256 + x) * 4) as usize;
338            texture_data[idx] = x as u8;
339            texture_data[idx + 1] = y as u8;
340            texture_data[idx + 2] = ((x + y) / 2) as u8;
341            texture_data[idx + 3] = 255;
342        }
343    }
344    texture_data
345}
346
347const SHADER_SOURCE: &str = r#"
348struct VertexInput {
349    @location(0) position: vec2<f32>,
350    @location(1) tex_coords: vec2<f32>,
351}
352
353struct VertexOutput {
354    @builtin(position) clip_position: vec4<f32>,
355    @location(0) tex_coords: vec2<f32>,
356}
357
358@vertex
359fn vs_main(in: VertexInput) -> VertexOutput {
360    var out: VertexOutput;
361    out.clip_position = vec4<f32>(in.position, 0.0, 1.0);
362    out.tex_coords = in.tex_coords;
363    return out;
364}
365
366@group(0) @binding(0)
367var t_diffuse: texture_2d<f32>;
368@group(0) @binding(1)
369var s_diffuse: sampler;
370
371@fragment
372fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
373    return textureSample(t_diffuse, s_diffuse, in.tex_coords);
374}
375"#;
376
377const BLIT_SHADER_SOURCE: &str = r#"
378struct VertexOutput {
379    @builtin(position) clip_position: vec4<f32>,
380    @location(0) tex_coords: vec2<f32>,
381}
382
383@vertex
384fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
385    // Fullscreen triangle
386    var positions = array<vec2<f32>, 3>(
387        vec2<f32>(-1.0, -1.0),
388        vec2<f32>(3.0, -1.0),
389        vec2<f32>(-1.0, 3.0)
390    );
391    var tex_coords = array<vec2<f32>, 3>(
392        vec2<f32>(0.0, 1.0),
393        vec2<f32>(2.0, 1.0),
394        vec2<f32>(0.0, -1.0)
395    );
396
397    var out: VertexOutput;
398    out.clip_position = vec4<f32>(positions[vertex_index], 0.0, 1.0);
399    out.tex_coords = tex_coords[vertex_index];
400    return out;
401}
402
403@group(0) @binding(0)
404var t_source: texture_2d<f32>;
405@group(0) @binding(1)
406var s_source: sampler;
407
408@fragment
409fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
410    return textureSample(t_source, s_source, in.tex_coords);
411}
412"#;