Skip to main content

textured_window/
textured_window.rs

1//! Basic textured quad rendering example.
2//!
3//! Demonstrates the fundamental wgpu rendering pipeline in Astrelis:
4//! 1. Shader compilation (WGSL)
5//! 2. Procedural texture creation and GPU upload
6//! 3. Sampler and bind group setup
7//! 4. Render pipeline configuration
8//! 5. Per-frame rendering via `clear_and_render`
9
10use astrelis_core::logging;
11use astrelis_render::{
12    Color, GraphicsContext, RenderTarget, RenderableWindow, WindowContextDescriptor,
13};
14use astrelis_winit::{
15    WindowId,
16    app::run_app,
17    window::{WindowBackend, WindowDescriptor},
18};
19
20struct App {
21    window: RenderableWindow,
22    window_id: WindowId,
23    pipeline: wgpu::RenderPipeline,
24    bind_group: wgpu::BindGroup,
25    vertex_buffer: wgpu::Buffer,
26}
27
28fn main() {
29    logging::init();
30
31    run_app(|ctx| {
32        let graphics_ctx = GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
33
34        let window = ctx
35            .create_window(WindowDescriptor {
36                title: "Textured Window".to_string(),
37                ..Default::default()
38            })
39            .expect("Failed to create window");
40
41        let window = RenderableWindow::new_with_descriptor(
42            window,
43            graphics_ctx.clone(),
44            WindowContextDescriptor {
45                format: Some(wgpu::TextureFormat::Bgra8UnormSrgb),
46                ..Default::default()
47            },
48        ).expect("Failed to create renderable window");
49
50        // --- Shader ---
51        let shader = graphics_ctx
52            .device()
53            .create_shader_module(wgpu::ShaderModuleDescriptor {
54                label: Some("Texture Shader"),
55                source: wgpu::ShaderSource::Wgsl(include_str!("textured_window.wgsl").into()),
56            });
57
58        // --- Texture Upload ---
59        // Create a 256x256 RGBA texture with a procedural gradient pattern.
60        // Rgba8UnormSrgb applies sRGB gamma correction so colors appear correct on screen.
61        let texture_size = wgpu::Extent3d {
62            width: 256,
63            height: 256,
64            depth_or_array_layers: 1,
65        };
66
67        let texture = graphics_ctx
68            .device()
69            .create_texture(&wgpu::TextureDescriptor {
70                label: Some("Example Texture"),
71                size: texture_size,
72                mip_level_count: 1,
73                sample_count: 1,
74                dimension: wgpu::TextureDimension::D2,
75                format: wgpu::TextureFormat::Rgba8UnormSrgb,
76                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
77                view_formats: &[],
78            });
79
80        let mut texture_data = vec![0u8; (256 * 256 * 4) as usize];
81        for y in 0..256 {
82            for x in 0..256 {
83                let idx = ((y * 256 + x) * 4) as usize;
84                texture_data[idx] = x as u8;
85                texture_data[idx + 1] = y as u8;
86                texture_data[idx + 2] = ((x + y) / 2) as u8;
87                texture_data[idx + 3] = 255;
88            }
89        }
90
91        graphics_ctx.queue().write_texture(
92            wgpu::TexelCopyTextureInfo {
93                texture: &texture,
94                mip_level: 0,
95                origin: wgpu::Origin3d::ZERO,
96                aspect: wgpu::TextureAspect::All,
97            },
98            &texture_data,
99            wgpu::TexelCopyBufferLayout {
100                offset: 0,
101                bytes_per_row: Some(256 * 4),
102                rows_per_image: Some(256),
103            },
104            texture_size,
105        );
106
107        // --- Sampler ---
108        let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
109        // ClampToEdge prevents sampling beyond texture edges; Linear mag filter
110        // gives smooth results when the quad is larger than the texture.
111        let sampler = graphics_ctx
112            .device()
113            .create_sampler(&wgpu::SamplerDescriptor {
114                address_mode_u: wgpu::AddressMode::ClampToEdge,
115                address_mode_v: wgpu::AddressMode::ClampToEdge,
116                address_mode_w: wgpu::AddressMode::ClampToEdge,
117                mag_filter: wgpu::FilterMode::Linear,
118                min_filter: wgpu::FilterMode::Nearest,
119                mipmap_filter: wgpu::FilterMode::Nearest,
120                ..Default::default()
121            });
122
123        // --- Bind Group ---
124        // Binding 0: texture (Float filterable = supports linear sampling)
125        // Binding 1: sampler (Filtering = pairs with filterable textures)
126        let bind_group_layout =
127            graphics_ctx
128                .device()
129                .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
130                    label: Some("Texture Bind Group Layout"),
131                    entries: &[
132                        wgpu::BindGroupLayoutEntry {
133                            binding: 0,
134                            visibility: wgpu::ShaderStages::FRAGMENT,
135                            ty: wgpu::BindingType::Texture {
136                                multisampled: false,
137                                view_dimension: wgpu::TextureViewDimension::D2,
138                                sample_type: wgpu::TextureSampleType::Float { filterable: true },
139                            },
140                            count: None,
141                        },
142                        wgpu::BindGroupLayoutEntry {
143                            binding: 1,
144                            visibility: wgpu::ShaderStages::FRAGMENT,
145                            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
146                            count: None,
147                        },
148                    ],
149                });
150
151        let bind_group = graphics_ctx
152            .device()
153            .create_bind_group(&wgpu::BindGroupDescriptor {
154                label: Some("Texture Bind Group"),
155                layout: &bind_group_layout,
156                entries: &[
157                    wgpu::BindGroupEntry {
158                        binding: 0,
159                        resource: wgpu::BindingResource::TextureView(&texture_view),
160                    },
161                    wgpu::BindGroupEntry {
162                        binding: 1,
163                        resource: wgpu::BindingResource::Sampler(&sampler),
164                    },
165                ],
166            });
167
168        // --- Pipeline ---
169        let pipeline_layout =
170            graphics_ctx
171                .device()
172                .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
173                    label: Some("Render Pipeline Layout"),
174                    bind_group_layouts: &[&bind_group_layout],
175                    push_constant_ranges: &[],
176                });
177
178        let pipeline =
179            graphics_ctx
180                .device()
181                .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
182                    label: Some("Render Pipeline"),
183                    layout: Some(&pipeline_layout),
184                    vertex: wgpu::VertexState {
185                        module: &shader,
186                        entry_point: Some("vs_main"),
187                        buffers: &[wgpu::VertexBufferLayout {
188                            // 4 floats × 4 bytes = 16 bytes per vertex (2×f32 pos + 2×f32 UV)
189                            array_stride: 4 * 4,
190                            step_mode: wgpu::VertexStepMode::Vertex,
191                            attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2],
192                        }],
193                        compilation_options: wgpu::PipelineCompilationOptions::default(),
194                    },
195                    fragment: Some(wgpu::FragmentState {
196                        module: &shader,
197                        entry_point: Some("fs_main"),
198                        targets: &[Some(wgpu::ColorTargetState {
199                            format: wgpu::TextureFormat::Bgra8UnormSrgb,
200                            blend: Some(wgpu::BlendState::REPLACE),
201                            write_mask: wgpu::ColorWrites::ALL,
202                        })],
203                        compilation_options: wgpu::PipelineCompilationOptions::default(),
204                    }),
205                    primitive: wgpu::PrimitiveState {
206                        topology: wgpu::PrimitiveTopology::TriangleList,
207                        strip_index_format: None,
208                        front_face: wgpu::FrontFace::Ccw,
209                        cull_mode: Some(wgpu::Face::Back),
210                        polygon_mode: wgpu::PolygonMode::Fill,
211                        unclipped_depth: false,
212                        conservative: false,
213                    },
214                    depth_stencil: None,
215                    multisample: wgpu::MultisampleState {
216                        count: 1,
217                        mask: !0,
218                        alpha_to_coverage_enabled: false,
219                    },
220                    multiview: None,
221                    cache: None,
222                });
223
224        #[rustfmt::skip]
225        let vertices: &[f32] = &[
226            -0.8, -0.8,  0.0, 1.0,
227             0.8, -0.8,  1.0, 1.0,
228             0.8,  0.8,  1.0, 0.0,
229            -0.8, -0.8,  0.0, 1.0,
230             0.8,  0.8,  1.0, 0.0,
231            -0.8,  0.8,  0.0, 0.0,
232        ];
233
234        let vertex_buffer = graphics_ctx.device().create_buffer(&wgpu::BufferDescriptor {
235            label: Some("Vertex Buffer"),
236            size: (vertices.len() * std::mem::size_of::<f32>()) as u64,
237            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
238            mapped_at_creation: false,
239        });
240
241        graphics_ctx
242            .queue()
243            .write_buffer(&vertex_buffer, 0, bytemuck::cast_slice(vertices));
244
245        let window_id = window.id();
246
247        Box::new(App {
248            window,
249            window_id,
250            pipeline,
251            bind_group,
252            vertex_buffer,
253        })
254    });
255}
256
257impl astrelis_winit::app::App for App {
258    fn update(&mut self, _ctx: &mut astrelis_winit::app::AppCtx, _time: &astrelis_winit::FrameTime) {
259        // Global logic (none needed for this example)
260    }
261
262    fn render(
263        &mut self,
264        _ctx: &mut astrelis_winit::app::AppCtx,
265        window_id: WindowId,
266        events: &mut astrelis_winit::event::EventBatch,
267    ) {
268        if window_id != self.window_id {
269            return;
270        }
271
272        // Handle window resize events
273        events.dispatch(|event| {
274            if let astrelis_winit::event::Event::WindowResized(size) = event {
275                self.window.resized(*size);
276                astrelis_winit::event::HandleStatus::consumed()
277            } else {
278                astrelis_winit::event::HandleStatus::ignored()
279            }
280        });
281
282        // --- Render Loop ---
283        let mut frame = self.window.begin_drawing();
284
285        // clear_and_render handles render pass scoping automatically:
286        // the pass is created, the closure runs, then the pass drops.
287        frame.clear_and_render(
288            RenderTarget::Surface,
289            Color::rgb(0.1, 0.2, 0.3),
290            |pass| {
291                let pass = pass.wgpu_pass();
292                pass.set_pipeline(&self.pipeline);
293                pass.set_bind_group(0, &self.bind_group, &[]);
294                pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
295                pass.draw(0..6, 0..1);
296            },
297        );
298
299        frame.finish();
300    }
301}