Render geometry binding (#1472)

* make `pixels::Color` have a stable representation (`repr(C)`)

this allows for faster code and is also needed for SDL_RenderGeometry

* add binding for SDL_RenderGeometryRaw

* add example of advanced use of `Canvas::render_geometry`

* update unsafe trait docs to include a needed guarantee

and refer to one doc comment instead of duplicating similar ones

* rework almost everything

- the macro was very hard to get sound, auto deref has to be prevented somehow
- the unsafe traits could not be implemented for types defined in other crates

the unsafety must be kept simple, users now only need to provide the
right offsets, which core::mem::offset_of does

update example

* add convenience impls for &Vec<_> to VertexIndices

* update example to use 2 ways of using render_geometry_raw, and use render_geometry

* mention alignment in safety comments in render_geometry example

* fix and improve docs

* update changelog
This commit is contained in:
Antoni Spaanderman
2025-03-31 09:20:58 +02:00
committed by GitHub
parent f0cdbb32b5
commit ecd03de215
5 changed files with 444 additions and 4 deletions

View File

@@ -135,6 +135,9 @@ name = "renderer-target"
[[example]]
name = "events"
[[example]]
name = "render-geometry"
[[example]]
name = "renderer-texture"

View File

@@ -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.

146
examples/render-geometry.rs Normal file
View File

@@ -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<u8>,
// 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::<RenderGeometryTextureParams<()>>,
&[[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::<RenderGeometryTextureParams<()>>,
&[[0, 1, 2], [1, 2, 3]],
)
}
.expect("render_geometry_raw failed (probably unsupported, see error message)");
canvas.present();
thread::sleep(Duration::from_millis(16));
}
}

View File

@@ -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::<Vec<_>>();
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::<sys::SDL_Color>(),
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,

View File

@@ -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<T: RenderTarget> Canvas<T> {
}
}
// `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::<c_void>(), indices.len() as c_int, 1),
Self::U16(indices) => (indices.as_ptr().cast::<c_void>(), indices.len() as c_int, 2),
Self::U32(indices) => (indices.as_ptr().cast::<c_void>(), 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::<u32>(), 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::<u8>(), 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::<u16>(), 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::<u32>(), 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::<u32>(), 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<T: RenderTarget> Canvas<T> {
/// 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<'_>>`][VertexIndices#trait-implementations].
#[doc(alias = "SDL_RenderGeometry")]
pub fn render_geometry<'a>(
&mut self,
vertices: &[Vertex],
texture: Option<&Texture>,
indices: impl Into<VertexIndices<'a>>,
) -> 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::<RenderGeometryTextureParams<()>>`.
///
/// # 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::<FPoint>() <= size_of::<PosVertex>()`: an [`FPoint`] must fit
/// in `PosVertex` at `position_offset` bytes from the start.
/// - `color_offset + size_of::<Color>() <= size_of::<ColorVertex>()`: a
/// [`Color`][pixels::Color] must fit in `ColorVertex` at `color_offset` bytes from the
/// start.
/// - `tex_coord_offset + size_of::<FPoint>() <= size_of::<TexCoordVertex>()`: 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::<PosVertex>() <= c_int::MAX`
/// - `size_of::<ColorVertex>() <= c_int::MAX`
/// - `size_of::<TexCoordVertex>() <= 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<RenderGeometryTextureParams<TexCoordVertex>>,
indices: impl Into<VertexIndices<'a>>,
) -> Result<(), String> {
let num_vertices = positions.len();
assert_eq!(num_vertices, colors.len());
assert!(position_offset + size_of::<FPoint>() <= size_of::<PosVertex>());
assert!(color_offset + size_of::<pixels::Color>() <= size_of::<ColorVertex>());
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::<FPoint>()
<= size_of::<TexCoordVertex>(),
);
(
texture_params.texture.raw,
texture_params
.tex_coords
.as_ptr()
.cast::<f32>()
.byte_offset(texture_params.tex_coord_offset.try_into().unwrap()),
size_of::<TexCoordVertex>().try_into().unwrap(),
)
} else {
(ptr::null_mut(), ptr::null(), 0)
};
let xy = positions
.as_ptr()
.cast::<f32>()
.byte_offset(position_offset.try_into().unwrap());
let xy_stride = size_of::<PosVertex>().try_into().unwrap();
let color = colors
.as_ptr()
.cast::<sys::SDL_Color>()
.byte_offset(color_offset.try_into().unwrap());
let color_stride = size_of::<ColorVertex>().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,