tetromino_impl/opengl/
renderer.rs

1// Copyright (C) 2023-2024 Daniel Mueller <deso@posteo.net>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use std::cell::Cell;
5use std::cell::RefCell;
6use std::mem::needs_drop;
7use std::mem::replace;
8use std::num::NonZeroU16;
9use std::num::NonZeroU32;
10use std::ops::Add;
11use std::ops::DerefMut as _;
12use std::ops::Sub;
13
14use crate::guard::Guard;
15use crate::Point;
16use crate::Rect;
17
18use super::gl;
19use super::Context;
20use super::Texture;
21
22
23/// The capacity of our vertex buffer.
24// TODO: We should consider sizing it more dynamically and just making
25//       this an upper limit instead.
26const VERTEX_BUFFER_CAPACITY: usize = 1024;
27
28
29#[derive(Clone, Copy, Debug)]
30#[allow(dead_code)]
31#[repr(packed)]
32struct Vertex {
33  // texture coordinates
34  u: gl::GLfloat,
35  v: gl::GLfloat,
36
37  // color
38  r: gl::GLubyte,
39  g: gl::GLubyte,
40  b: gl::GLubyte,
41  a: gl::GLubyte,
42
43  // position
44  x: gl::GLfloat,
45  y: gl::GLfloat,
46  z: gl::GLfloat,
47}
48
49
50#[derive(Clone, Copy, Debug, PartialEq)]
51#[repr(packed)]
52pub(crate) struct Color {
53  pub(crate) r: gl::GLubyte,
54  pub(crate) g: gl::GLubyte,
55  pub(crate) b: gl::GLubyte,
56  pub(crate) a: gl::GLubyte,
57}
58
59impl Color {
60  #[inline]
61  fn as_floats(&self) -> (gl::GLfloat, gl::GLfloat, gl::GLfloat, gl::GLfloat) {
62    (
63      self.r as gl::GLfloat / gl::GLubyte::MAX as gl::GLfloat,
64      self.g as gl::GLfloat / gl::GLubyte::MAX as gl::GLfloat,
65      self.b as gl::GLfloat / gl::GLubyte::MAX as gl::GLfloat,
66      self.a as gl::GLfloat / gl::GLubyte::MAX as gl::GLfloat,
67    )
68  }
69
70  /// A `const` version of `Add::add`.
71  const fn cadd(self, other: Color) -> Self {
72    Self {
73      r: self.r.saturating_add(other.r),
74      g: self.g.saturating_add(other.g),
75      b: self.b.saturating_add(other.b),
76      a: self.a.saturating_add(other.a),
77    }
78  }
79
80  /// A `const` version of `Sub::sub`.
81  pub const fn csub(self, other: Color) -> Self {
82    Self {
83      r: self.r.saturating_sub(other.r),
84      g: self.g.saturating_sub(other.g),
85      b: self.b.saturating_sub(other.b),
86      a: self.a.saturating_sub(other.a),
87    }
88  }
89
90
91  #[inline]
92  pub(crate) const fn black() -> Self {
93    Self {
94      r: gl::GLubyte::MIN,
95      g: gl::GLubyte::MIN,
96      b: gl::GLubyte::MIN,
97      a: gl::GLubyte::MAX,
98    }
99  }
100
101  #[inline]
102  pub(crate) const fn white() -> Self {
103    Self {
104      r: gl::GLubyte::MAX,
105      g: gl::GLubyte::MAX,
106      b: gl::GLubyte::MAX,
107      a: gl::GLubyte::MAX,
108    }
109  }
110
111  #[inline]
112  pub(crate) const fn red() -> Self {
113    Self {
114      r: gl::GLubyte::MAX,
115      g: gl::GLubyte::MIN,
116      b: gl::GLubyte::MIN,
117      a: gl::GLubyte::MAX,
118    }
119  }
120
121  #[inline]
122  pub(crate) const fn green() -> Self {
123    Self {
124      r: gl::GLubyte::MIN,
125      g: gl::GLubyte::MAX,
126      b: gl::GLubyte::MIN,
127      a: gl::GLubyte::MAX,
128    }
129  }
130
131  #[inline]
132  pub(crate) const fn blue() -> Self {
133    Self {
134      r: gl::GLubyte::MIN,
135      g: gl::GLubyte::MIN,
136      b: gl::GLubyte::MAX,
137      a: gl::GLubyte::MAX,
138    }
139  }
140
141  #[inline]
142  pub(crate) const fn yellow() -> Self {
143    Self::red().cadd(Self::green())
144  }
145
146  #[inline]
147  pub(crate) const fn violet() -> Self {
148    Self::red().cadd(Self::blue())
149  }
150
151  #[inline]
152  pub(crate) const fn cyan() -> Self {
153    Self::green().cadd(Self::blue())
154  }
155
156  #[inline]
157  pub(crate) const fn orange() -> Self {
158    Self {
159      r: gl::GLubyte::MAX,
160      g: gl::GLubyte::MAX / 2,
161      b: gl::GLubyte::MIN,
162      a: gl::GLubyte::MAX,
163    }
164  }
165
166  #[inline]
167  pub(crate) const fn gray() -> Self {
168    Self {
169      r: gl::GLubyte::MAX / 2,
170      g: gl::GLubyte::MAX / 2,
171      b: gl::GLubyte::MAX / 2,
172      a: gl::GLubyte::MAX,
173    }
174  }
175}
176
177impl Add<Color> for Color {
178  type Output = Color;
179
180  fn add(self, other: Color) -> Self::Output {
181    self.cadd(other)
182  }
183}
184
185impl Sub<Color> for Color {
186  type Output = Color;
187
188  fn sub(self, other: Color) -> Self::Output {
189    self.csub(other)
190  }
191}
192
193
194#[derive(Clone, Copy, Debug, Eq, PartialEq)]
195#[repr(u32)]
196enum Primitive {
197  Line = gl::LINES,
198  Quad = gl::QUADS,
199}
200
201impl Primitive {
202  fn as_glenum(&self) -> gl::GLenum {
203    *self as _
204  }
205}
206
207
208#[derive(Clone, Debug)]
209enum TextureState {
210  /// The texture has been bound and will be used when rendering.
211  Bound { bound: Texture },
212  /// The texture is active but has not yet been bound. The most
213  /// convenient way for ensuring that it is in fact bound once that is
214  /// required is via the [`ensure_bound`][Self::ensure_bound] method.
215  Unbound {
216    unbound: Texture,
217    still_bound: Texture,
218  },
219}
220
221impl TextureState {
222  /// Mark the provided texture as active, but don't bind it yet.
223  fn activate(&mut self, texture: Texture) -> Texture {
224    match self {
225      Self::Bound { bound } if texture == *bound => texture,
226      Self::Bound { bound } => {
227        let state = Self::Unbound {
228          unbound: texture,
229          still_bound: bound.clone(),
230        };
231        replace(self, state).into_texture()
232      },
233      Self::Unbound { unbound, .. } => replace(unbound, texture),
234    }
235  }
236
237  /// Make sure that the "active" texture is bound.
238  fn ensure_bound(&mut self) {
239    match self {
240      Self::Bound { .. } => (),
241      Self::Unbound {
242        unbound,
243        still_bound,
244      } => {
245        if unbound != still_bound {
246          let () = unbound.bind();
247        }
248
249        // The clone is reasonably cheap, but also entirely unnecessary at
250        // a conceptual level. We just want to flip the enum variant from
251        // `Unbound` to `Bound`. Thanks Rust...
252        let bound = unbound.clone();
253        let _prev = replace(self, Self::Bound { bound });
254      },
255    }
256  }
257
258  /// Retrieve the "active" texture.
259  fn texture(&self) -> &Texture {
260    match self {
261      Self::Bound { bound: texture }
262      | Self::Unbound {
263        unbound: texture, ..
264      } => texture,
265    }
266  }
267
268  /// Destruct the object into the "active" texture.
269  fn into_texture(self) -> Texture {
270    match self {
271      Self::Bound { bound: texture }
272      | Self::Unbound {
273        unbound: texture, ..
274      } => texture,
275    }
276  }
277}
278
279
280/// A type directly usable to render graphics primitives.
281#[derive(Debug)]
282pub struct ActiveRenderer<'renderer> {
283  /// The `Renderer` this object belongs to.
284  renderer: &'renderer Renderer,
285  /// An invalid texture.
286  invalid_texture: Texture,
287  /// The origin relative to which rendering happens.
288  origin: Cell<Point<i16>>,
289  /// The currently set color.
290  color: Cell<Color>,
291  /// The currently set texture.
292  texture: RefCell<TextureState>,
293  /// The vertex buffer we use.
294  vertices: RefCell<Vec<Vertex>>,
295  /// The type of primitive currently active for rendering.
296  primitive: Cell<Primitive>,
297}
298
299impl<'renderer> ActiveRenderer<'renderer> {
300  fn new(renderer: &'renderer Renderer) -> Self {
301    let invalid_texture = Texture::invalid();
302    Self {
303      renderer,
304      invalid_texture: invalid_texture.clone(),
305      origin: Cell::new(Point::default()),
306      color: Cell::new(Color::black()),
307      // We know that no texture is active, because we are called on the
308      // `Renderer::on_pre_render` path and it just cleared a bunch of
309      // state. So it's fine for us to claim that an "invalid" texture
310      // is bound already.
311      texture: RefCell::new(TextureState::Bound {
312        bound: invalid_texture,
313      }),
314      vertices: RefCell::new(Vec::with_capacity(VERTEX_BUFFER_CAPACITY)),
315      primitive: Cell::new(Primitive::Quad),
316    }
317  }
318
319  /// Set the origin relative to which rendering happens.
320  #[inline]
321  pub(crate) fn set_origin(&self, origin: Point<i16>) -> Guard<'_, impl FnOnce() + '_> {
322    let new_origin = self.origin.get() + origin;
323    let prev_origin = self.origin.replace(new_origin);
324    Guard::new(move || self.origin.set(prev_origin))
325  }
326
327  /// Set the color with which subsequent vertices are to be rendered.
328  #[inline]
329  pub(crate) fn set_color(&self, color: Color) -> Guard<'_, impl FnOnce() + '_> {
330    let prev_color = self.color.replace(color);
331    Guard::new(move || self.color.set(prev_color))
332  }
333
334  #[inline]
335  pub(crate) fn set_texture(&self, texture: &Texture) -> Guard<'_, impl FnOnce() + '_> {
336    fn set(renderer: &ActiveRenderer, texture: Texture) -> Texture {
337      let mut state = renderer.texture.borrow_mut();
338      let state = state.deref_mut();
339
340      if texture != *state.texture() {
341        let () = renderer.flush_vertex_buffer(state);
342      }
343
344      state.activate(texture)
345    }
346
347    let texture = texture.clone();
348    let prev_texture = set(self, texture);
349
350    Guard::new(move || {
351      let _prev = set(self, prev_texture);
352    })
353  }
354
355  #[inline]
356  pub(crate) fn set_no_texture(&self) -> Guard<'_, impl FnOnce() + '_> {
357    self.set_texture(&self.invalid_texture)
358  }
359
360  /// Clear the screen using the given color.
361  pub(crate) fn clear_screen(&self, color: Color) {
362    let (r, g, b, a) = color.as_floats();
363
364    unsafe { gl::ClearColor(r, g, b, a) };
365    unsafe { gl::Clear(gl::COLOR_BUFFER_BIT) };
366
367    debug_assert_eq!(unsafe { gl::GetError() }, gl::NO_ERROR);
368  }
369
370  /// Render a line.
371  pub(crate) fn render_line(&self, mut p1: Point<i16>, mut p2: Point<i16>) {
372    const VERTEX_COUNT_LINE: usize = 2;
373
374    let origin = self.origin.get();
375    p1 += origin;
376    p2 += origin;
377
378    let () = self.set_primitive(Primitive::Line, VERTEX_COUNT_LINE);
379    let color = self.color.get();
380
381    let mut vertex = Vertex {
382      u: 0.0,
383      v: 0.0,
384      r: color.r,
385      g: color.g,
386      b: color.b,
387      a: color.a,
388      x: p1.x.into(),
389      y: p1.y.into(),
390      z: 0.0,
391    };
392
393    let mut buffer = self.vertices.borrow_mut();
394    let vertices = buffer.spare_capacity_mut();
395    vertices[0].write(vertex);
396
397    // second point
398    vertex.x = p2.x.into();
399    vertex.y = p2.y.into();
400    vertices[1].write(vertex);
401
402    let len = buffer.len();
403    let () = unsafe { buffer.set_len(len + VERTEX_COUNT_LINE) };
404  }
405
406  /// Render a rectangle.
407  pub(crate) fn render_rect(&self, rect: Rect<i16>) {
408    let () = self.render_rect_f32(rect.into_other());
409  }
410
411  /// Render a rectangle.
412  pub(crate) fn render_rect_f32(&self, rect: Rect<f32>) {
413    // Texture coordinates for the quad. We always map the complete
414    // texture on it.
415    let coords = Rect::new(0.0, 0.0, 1.0, 1.0);
416    let () = self.render_rect_with_tex_coords_f32(rect, coords);
417  }
418
419  /// Render a rectangle.
420  pub(crate) fn render_rect_with_tex_coords(&self, rect: Rect<i16>, coords: Rect<f32>) {
421    self.render_rect_with_tex_coords_f32(rect.into_other(), coords)
422  }
423
424  /// Render a rectangle.
425  pub(crate) fn render_rect_with_tex_coords_f32(&self, mut rect: Rect<f32>, coords: Rect<f32>) {
426    const VERTEX_COUNT_QUAD: usize = 4;
427
428    let origin = self.origin.get();
429    rect += origin.into_other();
430
431    let () = self.set_primitive(Primitive::Quad, VERTEX_COUNT_QUAD);
432    let color = self.color.get();
433
434    let mut vertex = Vertex {
435      u: coords.x,
436      v: coords.y,
437      r: color.r,
438      g: color.g,
439      b: color.b,
440      a: color.a,
441      x: rect.x,
442      y: rect.y,
443      z: 0.0,
444    };
445
446    let mut buffer = self.vertices.borrow_mut();
447    let vertices = buffer.spare_capacity_mut();
448    vertices[0].write(vertex);
449
450    // lower right
451    vertex.u += coords.w;
452    vertex.x += rect.w;
453    vertices[1].write(vertex);
454
455    // upper right
456    vertex.v += coords.h;
457    vertex.y += rect.h;
458    vertices[2].write(vertex);
459
460    // upper left
461    vertex.u = coords.x;
462    vertex.x = rect.x;
463    vertices[3].write(vertex);
464
465    let len = buffer.len();
466    let () = unsafe { buffer.set_len(len + VERTEX_COUNT_QUAD) };
467  }
468
469  /// Set the type of primitive that we currently render and ensure that
470  /// there is space for at least `vertex_cnt` vertices in our vertex
471  /// buffer.
472  fn set_primitive(&self, primitive: Primitive, vertex_cnt: usize) {
473    if primitive != self.primitive.get()
474      || self.vertices.borrow_mut().spare_capacity_mut().len() < vertex_cnt
475    {
476      let () = self.flush_vertex_buffer(self.texture.borrow_mut().deref_mut());
477      let () = self.primitive.set(primitive);
478    }
479  }
480
481  /// Send the cached data to the graphics device for rendering.
482  fn flush_vertex_buffer(&self, texture: &mut TextureState) {
483    let mut buffer = self.vertices.borrow_mut();
484    let size = buffer.len() as _;
485    if size > 0 {
486      let () = texture.ensure_bound();
487
488      unsafe {
489        gl::InterleavedArrays(gl::T2F_C4UB_V3F, 0, buffer.as_ptr().cast());
490        gl::DrawArrays(self.primitive.get().as_glenum(), 0, size);
491
492        debug_assert_eq!(gl::GetError(), gl::NO_ERROR);
493      }
494
495      debug_assert!(!needs_drop::<Vertex>());
496      // SAFETY: We are strictly decreasing size and our vertices are
497      //         plain old data. No need to drop them properly.
498      unsafe { buffer.set_len(0) };
499    }
500  }
501
502  /// Retrieve the physical width of the rendering area, in pixels.
503  #[inline]
504  pub(crate) fn phys_width(&self) -> u32 {
505    self.renderer.phys_w as _
506  }
507
508  /// Retrieve the physical height of the rendering area, in pixels.
509  #[inline]
510  pub(crate) fn phys_height(&self) -> u32 {
511    self.renderer.phys_h as _
512  }
513
514  /// Retrieve the logical width of the rendering area, in game units,
515  /// but already expressed as a floating point value.
516  #[inline]
517  pub(crate) fn logic_width(&self) -> f32 {
518    self.renderer.logic_w as _
519  }
520
521  /// Retrieve the logical height of the rendering area, in game units,
522  /// but already expressed as a floating point value.
523  #[inline]
524  pub(crate) fn logic_height(&self) -> f32 {
525    self.renderer.logic_h as _
526  }
527}
528
529impl Drop for ActiveRenderer<'_> {
530  fn drop(&mut self) {
531    let () = self.flush_vertex_buffer(self.texture.borrow_mut().deref_mut());
532    let () = self.renderer.on_post_render();
533  }
534}
535
536
537/// A type enabling the rendering of graphics.
538#[derive(Debug)]
539pub struct Renderer {
540  /// The physical width of the window to which this renderer belongs.
541  phys_w: gl::GLsizei,
542  /// The physical height of the window to which this renderer belongs.
543  phys_h: gl::GLsizei,
544  /// The logical width of the view maintained by this renderer.
545  logic_w: gl::GLfloat,
546  /// The logical height of the view maintained by this renderer.
547  logic_h: gl::GLfloat,
548}
549
550impl Renderer {
551  /// Create a new [`Renderer`] object assuming the provide "physical"
552  /// and logical view dimensions.
553  pub fn new(
554    phys_w: NonZeroU32,
555    phys_h: NonZeroU32,
556    logic_w: NonZeroU16,
557    logic_h: NonZeroU16,
558  ) -> Self {
559    let (logic_w, logic_h) = Self::calculate_view(phys_w, phys_h, logic_w, logic_h);
560
561    Self {
562      phys_w: gl::GLsizei::try_from(phys_w.get()).unwrap_or(gl::GLsizei::MAX),
563      phys_h: gl::GLsizei::try_from(phys_h.get()).unwrap_or(gl::GLsizei::MAX),
564      logic_w,
565      logic_h,
566    }
567  }
568
569  fn calculate_view(
570    phys_w: NonZeroU32,
571    phys_h: NonZeroU32,
572    logic_w: NonZeroU16,
573    logic_h: NonZeroU16,
574  ) -> (gl::GLfloat, gl::GLfloat) {
575    let phys_w = phys_w.get() as gl::GLfloat;
576    let phys_h = phys_h.get() as gl::GLfloat;
577    let logic_w = logic_w.get() as gl::GLfloat;
578    let logic_h = logic_h.get() as gl::GLfloat;
579
580    let phys_ratio = phys_w / phys_h;
581    let logic_ratio = logic_w / logic_h;
582
583    let mut width = logic_w;
584    let mut height = logic_h;
585
586    // Our goal is to make the two ratios equal, that means:
587    //
588    // phys_w   logic_w + x
589    // ------ = -----------
590    // phys_h   logic_h + y
591    //
592    // where `x` is zero if `logic_ratio` > `phys_ratio`, otherwise `y`
593    // is zero. Resolve it to `x` or `y` to get the equation from above.
594    if logic_ratio > phys_ratio {
595      height += logic_w * phys_h / phys_w - logic_h;
596    } else {
597      width += logic_h * phys_w / phys_h - logic_w;
598    }
599    (width, height)
600  }
601
602  /// Update the view after the containing window or contained logical
603  /// dimensions have changed.
604  pub fn update_view(
605    &mut self,
606    phys_w: NonZeroU32,
607    phys_h: NonZeroU32,
608    logic_w: NonZeroU16,
609    logic_h: NonZeroU16,
610  ) {
611    let (logic_w, logic_h) = Self::calculate_view(phys_w, phys_h, logic_w, logic_h);
612
613    self.phys_w = gl::GLsizei::try_from(phys_w.get()).unwrap_or(gl::GLsizei::MAX);
614    self.phys_h = gl::GLsizei::try_from(phys_h.get()).unwrap_or(gl::GLsizei::MAX);
615    self.logic_w = logic_w;
616    self.logic_h = logic_h;
617  }
618
619  fn push_states(&self) {
620    unsafe {
621      gl::PushAttrib(
622        gl::CURRENT_BIT
623          | gl::COLOR_BUFFER_BIT
624          | gl::DEPTH_BUFFER_BIT
625          | gl::ENABLE_BIT
626          | gl::FOG_BIT
627          | gl::LIGHTING_BIT
628          | gl::LINE_BIT
629          | gl::POINT_BIT
630          | gl::SCISSOR_BIT
631          | gl::STENCIL_BUFFER_BIT
632          | gl::TEXTURE_BIT
633          | gl::TRANSFORM_BIT
634          | gl::VIEWPORT_BIT,
635      );
636
637      gl::Disable(gl::FOG);
638      gl::Disable(gl::LIGHTING);
639      gl::Disable(gl::COLOR_MATERIAL);
640      gl::Disable(gl::DEPTH_TEST);
641      gl::Disable(gl::SCISSOR_TEST);
642      gl::Disable(gl::CULL_FACE);
643
644      gl::Enable(gl::TEXTURE_2D);
645
646      gl::PointSize(1.0);
647      gl::LineWidth(1.0);
648
649      gl::Viewport(0, 0, self.phys_w, self.phys_h);
650
651      debug_assert_eq!(gl::GetError(), gl::NO_ERROR);
652    }
653  }
654
655  fn pop_states(&self) {
656    unsafe {
657      gl::PopAttrib();
658
659      debug_assert_eq!(gl::GetError(), gl::NO_ERROR);
660    }
661  }
662
663  fn push_matrizes(&self) {
664    unsafe {
665      // We create an orthogonal projection matrix with bounds
666      // sufficient to contain the logical view.
667      gl::MatrixMode(gl::PROJECTION);
668      gl::PushMatrix();
669      gl::LoadIdentity();
670      // Our renderer will render everything with z-coordinate of 0.0f,
671      // this must lie inside the range [zNear, zFar] (last two
672      // parameters).
673      gl::Ortho(
674        0.0,
675        self.logic_w.into(),
676        0.0,
677        self.logic_h.into(),
678        -0.5,
679        0.5,
680      );
681
682      gl::MatrixMode(gl::TEXTURE);
683      gl::PushMatrix();
684      gl::LoadIdentity();
685
686      gl::MatrixMode(gl::MODELVIEW);
687      gl::PushMatrix();
688      gl::LoadIdentity();
689
690      debug_assert_eq!(gl::GetError(), gl::NO_ERROR);
691    }
692  }
693
694  fn pop_matrizes(&self) {
695    unsafe {
696      gl::MatrixMode(gl::MODELVIEW);
697      gl::PopMatrix();
698
699      gl::MatrixMode(gl::TEXTURE);
700      gl::PopMatrix();
701
702      gl::MatrixMode(gl::PROJECTION);
703      gl::PopMatrix();
704
705      debug_assert_eq!(gl::GetError(), gl::NO_ERROR);
706    }
707  }
708
709  /// Activate the renderer with the given [`Context`] in preparation
710  /// for rendering to take place.
711  // This method requires an exclusive `Context` reference do ensure
712  // that while a renderer is active the context can't swap buffers, for
713  // example.
714  pub fn on_pre_render<'ctx>(&'ctx self, context: &'ctx mut Context) -> ActiveRenderer<'ctx> {
715    let _ = context;
716    let () = self.push_states();
717    let () = self.push_matrizes();
718
719    ActiveRenderer::new(self)
720  }
721
722  fn on_post_render(&self) {
723    let () = self.pop_matrizes();
724    let () = self.pop_states();
725  }
726}
727
728
729#[cfg(test)]
730mod tests {
731  use super::*;
732
733
734  /// Check that we can convert a `Color` the corresponding floating
735  /// point representation.
736  #[test]
737  fn color_float_conversion() {
738    let (r, g, b, a) = Color::white().as_floats();
739    let e = gl::GLfloat::EPSILON;
740    assert!(1.0 - e <= r && r <= 1.0 + e);
741    assert!(1.0 - e <= g && g <= 1.0 + e);
742    assert!(1.0 - e <= b && b <= 1.0 + e);
743    assert!(1.0 - e <= a && a <= 1.0 + e);
744  }
745}