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