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