Skip to main content

cotis_wgpu/
renderer.rs

1//! [`CotisWgpuRenderer`] and window configuration for the wgpu backend.
2//!
3//! This module owns the winit event loop, wgpu device/surface, and frame presentation logic.
4//! [`cotis::renders::CotisRenderer`] and [`cotis::renders::CotisRendererAsync`] trait implementations
5//! for [`CotisWgpuRenderer`] are defined at the bottom of this file; Cotis context and interactivity
6//! traits are in [`crate::cotis_traits`].
7
8use std::sync::Arc;
9use std::time::{Duration, Instant};
10
11use cotis::renders::{CotisRenderer, CotisRendererAsync, RenderCompatibleWith};
12use cotis_defaults::render_commands::render_t_list::{IsRenderList, RenderTList};
13use cotis_utils::interactivity::keyboard::KeyboardKey;
14use cotis_utils::interactivity::mouse::MouseButton;
15use cotis_utils::math::{Dimensions, Vector2};
16use winit::application::ApplicationHandler;
17use winit::dpi::LogicalSize;
18use winit::error::EventLoopError;
19use winit::event::{ElementState, MouseScrollDelta, WindowEvent};
20use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
21use winit::keyboard::{KeyCode, PhysicalKey};
22use winit::platform::pump_events::{EventLoopExtPumpEvents, PumpStatus};
23use winit::window::{Window, WindowId};
24
25use crate::device::GpuDevice;
26use crate::drawable::{WgpuDrawContext, WgpuDrawable};
27use crate::error::WgpuBackendError;
28use crate::input::InputSnapshot;
29use crate::pipeline::GeometryBatch;
30use crate::surface::SurfaceState;
31use crate::text::TextState;
32
33/// Window creation settings for [`CotisWgpuRenderer`].
34#[derive(Debug, Clone)]
35pub struct WindowConfig {
36    /// Window title bar text.
37    pub title: String,
38    /// Initial inner width in logical pixels.
39    pub width: u32,
40    /// Initial inner height in logical pixels.
41    pub height: u32,
42}
43
44impl Default for WindowConfig {
45    /// Returns the default configuration: title `"cotis-wgpu"`, size 1000×800.
46    ///
47    /// # Examples
48    ///
49    /// ```
50    /// use cotis_wgpu::WindowConfig;
51    ///
52    /// let cfg = WindowConfig::default();
53    /// assert_eq!(cfg.title, "cotis-wgpu");
54    /// assert_eq!(cfg.width, 1000);
55    /// assert_eq!(cfg.height, 800);
56    /// ```
57    fn default() -> Self {
58        Self {
59            title: "cotis-wgpu".to_string(),
60            width: 1000,
61            height: 800,
62        }
63    }
64}
65
66struct WgpuRendererInner {
67    window: Arc<Window>,
68    gpu: GpuDevice,
69    surface: SurfaceState,
70    geometry: GeometryBatch,
71    pub(crate) text: TextState,
72    background: wgpu::Color,
73    size: (u32, u32),
74    pub(crate) dpi_scale: f32,
75    pub(crate) input: InputSnapshot,
76    last_frame: Instant,
77    close_requested: bool,
78}
79
80/// wgpu-backed renderer implementing the shared cotis render traits.
81///
82/// Owns a winit [`EventLoop`] and pumps it on each
83/// [`cotis::renders::CotisRenderer::draw_frame`] call and on [`Self::window_should_close`].
84/// GPU resources (surface, geometry batch, text state) are initialized during construction.
85pub struct CotisWgpuRenderer {
86    event_loop: EventLoop<()>,
87    inner: WgpuRendererInner,
88}
89
90impl CotisWgpuRenderer {
91    /// Creates a renderer with a default window and background color.
92    ///
93    /// Uses [`WindowConfig::default`] and a dark gray background `(45, 45, 55) / 255`.
94    ///
95    /// # Panics
96    ///
97    /// Panics if GPU initialization or window creation fails. For fallible construction, use
98    /// [`Self::new`] or [`Self::new_with_config`].
99    pub fn auto_start() -> Self {
100        Self::new(wgpu::Color {
101            r: 45.0 / 255.0,
102            g: 45.0 / 255.0,
103            b: 55.0 / 255.0,
104            a: 1.0,
105        })
106        .expect("failed to start cotis-wgpu renderer")
107    }
108
109    /// Creates a renderer with the default [`WindowConfig`].
110    ///
111    /// # Errors
112    ///
113    /// Returns [`WgpuBackendError::Init`] if the event loop cannot be created or the window is
114    /// closed during bootstrap, [`WgpuBackendError::NoAdapter`] if no GPU adapter is found,
115    /// [`WgpuBackendError::CreateSurface`] or [`WgpuBackendError::RequestDevice`] if wgpu setup
116    /// fails.
117    pub fn new(background: wgpu::Color) -> Result<Self, WgpuBackendError> {
118        Self::new_with_config(WindowConfig::default(), background)
119    }
120
121    /// Creates a renderer with custom window settings.
122    ///
123    /// Boots the winit event loop and creates the window on the first `Resumed` event.
124    ///
125    /// # Errors
126    ///
127    /// Same variants as [`Self::new`], plus [`WgpuBackendError::Init`] if window creation fails.
128    pub fn new_with_config(
129        config: WindowConfig,
130        background: wgpu::Color,
131    ) -> Result<Self, WgpuBackendError> {
132        let mut event_loop = EventLoop::new()?;
133        event_loop.set_control_flow(ControlFlow::Poll);
134
135        let mut bootstrap = BootstrapHandler {
136            config,
137            background,
138            inner: None,
139            error: None,
140            close_requested: false,
141        };
142
143        loop {
144            let status = event_loop.pump_app_events(Some(Duration::ZERO), &mut bootstrap);
145
146            if let Some(inner) = bootstrap.inner.take() {
147                return Ok(Self { event_loop, inner });
148            }
149
150            if bootstrap.close_requested || matches!(status, PumpStatus::Exit(_)) {
151                return Err(bootstrap.error.unwrap_or(WgpuBackendError::Init(
152                    "window closed during startup".into(),
153                )));
154            }
155        }
156    }
157
158    /// Returns whether the user requested the window to close.
159    ///
160    /// Pumps pending winit events before checking the close flag, so this method requires
161    /// `&mut self` even though it only reads state.
162    pub fn window_should_close(&mut self) -> bool {
163        self.pump_events();
164        self.inner.close_requested
165    }
166
167    /// Returns a shared handle to the underlying winit window.
168    ///
169    /// Useful for platform integration or querying window properties directly.
170    pub fn window(&self) -> &Arc<Window> {
171        &self.inner.window
172    }
173
174    /// Sets the swapchain clear color for subsequent frames.
175    ///
176    /// Takes a normalized [`wgpu::Color`] (0.0..=1.0 per channel), not a cotis
177    /// [`cotis_defaults::colors::Color`]. Use [`crate::color::cotis_color_to_wgpu`] to convert.
178    pub fn set_background(&mut self, background: wgpu::Color) {
179        self.inner.background = background;
180    }
181
182    pub(crate) fn window_dimensions(&self) -> Dimensions {
183        self.inner.window_dimensions()
184    }
185
186    pub(crate) fn delta_time(&self) -> f32 {
187        self.inner.delta_time()
188    }
189
190    pub(crate) fn dpi_scale(&self) -> f32 {
191        self.inner.dpi_scale
192    }
193
194    pub(crate) fn input(&self) -> &InputSnapshot {
195        &self.inner.input
196    }
197
198    pub(crate) fn text(&self) -> &TextState {
199        &self.inner.text
200    }
201
202    fn pump_events(&mut self) {
203        let mut handler = RuntimeHandler {
204            inner: &mut self.inner,
205        };
206        let status = self
207            .event_loop
208            .pump_app_events(Some(Duration::ZERO), &mut handler);
209
210        if matches!(status, PumpStatus::Exit(_)) {
211            self.inner.close_requested = true;
212        }
213    }
214}
215
216impl WgpuRendererInner {
217    fn from_window(window: Arc<Window>, background: wgpu::Color) -> Result<Self, WgpuBackendError> {
218        let size = window.inner_size();
219        let dpi_scale = window.scale_factor() as f32;
220        let gpu = GpuDevice::new(&window)?;
221        let surface = SurfaceState::new(&gpu, &window, size.width, size.height)?;
222        let geometry = GeometryBatch::new(
223            &gpu.device,
224            surface.format(),
225            (size.width as i32, size.height as i32),
226        );
227        let text = TextState::new(
228            &gpu.device,
229            &gpu.queue,
230            surface.format(),
231            (size.width, size.height),
232            dpi_scale,
233        );
234
235        Ok(WgpuRendererInner {
236            window,
237            gpu,
238            surface,
239            geometry,
240            text,
241            background,
242            size: (size.width, size.height),
243            dpi_scale,
244            input: InputSnapshot::default(),
245            last_frame: Instant::now(),
246            close_requested: false,
247        })
248    }
249
250    fn resize(&mut self, width: u32, height: u32) {
251        if width == 0 || height == 0 {
252            return;
253        }
254
255        self.size = (width, height);
256        self.surface.resize(&self.gpu, width, height);
257        self.geometry.resize_vertices((width as i32, height as i32));
258    }
259
260    fn set_dpi_scale(&mut self, dpi_scale: f32) {
261        self.dpi_scale = dpi_scale;
262        self.text.set_dpi_scale(dpi_scale);
263    }
264
265    fn present_frame(
266        &mut self,
267        draw: impl FnOnce(&mut WgpuDrawContext<'_>),
268    ) -> Result<(), WgpuBackendError> {
269        let drawable = self.surface.acquire_frame(&self.gpu)?;
270        let view = drawable
271            .texture
272            .create_view(&wgpu::TextureViewDescriptor::default());
273
274        let mut encoder = self
275            .gpu
276            .device
277            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
278                label: Some("cotis-wgpu frame encoder"),
279            });
280
281        {
282            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
283                label: Some("cotis-wgpu frame pass"),
284                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
285                    view: &view,
286                    resolve_target: None,
287                    ops: wgpu::Operations {
288                        load: wgpu::LoadOp::Clear(self.background),
289                        store: wgpu::StoreOp::Store,
290                    },
291                })],
292                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
293                    view: &self.surface.depth.view,
294                    depth_ops: Some(wgpu::Operations {
295                        load: wgpu::LoadOp::Clear(1.0),
296                        store: wgpu::StoreOp::Store,
297                    }),
298                    stencil_ops: None,
299                }),
300                timestamp_writes: None,
301                occlusion_query_set: None,
302            });
303
304            let mut depth = 0.1_f32;
305            let mut draw_context = WgpuDrawContext {
306                geometry: &mut self.geometry,
307                text: &mut self.text,
308                scissor_active: false,
309                scissor_position: crate::pipeline::UIPosition::new(),
310                scissor_bounds: crate::pipeline::UIPosition::new(),
311                depth: &mut depth,
312                dpi_scale: self.dpi_scale,
313            };
314
315            draw(&mut draw_context);
316
317            self.geometry.flush(&mut render_pass, &self.gpu.queue);
318            self.text.flush(
319                &self.gpu.device,
320                &self.gpu.queue,
321                &mut render_pass,
322                &self.surface.config,
323            );
324        }
325
326        self.gpu.queue.submit(std::iter::once(encoder.finish()));
327        drawable.present();
328        self.last_frame = Instant::now();
329        Ok(())
330    }
331
332    fn draw_commands<'a, T: WgpuDrawable + 'a>(
333        &mut self,
334        render_commands: impl Iterator<Item = T>,
335    ) -> Result<(), WgpuBackendError> {
336        self.present_frame(|draw_context| {
337            for command in render_commands {
338                Box::new(command).draw(draw_context);
339            }
340        })
341    }
342
343    fn draw_boxed_commands<'b>(
344        &mut self,
345        render_commands: impl Iterator<Item = Box<dyn WgpuDrawable + 'b>>,
346    ) -> Result<(), WgpuBackendError> {
347        self.present_frame(|draw_context| {
348            for command in render_commands {
349                command.draw(draw_context);
350            }
351        })
352    }
353
354    pub(crate) fn window_dimensions(&self) -> Dimensions {
355        Dimensions::new(self.size.0 as f32, self.size.1 as f32)
356    }
357
358    pub(crate) fn delta_time(&self) -> f32 {
359        self.last_frame.elapsed().as_secs_f32()
360    }
361}
362
363impl<'b> CotisRenderer<Box<dyn WgpuDrawable + 'b>> for CotisWgpuRenderer {
364    fn draw_frame(&mut self, render_commands: impl Iterator<Item = Box<dyn WgpuDrawable + 'b>>) {
365        self.pump_events();
366        if let Err(error) = self.inner.draw_boxed_commands(render_commands) {
367            log::warn!("frame render failed: {error}");
368        }
369    }
370}
371
372impl<T: WgpuDrawable, L: WgpuDrawable + IsRenderList> CotisRenderer<RenderTList<T, L>>
373    for CotisWgpuRenderer
374{
375    fn draw_frame(&mut self, render_commands: impl Iterator<Item = RenderTList<T, L>>) {
376        self.pump_events();
377        if let Err(error) = self.inner.draw_commands(render_commands) {
378            log::warn!("frame render failed: {error}");
379        }
380    }
381}
382
383impl<T: WgpuDrawable> CotisRenderer<RenderTList<T, ()>> for CotisWgpuRenderer {
384    fn draw_frame(&mut self, render_commands: impl Iterator<Item = RenderTList<T, ()>>) {
385        self.pump_events();
386        if let Err(error) = self.inner.draw_commands(render_commands) {
387            log::warn!("frame render failed: {error}");
388        }
389    }
390}
391
392impl<'b> CotisRendererAsync<Box<dyn WgpuDrawable + 'b>> for CotisWgpuRenderer {
393    async fn draw_frame(
394        &mut self,
395        render_commands: impl Iterator<Item = Box<dyn WgpuDrawable + 'b>>,
396    ) {
397        CotisRenderer::draw_frame(self, render_commands);
398    }
399}
400
401impl<T: WgpuDrawable, L: WgpuDrawable + IsRenderList> CotisRendererAsync<RenderTList<T, L>>
402    for CotisWgpuRenderer
403{
404    async fn draw_frame(&mut self, render_commands: impl Iterator<Item = RenderTList<T, L>>) {
405        CotisRenderer::draw_frame(self, render_commands);
406    }
407}
408
409impl<T: WgpuDrawable> CotisRendererAsync<RenderTList<T, ()>> for CotisWgpuRenderer {
410    async fn draw_frame(&mut self, render_commands: impl Iterator<Item = RenderTList<T, ()>>) {
411        CotisRenderer::draw_frame(self, render_commands);
412    }
413}
414
415impl<Manager> RenderCompatibleWith<Manager> for CotisWgpuRenderer {
416    fn init(&mut self, _layout_manager: &mut Manager) {}
417}
418
419struct BootstrapHandler {
420    config: WindowConfig,
421    background: wgpu::Color,
422    inner: Option<WgpuRendererInner>,
423    error: Option<WgpuBackendError>,
424    close_requested: bool,
425}
426
427impl BootstrapHandler {
428    fn try_create(&mut self, event_loop: &ActiveEventLoop) {
429        if self.inner.is_some() {
430            return;
431        }
432
433        let window_attributes = Window::default_attributes()
434            .with_title(self.config.title.clone())
435            .with_inner_size(LogicalSize::new(self.config.width, self.config.height));
436
437        let window = match event_loop.create_window(window_attributes) {
438            Ok(window) => Arc::new(window),
439            Err(error) => {
440                self.error = Some(WgpuBackendError::Init(error.to_string()));
441                event_loop.exit();
442                return;
443            }
444        };
445
446        match WgpuRendererInner::from_window(window, self.background) {
447            Ok(inner) => {
448                self.inner = Some(inner);
449            }
450            Err(error) => {
451                self.error = Some(error);
452                event_loop.exit();
453            }
454        }
455    }
456}
457
458impl ApplicationHandler for BootstrapHandler {
459    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
460        self.try_create(event_loop);
461    }
462
463    fn window_event(
464        &mut self,
465        event_loop: &ActiveEventLoop,
466        _window_id: WindowId,
467        event: WindowEvent,
468    ) {
469        if matches!(event, WindowEvent::CloseRequested) {
470            self.close_requested = true;
471            event_loop.exit();
472        }
473    }
474}
475
476struct RuntimeHandler<'a> {
477    inner: &'a mut WgpuRendererInner,
478}
479
480impl ApplicationHandler for RuntimeHandler<'_> {
481    fn resumed(&mut self, _event_loop: &ActiveEventLoop) {}
482
483    fn window_event(
484        &mut self,
485        _event_loop: &ActiveEventLoop,
486        _window_id: WindowId,
487        event: WindowEvent,
488    ) {
489        match event {
490            WindowEvent::CloseRequested => self.inner.close_requested = true,
491            WindowEvent::Resized(size) => {
492                self.inner.resize(size.width, size.height);
493            }
494            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
495                self.inner.set_dpi_scale(scale_factor as f32);
496            }
497            WindowEvent::CursorMoved { position, .. } => {
498                self.inner.input.mouse_position =
499                    Vector2::new(position.x as f32, position.y as f32);
500            }
501            WindowEvent::MouseInput { state, button, .. } => {
502                if let Some(mapped) = map_mouse_button(button) {
503                    if state == ElementState::Pressed {
504                        self.inner.input.mouse_buttons.insert(mapped);
505                    } else {
506                        self.inner.input.mouse_buttons.swap_remove(&mapped);
507                    }
508                }
509            }
510            WindowEvent::MouseWheel { delta, .. } => {
511                self.inner.input.scroll_delta = match delta {
512                    MouseScrollDelta::LineDelta(x, y) => Vector2::new(x, y),
513                    MouseScrollDelta::PixelDelta(position) => {
514                        Vector2::new(position.x as f32, position.y as f32)
515                    }
516                };
517            }
518            WindowEvent::KeyboardInput { event, .. } => {
519                if let PhysicalKey::Code(code) = event.physical_key
520                    && let Some(key) = map_keyboard_key(code)
521                {
522                    if event.state == ElementState::Pressed {
523                        self.inner.input.pressed_keys.insert(key);
524                    } else {
525                        self.inner.input.pressed_keys.swap_remove(&key);
526                    }
527                }
528            }
529            WindowEvent::Ime(winit::event::Ime::Preedit(text, _)) => {
530                self.inner.input.pressed_chars.extend(text.chars());
531            }
532            _ => {}
533        }
534    }
535}
536
537impl From<EventLoopError> for WgpuBackendError {
538    fn from(value: EventLoopError) -> Self {
539        Self::Init(value.to_string())
540    }
541}
542
543fn map_mouse_button(button: winit::event::MouseButton) -> Option<MouseButton> {
544    match button {
545        winit::event::MouseButton::Left => Some(MouseButton::MouseButtonLeft),
546        winit::event::MouseButton::Right => Some(MouseButton::MouseButtonRight),
547        winit::event::MouseButton::Middle => Some(MouseButton::MouseButtonMiddle),
548        winit::event::MouseButton::Back => Some(MouseButton::MouseButtonBack),
549        winit::event::MouseButton::Forward => Some(MouseButton::MouseButtonForward),
550        _ => None,
551    }
552}
553
554fn map_keyboard_key(code: KeyCode) -> Option<KeyboardKey> {
555    let value = match code {
556        KeyCode::Space => KeyboardKey::KeySpace,
557        KeyCode::Escape => KeyboardKey::KeyEscape,
558        KeyCode::Enter => KeyboardKey::KeyEnter,
559        KeyCode::Tab => KeyboardKey::KeyTab,
560        KeyCode::Backspace => KeyboardKey::KeyBackspace,
561        KeyCode::Insert => KeyboardKey::KeyInsert,
562        KeyCode::Delete => KeyboardKey::KeyDelete,
563        KeyCode::ArrowRight => KeyboardKey::KeyRight,
564        KeyCode::ArrowLeft => KeyboardKey::KeyLeft,
565        KeyCode::ArrowDown => KeyboardKey::KeyDown,
566        KeyCode::ArrowUp => KeyboardKey::KeyUp,
567        KeyCode::PageUp => KeyboardKey::KeyPageUp,
568        KeyCode::PageDown => KeyboardKey::KeyPageDown,
569        KeyCode::Home => KeyboardKey::KeyHome,
570        KeyCode::End => KeyboardKey::KeyEnd,
571        KeyCode::KeyA => KeyboardKey::KeyA,
572        KeyCode::KeyB => KeyboardKey::KeyB,
573        KeyCode::KeyC => KeyboardKey::KeyC,
574        KeyCode::KeyD => KeyboardKey::KeyD,
575        KeyCode::KeyE => KeyboardKey::KeyE,
576        KeyCode::KeyF => KeyboardKey::KeyF,
577        KeyCode::KeyG => KeyboardKey::KeyG,
578        KeyCode::KeyH => KeyboardKey::KeyH,
579        KeyCode::KeyI => KeyboardKey::KeyI,
580        KeyCode::KeyJ => KeyboardKey::KeyJ,
581        KeyCode::KeyK => KeyboardKey::KeyK,
582        KeyCode::KeyL => KeyboardKey::KeyL,
583        KeyCode::KeyM => KeyboardKey::KeyM,
584        KeyCode::KeyN => KeyboardKey::KeyN,
585        KeyCode::KeyO => KeyboardKey::KeyO,
586        KeyCode::KeyP => KeyboardKey::KeyP,
587        KeyCode::KeyQ => KeyboardKey::KeyQ,
588        KeyCode::KeyR => KeyboardKey::KeyR,
589        KeyCode::KeyS => KeyboardKey::KeyS,
590        KeyCode::KeyT => KeyboardKey::KeyT,
591        KeyCode::KeyU => KeyboardKey::KeyU,
592        KeyCode::KeyV => KeyboardKey::KeyV,
593        KeyCode::KeyW => KeyboardKey::KeyW,
594        KeyCode::KeyX => KeyboardKey::KeyX,
595        KeyCode::KeyY => KeyboardKey::KeyY,
596        KeyCode::KeyZ => KeyboardKey::KeyZ,
597        KeyCode::Digit0 => KeyboardKey::KeyZero,
598        KeyCode::Digit1 => KeyboardKey::KeyOne,
599        KeyCode::Digit2 => KeyboardKey::KeyTwo,
600        KeyCode::Digit3 => KeyboardKey::KeyThree,
601        KeyCode::Digit4 => KeyboardKey::KeyFour,
602        KeyCode::Digit5 => KeyboardKey::KeyFive,
603        KeyCode::Digit6 => KeyboardKey::KeySix,
604        KeyCode::Digit7 => KeyboardKey::KeySeven,
605        KeyCode::Digit8 => KeyboardKey::KeyEight,
606        KeyCode::Digit9 => KeyboardKey::KeyNine,
607        _ => return None,
608    };
609    Some(value)
610}