diff --git a/Cargo.toml b/Cargo.toml index 46bcc04f27..5053f7cbec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -135,6 +135,9 @@ name = "renderer-target" [[example]] name = "events" +[[example]] +name = "render-geometry" + [[example]] name = "renderer-texture" diff --git a/changelog.md b/changelog.md index 7dcbfaaace..80c052d7b4 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,8 @@ when upgrading from a version of rust-sdl2 to another. ### Next +[PR #1472](https://github.com/Rust-SDL2/rust-sdl2/pull/1472) Add `Canvas::render_geometry`, `Canvas::render_geometry_raw` and an example that uses them both. Both these bindings use `SDL_RenderGeometryRaw`. + [PR #1473](https://github.com/Rust-SDL2/rust-sdl2/pull/1473) **BREAKING CHANGE** Fix UB in `ToColor` implementations and remove implementation for `isize`. [PR #1414](https://github.com/Rust-SDL2/rust-sdl2/pull/1414) Use `TTF_GlyphIsProvided32` and `TTF_GlyphMetrics32` instead of the 16 bit ones. diff --git a/examples/render-geometry.rs b/examples/render-geometry.rs new file mode 100644 index 0000000000..072e97d357 --- /dev/null +++ b/examples/render-geometry.rs @@ -0,0 +1,146 @@ +extern crate sdl2; + +use sdl2::event::Event; +use sdl2::keyboard::Keycode; +use sdl2::pixels::Color; +use sdl2::rect::FPoint; +use sdl2::render::{RenderGeometryTextureParams, Vertex, VertexIndices}; +use std::mem::offset_of; +use std::thread; +use std::time::Duration; + +fn main() { + let sdl_context = sdl2::init().unwrap(); + let video_subsystem = sdl_context.video().unwrap(); + + let window = video_subsystem + .window("Rust SDL2 render_geometry custom struct example", 800, 600) + .position_centered() + .opengl() + .build() + .unwrap(); + + let mut canvas = window.into_canvas().build().unwrap(); + + 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 `sdl2::render::Vertex`. + let vertices = [ + Vertex { + position: FPoint::new(100.0, 200.0), + color: Color::RED, + tex_coord: FPoint::new(0.0, 0.0), + }, + Vertex { + position: FPoint::new(200.0, 200.0), + color: Color::GREEN, + tex_coord: FPoint::new(0.0, 0.0), + }, + Vertex { + position: FPoint::new(150.0, 100.0), + color: Color::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 { + // For demonstration purposes color is `[u8; 4]` here. `[u8; 4]` is layout-compatible + // with `sdl2::pixels::Color` + color: [u8; 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 `Color` 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, 0xff], + foo: b"some".to_vec(), + pos: FPoint::new(300.0, 100.0), + }, + MyVertex { + color: [0, 0xff, 0, 0xff], + foo: b"unrelated".to_vec(), + pos: FPoint::new(400.0, 100.0), + }, + MyVertex { + color: [0xff, 0, 0, 0xff], + foo: b"data".to_vec(), + pos: FPoint::new(300.0, 200.0), + }, + MyVertex { + color: [0xff, 0xff, 0, 0xff], + 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/sdl2/pixels.rs b/src/sdl2/pixels.rs index 867182cbc5..02a35ee07b 100644 --- a/src/sdl2/pixels.rs +++ b/src/sdl2/pixels.rs @@ -48,9 +48,14 @@ impl Palette { // Already validated, so don't check again let ncolors = colors.len() as ::libc::c_int; - let colors = colors.iter().map(|color| color.raw()).collect::>(); - - let result = unsafe { sys::SDL_SetPaletteColors(pal.raw, colors.as_ptr(), 0, ncolors) }; + let result = unsafe { + sys::SDL_SetPaletteColors( + pal.raw, + colors.as_ptr().cast::(), + 0, + ncolors, + ) + }; if result < 0 { Err(get_error()) @@ -88,6 +93,7 @@ fn create_palette() { assert!(palette.len() == 255); } +#[repr(C)] #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] pub struct Color { pub r: u8, diff --git a/src/sdl2/render.rs b/src/sdl2/render.rs index 40f8c5cf2e..6d48ff3941 100644 --- a/src/sdl2/render.rs +++ b/src/sdl2/render.rs @@ -41,7 +41,7 @@ use crate::surface::{Surface, SurfaceContext, SurfaceRef}; use crate::video::{Window, WindowContext}; use libc::c_void; use libc::{c_double, c_int}; -use std::convert::TryFrom; +use std::convert::{TryFrom, TryInto}; use std::error::Error; use std::ffi::CStr; use std::fmt; @@ -52,6 +52,7 @@ use std::mem::{transmute, MaybeUninit}; use std::ops::Deref; use std::ptr; use std::rc::Rc; +use std::slice; use crate::sys; use crate::sys::SDL_BlendMode; @@ -1964,6 +1965,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::Color, + 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<(), String> { + 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<(), String> { + 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::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 == 0 { + Ok(()) + } else { + Err(get_error()) + } + } +} + #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] pub struct TextureQuery { pub format: pixels::PixelFormatEnum,