Skip to main content

arcane_engine/renderer/
mod.rs

1mod gpu;
2mod sprite;
3mod texture;
4mod camera;
5mod tilemap;
6mod lighting;
7pub mod font;
8
9pub use gpu::GpuContext;
10pub use sprite::{SpriteCommand, SpritePipeline};
11pub use texture::{TextureId, TextureStore};
12pub use camera::Camera2D;
13pub use tilemap::{Tilemap, TilemapStore};
14pub use lighting::{LightingState, LightingUniform, PointLight, LightData, MAX_LIGHTS};
15
16use anyhow::Result;
17
18/// Top-level renderer that owns the GPU context, sprite pipeline, and textures.
19pub struct Renderer {
20    pub gpu: GpuContext,
21    pub sprites: SpritePipeline,
22    pub textures: TextureStore,
23    pub camera: Camera2D,
24    pub lighting: LightingState,
25    /// Sprite commands queued for the current frame.
26    pub frame_commands: Vec<SpriteCommand>,
27    /// Display scale factor (e.g. 2.0 on Retina). Used to convert physical → logical pixels.
28    pub scale_factor: f32,
29    /// Clear color for the render pass background. Default: dark blue-gray.
30    pub clear_color: [f32; 4],
31}
32
33impl Renderer {
34    /// Create a new renderer attached to a winit window.
35    pub fn new(window: std::sync::Arc<winit::window::Window>) -> Result<Self> {
36        let scale_factor = window.scale_factor() as f32;
37        let gpu = GpuContext::new(window)?;
38        let sprites = SpritePipeline::new(&gpu);
39        let textures = TextureStore::new();
40        // Set camera viewport to logical pixels so world units are DPI-independent
41        let logical_w = gpu.config.width as f32 / scale_factor;
42        let logical_h = gpu.config.height as f32 / scale_factor;
43        let camera = Camera2D {
44            viewport_size: [logical_w, logical_h],
45            ..Camera2D::default()
46        };
47        Ok(Self {
48            gpu,
49            sprites,
50            textures,
51            camera,
52            lighting: LightingState::default(),
53            frame_commands: Vec::new(),
54            scale_factor,
55            clear_color: [0.1, 0.1, 0.15, 1.0],
56        })
57    }
58
59    /// Render the current frame's sprite commands and present.
60    pub fn render_frame(&mut self) -> Result<()> {
61        let output = self.gpu.surface.get_current_texture()?;
62        let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
63
64        let mut encoder = self.gpu.device.create_command_encoder(
65            &wgpu::CommandEncoderDescriptor { label: Some("frame_encoder") },
66        );
67
68        // Sort by layer, then by texture for batching
69        self.frame_commands.sort_by(|a, b| {
70            a.layer.cmp(&b.layer).then(a.texture_id.cmp(&b.texture_id))
71        });
72
73        let lighting_uniform = self.lighting.to_uniform();
74        let clear_color = wgpu::Color {
75            r: self.clear_color[0] as f64,
76            g: self.clear_color[1] as f64,
77            b: self.clear_color[2] as f64,
78            a: self.clear_color[3] as f64,
79        };
80
81        self.sprites.render(
82            &self.gpu,
83            &self.textures,
84            &self.camera,
85            &lighting_uniform,
86            &self.frame_commands,
87            &view,
88            &mut encoder,
89            clear_color,
90        );
91
92        self.gpu.queue.submit(std::iter::once(encoder.finish()));
93        output.present();
94
95        self.frame_commands.clear();
96        Ok(())
97    }
98
99    /// Resize the surface when the window size changes.
100    /// GPU surface uses physical pixels; camera viewport uses logical pixels.
101    pub fn resize(&mut self, physical_width: u32, physical_height: u32, scale_factor: f32) {
102        if physical_width > 0 && physical_height > 0 {
103            self.scale_factor = scale_factor;
104            self.gpu.config.width = physical_width;
105            self.gpu.config.height = physical_height;
106            self.gpu.surface.configure(&self.gpu.device, &self.gpu.config);
107            // Camera uses logical pixels so 1 world unit ≈ 1 logical pixel at zoom 1
108            self.camera.viewport_size = [
109                physical_width as f32 / scale_factor,
110                physical_height as f32 / scale_factor,
111            ];
112        }
113    }
114}