Skip to main content

astrelis_render/
renderer.rs

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