diff --git a/Cargo.toml b/Cargo.toml index 7df869ab..f87ab4d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -223,6 +223,9 @@ required-features = ["unsafe_textures"] name = "renderer_09_scaling_textures" path = "examples/renderer/a09_scaling_textures.rs" +[[example]] +name = "render-geometry" + [[example]] name = "renderer-texture" diff --git a/examples/render-geometry.rs b/examples/render-geometry.rs new file mode 100644 index 00000000..810eaa70 --- /dev/null +++ b/examples/render-geometry.rs @@ -0,0 +1,141 @@ +use sdl3::event::Event; +use sdl3::keyboard::Keycode; +use sdl3::pixels::{Color, FColor}; +use sdl3::render::{FPoint, RenderGeometryTextureParams, Vertex, VertexIndices}; +use std::mem::offset_of; +use std::thread; +use std::time::Duration; + +fn main() { + let sdl_context = sdl3::init().unwrap(); + let video_subsystem = sdl_context.video().unwrap(); + + let window = video_subsystem + .window("Rust SDL3 render_geometry custom struct example", 800, 600) + .position_centered() + .opengl() + .build() + .unwrap(); + + let mut canvas = window.into_canvas(); + + let mut event_pump = sdl_context.event_pump().unwrap(); + let mut running = true; + + while running { + for event in event_pump.poll_iter() { + match event { + Event::Quit { .. } + | Event::KeyDown { + keycode: Some(Keycode::Escape), + .. + } => { + running = false; + } + _ => {} + } + } + + // black background + canvas.set_draw_color(Color::BLACK); + canvas.clear(); + + // First, draw a triangle using `render_geometry`. The `tex_coord` fields are unused but + // must be provided, `render_geometry` only supports `sdl3::render::Vertex`. + let vertices = [ + Vertex { + position: FPoint::new(100.0, 200.0), + color: FColor::RED, + tex_coord: FPoint::new(0.0, 0.0), + }, + Vertex { + position: FPoint::new(200.0, 200.0), + color: FColor::GREEN, + tex_coord: FPoint::new(0.0, 0.0), + }, + Vertex { + position: FPoint::new(150.0, 100.0), + color: FColor::BLUE, + tex_coord: FPoint::new(0.0, 0.0), + }, + ]; + canvas + .render_geometry(&vertices, None, VertexIndices::Sequential) + .expect("render_geometry failed (probably unsupported, see error message)"); + + // `render_geometry_raw` supports any custom struct as long as it contains the needed data + // (or other layout compatible of the needed data). + // The struct does not need to be `repr(C)` or `Copy` for example. + struct MyVertex { + color: [f32; 4], + // The struct may contain data not needed by SDL. + #[expect(dead_code)] + foo: Vec, + // When defining your own vertex struct, using `FPoint` for position and tex_coord + // (and `FColor` for color) is the easiest way. These are obviously layout-compatible + // with `FPoint` and `Color`, respectively. + pos: FPoint, + } + + // Define the vertices of a square + let vertices = [ + MyVertex { + color: [0., 0., 0., 1.], + foo: b"some".to_vec(), + pos: FPoint::new(300.0, 100.0), + }, + MyVertex { + color: [0., 1., 0., 1.], + foo: b"unrelated".to_vec(), + pos: FPoint::new(400.0, 100.0), + }, + MyVertex { + color: [1., 0., 0., 1.], + foo: b"data".to_vec(), + pos: FPoint::new(300.0, 200.0), + }, + MyVertex { + color: [1., 1., 0., 1.], + foo: b"!".to_vec(), + pos: FPoint::new(400.0, 200.0), + }, + ]; + + // A square is rendered as two triangles (see indices) + // SAFETY: core::mem::offset_of makes sure the offsets are right and alignment is respected. + unsafe { + canvas.render_geometry_raw( + &vertices, + offset_of!(MyVertex, pos), + &vertices, + offset_of!(MyVertex, color), + None::>, + &[[0, 1, 2], [1, 2, 3]], + ) + } + .expect("render_geometry_raw failed (probably unsupported, see error message)"); + + // Parameters can be reused, here only the positions are swapped out for new ones. + // SAFETY: core::mem::offset_of makes sure the offsets are right and alignment is respected. + // The offset 0 is correct because the element type of positions is `[f32; 2]`. + unsafe { + canvas.render_geometry_raw( + &[ + [500.0f32, 100.0], + [600.0, 100.0], + [500.0, 200.0], + [600.0, 200.0], + ], + 0, + &vertices, + offset_of!(MyVertex, color), + None::>, + &[[0, 1, 2], [1, 2, 3]], + ) + } + .expect("render_geometry_raw failed (probably unsupported, see error message)"); + + canvas.present(); + thread::sleep(Duration::from_millis(16)); + } +} diff --git a/src/sdl3/pixels.rs b/src/sdl3/pixels.rs index 98f496eb..536840be 100644 --- a/src/sdl3/pixels.rs +++ b/src/sdl3/pixels.rs @@ -214,6 +214,110 @@ impl From<(u8, u8, u8, u8)> for Color { } } +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct FColor { + pub r: f32, + pub g: f32, + pub b: f32, + pub a: f32, +} + +impl FColor { + #[inline] + #[allow(non_snake_case)] + pub const fn RGB(r: f32, g: f32, b: f32) -> FColor { + FColor { r, g, b, a: 1. } + } + + #[inline] + #[allow(non_snake_case)] + pub const fn RGBA(r: f32, g: f32, b: f32, a: f32) -> FColor { + FColor { r, g, b, a } + } + + pub fn invert(self) -> FColor { + FColor::RGBA(1. - self.r, 1. - self.g, 1. - self.b, 1. - self.a) + } + + #[inline] + pub const fn rgb(self) -> (f32, f32, f32) { + (self.r, self.g, self.b) + } + + #[inline] + pub const fn rgba(self) -> (f32, f32, f32, f32) { + (self.r, self.g, self.b, self.a) + } + + // Implemented manually and kept private, because reasons + #[inline] + const fn raw(self) -> sys::pixels::SDL_FColor { + sys::pixels::SDL_FColor { + r: self.r, + g: self.g, + b: self.b, + a: self.a, + } + } + + pub const WHITE: FColor = FColor::RGBA(1., 1., 1., 1.); + pub const BLACK: FColor = FColor::RGBA(0., 0., 0., 1.); + pub const GRAY: FColor = FColor::RGBA(0.5, 0.5, 0.5, 1.); + pub const GREY: FColor = FColor::GRAY; + pub const RED: FColor = FColor::RGBA(1., 0., 0., 1.); + pub const GREEN: FColor = FColor::RGBA(0., 1., 0., 1.); + pub const BLUE: FColor = FColor::RGBA(0., 0., 1., 1.); + pub const MAGENTA: FColor = FColor::RGBA(1., 0., 1., 1.); + pub const YELLOW: FColor = FColor::RGBA(1., 1., 0., 1.); + pub const CYAN: FColor = FColor::RGBA(0., 1., 1., 1.); +} + +impl From for sys::pixels::SDL_FColor { + fn from(val: FColor) -> Self { + val.raw() + } +} + +impl From for FColor { + fn from(raw: sys::pixels::SDL_FColor) -> FColor { + FColor::RGBA(raw.r, raw.g, raw.b, raw.a) + } +} + +impl From<(f32, f32, f32)> for FColor { + fn from((r, g, b): (f32, f32, f32)) -> FColor { + FColor::RGB(r, g, b) + } +} + +impl From<(f32, f32, f32, f32)> for FColor { + fn from((r, g, b, a): (f32, f32, f32, f32)) -> FColor { + FColor::RGBA(r, g, b, a) + } +} + +impl From for FColor { + fn from(val: Color) -> FColor { + FColor { + r: val.r as f32 / 255., + g: val.g as f32 / 255., + b: val.b as f32 / 255., + a: val.a as f32 / 255., + } + } +} + +impl From for Color { + fn from(val: FColor) -> Color { + Color { + r: (val.r * 255.).round().clamp(0., 255.) as u8, + g: (val.g * 255.).round().clamp(0., 255.) as u8, + b: (val.b * 255.).round().clamp(0., 255.) as u8, + a: (val.a * 255.).round().clamp(0., 255.) as u8, + } + } +} + pub struct PixelMasks { /// Bits per pixel; usually 15, 16, or 32 pub bpp: u8, diff --git a/src/sdl3/render.rs b/src/sdl3/render.rs index c7c8dd5f..0458c035 100644 --- a/src/sdl3/render.rs +++ b/src/sdl3/render.rs @@ -37,7 +37,7 @@ use crate::surface::{Surface, SurfaceContext, SurfaceRef}; use crate::sys; use crate::video::{Window, WindowContext}; use crate::Error; -use libc::{c_double, c_int}; +use libc::{c_double, c_int, c_void}; use pixels::PixelFormat; use std::convert::{Into, TryFrom, TryInto}; use std::error; @@ -50,6 +50,7 @@ use std::mem::{transmute, MaybeUninit}; use std::ops::Deref; use std::ptr; use std::rc::Rc; +use std::slice; use std::sync::Arc; use sys::blendmode::SDL_BlendMode; use sys::everything::SDL_PropertiesID; @@ -1757,6 +1758,288 @@ impl Canvas { } } +// `repr(C)` is not needed for soundness in this crate, but users may expect it to be +// layout-compatible with `sys::SDL_Vertex`. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct Vertex { + pub position: FPoint, + pub color: pixels::FColor, + pub tex_coord: FPoint, +} + +#[derive(Debug, Clone, Copy)] +pub enum VertexIndices<'a> { + /// Vertices are rendered in sequential order. + Sequential, + /// [`u8`] vertex indices. + U8(&'a [u8]), + /// [`u16`] vertex indices. + U16(&'a [u16]), + /// [`u32`] vertex indices. If any index is larger than [`i32::MAX`], SDL will return an error. + U32(&'a [u32]), +} + +impl VertexIndices<'_> { + /// Returns (indices, num_indices, size_indices) + fn into_raw(self) -> (*const c_void, c_int, c_int) { + match self { + Self::Sequential => (ptr::null(), 0, 0), + Self::U8(indices) => (indices.as_ptr().cast::(), indices.len() as c_int, 1), + Self::U16(indices) => (indices.as_ptr().cast::(), indices.len() as c_int, 2), + Self::U32(indices) => (indices.as_ptr().cast::(), indices.len() as c_int, 4), + } + } +} + +impl<'a> From<&'a [u8]> for VertexIndices<'a> { + fn from(value: &'a [u8]) -> Self { + Self::U8(value) + } +} + +impl<'a> From<&'a [u16]> for VertexIndices<'a> { + fn from(value: &'a [u16]) -> Self { + Self::U16(value) + } +} + +impl<'a> From<&'a [u32]> for VertexIndices<'a> { + fn from(value: &'a [u32]) -> Self { + Self::U32(value) + } +} + +impl<'a> From<&'a [i32]> for VertexIndices<'a> { + fn from(value: &'a [i32]) -> Self { + Self::U32(unsafe { slice::from_raw_parts(value.as_ptr().cast::(), value.len()) }) + } +} + +impl<'a> From<&'a [[u8; 3]]> for VertexIndices<'a> { + fn from(value: &'a [[u8; 3]]) -> Self { + Self::U8(unsafe { slice::from_raw_parts(value.as_ptr().cast::(), value.len() * 3) }) + } +} + +impl<'a> From<&'a [[u16; 3]]> for VertexIndices<'a> { + fn from(value: &'a [[u16; 3]]) -> Self { + Self::U16(unsafe { slice::from_raw_parts(value.as_ptr().cast::(), value.len() * 3) }) + } +} + +impl<'a> From<&'a [[u32; 3]]> for VertexIndices<'a> { + fn from(value: &'a [[u32; 3]]) -> Self { + Self::U32(unsafe { slice::from_raw_parts(value.as_ptr().cast::(), value.len() * 3) }) + } +} + +impl<'a> From<&'a [[i32; 3]]> for VertexIndices<'a> { + fn from(value: &'a [[i32; 3]]) -> Self { + Self::U32(unsafe { slice::from_raw_parts(value.as_ptr().cast::(), value.len() * 3) }) + } +} + +macro_rules! impl_into_vertex_indices_forward { + ($($ty:ty)*) => { + $( + impl<'a> From<&'a Vec<$ty>> for VertexIndices<'a> { + fn from(value: &'a Vec<$ty>) -> Self { + Self::from(value.as_slice()) + } + } + + impl<'a, const N: usize> From<&'a [$ty; N]> for VertexIndices<'a> { + fn from(value: &'a [$ty; N]) -> Self { + Self::from(value.as_slice()) + } + } + )* + }; +} + +impl_into_vertex_indices_forward!(u8 u16 u32 i32 [u8; 3] [u16; 3] [u32; 3] [i32; 3]); + +#[derive(Clone, Copy)] +pub struct RenderGeometryTextureParams<'a, TexCoordVertex> { + #[cfg(not(feature = "unsafe_textures"))] + pub texture: &'a Texture<'a>, + #[cfg(feature = "unsafe_textures")] + pub texture: &'a Texture, + pub tex_coords: &'a [TexCoordVertex], + pub tex_coord_offset: usize, +} + +impl Canvas { + /// Render triangles, optionally using a texture. + /// + /// If you have vertices in a different format, [`Canvas::render_geometry_raw`] (the advanced + /// version of this function) might allow you to do the same as this function without having to + /// convert the parameters to use [`Vertex`]. + /// + /// `indices` can be of any of the following: + /// - [`VertexIndices::Sequential`]: Vertices are rendered in sequential order. The number of + /// *vertices* must be divisible by 3, when using a list of indices, the number of *indices* + /// must be divisible by 3. SDL will check this and return an error if this is not the case. + /// - `&[u8]`: Indices of the type [`u8`], note that this only works when using [`u8::MAX`] + /// (255) or less vertices. + /// - `&[u16]`: Indices of the type [`u16`], note that this only works when using [`u16::MAX`] + /// (65535) or less vertices. + /// - `&[u32]` or `&[i32]`: Indices of the type [`i32`], possibly represented using [`u32`]. + /// Note that regardless of the type, indices must be positive and fit in [`i32`]. + /// - `&[[u8; 3]]`, `&[[u16; 3]]`, `&[[u32; 3]]` or `&[[i32; 3]]`, which work like `&[u8]`, + /// `&[u16]`, `&[u32]` or `&[i32]` but ensure at compile time that the number of indices is + /// divisible by 3. + /// - Any other (possibly user defined) type that implements + /// [`Into>`][VertexIndices#trait-implementations]. + #[doc(alias = "SDL_RenderGeometry")] + pub fn render_geometry<'a>( + &mut self, + vertices: &[Vertex], + texture: Option<&Texture>, + indices: impl Into>, + ) -> Result<(), Error> { + unsafe { + self.render_geometry_raw( + vertices, + mem::offset_of!(Vertex, position), + vertices, + mem::offset_of!(Vertex, color), + texture.map(|texture| RenderGeometryTextureParams { + texture, + tex_coords: vertices, + tex_coord_offset: mem::offset_of!(Vertex, tex_coord), + }), + indices, + ) + } + } + + /// Render triangles, optionally using a texture. + /// + /// In the documentation of this function field names of [`RenderGeometryTextureParams`] are + /// used as if they are parameters. When they are used it only applies when a texture is used + /// (`texture_params` is [`Some(_)`][Some]). + /// + /// Vertices may be passed as up to 3 separate lists. Elements of the lists may contain + /// anything, but must contain the valid values at the given offset (in bytes). + /// Using [`offset_of`][core::mem::offset_of] is advised to get the right offset of a field, + /// especially when the element type does not have a stable layout. + /// + /// See the documentation of [`Canvas::render_geometry`] for usage of the `indices` parameter. + /// + /// When a texture is not used, the vertex type must still be specified (because of a Rust + /// limitation, more info [here](https://github.com/rust-lang/rust/issues/36887)). This can be + /// done with turbofish syntax after `None` like so: `None::>`. + /// + /// # Safety + /// + /// It must be sound to [create references to](core::ptr#pointer-to-reference-conversion): + /// - `positions` offset by `position_offset` bytes, of type [`FPoint`] (or `[f32; 2]`), + /// - `colors` offset by `color_offset` bytes, of type [`Color`][pixels::Color] (or `[u8; 4]`) + /// - `tex_coords` offset by `tex_coord_offset` bytes, of type [`FPoint`] (or `[f32; 2]`). + /// + /// For the above to hold, make sure that the hypothetical references are properly aligned and + /// point to valid values ([`f32`] and [`u8`] allow any bit pattern, but not + /// uninitialized/padding bytes). + /// + /// # Panics + /// + /// The following conditions are checked. All must hold or the function panics. + /// + /// Vertices are passed as separate lists, all lists must have the same length. + /// - `positions.len() == colors.len()` + /// - `positions.len() == tex_coords.len()` + /// + /// Offsets must be correct. + /// - `position_offset + size_of::() <= size_of::()`: an [`FPoint`] must fit + /// in `PosVertex` at `position_offset` bytes from the start. + /// - `color_offset + size_of::() <= size_of::()`: a + /// [`Color`][pixels::Color] must fit in `ColorVertex` at `color_offset` bytes from the + /// start. + /// - `tex_coord_offset + size_of::() <= size_of::()`: an [`FPoint`] + /// must fit in `TexCoordVertex` at `tex_coord_offset` bytes from the start. + /// + /// Various sizes must fit in a [C `int`][c_int]. + /// - `positions.len() <= c_int::MAX` + /// - `size_of::() <= c_int::MAX` + /// - `size_of::() <= c_int::MAX` + /// - `size_of::() <= c_int::MAX` + #[inline] + #[doc(alias = "SDL_RenderGeometryRaw")] + pub unsafe fn render_geometry_raw<'a, PosVertex, ColorVertex, TexCoordVertex>( + &mut self, + positions: &[PosVertex], + position_offset: usize, + colors: &[ColorVertex], + color_offset: usize, + texture_params: Option>, + indices: impl Into>, + ) -> Result<(), Error> { + let num_vertices = positions.len(); + assert_eq!(num_vertices, colors.len()); + + assert!(position_offset + size_of::() <= size_of::()); + assert!(color_offset + size_of::() <= size_of::()); + let (texture, uv, uv_stride) = if let Some(texture_params) = texture_params { + assert_eq!(num_vertices, texture_params.tex_coords.len()); + assert!( + texture_params.tex_coord_offset + size_of::() + <= size_of::(), + ); + + ( + texture_params.texture.raw, + texture_params + .tex_coords + .as_ptr() + .cast::() + .byte_offset(texture_params.tex_coord_offset.try_into().unwrap()), + size_of::().try_into().unwrap(), + ) + } else { + (ptr::null_mut(), ptr::null(), 0) + }; + + let xy = positions + .as_ptr() + .cast::() + .byte_offset(position_offset.try_into().unwrap()); + let xy_stride = size_of::().try_into().unwrap(); + + let color = colors + .as_ptr() + .cast::() + .byte_offset(color_offset.try_into().unwrap()); + let color_stride = size_of::().try_into().unwrap(); + + let (indices, num_indices, size_indices) = indices.into().into_raw(); + + let ret = unsafe { + sys::render::SDL_RenderGeometryRaw( + self.context.raw, + texture, + xy, + xy_stride, + color, + color_stride, + uv, + uv_stride, + num_vertices.try_into().unwrap(), + indices, + num_indices, + size_indices, + ) + }; + + if ret { + Ok(()) + } else { + Err(get_error()) + } + } +} + #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] pub struct TextureQuery { pub format: pixels::PixelFormat,