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