astrelis_render/
renderer.rs

1use crate::context::GraphicsContext;
2
3/// Low-level extensible renderer that simplifies WGPU resource management.
4///
5/// This provides a foundation for higher-level renderers like TextRenderer, SceneRenderer, etc.
6/// It manages common rendering state and provides utilities for resource creation.
7pub struct Renderer {
8    context: &'static GraphicsContext,
9}
10
11impl Renderer {
12    /// Create a new renderer with the given graphics context.
13    pub fn new(context: &'static GraphicsContext) -> Self {
14        Self { context }
15    }
16
17    /// Get the graphics context.
18    pub fn context(&self) -> &'static GraphicsContext {
19        self.context
20    }
21
22    /// Get the device.
23    pub fn device(&self) -> &wgpu::Device {
24        &self.context.device
25    }
26
27    /// Get the queue.
28    pub fn queue(&self) -> &wgpu::Queue {
29        &self.context.queue
30    }
31
32    /// Create a shader module from WGSL source.
33    pub fn create_shader(&self, label: Option<&str>, source: &str) -> wgpu::ShaderModule {
34        self.context
35            .device
36            .create_shader_module(wgpu::ShaderModuleDescriptor {
37                label,
38                source: wgpu::ShaderSource::Wgsl(source.into()),
39            })
40    }
41
42    /// Create a vertex buffer with data.
43    pub fn create_vertex_buffer<T: bytemuck::Pod>(
44        &self,
45        label: Option<&str>,
46        data: &[T],
47    ) -> wgpu::Buffer {
48        let buffer = self.context.device.create_buffer(&wgpu::BufferDescriptor {
49            label,
50            size: std::mem::size_of_val(data) as u64,
51            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
52            mapped_at_creation: false,
53        });
54
55        self.context
56            .queue
57            .write_buffer(&buffer, 0, bytemuck::cast_slice(data));
58
59        buffer
60    }
61
62    /// Create an index buffer with data.
63    pub fn create_index_buffer<T: bytemuck::Pod>(
64        &self,
65        label: Option<&str>,
66        data: &[T],
67    ) -> wgpu::Buffer {
68        let buffer = self.context.device.create_buffer(&wgpu::BufferDescriptor {
69            label,
70            size: std::mem::size_of_val(data) as u64,
71            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
72            mapped_at_creation: false,
73        });
74
75        self.context
76            .queue
77            .write_buffer(&buffer, 0, bytemuck::cast_slice(data));
78
79        buffer
80    }
81
82    /// Create a uniform buffer with data.
83    pub fn create_uniform_buffer<T: bytemuck::Pod>(
84        &self,
85        label: Option<&str>,
86        data: &T,
87    ) -> wgpu::Buffer {
88        let buffer = self.context.device.create_buffer(&wgpu::BufferDescriptor {
89            label,
90            size: std::mem::size_of::<T>() as u64,
91            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
92            mapped_at_creation: false,
93        });
94
95        self.context.queue.write_buffer(
96            &buffer,
97            0,
98            bytemuck::cast_slice(std::slice::from_ref(data)),
99        );
100
101        buffer
102    }
103
104    /// Update a uniform buffer with new data.
105    pub fn update_uniform_buffer<T: bytemuck::Pod>(&self, buffer: &wgpu::Buffer, data: &T) {
106        self.context.queue.write_buffer(
107            buffer,
108            0,
109            bytemuck::cast_slice(std::slice::from_ref(data)),
110        );
111    }
112
113    /// Create an empty storage buffer.
114    ///
115    /// # Arguments
116    ///
117    /// * `label` - Optional debug label
118    /// * `size` - Size in bytes
119    /// * `read_only` - If true, creates a read-only storage buffer (STORAGE),
120    ///                 otherwise creates a read-write storage buffer (STORAGE | COPY_DST)
121    pub fn create_storage_buffer(
122        &self,
123        label: Option<&str>,
124        size: u64,
125        read_only: bool,
126    ) -> wgpu::Buffer {
127        let usage = if read_only {
128            wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST
129        } else {
130            wgpu::BufferUsages::STORAGE
131                | wgpu::BufferUsages::COPY_DST
132                | wgpu::BufferUsages::COPY_SRC
133        };
134
135        self.context.device.create_buffer(&wgpu::BufferDescriptor {
136            label,
137            size,
138            usage,
139            mapped_at_creation: false,
140        })
141    }
142
143    /// Create a storage buffer initialized with data.
144    ///
145    /// # Arguments
146    ///
147    /// * `label` - Optional debug label
148    /// * `data` - Initial data to write to the buffer
149    /// * `read_only` - If true, creates a read-only storage buffer,
150    ///                 otherwise creates a read-write storage buffer
151    pub fn create_storage_buffer_init<T: bytemuck::Pod>(
152        &self,
153        label: Option<&str>,
154        data: &[T],
155        read_only: bool,
156    ) -> wgpu::Buffer {
157        let usage = if read_only {
158            wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST
159        } else {
160            wgpu::BufferUsages::STORAGE
161                | wgpu::BufferUsages::COPY_DST
162                | wgpu::BufferUsages::COPY_SRC
163        };
164
165        let buffer = self.context.device.create_buffer(&wgpu::BufferDescriptor {
166            label,
167            size: std::mem::size_of_val(data) as u64,
168            usage,
169            mapped_at_creation: false,
170        });
171
172        self.context
173            .queue
174            .write_buffer(&buffer, 0, bytemuck::cast_slice(data));
175
176        buffer
177    }
178
179    /// Update a storage buffer with new data at the specified offset.
180    ///
181    /// # Arguments
182    ///
183    /// * `buffer` - The buffer to update
184    /// * `offset` - Byte offset into the buffer
185    /// * `data` - Data to write
186    pub fn update_storage_buffer<T: bytemuck::Pod>(
187        &self,
188        buffer: &wgpu::Buffer,
189        offset: u64,
190        data: &[T],
191    ) {
192        self.context
193            .queue
194            .write_buffer(buffer, offset, bytemuck::cast_slice(data));
195    }
196
197    /// Create a texture with descriptor.
198    pub fn create_texture(&self, descriptor: &wgpu::TextureDescriptor) -> wgpu::Texture {
199        self.context.device.create_texture(descriptor)
200    }
201
202    /// Create a 2D texture with data.
203    pub fn create_texture_2d(
204        &self,
205        label: Option<&str>,
206        width: u32,
207        height: u32,
208        format: wgpu::TextureFormat,
209        usage: wgpu::TextureUsages,
210        data: &[u8],
211    ) -> wgpu::Texture {
212        let size = wgpu::Extent3d {
213            width,
214            height,
215            depth_or_array_layers: 1,
216        };
217
218        let texture = self
219            .context
220            .device
221            .create_texture(&wgpu::TextureDescriptor {
222                label,
223                size,
224                mip_level_count: 1,
225                sample_count: 1,
226                dimension: wgpu::TextureDimension::D2,
227                format,
228                usage: usage | wgpu::TextureUsages::COPY_DST,
229                view_formats: &[],
230            });
231
232        let bytes_per_pixel = format.block_copy_size(None).unwrap();
233
234        self.context.queue.write_texture(
235            wgpu::TexelCopyTextureInfo {
236                texture: &texture,
237                mip_level: 0,
238                origin: wgpu::Origin3d::ZERO,
239                aspect: wgpu::TextureAspect::All,
240            },
241            data,
242            wgpu::TexelCopyBufferLayout {
243                offset: 0,
244                bytes_per_row: Some(width * bytes_per_pixel),
245                rows_per_image: Some(height),
246            },
247            size,
248        );
249
250        texture
251    }
252
253    /// Create a sampler with descriptor.
254    pub fn create_sampler(&self, descriptor: &wgpu::SamplerDescriptor) -> wgpu::Sampler {
255        self.context.device.create_sampler(descriptor)
256    }
257
258    /// Create a simple linear sampler.
259    pub fn create_linear_sampler(&self, label: Option<&str>) -> wgpu::Sampler {
260        self.context
261            .device
262            .create_sampler(&wgpu::SamplerDescriptor {
263                label,
264                address_mode_u: wgpu::AddressMode::ClampToEdge,
265                address_mode_v: wgpu::AddressMode::ClampToEdge,
266                address_mode_w: wgpu::AddressMode::ClampToEdge,
267                mag_filter: wgpu::FilterMode::Linear,
268                min_filter: wgpu::FilterMode::Linear,
269                mipmap_filter: wgpu::FilterMode::Nearest,
270                ..Default::default()
271            })
272    }
273
274    /// Create a simple nearest sampler.
275    pub fn create_nearest_sampler(&self, label: Option<&str>) -> wgpu::Sampler {
276        self.context
277            .device
278            .create_sampler(&wgpu::SamplerDescriptor {
279                label,
280                address_mode_u: wgpu::AddressMode::ClampToEdge,
281                address_mode_v: wgpu::AddressMode::ClampToEdge,
282                address_mode_w: wgpu::AddressMode::ClampToEdge,
283                mag_filter: wgpu::FilterMode::Nearest,
284                min_filter: wgpu::FilterMode::Nearest,
285                mipmap_filter: wgpu::FilterMode::Nearest,
286                ..Default::default()
287            })
288    }
289
290    /// Create a bind group layout.
291    pub fn create_bind_group_layout(
292        &self,
293        label: Option<&str>,
294        entries: &[wgpu::BindGroupLayoutEntry],
295    ) -> wgpu::BindGroupLayout {
296        self.context
297            .device
298            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label, entries })
299    }
300
301    /// Create a bind group.
302    pub fn create_bind_group(
303        &self,
304        label: Option<&str>,
305        layout: &wgpu::BindGroupLayout,
306        entries: &[wgpu::BindGroupEntry],
307    ) -> wgpu::BindGroup {
308        self.context
309            .device
310            .create_bind_group(&wgpu::BindGroupDescriptor {
311                label,
312                layout,
313                entries,
314            })
315    }
316
317    /// Create a pipeline layout.
318    pub fn create_pipeline_layout(
319        &self,
320        label: Option<&str>,
321        bind_group_layouts: &[&wgpu::BindGroupLayout],
322        push_constant_ranges: &[wgpu::PushConstantRange],
323    ) -> wgpu::PipelineLayout {
324        self.context
325            .device
326            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
327                label,
328                bind_group_layouts,
329                push_constant_ranges,
330            })
331    }
332
333    /// Create a render pipeline.
334    pub fn create_render_pipeline(
335        &self,
336        descriptor: &wgpu::RenderPipelineDescriptor,
337    ) -> wgpu::RenderPipeline {
338        self.context.device.create_render_pipeline(descriptor)
339    }
340
341    /// Create a compute pipeline.
342    pub fn create_compute_pipeline(
343        &self,
344        descriptor: &wgpu::ComputePipelineDescriptor,
345    ) -> wgpu::ComputePipeline {
346        self.context.device.create_compute_pipeline(descriptor)
347    }
348
349    /// Create a command encoder.
350    pub fn create_command_encoder(&self, label: Option<&str>) -> wgpu::CommandEncoder {
351        self.context
352            .device
353            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label })
354    }
355
356    /// Submit command buffers to the queue.
357    pub fn submit<I>(&self, command_buffers: I)
358    where
359        I: IntoIterator<Item = wgpu::CommandBuffer>,
360    {
361        self.context.queue.submit(command_buffers);
362    }
363}
364
365/// Builder for creating a render pipeline with common defaults.
366pub struct RenderPipelineBuilder<'a> {
367    renderer: &'a Renderer,
368    label: Option<&'a str>,
369    shader: Option<&'a wgpu::ShaderModule>,
370    vertex_entry: &'a str,
371    fragment_entry: &'a str,
372    layout: Option<&'a wgpu::PipelineLayout>,
373    vertex_buffers: Vec<wgpu::VertexBufferLayout<'a>>,
374    color_targets: Vec<Option<wgpu::ColorTargetState>>,
375    primitive: wgpu::PrimitiveState,
376    depth_stencil: Option<wgpu::DepthStencilState>,
377    multisample: wgpu::MultisampleState,
378}
379
380impl<'a> RenderPipelineBuilder<'a> {
381    pub fn new(renderer: &'a Renderer) -> Self {
382        Self {
383            renderer,
384            label: None,
385            shader: None,
386            vertex_entry: "vs_main",
387            fragment_entry: "fs_main",
388            layout: None,
389            vertex_buffers: Vec::new(),
390            color_targets: Vec::new(),
391            primitive: wgpu::PrimitiveState {
392                topology: wgpu::PrimitiveTopology::TriangleList,
393                strip_index_format: None,
394                front_face: wgpu::FrontFace::Ccw,
395                cull_mode: Some(wgpu::Face::Back),
396                polygon_mode: wgpu::PolygonMode::Fill,
397                unclipped_depth: false,
398                conservative: false,
399            },
400            depth_stencil: None,
401            multisample: wgpu::MultisampleState {
402                count: 1,
403                mask: !0,
404                alpha_to_coverage_enabled: false,
405            },
406        }
407    }
408
409    pub fn label(mut self, label: &'a str) -> Self {
410        self.label = Some(label);
411        self
412    }
413
414    pub fn shader(mut self, shader: &'a wgpu::ShaderModule) -> Self {
415        self.shader = Some(shader);
416        self
417    }
418
419    pub fn vertex_entry(mut self, entry: &'a str) -> Self {
420        self.vertex_entry = entry;
421        self
422    }
423
424    pub fn fragment_entry(mut self, entry: &'a str) -> Self {
425        self.fragment_entry = entry;
426        self
427    }
428
429    pub fn layout(mut self, layout: &'a wgpu::PipelineLayout) -> Self {
430        self.layout = Some(layout);
431        self
432    }
433
434    pub fn vertex_buffer(mut self, layout: wgpu::VertexBufferLayout<'a>) -> Self {
435        self.vertex_buffers.push(layout);
436        self
437    }
438
439    pub fn color_target(mut self, target: wgpu::ColorTargetState) -> Self {
440        self.color_targets.push(Some(target));
441        self
442    }
443
444    pub fn primitive(mut self, primitive: wgpu::PrimitiveState) -> Self {
445        self.primitive = primitive;
446        self
447    }
448
449    pub fn depth_stencil(mut self, depth_stencil: wgpu::DepthStencilState) -> Self {
450        self.depth_stencil = Some(depth_stencil);
451        self
452    }
453
454    pub fn multisample(mut self, multisample: wgpu::MultisampleState) -> Self {
455        self.multisample = multisample;
456        self
457    }
458
459    pub fn build(self) -> wgpu::RenderPipeline {
460        let shader = self.shader.expect("Shader module is required");
461        let layout = self.layout.expect("Pipeline layout is required");
462
463        self.renderer
464            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
465                label: self.label,
466                layout: Some(layout),
467                vertex: wgpu::VertexState {
468                    module: shader,
469                    entry_point: Some(self.vertex_entry),
470                    buffers: &self.vertex_buffers,
471                    compilation_options: wgpu::PipelineCompilationOptions::default(),
472                },
473                fragment: Some(wgpu::FragmentState {
474                    module: shader,
475                    entry_point: Some(self.fragment_entry),
476                    targets: &self.color_targets,
477                    compilation_options: wgpu::PipelineCompilationOptions::default(),
478                }),
479                primitive: self.primitive,
480                depth_stencil: self.depth_stencil,
481                multisample: self.multisample,
482                multiview: None,
483                cache: None,
484            })
485    }
486}
487
488/// Builder for creating a compute pipeline with common defaults.
489///
490/// # Example
491///
492/// ```ignore
493/// let pipeline = ComputePipelineBuilder::new(&renderer)
494///     .label("My Compute Pipeline")
495///     .shader(&shader)
496///     .entry("main")
497///     .layout(&layout)
498///     .build();
499/// ```
500pub struct ComputePipelineBuilder<'a> {
501    renderer: &'a Renderer,
502    label: Option<&'a str>,
503    shader: Option<&'a wgpu::ShaderModule>,
504    entry: &'a str,
505    layout: Option<&'a wgpu::PipelineLayout>,
506}
507
508impl<'a> ComputePipelineBuilder<'a> {
509    /// Create a new compute pipeline builder.
510    pub fn new(renderer: &'a Renderer) -> Self {
511        Self {
512            renderer,
513            label: None,
514            shader: None,
515            entry: "main",
516            layout: None,
517        }
518    }
519
520    /// Set a debug label for the pipeline.
521    pub fn label(mut self, label: &'a str) -> Self {
522        self.label = Some(label);
523        self
524    }
525
526    /// Set the shader module.
527    pub fn shader(mut self, shader: &'a wgpu::ShaderModule) -> Self {
528        self.shader = Some(shader);
529        self
530    }
531
532    /// Set the entry point function name.
533    ///
534    /// Defaults to "main".
535    pub fn entry(mut self, entry: &'a str) -> Self {
536        self.entry = entry;
537        self
538    }
539
540    /// Set the pipeline layout.
541    pub fn layout(mut self, layout: &'a wgpu::PipelineLayout) -> Self {
542        self.layout = Some(layout);
543        self
544    }
545
546    /// Build the compute pipeline.
547    ///
548    /// # Panics
549    ///
550    /// Panics if shader or layout is not set.
551    pub fn build(self) -> wgpu::ComputePipeline {
552        let shader = self.shader.expect("Shader module is required");
553        let layout = self.layout.expect("Pipeline layout is required");
554
555        self.renderer
556            .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
557                label: self.label,
558                layout: Some(layout),
559                module: shader,
560                entry_point: Some(self.entry),
561                compilation_options: wgpu::PipelineCompilationOptions::default(),
562                cache: None,
563            })
564    }
565}