rust: Implement lessons 1-7

This commit is contained in:
2025-06-12 20:09:07 +10:00
parent 69eef16b6a
commit 1b4a78f5cb
16 changed files with 2558 additions and 0 deletions

38
Cargo.toml Normal file
View File

@@ -0,0 +1,38 @@
[package]
name = "NeHe-SDL_GPU"
edition = "2024"
[dependencies]
sdl3-sys = { version = "0.5.1", features = [ "link-framework" ] }
[lib]
name = "nehe"
path = "src/rust/nehe/lib.rs"
[[bin]]
name = "lesson1"
path = "src/rust/lesson1.rs"
[[bin]]
name = "lesson2"
path = "src/rust/lesson2.rs"
[[bin]]
name = "lesson3"
path = "src/rust/lesson3.rs"
[[bin]]
name = "lesson4"
path = "src/rust/lesson4.rs"
[[bin]]
name = "lesson5"
path = "src/rust/lesson5.rs"
[[bin]]
name = "lesson6"
path = "src/rust/lesson6.rs"
[[bin]]
name = "lesson7"
path = "src/rust/lesson7.rs"

53
build.rs Normal file
View File

@@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
use std::path::{Path, PathBuf};
pub fn get_source_dir() -> PathBuf
{
std::env::current_dir().unwrap().join("src")
}
pub fn get_target_dir() -> PathBuf
{
//let out_dir = std::env::var("OUT_DIR").unwrap();
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let build_type = std::env::var("PROFILE").unwrap();
Path::new(&manifest_dir).join("target").join(build_type)
}
pub fn copy_resources<const N: usize>(src_dir: &PathBuf, dst_dir: &PathBuf, resources: &[&str; N])
{
if !dst_dir.is_dir()
{
std::fs::create_dir(&dst_dir).unwrap();
}
for resource in resources
{
std::fs::copy(&src_dir.join(resource), &dst_dir.join(resource)).unwrap();
}
}
pub fn main()
{
#[cfg(target_os="macos")]
println!("cargo:rustc-link-arg=-Wl,-rpath,/Library/Frameworks");
let src_dir = std::env::current_dir().unwrap().join("data");
let dst_dir = get_target_dir().join("Data");
copy_resources(&src_dir, &dst_dir,
&[
"NeHe.bmp",
"Crate.bmp",
]);
copy_resources(&src_dir.join("shaders"), &dst_dir.join("Shaders"),
&[
"lesson2.metallib",
"lesson3.metallib",
"lesson6.metallib",
"lesson7.metallib",
]);
}

43
src/rust/lesson1.rs Normal file
View File

@@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
use nehe::application::config::AppImplementation;
use nehe::application::run;
use nehe::context::NeHeContext;
use sdl3_sys::gpu::*;
use sdl3_sys::pixels::SDL_FColor;
use std::process::ExitCode;
use std::ptr::null_mut;
#[derive(Default)]
struct Lesson1;
impl AppImplementation for Lesson1
{
const TITLE: &'static str = "NeHe's OpenGL Framework";
const WIDTH: i32 = 640;
const HEIGHT: i32 = 480;
fn draw(&mut self, _ctx: &NeHeContext, cmd: *mut SDL_GPUCommandBuffer, swapchain: *mut SDL_GPUTexture)
{
let mut color_info = SDL_GPUColorTargetInfo::default();
color_info.texture = swapchain;
color_info.clear_color = SDL_FColor { r: 0.0, g: 0.0, b: 0.0, a: 0.5 };
color_info.load_op = SDL_GPU_LOADOP_CLEAR;
color_info.store_op = SDL_GPU_STOREOP_STORE;
unsafe
{
let pass = SDL_BeginGPURenderPass(cmd, &color_info, 1, null_mut());
SDL_EndGPURenderPass(pass);
}
}
}
pub fn main() -> Result<ExitCode, Box<dyn std::error::Error>>
{
run::<Lesson1>()?;
Ok(ExitCode::SUCCESS)
}

199
src/rust/lesson2.rs Normal file
View File

@@ -0,0 +1,199 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
use nehe::application::config::AppImplementation;
use nehe::application::run;
use nehe::context::NeHeContext;
use nehe::error::NeHeError;
use nehe::matrix::Mtx;
use sdl3_sys::gpu::*;
use sdl3_sys::pixels::SDL_FColor;
use std::cmp::max;
use std::ffi::c_void;
use std::mem::offset_of;
use std::process::ExitCode;
use std::ptr::{null, null_mut};
#[repr(C)]
struct Vertex { x: f32, y: f32, z: f32 }
const VERTICES: &'static [Vertex] =
&[
// Triangle
Vertex { x: 0.0, y: 1.0, z: 0.0 }, // Top
Vertex { x: -1.0, y: -1.0, z: 0.0 }, // Bottom left
Vertex { x: 1.0, y: -1.0, z: 0.0 }, // Bottom right
// Quad
Vertex { x: -1.0, y: 1.0, z: 0.0 }, // Top left
Vertex { x: 1.0, y: 1.0, z: 0.0 }, // Top right
Vertex { x: 1.0, y: -1.0, z: 0.0 }, // Bottom right
Vertex { x: -1.0, y: -1.0, z: 0.0 }, // Bottom left
];
const INDICES: &'static [i16] =
&[
// Triangle
0, 1, 2,
// Quad
3, 4, 5, 5, 6, 3,
];
//#[derive(Default)]
struct Lesson2
{
pso: *mut SDL_GPUGraphicsPipeline,
vtx_buffer: *mut SDL_GPUBuffer,
idx_buffer: *mut SDL_GPUBuffer,
projection: Mtx,
}
//FIXME: remove when `raw_ptr_default`
impl Default for Lesson2
{
fn default() -> Self
{
Self
{
pso: null_mut(),
vtx_buffer: null_mut(),
idx_buffer: null_mut(),
projection: Mtx::IDENTITY,
}
}
}
impl AppImplementation for Lesson2
{
const TITLE: &'static str = "NeHe's First Polygon Tutorial";
const WIDTH: i32 = 640;
const HEIGHT: i32 = 480;
fn init(&mut self, ctx: &NeHeContext) -> Result<(), NeHeError>
{
let (vertex_shader, fragment_shader) = ctx.load_shaders("lesson2", 1, 0, 0)?;
const VERTEX_DESCRIPTIONS: &'static [SDL_GPUVertexBufferDescription] =
&[
SDL_GPUVertexBufferDescription
{
slot: 0,
pitch: size_of::<Vertex>() as u32,
input_rate: SDL_GPU_VERTEXINPUTRATE_VERTEX,
instance_step_rate: 0,
},
];
const VERTEX_ATTRIBS: &'static [SDL_GPUVertexAttribute] =
&[
SDL_GPUVertexAttribute
{
location: 0,
buffer_slot: 0,
format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3,
offset: offset_of!(Vertex, x) as u32,
},
];
let mut info = SDL_GPUGraphicsPipelineCreateInfo::default();
info.vertex_shader = vertex_shader;
info.fragment_shader = fragment_shader;
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
info.vertex_input_state = SDL_GPUVertexInputState
{
vertex_buffer_descriptions: VERTEX_DESCRIPTIONS.as_ptr(),
num_vertex_buffers: VERTEX_DESCRIPTIONS.len() as u32,
vertex_attributes: VERTEX_ATTRIBS.as_ptr(),
num_vertex_attributes: VERTEX_ATTRIBS.len() as u32,
};
info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE;
info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
let colour_targets: &[SDL_GPUColorTargetDescription] =
&[
SDL_GPUColorTargetDescription
{
format: unsafe { SDL_GetGPUSwapchainTextureFormat(ctx.device, ctx.window) },
blend_state: SDL_GPUColorTargetBlendState::default(),
}
];
info.target_info.color_target_descriptions = colour_targets.as_ptr();
info.target_info.num_color_targets = colour_targets.len() as u32;
self.pso = unsafe { SDL_CreateGPUGraphicsPipeline(ctx.device, &info) };
unsafe
{
SDL_ReleaseGPUShader(ctx.device, fragment_shader);
SDL_ReleaseGPUShader(ctx.device, vertex_shader);
}
if self.pso.is_null()
{
return Err(NeHeError::sdl("SDL_CreateGPUGraphicsPipeline"));
}
ctx.copy_pass(|pass|
{
self.vtx_buffer = pass.create_buffer(SDL_GPU_BUFFERUSAGE_VERTEX, VERTICES)?;
self.idx_buffer = pass.create_buffer(SDL_GPU_BUFFERUSAGE_INDEX, INDICES)?;
Ok(())
})
}
fn quit(&mut self, ctx: &NeHeContext)
{
unsafe
{
SDL_ReleaseGPUBuffer(ctx.device, self.idx_buffer);
SDL_ReleaseGPUBuffer(ctx.device, self.vtx_buffer);
SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.pso);
}
}
fn resize(&mut self, _ctx: &NeHeContext, width: i32, height: i32)
{
let aspect = width as f32 / max(height, 1) as f32;
self.projection = Mtx::perspective(45.0, aspect, 0.1, 100.0);
}
fn draw(&mut self, _ctx: &NeHeContext, cmd: *mut SDL_GPUCommandBuffer, swapchain: *mut SDL_GPUTexture)
{
let mut color_info = SDL_GPUColorTargetInfo::default();
color_info.texture = swapchain;
color_info.clear_color = SDL_FColor { r: 0.0, g: 0.0, b: 0.0, a: 0.5 };
color_info.load_op = SDL_GPU_LOADOP_CLEAR;
color_info.store_op = SDL_GPU_STOREOP_STORE;
unsafe
{
// Begin pass & bind pipeline state
let pass = SDL_BeginGPURenderPass(cmd, &color_info, 1, null());
SDL_BindGPUGraphicsPipeline(pass, self.pso);
// Bind vertex & index buffers
let vtx_binding = SDL_GPUBufferBinding { buffer: self.vtx_buffer, offset: 0 };
let idx_binding = SDL_GPUBufferBinding { buffer: self.idx_buffer, offset: 0 };
SDL_BindGPUVertexBuffers(pass, 0, &vtx_binding, 1);
SDL_BindGPUIndexBuffer(pass, &idx_binding, SDL_GPU_INDEXELEMENTSIZE_16BIT);
// Draw triangle 1.5 units to the left and 6 units into the camera
let mut model = Mtx::translation(-1.5, 0.0, -6.0);
let mut viewproj = self.projection * model;
SDL_PushGPUVertexUniformData(cmd, 0, viewproj.as_ptr() as *const c_void, size_of::<Mtx>() as u32);
SDL_DrawGPUIndexedPrimitives(pass, 3, 1, 0, 0, 0);
// Move to the right by 3 units and draw quad
model.translate(3.0, 0.0, 0.0);
viewproj = self.projection * model;
SDL_PushGPUVertexUniformData(cmd, 0, viewproj.as_ptr() as *const c_void, size_of::<Mtx>() as u32);
SDL_DrawGPUIndexedPrimitives(pass, 6, 1, 3, 0, 0);
SDL_EndGPURenderPass(pass);
}
}
}
pub fn main() -> Result<ExitCode, Box<dyn std::error::Error>>
{
run::<Lesson2>()?;
Ok(ExitCode::SUCCESS)
}

209
src/rust/lesson3.rs Normal file
View File

@@ -0,0 +1,209 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
use nehe::application::config::AppImplementation;
use nehe::application::run;
use nehe::context::NeHeContext;
use nehe::error::NeHeError;
use nehe::matrix::Mtx;
use sdl3_sys::gpu::*;
use sdl3_sys::pixels::SDL_FColor;
use std::cmp::max;
use std::ffi::c_void;
use std::mem::offset_of;
use std::process::ExitCode;
use std::ptr::{null, null_mut};
#[repr(C)]
struct Vertex
{
x: f32, y: f32, z: f32,
r: f32, g: f32, b: f32, a: f32,
}
const VERTICES: &'static [Vertex] =
&[
// Triangle
Vertex { x: 0.0, y: 1.0, z: 0.0, r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, // Top (red)
Vertex { x: -1.0, y: -1.0, z: 0.0, r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, // Bottom left (green)
Vertex { x: 1.0, y: -1.0, z: 0.0, r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, // Bottom right (blue)
// Quad
Vertex { x: -1.0, y: 1.0, z: 0.0, r: 0.5, g: 0.5, b: 1.0, a: 1.0 }, // Top left
Vertex { x: 1.0, y: 1.0, z: 0.0, r: 0.5, g: 0.5, b: 1.0, a: 1.0 }, // Top right
Vertex { x: 1.0, y: -1.0, z: 0.0, r: 0.5, g: 0.5, b: 1.0, a: 1.0 }, // Bottom right
Vertex { x: -1.0, y: -1.0, z: 0.0, r: 0.5, g: 0.5, b: 1.0, a: 1.0 }, // Bottom left
];
const INDICES: &'static [i16] =
&[
// Triangle
0, 1, 2,
// Quad
3, 4, 5, 5, 6, 3,
];
struct Lesson3
{
pso: *mut SDL_GPUGraphicsPipeline,
vtx_buffer: *mut SDL_GPUBuffer,
idx_buffer: *mut SDL_GPUBuffer,
projection: Mtx,
}
//FIXME: remove when `raw_ptr_default`
impl Default for Lesson3
{
fn default() -> Self
{
Self
{
pso: null_mut(),
vtx_buffer: null_mut(),
idx_buffer: null_mut(),
projection: Mtx::IDENTITY,
}
}
}
impl AppImplementation for Lesson3
{
const TITLE: &'static str = "NeHe's Color Tutorial";
const WIDTH: i32 = 640;
const HEIGHT: i32 = 480;
fn init(&mut self, ctx: &NeHeContext) -> Result<(), NeHeError>
{
let (vertex_shader, fragment_shader) = ctx.load_shaders("lesson3", 1, 0, 0)?;
const VERTEX_DESCRIPTIONS: &'static [SDL_GPUVertexBufferDescription] =
&[
SDL_GPUVertexBufferDescription
{
slot: 0,
pitch: size_of::<Vertex>() as u32,
input_rate: SDL_GPU_VERTEXINPUTRATE_VERTEX,
instance_step_rate: 0,
},
];
const VERTEX_ATTRIBS: &'static [SDL_GPUVertexAttribute] =
&[
SDL_GPUVertexAttribute
{
location: 0,
buffer_slot: 0,
format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3,
offset: offset_of!(Vertex, x) as u32,
},
SDL_GPUVertexAttribute
{
location: 1,
buffer_slot: 0,
format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4,
offset: offset_of!(Vertex, r) as u32,
},
];
let mut info = SDL_GPUGraphicsPipelineCreateInfo::default();
info.vertex_shader = vertex_shader;
info.fragment_shader = fragment_shader;
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
info.vertex_input_state = SDL_GPUVertexInputState
{
vertex_buffer_descriptions: VERTEX_DESCRIPTIONS.as_ptr(),
num_vertex_buffers: VERTEX_DESCRIPTIONS.len() as u32,
vertex_attributes: VERTEX_ATTRIBS.as_ptr(),
num_vertex_attributes: VERTEX_ATTRIBS.len() as u32,
};
info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE;
info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
let colour_targets: &[SDL_GPUColorTargetDescription] =
&[
SDL_GPUColorTargetDescription
{
format: unsafe { SDL_GetGPUSwapchainTextureFormat(ctx.device, ctx.window) },
blend_state: SDL_GPUColorTargetBlendState::default(),
}
];
info.target_info.color_target_descriptions = colour_targets.as_ptr();
info.target_info.num_color_targets = colour_targets.len() as u32;
self.pso = unsafe { SDL_CreateGPUGraphicsPipeline(ctx.device, &info) };
unsafe
{
SDL_ReleaseGPUShader(ctx.device, fragment_shader);
SDL_ReleaseGPUShader(ctx.device, vertex_shader);
}
if self.pso.is_null()
{
return Err(NeHeError::sdl("SDL_CreateGPUGraphicsPipeline"));
}
ctx.copy_pass(|pass|
{
self.vtx_buffer = pass.create_buffer(SDL_GPU_BUFFERUSAGE_VERTEX, VERTICES)?;
self.idx_buffer = pass.create_buffer(SDL_GPU_BUFFERUSAGE_INDEX, INDICES)?;
Ok(())
})
}
fn quit(&mut self, ctx: &NeHeContext)
{
unsafe
{
SDL_ReleaseGPUBuffer(ctx.device, self.idx_buffer);
SDL_ReleaseGPUBuffer(ctx.device, self.vtx_buffer);
SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.pso);
}
}
fn resize(&mut self, _ctx: &NeHeContext, width: i32, height: i32)
{
let aspect = width as f32 / max(height, 1) as f32;
self.projection = Mtx::perspective(45.0, aspect, 0.1, 100.0);
}
fn draw(&mut self, _ctx: &NeHeContext, cmd: *mut SDL_GPUCommandBuffer, swapchain: *mut SDL_GPUTexture)
{
let mut color_info = SDL_GPUColorTargetInfo::default();
color_info.texture = swapchain;
color_info.clear_color = SDL_FColor { r: 0.0, g: 0.0, b: 0.0, a: 0.5 };
color_info.load_op = SDL_GPU_LOADOP_CLEAR;
color_info.store_op = SDL_GPU_STOREOP_STORE;
unsafe
{
// Begin pass & bind pipeline state
let pass = SDL_BeginGPURenderPass(cmd, &color_info, 1, null());
SDL_BindGPUGraphicsPipeline(pass, self.pso);
// Bind vertex & index buffers
let vtx_binding = SDL_GPUBufferBinding { buffer: self.vtx_buffer, offset: 0 };
let idx_binding = SDL_GPUBufferBinding { buffer: self.idx_buffer, offset: 0 };
SDL_BindGPUVertexBuffers(pass, 0, &vtx_binding, 1);
SDL_BindGPUIndexBuffer(pass, &idx_binding, SDL_GPU_INDEXELEMENTSIZE_16BIT);
// Draw triangle 1.5 units to the left and 6 units into the camera
let mut model = Mtx::translation(-1.5, 0.0, -6.0);
let mut viewproj = self.projection * model;
SDL_PushGPUVertexUniformData(cmd, 0, viewproj.as_ptr() as *const c_void, size_of::<Mtx>() as u32);
SDL_DrawGPUIndexedPrimitives(pass, 3, 1, 0, 0, 0);
// Move to the right by 3 units and draw quad
model.translate(3.0, 0.0, 0.0);
viewproj = self.projection * model;
SDL_PushGPUVertexUniformData(cmd, 0, viewproj.as_ptr() as *const c_void, size_of::<Mtx>() as u32);
SDL_DrawGPUIndexedPrimitives(pass, 6, 1, 3, 0, 0);
SDL_EndGPURenderPass(pass);
}
}
}
pub fn main() -> Result<ExitCode, Box<dyn std::error::Error>>
{
run::<Lesson3>()?;
Ok(ExitCode::SUCCESS)
}

220
src/rust/lesson4.rs Normal file
View File

@@ -0,0 +1,220 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
use nehe::application::config::AppImplementation;
use nehe::application::run;
use nehe::context::NeHeContext;
use nehe::error::NeHeError;
use nehe::matrix::Mtx;
use sdl3_sys::gpu::*;
use sdl3_sys::pixels::SDL_FColor;
use std::cmp::max;
use std::ffi::c_void;
use std::mem::offset_of;
use std::process::ExitCode;
use std::ptr::{null, null_mut};
#[repr(C)]
struct Vertex
{
x: f32, y: f32, z: f32,
r: f32, g: f32, b: f32, a: f32,
}
const VERTICES: &'static [Vertex] =
&[
// Triangle
Vertex { x: 0.0, y: 1.0, z: 0.0, r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, // Top (red)
Vertex { x: -1.0, y: -1.0, z: 0.0, r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, // Bottom left (green)
Vertex { x: 1.0, y: -1.0, z: 0.0, r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, // Bottom right (blue)
// Quad
Vertex { x: -1.0, y: 1.0, z: 0.0, r: 0.5, g: 0.5, b: 1.0, a: 1.0 }, // Top left
Vertex { x: 1.0, y: 1.0, z: 0.0, r: 0.5, g: 0.5, b: 1.0, a: 1.0 }, // Top right
Vertex { x: 1.0, y: -1.0, z: 0.0, r: 0.5, g: 0.5, b: 1.0, a: 1.0 }, // Bottom right
Vertex { x: -1.0, y: -1.0, z: 0.0, r: 0.5, g: 0.5, b: 1.0, a: 1.0 }, // Bottom left
];
const INDICES: &'static [i16] =
&[
// Triangle
0, 1, 2,
// Quad
3, 4, 5, 5, 6, 3,
];
struct Lesson4
{
pso: *mut SDL_GPUGraphicsPipeline,
vtx_buffer: *mut SDL_GPUBuffer,
idx_buffer: *mut SDL_GPUBuffer,
projection: Mtx,
rot_tri: f32,
rot_quad: f32,
}
//FIXME: remove when `raw_ptr_default`
impl Default for Lesson4
{
fn default() -> Self
{
Self
{
pso: null_mut(),
vtx_buffer: null_mut(),
idx_buffer: null_mut(),
projection: Mtx::IDENTITY,
rot_tri: 0.0,
rot_quad: 0.0,
}
}
}
impl AppImplementation for Lesson4
{
const TITLE: &'static str = "NeHe's Rotation Tutorial";
const WIDTH: i32 = 640;
const HEIGHT: i32 = 480;
fn init(&mut self, ctx: &NeHeContext) -> Result<(), NeHeError>
{
let (vertex_shader, fragment_shader) = ctx.load_shaders("lesson3", 1, 0, 0)?;
const VERTEX_DESCRIPTIONS: &'static [SDL_GPUVertexBufferDescription] =
&[
SDL_GPUVertexBufferDescription
{
slot: 0,
pitch: size_of::<Vertex>() as u32,
input_rate: SDL_GPU_VERTEXINPUTRATE_VERTEX,
instance_step_rate: 0,
},
];
const VERTEX_ATTRIBS: &'static [SDL_GPUVertexAttribute] =
&[
SDL_GPUVertexAttribute
{
location: 0,
buffer_slot: 0,
format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3,
offset: offset_of!(Vertex, x) as u32,
},
SDL_GPUVertexAttribute
{
location: 1,
buffer_slot: 0,
format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4,
offset: offset_of!(Vertex, r) as u32,
},
];
let mut info = SDL_GPUGraphicsPipelineCreateInfo::default();
info.vertex_shader = vertex_shader;
info.fragment_shader = fragment_shader;
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
info.vertex_input_state = SDL_GPUVertexInputState
{
vertex_buffer_descriptions: VERTEX_DESCRIPTIONS.as_ptr(),
num_vertex_buffers: VERTEX_DESCRIPTIONS.len() as u32,
vertex_attributes: VERTEX_ATTRIBS.as_ptr(),
num_vertex_attributes: VERTEX_ATTRIBS.len() as u32,
};
info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE;
info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
let colour_targets: &[SDL_GPUColorTargetDescription] =
&[
SDL_GPUColorTargetDescription
{
format: unsafe { SDL_GetGPUSwapchainTextureFormat(ctx.device, ctx.window) },
blend_state: SDL_GPUColorTargetBlendState::default(),
}
];
info.target_info.color_target_descriptions = colour_targets.as_ptr();
info.target_info.num_color_targets = colour_targets.len() as u32;
self.pso = unsafe { SDL_CreateGPUGraphicsPipeline(ctx.device, &info) };
unsafe
{
SDL_ReleaseGPUShader(ctx.device, fragment_shader);
SDL_ReleaseGPUShader(ctx.device, vertex_shader);
}
if self.pso.is_null()
{
return Err(NeHeError::sdl("SDL_CreateGPUGraphicsPipeline"));
}
ctx.copy_pass(|pass|
{
self.vtx_buffer = pass.create_buffer(SDL_GPU_BUFFERUSAGE_VERTEX, VERTICES)?;
self.idx_buffer = pass.create_buffer(SDL_GPU_BUFFERUSAGE_INDEX, INDICES)?;
Ok(())
})
}
fn quit(&mut self, ctx: &NeHeContext)
{
unsafe
{
SDL_ReleaseGPUBuffer(ctx.device, self.idx_buffer);
SDL_ReleaseGPUBuffer(ctx.device, self.vtx_buffer);
SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.pso);
}
}
fn resize(&mut self, _ctx: &NeHeContext, width: i32, height: i32)
{
let aspect = width as f32 / max(height, 1) as f32;
self.projection = Mtx::perspective(45.0, aspect, 0.1, 100.0);
}
fn draw(&mut self, _ctx: &NeHeContext, cmd: *mut SDL_GPUCommandBuffer, swapchain: *mut SDL_GPUTexture)
{
let mut color_info = SDL_GPUColorTargetInfo::default();
color_info.texture = swapchain;
color_info.clear_color = SDL_FColor { r: 0.0, g: 0.0, b: 0.0, a: 0.5 };
color_info.load_op = SDL_GPU_LOADOP_CLEAR;
color_info.store_op = SDL_GPU_STOREOP_STORE;
unsafe
{
// Begin pass & bind pipeline state
let pass = SDL_BeginGPURenderPass(cmd, &color_info, 1, null());
SDL_BindGPUGraphicsPipeline(pass, self.pso);
// Bind vertex & index buffers
let vtx_binding = SDL_GPUBufferBinding { buffer: self.vtx_buffer, offset: 0 };
let idx_binding = SDL_GPUBufferBinding { buffer: self.idx_buffer, offset: 0 };
SDL_BindGPUVertexBuffers(pass, 0, &vtx_binding, 1);
SDL_BindGPUIndexBuffer(pass, &idx_binding, SDL_GPU_INDEXELEMENTSIZE_16BIT);
// Draw triangle 1.5 units to the left and 6 units into the camera
let mut model = Mtx::translation(-1.5, 0.0, -6.0);
model.rotate(self.rot_tri, 0.0, 1.0, 0.0);
let mut viewproj = self.projection * model;
SDL_PushGPUVertexUniformData(cmd, 0, viewproj.as_ptr() as *const c_void, size_of::<Mtx>() as u32);
SDL_DrawGPUIndexedPrimitives(pass, 3, 1, 0, 0, 0);
// Draw quad 1.5 units to the right and 6 units into the camera
model = Mtx::translation(1.5,0.0,-6.0);
model.rotate(self.rot_quad, 1.0, 0.0, 0.0);
viewproj = self.projection * model;
SDL_PushGPUVertexUniformData(cmd, 0, viewproj.as_ptr() as *const c_void, size_of::<Mtx>() as u32);
SDL_DrawGPUIndexedPrimitives(pass, 6, 1, 3, 0, 0);
SDL_EndGPURenderPass(pass);
}
self.rot_tri += 0.2;
self.rot_quad -= 0.15;
}
}
pub fn main() -> Result<ExitCode, Box<dyn std::error::Error>>
{
run::<Lesson4>()?;
Ok(ExitCode::SUCCESS)
}

266
src/rust/lesson5.rs Normal file
View File

@@ -0,0 +1,266 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
use nehe::application::config::AppImplementation;
use nehe::application::run;
use nehe::context::NeHeContext;
use nehe::error::NeHeError;
use nehe::matrix::Mtx;
use sdl3_sys::gpu::*;
use sdl3_sys::pixels::SDL_FColor;
use std::cmp::max;
use std::ffi::c_void;
use std::mem::offset_of;
use std::process::ExitCode;
use std::ptr::null_mut;
#[repr(C)]
struct Vertex
{
x: f32, y: f32, z: f32,
r: f32, g: f32, b: f32, a: f32,
}
const VERTICES: &'static [Vertex] =
&[
// Pyramid
Vertex { x: 0.0, y: 1.0, z: 0.0, r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, // Top of pyramid (Red)
Vertex { x: -1.0, y: -1.0, z: 1.0, r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, // Front-left of pyramid (Green)
Vertex { x: 1.0, y: -1.0, z: 1.0, r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, // Front-right of pyramid (Blue)
Vertex { x: 1.0, y: -1.0, z: -1.0, r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, // Back-right of pyramid (Green)
Vertex { x: -1.0, y: -1.0, z: -1.0, r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, // Back-left of pyramid (Blue)
// Cube
Vertex { x: 1.0, y: 1.0, z: -1.0, r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, // Top-right of top face (Green)
Vertex { x: -1.0, y: 1.0, z: -1.0, r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, // Top-left of top face (Green)
Vertex { x: -1.0, y: 1.0, z: 1.0, r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, // Bottom-left of top face (Green)
Vertex { x: 1.0, y: 1.0, z: 1.0, r: 0.0, g: 1.0, b: 0.0, a: 1.0 }, // Bottom-right of top face (Green)
Vertex { x: 1.0, y: -1.0, z: 1.0, r: 1.0, g: 0.5, b: 0.0, a: 1.0 }, // Top-right of bottom face (Orange)
Vertex { x: -1.0, y: -1.0, z: 1.0, r: 1.0, g: 0.5, b: 0.0, a: 1.0 }, // Top-left of bottom face (Orange)
Vertex { x: -1.0, y: -1.0, z: -1.0, r: 1.0, g: 0.5, b: 0.0, a: 1.0 }, // Bottom-left of bottom face (Orange)
Vertex { x: 1.0, y: -1.0, z: -1.0, r: 1.0, g: 0.5, b: 0.0, a: 1.0 }, // Bottom-right of bottom face (Orange)
Vertex { x: 1.0, y: 1.0, z: 1.0, r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, // Top-right of front face (Red)
Vertex { x: -1.0, y: 1.0, z: 1.0, r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, // Top-left of front face (Red)
Vertex { x: -1.0, y: -1.0, z: 1.0, r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, // Bottom-left of front face (Red)
Vertex { x: 1.0, y: -1.0, z: 1.0, r: 1.0, g: 0.0, b: 0.0, a: 1.0 }, // Bottom-right of front face (Red)
Vertex { x: 1.0, y: -1.0, z: -1.0, r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, // Top-right of back face (Yellow)
Vertex { x: -1.0, y: -1.0, z: -1.0, r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, // Top-left of back face (Yellow)
Vertex { x: -1.0, y: 1.0, z: -1.0, r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, // Bottom-left of back face (Yellow)
Vertex { x: 1.0, y: 1.0, z: -1.0, r: 1.0, g: 1.0, b: 0.0, a: 1.0 }, // Bottom-right of back face (Yellow)
Vertex { x: -1.0, y: 1.0, z: 1.0, r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, // Top-right of left face (Blue)
Vertex { x: -1.0, y: 1.0, z: -1.0, r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, // Top-left of left face (Blue)
Vertex { x: -1.0, y: -1.0, z: -1.0, r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, // Bottom-left of left face (Blue)
Vertex { x: -1.0, y: -1.0, z: 1.0, r: 0.0, g: 0.0, b: 1.0, a: 1.0 }, // Bottom-right of left face (Blue)
Vertex { x: 1.0, y: 1.0, z: -1.0, r: 1.0, g: 0.0, b: 1.0, a: 1.0 }, // Top-right of right face (Violet)
Vertex { x: 1.0, y: 1.0, z: 1.0, r: 1.0, g: 0.0, b: 1.0, a: 1.0 }, // Top-left of right face (Violet)
Vertex { x: 1.0, y: -1.0, z: 1.0, r: 1.0, g: 0.0, b: 1.0, a: 1.0 }, // Bottom-left of right face (Violet)
Vertex { x: 1.0, y: -1.0, z: -1.0, r: 1.0, g: 0.0, b: 1.0, a: 1.0 }, // Bottom-right of right face (Violet)
];
const INDICES: &'static [u16] =
&[
// Pyramid
0, 1, 2, // Front
0, 2, 3, // Right
0, 3, 4, // Back
0, 4, 1, // Left
// Cube
5, 6, 7, 7, 8, 5, // Top
9, 10, 11, 11, 12, 9, // Bottom
13, 14, 15, 15, 16, 13, // Front
17, 18, 19, 19, 20, 17, // Back
21, 22, 23, 23, 24, 21, // Left
25, 26, 27, 27, 28, 25, // Right
];
struct Lesson5
{
pso: *mut SDL_GPUGraphicsPipeline,
vtx_buffer: *mut SDL_GPUBuffer,
idx_buffer: *mut SDL_GPUBuffer,
projection: Mtx,
rot_tri: f32,
rot_quad: f32,
}
//FIXME: remove when `raw_ptr_default`
impl Default for Lesson5
{
fn default() -> Self
{
Self
{
pso: null_mut(),
vtx_buffer: null_mut(),
idx_buffer: null_mut(),
projection: Mtx::IDENTITY,
rot_tri: 0.0,
rot_quad: 0.0,
}
}
}
impl AppImplementation for Lesson5
{
const TITLE: &'static str = "NeHe's Solid Object Tutorial";
const WIDTH: i32 = 640;
const HEIGHT: i32 = 480;
const CREATE_DEPTH_BUFFER: SDL_GPUTextureFormat = SDL_GPU_TEXTUREFORMAT_D16_UNORM;
fn init(&mut self, ctx: &NeHeContext) -> Result<(), NeHeError>
{
let (vertex_shader, fragment_shader) = ctx.load_shaders("lesson3", 1, 0, 0)?;
const VERTEX_DESCRIPTIONS: &'static [SDL_GPUVertexBufferDescription] =
&[
SDL_GPUVertexBufferDescription
{
slot: 0,
pitch: size_of::<Vertex>() as u32,
input_rate: SDL_GPU_VERTEXINPUTRATE_VERTEX,
instance_step_rate: 0,
},
];
const VERTEX_ATTRIBS: &'static [SDL_GPUVertexAttribute] =
&[
SDL_GPUVertexAttribute
{
location: 0,
buffer_slot: 0,
format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3,
offset: offset_of!(Vertex, x) as u32,
},
SDL_GPUVertexAttribute
{
location: 1,
buffer_slot: 0,
format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4,
offset: offset_of!(Vertex, r) as u32,
},
];
let mut info = SDL_GPUGraphicsPipelineCreateInfo::default();
info.vertex_shader = vertex_shader;
info.fragment_shader = fragment_shader;
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
info.vertex_input_state = SDL_GPUVertexInputState
{
vertex_buffer_descriptions: VERTEX_DESCRIPTIONS.as_ptr(),
num_vertex_buffers: VERTEX_DESCRIPTIONS.len() as u32,
vertex_attributes: VERTEX_ATTRIBS.as_ptr(),
num_vertex_attributes: VERTEX_ATTRIBS.len() as u32,
};
info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE;
info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
let colour_targets: &[SDL_GPUColorTargetDescription] =
&[
SDL_GPUColorTargetDescription
{
format: unsafe { SDL_GetGPUSwapchainTextureFormat(ctx.device, ctx.window) },
blend_state: SDL_GPUColorTargetBlendState::default(),
}
];
info.target_info.color_target_descriptions = colour_targets.as_ptr();
info.target_info.num_color_targets = colour_targets.len() as u32;
info.target_info.depth_stencil_format = SDL_GPU_TEXTUREFORMAT_D16_UNORM;
info.target_info.has_depth_stencil_target = true;
info.depth_stencil_state.compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL;
info.depth_stencil_state.enable_depth_test = true;
info.depth_stencil_state.enable_depth_write = true;
self.pso = unsafe { SDL_CreateGPUGraphicsPipeline(ctx.device, &info) };
unsafe
{
SDL_ReleaseGPUShader(ctx.device, fragment_shader);
SDL_ReleaseGPUShader(ctx.device, vertex_shader);
}
if self.pso.is_null()
{
return Err(NeHeError::sdl("SDL_CreateGPUGraphicsPipeline"));
}
ctx.copy_pass(|pass|
{
self.vtx_buffer = pass.create_buffer(SDL_GPU_BUFFERUSAGE_VERTEX, VERTICES)?;
self.idx_buffer = pass.create_buffer(SDL_GPU_BUFFERUSAGE_INDEX, INDICES)?;
Ok(())
})
}
fn quit(&mut self, ctx: &NeHeContext)
{
unsafe
{
SDL_ReleaseGPUBuffer(ctx.device, self.idx_buffer);
SDL_ReleaseGPUBuffer(ctx.device, self.vtx_buffer);
SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.pso);
}
}
fn resize(&mut self, _ctx: &NeHeContext, width: i32, height: i32)
{
let aspect = width as f32 / max(height, 1) as f32;
self.projection = Mtx::perspective(45.0, aspect, 0.1, 100.0);
}
fn draw(&mut self, ctx: &NeHeContext, cmd: *mut SDL_GPUCommandBuffer, swapchain: *mut SDL_GPUTexture)
{
let mut color_info = SDL_GPUColorTargetInfo::default();
color_info.texture = swapchain;
color_info.clear_color = SDL_FColor { r: 0.0, g: 0.0, b: 0.0, a: 0.5 };
color_info.load_op = SDL_GPU_LOADOP_CLEAR;
color_info.store_op = SDL_GPU_STOREOP_STORE;
let mut depth_info = SDL_GPUDepthStencilTargetInfo::default();
depth_info.texture = ctx.depth_texture;
depth_info.clear_depth = 1.0;
depth_info.load_op = SDL_GPU_LOADOP_CLEAR;
depth_info.store_op = SDL_GPU_STOREOP_DONT_CARE;
depth_info.stencil_load_op = SDL_GPU_LOADOP_DONT_CARE;
depth_info.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE;
depth_info.cycle = true;
unsafe
{
// Begin pass & bind pipeline state
let pass = SDL_BeginGPURenderPass(cmd, &color_info, 1, &depth_info);
SDL_BindGPUGraphicsPipeline(pass, self.pso);
// Bind vertex & index buffers
let vtx_binding = SDL_GPUBufferBinding { buffer: self.vtx_buffer, offset: 0 };
let idx_binding = SDL_GPUBufferBinding { buffer: self.idx_buffer, offset: 0 };
SDL_BindGPUVertexBuffers(pass, 0, &vtx_binding, 1);
SDL_BindGPUIndexBuffer(pass, &idx_binding, SDL_GPU_INDEXELEMENTSIZE_16BIT);
// Draw pyramid 1.5 units to the left and 6 units into the camera
let mut model = Mtx::translation(-1.5, 0.0, -6.0);
model.rotate(self.rot_tri, 0.0, 1.0, 0.0);
let mut viewproj = self.projection * model;
SDL_PushGPUVertexUniformData(cmd, 0, viewproj.as_ptr() as *const c_void, size_of::<Mtx>() as u32);
SDL_DrawGPUIndexedPrimitives(pass, 12, 1, 0, 0, 0);
// Draw cube 1.5 units to the right and 7 units into the camera
model = Mtx::translation(1.5, 0.0, -7.0);
model.rotate(self.rot_quad, 1.0, 1.0, 1.0);
viewproj = self.projection * model;
SDL_PushGPUVertexUniformData(cmd, 0, viewproj.as_ptr() as *const c_void, size_of::<Mtx>() as u32);
SDL_DrawGPUIndexedPrimitives(pass, 36, 1, 12, 0, 0);
SDL_EndGPURenderPass(pass);
}
self.rot_tri += 0.2;
self.rot_quad -= 0.15;
}
}
pub fn main() -> Result<ExitCode, Box<dyn std::error::Error>>
{
run::<Lesson5>()?;
Ok(ExitCode::SUCCESS)
}

278
src/rust/lesson6.rs Normal file
View File

@@ -0,0 +1,278 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
use nehe::application::config::AppImplementation;
use nehe::application::run;
use nehe::context::NeHeContext;
use nehe::error::NeHeError;
use nehe::matrix::Mtx;
use sdl3_sys::gpu::*;
use sdl3_sys::pixels::SDL_FColor;
use std::cmp::max;
use std::ffi::c_void;
use std::mem::offset_of;
use std::process::ExitCode;
use std::ptr::{addr_of, null_mut};
#[repr(C)]
struct Vertex
{
x: f32, y: f32, z: f32,
u: f32, v: f32,
}
const VERTICES: &'static [Vertex] =
&[
// Front Face
Vertex { x: -1.0, y: -1.0, z: 1.0, u: 0.0, v: 0.0 },
Vertex { x: 1.0, y: -1.0, z: 1.0, u: 1.0, v: 0.0 },
Vertex { x: 1.0, y: 1.0, z: 1.0, u: 1.0, v: 1.0 },
Vertex { x: -1.0, y: 1.0, z: 1.0, u: 0.0, v: 1.0 },
// Back Face
Vertex { x: -1.0, y: -1.0, z: -1.0, u: 1.0, v: 0.0 },
Vertex { x: -1.0, y: 1.0, z: -1.0, u: 1.0, v: 1.0 },
Vertex { x: 1.0, y: 1.0, z: -1.0, u: 0.0, v: 1.0 },
Vertex { x: 1.0, y: -1.0, z: -1.0, u: 0.0, v: 0.0 },
// Top Face
Vertex { x: -1.0, y: 1.0, z: -1.0, u: 0.0, v: 1.0 },
Vertex { x: -1.0, y: 1.0, z: 1.0, u: 0.0, v: 0.0 },
Vertex { x: 1.0, y: 1.0, z: 1.0, u: 1.0, v: 0.0 },
Vertex { x: 1.0, y: 1.0, z: -1.0, u: 1.0, v: 1.0 },
// Bottom Face
Vertex { x: -1.0, y: -1.0, z: -1.0, u: 1.0, v: 1.0 },
Vertex { x: 1.0, y: -1.0, z: -1.0, u: 0.0, v: 1.0 },
Vertex { x: 1.0, y: -1.0, z: 1.0, u: 0.0, v: 0.0 },
Vertex { x: -1.0, y: -1.0, z: 1.0, u: 1.0, v: 0.0 },
// Right face
Vertex { x: 1.0, y: -1.0, z: -1.0, u: 1.0, v: 0.0 },
Vertex { x: 1.0, y: 1.0, z: -1.0, u: 1.0, v: 1.0 },
Vertex { x: 1.0, y: 1.0, z: 1.0, u: 0.0, v: 1.0 },
Vertex { x: 1.0, y: -1.0, z: 1.0, u: 0.0, v: 0.0 },
// Left Face
Vertex { x: -1.0, y: -1.0, z: -1.0, u: 0.0, v: 0.0 },
Vertex { x: -1.0, y: -1.0, z: 1.0, u: 1.0, v: 0.0 },
Vertex { x: -1.0, y: 1.0, z: 1.0, u: 1.0, v: 1.0 },
Vertex { x: -1.0, y: 1.0, z: -1.0, u: 0.0, v: 1.0 },
];
const INDICES: &'static [u16] =
&[
0, 1, 2, 2, 3, 0, // Front
4, 5, 6, 6, 7, 4, // Back
8, 9, 10, 10, 11, 8, // Top
12, 13, 14, 14, 15, 12, // Bottom
16, 17, 18, 18, 19, 16, // Right
20, 21, 22, 22, 23, 20 // Left
];
struct Lesson6
{
pso: *mut SDL_GPUGraphicsPipeline,
vtx_buffer: *mut SDL_GPUBuffer,
idx_buffer: *mut SDL_GPUBuffer,
sampler: *mut SDL_GPUSampler,
texture: *mut SDL_GPUTexture,
projection: Mtx,
rot: (f32, f32, f32),
}
//FIXME: remove when `raw_ptr_default`
impl Default for Lesson6
{
fn default() -> Self
{
Self
{
pso: null_mut(),
vtx_buffer: null_mut(),
idx_buffer: null_mut(),
sampler: null_mut(),
texture: null_mut(),
projection: Mtx::IDENTITY,
rot: (0.0, 0.0, 0.0),
}
}
}
impl AppImplementation for Lesson6
{
const TITLE: &'static str = "NeHe's Texture Mapping Tutorial";
const WIDTH: i32 = 640;
const HEIGHT: i32 = 480;
const CREATE_DEPTH_BUFFER: SDL_GPUTextureFormat = SDL_GPU_TEXTUREFORMAT_D16_UNORM;
fn init(&mut self, ctx: &NeHeContext) -> Result<(), NeHeError>
{
let (vertex_shader, fragment_shader) = ctx.load_shaders("lesson6", 1, 0, 1)?;
const VERTEX_DESCRIPTIONS: &'static [SDL_GPUVertexBufferDescription] =
&[
SDL_GPUVertexBufferDescription
{
slot: 0,
pitch: size_of::<Vertex>() as u32,
input_rate: SDL_GPU_VERTEXINPUTRATE_VERTEX,
instance_step_rate: 0,
},
];
const VERTEX_ATTRIBS: &'static [SDL_GPUVertexAttribute] =
&[
SDL_GPUVertexAttribute
{
location: 0,
buffer_slot: 0,
format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3,
offset: offset_of!(Vertex, x) as u32,
},
SDL_GPUVertexAttribute
{
location: 1,
buffer_slot: 0,
format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2,
offset: offset_of!(Vertex, u) as u32,
},
];
let mut info = SDL_GPUGraphicsPipelineCreateInfo::default();
info.vertex_shader = vertex_shader;
info.fragment_shader = fragment_shader;
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
info.vertex_input_state = SDL_GPUVertexInputState
{
vertex_buffer_descriptions: VERTEX_DESCRIPTIONS.as_ptr(),
num_vertex_buffers: VERTEX_DESCRIPTIONS.len() as u32,
vertex_attributes: VERTEX_ATTRIBS.as_ptr(),
num_vertex_attributes: VERTEX_ATTRIBS.len() as u32,
};
info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE;
info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
let colour_targets: &[SDL_GPUColorTargetDescription] =
&[
SDL_GPUColorTargetDescription
{
format: unsafe { SDL_GetGPUSwapchainTextureFormat(ctx.device, ctx.window) },
blend_state: SDL_GPUColorTargetBlendState::default(),
}
];
info.target_info.color_target_descriptions = colour_targets.as_ptr();
info.target_info.num_color_targets = colour_targets.len() as u32;
info.target_info.depth_stencil_format = SDL_GPU_TEXTUREFORMAT_D16_UNORM;
info.target_info.has_depth_stencil_target = true;
info.depth_stencil_state.compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL;
info.depth_stencil_state.enable_depth_test = true;
info.depth_stencil_state.enable_depth_write = true;
self.pso = unsafe { SDL_CreateGPUGraphicsPipeline(ctx.device, &info) };
unsafe
{
SDL_ReleaseGPUShader(ctx.device, fragment_shader);
SDL_ReleaseGPUShader(ctx.device, vertex_shader);
}
if self.pso.is_null()
{
return Err(NeHeError::sdl("SDL_CreateGPUGraphicsPipeline"));
}
// Create texture sampler
let mut sampler_info = SDL_GPUSamplerCreateInfo::default();
sampler_info.min_filter = SDL_GPU_FILTER_LINEAR;
sampler_info.mag_filter = SDL_GPU_FILTER_LINEAR;
self.sampler = unsafe { SDL_CreateGPUSampler(ctx.device, &sampler_info).as_mut() }
.ok_or(NeHeError::sdl("SDL_CreateGPUSampler"))?;
ctx.copy_pass(|pass|
{
self.texture = pass.load_texture("Data/NeHe.bmp", true, false)?;
self.vtx_buffer = pass.create_buffer(SDL_GPU_BUFFERUSAGE_VERTEX, VERTICES)?;
self.idx_buffer = pass.create_buffer(SDL_GPU_BUFFERUSAGE_INDEX, INDICES)?;
Ok(())
})
}
fn quit(&mut self, ctx: &NeHeContext)
{
unsafe
{
SDL_ReleaseGPUBuffer(ctx.device, self.idx_buffer);
SDL_ReleaseGPUBuffer(ctx.device, self.vtx_buffer);
SDL_ReleaseGPUTexture(ctx.device, self.texture);
SDL_ReleaseGPUSampler(ctx.device, self.sampler);
SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.pso);
}
}
fn resize(&mut self, _ctx: &NeHeContext, width: i32, height: i32)
{
let aspect = width as f32 / max(height, 1) as f32;
self.projection = Mtx::perspective(45.0, aspect, 0.1, 100.0);
}
fn draw(&mut self, ctx: &NeHeContext, cmd: *mut SDL_GPUCommandBuffer, swapchain: *mut SDL_GPUTexture)
{
let mut color_info = SDL_GPUColorTargetInfo::default();
color_info.texture = swapchain;
color_info.clear_color = SDL_FColor { r: 0.0, g: 0.0, b: 0.0, a: 0.5 };
color_info.load_op = SDL_GPU_LOADOP_CLEAR;
color_info.store_op = SDL_GPU_STOREOP_STORE;
let mut depth_info = SDL_GPUDepthStencilTargetInfo::default();
depth_info.texture = ctx.depth_texture;
depth_info.clear_depth = 1.0;
depth_info.load_op = SDL_GPU_LOADOP_CLEAR;
depth_info.store_op = SDL_GPU_STOREOP_DONT_CARE;
depth_info.stencil_load_op = SDL_GPU_LOADOP_DONT_CARE;
depth_info.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE;
depth_info.cycle = true;
unsafe
{
// Begin pass & bind pipeline state
let pass = SDL_BeginGPURenderPass(cmd, &color_info, 1, &depth_info);
SDL_BindGPUGraphicsPipeline(pass, self.pso);
// Bind texture
let texture_binding = SDL_GPUTextureSamplerBinding { texture: self.texture, sampler: self.sampler };
SDL_BindGPUFragmentSamplers(pass, 0, &texture_binding, 1);
// Bind vertex & index buffers
let vtx_binding = SDL_GPUBufferBinding { buffer: self.vtx_buffer, offset: 0 };
let idx_binding = SDL_GPUBufferBinding { buffer: self.idx_buffer, offset: 0 };
SDL_BindGPUVertexBuffers(pass, 0, &vtx_binding, 1);
SDL_BindGPUIndexBuffer(pass, &idx_binding, SDL_GPU_INDEXELEMENTSIZE_16BIT);
// Move cube 5 units into the screen and apply some rotations
let mut model = Mtx::translation(0.0, 0.0, -5.0);
model.rotate(self.rot.0, 1.0, 0.0, 0.0);
model.rotate(self.rot.1, 0.0, 1.0, 0.0);
model.rotate(self.rot.2, 0.0, 0.0, 1.0);
#[allow(dead_code)]
struct Uniforms { model_view_proj: Mtx, color: [f32; 4] }
// Push shader uniforms
let u = Uniforms { model_view_proj: self.projection * model, color: [1.0; 4] };
SDL_PushGPUVertexUniformData(cmd, 0, addr_of!(u) as *const c_void, size_of::<Uniforms>() as u32);
// Draw the textured cube
SDL_DrawGPUIndexedPrimitives(pass, INDICES.len() as u32, 1, 0, 0, 0);
SDL_EndGPURenderPass(pass);
}
self.rot.0 += 0.3;
self.rot.1 += 0.2;
self.rot.2 += 0.4;
}
}
pub fn main() -> Result<ExitCode, Box<dyn std::error::Error>>
{
run::<Lesson6>()?;
Ok(ExitCode::SUCCESS)
}

381
src/rust/lesson7.rs Normal file
View File

@@ -0,0 +1,381 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
use nehe::application::config::AppImplementation;
use nehe::application::run;
use nehe::context::NeHeContext;
use nehe::error::NeHeError;
use nehe::matrix::Mtx;
use sdl3_sys::gpu::*;
use sdl3_sys::keyboard::SDL_GetKeyboardState;
use sdl3_sys::keycode::{SDL_Keycode, SDLK_F, SDLK_L};
use sdl3_sys::pixels::SDL_FColor;
use sdl3_sys::scancode::{SDL_SCANCODE_DOWN, SDL_SCANCODE_LEFT, SDL_SCANCODE_PAGEDOWN, SDL_SCANCODE_PAGEUP, SDL_SCANCODE_RIGHT, SDL_SCANCODE_UP};
use std::cmp::max;
use std::ffi::c_void;
use std::mem::offset_of;
use std::process::ExitCode;
use std::ptr::{addr_of, null_mut};
#[repr(C)]
struct Vertex
{
x: f32, y: f32, z: f32,
nx: f32, ny: f32, nz: f32,
u: f32, v: f32,
}
const VERTICES: &'static [Vertex] =
&[
// Front Face
Vertex { x: -1.0, y: -1.0, z: 1.0, nx: 0.0, ny: 0.0, nz: 1.0, u: 0.0, v: 0.0 },
Vertex { x: 1.0, y: -1.0, z: 1.0, nx: 0.0, ny: 0.0, nz: 1.0, u: 1.0, v: 0.0 },
Vertex { x: 1.0, y: 1.0, z: 1.0, nx: 0.0, ny: 0.0, nz: 1.0, u: 1.0, v: 1.0 },
Vertex { x: -1.0, y: 1.0, z: 1.0, nx: 0.0, ny: 0.0, nz: 1.0, u: 0.0, v: 1.0 },
// Back Face
Vertex { x: -1.0, y: -1.0, z: -1.0, nx: 0.0, ny: 0.0, nz: -1.0, u: 1.0, v: 0.0 },
Vertex { x: -1.0, y: 1.0, z: -1.0, nx: 0.0, ny: 0.0, nz: -1.0, u: 1.0, v: 1.0 },
Vertex { x: 1.0, y: 1.0, z: -1.0, nx: 0.0, ny: 0.0, nz: -1.0, u: 0.0, v: 1.0 },
Vertex { x: 1.0, y: -1.0, z: -1.0, nx: 0.0, ny: 0.0, nz: -1.0, u: 0.0, v: 0.0 },
// Top Face
Vertex { x: -1.0, y: 1.0, z: -1.0, nx: 0.0, ny: 1.0, nz: 0.0, u: 0.0, v: 1.0 },
Vertex { x: -1.0, y: 1.0, z: 1.0, nx: 0.0, ny: 1.0, nz: 0.0, u: 0.0, v: 0.0 },
Vertex { x: 1.0, y: 1.0, z: 1.0, nx: 0.0, ny: 1.0, nz: 0.0, u: 1.0, v: 0.0 },
Vertex { x: 1.0, y: 1.0, z: -1.0, nx: 0.0, ny: 1.0, nz: 0.0, u: 1.0, v: 1.0 },
// Bottom Face
Vertex { x: -1.0, y: -1.0, z: -1.0, nx: 0.0, ny: -1.0, nz: 0.0, u: 1.0, v: 1.0 },
Vertex { x: 1.0, y: -1.0, z: -1.0, nx: 0.0, ny: -1.0, nz: 0.0, u: 0.0, v: 1.0 },
Vertex { x: 1.0, y: -1.0, z: 1.0, nx: 0.0, ny: -1.0, nz: 0.0, u: 0.0, v: 0.0 },
Vertex { x: -1.0, y: -1.0, z: 1.0, nx: 0.0, ny: -1.0, nz: 0.0, u: 1.0, v: 0.0 },
// Right face
Vertex { x: 1.0, y: -1.0, z: -1.0, nx: 1.0, ny: 0.0, nz: 0.0, u: 1.0, v: 0.0 },
Vertex { x: 1.0, y: 1.0, z: -1.0, nx: 1.0, ny: 0.0, nz: 0.0, u: 1.0, v: 1.0 },
Vertex { x: 1.0, y: 1.0, z: 1.0, nx: 1.0, ny: 0.0, nz: 0.0, u: 0.0, v: 1.0 },
Vertex { x: 1.0, y: -1.0, z: 1.0, nx: 1.0, ny: 0.0, nz: 0.0, u: 0.0, v: 0.0 },
// Left Face
Vertex { x: -1.0, y: -1.0, z: -1.0, nx: -1.0, ny: 0.0, nz: 0.0, u: 0.0, v: 0.0 },
Vertex { x: -1.0, y: -1.0, z: 1.0, nx: -1.0, ny: 0.0, nz: 0.0, u: 1.0, v: 0.0 },
Vertex { x: -1.0, y: 1.0, z: 1.0, nx: -1.0, ny: 0.0, nz: 0.0, u: 1.0, v: 1.0 },
Vertex { x: -1.0, y: 1.0, z: -1.0, nx: -1.0, ny: 0.0, nz: 0.0, u: 0.0, v: 1.0 },
];
const INDICES: &'static [u16] =
&[
0, 1, 2, 2, 3, 0, // Front
4, 5, 6, 6, 7, 4, // Back
8, 9, 10, 10, 11, 8, // Top
12, 13, 14, 14, 15, 12, // Bottom
16, 17, 18, 18, 19, 16, // Right
20, 21, 22, 22, 23, 20 // Left
];
#[allow(dead_code)]
struct Light
{
ambient: (f32, f32, f32, f32),
diffuse: (f32, f32, f32, f32),
position: (f32, f32, f32, f32),
}
struct Lesson7
{
pso_unlit: *mut SDL_GPUGraphicsPipeline,
pso_light: *mut SDL_GPUGraphicsPipeline,
vtx_buffer: *mut SDL_GPUBuffer,
idx_buffer: *mut SDL_GPUBuffer,
samplers: [*mut SDL_GPUSampler; 3],
texture: *mut SDL_GPUTexture,
projection: Mtx,
lighting: bool,
light: Light,
filter: usize,
rot: (f32, f32),
speed: (f32, f32),
z: f32,
}
impl Default for Lesson7
{
fn default() -> Self
{
Self
{
pso_unlit: null_mut(),
pso_light: null_mut(),
vtx_buffer: null_mut(),
idx_buffer: null_mut(),
samplers: [null_mut(); 3],
texture: null_mut(),
projection: Mtx::IDENTITY,
lighting: false,
light: Light
{
ambient: (0.5, 0.5, 0.5, 1.0),
diffuse: (1.0, 1.0, 1.0, 1.0),
position: (0.0, 0.0, 2.0, 1.0),
},
filter: 0,
rot: (0.0, 0.0),
speed: (0.0, 0.0),
z: -5.0,
}
}
}
impl AppImplementation for Lesson7
{
const TITLE: &'static str = "NeHe's Textures, Lighting & Keyboard Tutorial";
const WIDTH: i32 = 640;
const HEIGHT: i32 = 480;
const CREATE_DEPTH_BUFFER: SDL_GPUTextureFormat = SDL_GPU_TEXTUREFORMAT_D16_UNORM;
fn init(&mut self, ctx: &NeHeContext) -> Result<(), NeHeError>
{
let (vertex_shader_unlit, fragment_shader_unlit) = ctx.load_shaders("lesson6", 1, 0, 1)?;
let (vertex_shader_light, fragment_shader_light) = ctx.load_shaders("lesson7", 2, 0, 1)?;
const VERTEX_DESCRIPTIONS: &'static [SDL_GPUVertexBufferDescription] =
&[
SDL_GPUVertexBufferDescription
{
slot: 0,
pitch: size_of::<Vertex>() as u32,
input_rate: SDL_GPU_VERTEXINPUTRATE_VERTEX,
instance_step_rate: 0,
},
];
const VERTEX_ATTRIBS: &'static [SDL_GPUVertexAttribute] =
&[
SDL_GPUVertexAttribute
{
location: 0,
buffer_slot: 0,
format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3,
offset: offset_of!(Vertex, x) as u32,
},
SDL_GPUVertexAttribute
{
location: 2,
buffer_slot: 0,
format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3,
offset: offset_of!(Vertex, nx) as u32,
},
SDL_GPUVertexAttribute
{
location: 1,
buffer_slot: 0,
format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2,
offset: offset_of!(Vertex, u) as u32,
},
];
let mut info = SDL_GPUGraphicsPipelineCreateInfo::default();
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
info.vertex_input_state = SDL_GPUVertexInputState
{
vertex_buffer_descriptions: VERTEX_DESCRIPTIONS.as_ptr(),
num_vertex_buffers: VERTEX_DESCRIPTIONS.len() as u32,
vertex_attributes: VERTEX_ATTRIBS.as_ptr(),
num_vertex_attributes: VERTEX_ATTRIBS.len() as u32,
};
info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE;
info.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
let colour_targets: &[SDL_GPUColorTargetDescription] =
&[
SDL_GPUColorTargetDescription
{
format: unsafe { SDL_GetGPUSwapchainTextureFormat(ctx.device, ctx.window) },
blend_state: SDL_GPUColorTargetBlendState::default(),
}
];
info.target_info.color_target_descriptions = colour_targets.as_ptr();
info.target_info.num_color_targets = colour_targets.len() as u32;
info.target_info.depth_stencil_format = SDL_GPU_TEXTUREFORMAT_D16_UNORM;
info.target_info.has_depth_stencil_target = true;
info.depth_stencil_state.compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL;
info.depth_stencil_state.enable_depth_test = true;
info.depth_stencil_state.enable_depth_write = true;
// Create unlit pipeline
info.vertex_shader = vertex_shader_unlit;
info.fragment_shader = fragment_shader_unlit;
self.pso_unlit = unsafe { SDL_CreateGPUGraphicsPipeline(ctx.device, &info) };
unsafe
{
SDL_ReleaseGPUShader(ctx.device, fragment_shader_unlit);
SDL_ReleaseGPUShader(ctx.device, vertex_shader_unlit);
}
if self.pso_unlit.is_null()
{
let err = NeHeError::sdl("SDL_CreateGPUGraphicsPipeline");
unsafe
{
SDL_ReleaseGPUShader(ctx.device, fragment_shader_light);
SDL_ReleaseGPUShader(ctx.device, vertex_shader_light);
}
return Err(err);
}
// Create lit pipeline
info.vertex_shader = vertex_shader_light;
info.fragment_shader = fragment_shader_light;
self.pso_light = unsafe { SDL_CreateGPUGraphicsPipeline(ctx.device, &info) };
unsafe
{
SDL_ReleaseGPUShader(ctx.device, fragment_shader_light);
SDL_ReleaseGPUShader(ctx.device, vertex_shader_light);
}
if self.pso_light.is_null()
{
return Err(NeHeError::sdl("SDL_CreateGPUGraphicsPipeline"));
}
// Create texture samplers
let create_sampler = |filter: SDL_GPUFilter, enable_mip: bool| -> Result<&mut SDL_GPUSampler, NeHeError>
{
let mut sampler_info: SDL_GPUSamplerCreateInfo = unsafe { std::mem::zeroed() };
sampler_info.min_filter = filter;
sampler_info.mag_filter = filter;
sampler_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
sampler_info.max_lod = if enable_mip { f32::MAX } else { 0.0 };
unsafe { SDL_CreateGPUSampler(ctx.device, &sampler_info).as_mut() }
.ok_or(NeHeError::sdl("SDL_CreateGPUSampler"))
};
self.samplers[0] = create_sampler(SDL_GPU_FILTER_NEAREST, false)?;
self.samplers[1] = create_sampler(SDL_GPU_FILTER_LINEAR, false)?;
self.samplers[2] = create_sampler(SDL_GPU_FILTER_LINEAR, true)?;
ctx.copy_pass(|pass|
{
self.texture = pass.load_texture("Data/Crate.bmp", true, true)?;
self.vtx_buffer = pass.create_buffer(SDL_GPU_BUFFERUSAGE_VERTEX, VERTICES)?;
self.idx_buffer = pass.create_buffer(SDL_GPU_BUFFERUSAGE_INDEX, INDICES)?;
Ok(())
})
}
fn quit(&mut self, ctx: &NeHeContext)
{
unsafe
{
SDL_ReleaseGPUBuffer(ctx.device, self.idx_buffer);
SDL_ReleaseGPUBuffer(ctx.device, self.vtx_buffer);
SDL_ReleaseGPUTexture(ctx.device, self.texture);
self.samplers.iter().rev().for_each(|sampler| SDL_ReleaseGPUSampler(ctx.device, *sampler));
SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.pso_light);
SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.pso_unlit);
}
}
fn resize(&mut self, _ctx: &NeHeContext, width: i32, height: i32)
{
let aspect = width as f32 / max(height, 1) as f32;
self.projection = Mtx::perspective(45.0, aspect, 0.1, 100.0);
}
fn draw(&mut self, ctx: &NeHeContext, cmd: *mut SDL_GPUCommandBuffer, swapchain: *mut SDL_GPUTexture)
{
let mut color_info = SDL_GPUColorTargetInfo::default();
color_info.texture = swapchain;
color_info.clear_color = SDL_FColor { r: 0.0, g: 0.0, b: 0.0, a: 0.5 };
color_info.load_op = SDL_GPU_LOADOP_CLEAR;
color_info.store_op = SDL_GPU_STOREOP_STORE;
let mut depth_info = SDL_GPUDepthStencilTargetInfo::default();
depth_info.texture = ctx.depth_texture;
depth_info.clear_depth = 1.0;
depth_info.load_op = SDL_GPU_LOADOP_CLEAR;
depth_info.store_op = SDL_GPU_STOREOP_DONT_CARE;
depth_info.stencil_load_op = SDL_GPU_LOADOP_DONT_CARE;
depth_info.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE;
depth_info.cycle = true;
unsafe
{
// Begin pass & bind pipeline state
let pass = SDL_BeginGPURenderPass(cmd, &color_info, 1, &depth_info);
SDL_BindGPUGraphicsPipeline(pass, if self.lighting { self.pso_light } else { self.pso_unlit });
// Bind texture
let texture_binding = SDL_GPUTextureSamplerBinding
{
texture: self.texture,
sampler: self.samplers[self.filter],
};
SDL_BindGPUFragmentSamplers(pass, 0, &texture_binding, 1);
// Bind vertex & index buffers
let vtx_binding = SDL_GPUBufferBinding { buffer: self.vtx_buffer, offset: 0 };
let idx_binding = SDL_GPUBufferBinding { buffer: self.idx_buffer, offset: 0 };
SDL_BindGPUVertexBuffers(pass, 0, &vtx_binding, 1);
SDL_BindGPUIndexBuffer(pass, &idx_binding, SDL_GPU_INDEXELEMENTSIZE_16BIT);
// Setup the cube's model matrix
let mut model = Mtx::translation(0.0, 0.0, self.z);
model.rotate(self.rot.0, 1.0, 0.0, 0.0);
model.rotate(self.rot.1, 0.0, 1.0, 0.0);
// Push shader uniforms
if self.lighting
{
#[allow(dead_code)]
struct Uniforms { model: Mtx, projection: Mtx }
let u = Uniforms { model, projection: self.projection };
SDL_PushGPUVertexUniformData(cmd, 0, addr_of!(u) as *const c_void, size_of::<Uniforms>() as u32);
SDL_PushGPUVertexUniformData(cmd, 1, addr_of!(self.light) as *const c_void, size_of::<Light>() as u32);
}
else
{
#[allow(dead_code)]
struct Uniforms { model_view_proj: Mtx, color: [f32; 4] }
let u = Uniforms { model_view_proj: self.projection * model, color: [1.0; 4] };
SDL_PushGPUVertexUniformData(cmd, 0, addr_of!(u) as *const c_void, size_of::<Uniforms>() as u32);
}
// Draw the textured cube
SDL_DrawGPUIndexedPrimitives(pass, INDICES.len() as u32, 1, 0, 0, 0);
SDL_EndGPURenderPass(pass);
}
let keys = unsafe
{
let mut numkeys: std::ffi::c_int = 0;
let keys = SDL_GetKeyboardState(&mut numkeys);
std::slice::from_raw_parts(keys, numkeys as usize)
};
if keys[SDL_SCANCODE_PAGEUP.0 as usize] { self.z -= 0.02 }
if keys[SDL_SCANCODE_PAGEDOWN.0 as usize] { self.z += 0.02; }
if keys[SDL_SCANCODE_UP.0 as usize] { self.speed.0 -= 0.01; }
if keys[SDL_SCANCODE_DOWN.0 as usize] { self.speed.0 += 0.01; }
if keys[SDL_SCANCODE_RIGHT.0 as usize] { self.speed.1 += 0.1; }
if keys[SDL_SCANCODE_LEFT.0 as usize] { self.speed.1 -= 0.1; }
self.rot.0 += self.speed.0;
self.rot.1 += self.speed.1;
}
fn key(&mut self, _ctx: &NeHeContext, key: SDL_Keycode, down: bool, _repeat: bool)
{
match key
{
SDLK_L if down => self.lighting = !self.lighting,
SDLK_F if down => self.filter = (self.filter + 1) % self.samplers.len(),
_ => ()
}
}
}
pub fn main() -> Result<ExitCode, Box<dyn std::error::Error>>
{
run::<Lesson7>()?;
Ok(ExitCode::SUCCESS)
}

View File

@@ -0,0 +1,107 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
pub mod config;
use crate::context::NeHeContext;
use crate::application::config::AppImplementation;
use crate::error::NeHeError;
use sdl3_sys::events::*;
use sdl3_sys::gpu::*;
use sdl3_sys::init::{SDL_Init, SDL_Quit, SDL_INIT_VIDEO};
use sdl3_sys::keycode::{SDLK_ESCAPE, SDLK_F1};
use std::mem::MaybeUninit;
use std::ptr::addr_of_mut;
use sdl3_sys::video::{SDL_GetWindowSizeInPixels, SDL_SetWindowFullscreen};
pub fn run<App: AppImplementation + Default + 'static>() -> Result<(), NeHeError>
{
// Initialise SDL
if !unsafe { SDL_Init(SDL_INIT_VIDEO) }
{
return Err(NeHeError::sdl("SDL_Init"))
}
// Initialise GPU context
let mut ctx = NeHeContext::init(App::TITLE, App::WIDTH, App::HEIGHT)?;
// Handle depth buffer texture creation if requested
if App::CREATE_DEPTH_BUFFER != SDL_GPU_TEXTUREFORMAT_INVALID
{
let (mut backbuf_width, mut backbuf_height) = (0, 0);
unsafe { SDL_GetWindowSizeInPixels(ctx.window, &mut backbuf_width, &mut backbuf_height) };
ctx.setup_depth_texture(backbuf_width as u32, backbuf_height as u32, App::CREATE_DEPTH_BUFFER, 1.0)?;
}
// Start application
let mut app = App::default();
app.init(&ctx)?;
let mut fullscreen = false;
'quit: loop
{
unsafe
{
let mut event: SDL_Event = std::mem::zeroed();
while SDL_PollEvent(addr_of_mut!(event))
{
match SDL_EventType(event.r#type)
{
SDL_EVENT_QUIT => break 'quit,
SDL_EVENT_WINDOW_ENTER_FULLSCREEN => fullscreen = true,
SDL_EVENT_WINDOW_LEAVE_FULLSCREEN => fullscreen = false,
SDL_EVENT_KEY_DOWN | SDL_EVENT_KEY_UP => match event.key.key
{
SDLK_ESCAPE if event.key.down => break 'quit,
SDLK_F1 if event.key.down => _ = SDL_SetWindowFullscreen(ctx.window, !fullscreen),
_ => app.key(&ctx, event.key.key, event.key.down, event.key.repeat)
}
SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED => app.resize(&ctx, event.window.data1, event.window.data2),
_ => ()
}
}
}
let cmd = unsafe { SDL_AcquireGPUCommandBuffer(ctx.device) };
if cmd.is_null()
{
return Err(NeHeError::sdl("SDL_AcquireGPUCommandBuffer"));
}
let mut swapchain_texture_out = MaybeUninit::<*mut SDL_GPUTexture>::uninit();
let (mut swapchain_width, mut swapchain_height) = (0, 0);
if !unsafe { SDL_WaitAndAcquireGPUSwapchainTexture(cmd, ctx.window,
swapchain_texture_out.as_mut_ptr(), &mut swapchain_width, &mut swapchain_height) }
{
let err = NeHeError::sdl("SDL_WaitAndAcquireGPUSwapchainTexture");
unsafe { SDL_CancelGPUCommandBuffer(cmd); }
return Err(err);
}
let swapchain_texture = unsafe { swapchain_texture_out.assume_init() };
if swapchain_texture.is_null()
{
unsafe { SDL_CancelGPUCommandBuffer(cmd); }
continue;
}
if App::CREATE_DEPTH_BUFFER != SDL_GPU_TEXTUREFORMAT_INVALID
&& !ctx.depth_texture.is_null()
&& ctx.depth_texture_size != (swapchain_width, swapchain_height)
{
ctx.setup_depth_texture(swapchain_width, swapchain_height, App::CREATE_DEPTH_BUFFER, 1.0)?;
}
app.draw(&ctx, cmd, swapchain_texture);
unsafe { SDL_SubmitGPUCommandBuffer(cmd); }
}
// Cleanup & quit
app.quit(&ctx);
drop(ctx);
unsafe { SDL_Quit() };
Ok(())
}

View File

@@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
use sdl3_sys::gpu::{SDL_GPUCommandBuffer, SDL_GPUTexture, SDL_GPUTextureFormat, SDL_GPU_TEXTUREFORMAT_INVALID};
use sdl3_sys::keycode::SDL_Keycode;
use crate::context::NeHeContext;
use crate::error::NeHeError;
#[allow(unused_variables)]
pub trait AppImplementation
{
const TITLE: &'static str;
const WIDTH: i32;
const HEIGHT: i32;
const CREATE_DEPTH_BUFFER: SDL_GPUTextureFormat = SDL_GPU_TEXTUREFORMAT_INVALID;
fn init(&mut self, ctx: &NeHeContext) -> Result<(), NeHeError> { Ok(()) }
fn quit(&mut self, ctx: &NeHeContext) {}
fn resize(&mut self, ctx: &NeHeContext, width: i32, height: i32) {}
fn draw(&mut self, ctx: &NeHeContext, cmd: *mut SDL_GPUCommandBuffer, swapchain: *mut SDL_GPUTexture);
fn key(&mut self, ctx: &NeHeContext, key: SDL_Keycode, down: bool, repeat: bool) {}
}

259
src/rust/nehe/context.rs Normal file
View File

@@ -0,0 +1,259 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
mod copypass;
use crate::context::copypass::NeHeCopyPass;
use crate::error::NeHeError;
use sdl3_sys::filesystem::SDL_GetBasePath;
use sdl3_sys::gpu::*;
use sdl3_sys::properties::{SDL_CreateProperties, SDL_DestroyProperties, SDL_SetFloatProperty};
use sdl3_sys::video::*;
use std::ffi::{CStr, CString};
use std::fs;
use std::path::{Path, PathBuf};
use std::ptr::{null, null_mut};
use std::str::from_utf8;
pub struct NeHeContext
{
pub window: *mut SDL_Window,
pub device: *mut SDL_GPUDevice,
pub depth_texture: *mut SDL_GPUTexture,
pub depth_texture_size: (u32, u32)
}
impl NeHeContext
{
#[allow(unsafe_op_in_unsafe_fn)]
pub(in crate) fn init(title: &str, w: i32, h: i32) -> Result<Self, NeHeError>
{
// Create window
let flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY;
let window = unsafe
{
let title = CString::new(title).unwrap();
SDL_CreateWindow(title.as_ptr(), w, h, flags)
};
if window.is_null()
{
return Err(NeHeError::sdl("SDL_CreateWindow"));
}
// Open GPU device
let formats = SDL_GPU_SHADERFORMAT_METALLIB | SDL_GPU_SHADERFORMAT_MSL |
SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_DXIL;
let Some(device) = (unsafe { SDL_CreateGPUDevice(formats, true, null()).as_mut() }) else
{
return Err(NeHeError::sdl("SDL_CreateGPUDevice"));
};
// Attach window to the GPU device
if !unsafe { SDL_ClaimWindowForGPUDevice(device, window) }
{
return Err(NeHeError::sdl("SDL_ClaimWindowForGPUDevice"));
}
// Enable VSync
unsafe
{
SDL_SetGPUSwapchainParameters(device, window,
SDL_GPU_SWAPCHAINCOMPOSITION_SDR,
SDL_GPU_PRESENTMODE_VSYNC);
}
Ok(Self
{
window, device,
depth_texture: null_mut(),
depth_texture_size: (0, 0),
})
}
}
impl Drop for NeHeContext
{
fn drop(&mut self)
{
unsafe
{
if !self.depth_texture.is_null()
{
SDL_ReleaseGPUTexture(self.device, self.depth_texture);
}
SDL_ReleaseWindowFromGPUDevice(self.device, self.window);
SDL_DestroyGPUDevice(self.device);
SDL_DestroyWindow(self.window);
}
}
}
#[allow(dead_code)]
impl NeHeContext
{
pub fn setup_depth_texture(&mut self, width: u32, height: u32, format: SDL_GPUTextureFormat, clear_depth: f32) -> Result<(), NeHeError>
{
if !self.depth_texture.is_null()
{
unsafe { SDL_ReleaseGPUTexture(self.device, self.depth_texture) };
self.depth_texture = null_mut();
}
let props = unsafe { SDL_CreateProperties() };
if props == 0
{
return Err(NeHeError::sdl("SDL_CreateProperties"));
}
// Workaround for https://github.com/libsdl-org/SDL/issues/10758
unsafe { SDL_SetFloatProperty(props, SDL_PROP_GPU_TEXTURE_CREATE_D3D12_CLEAR_DEPTH_FLOAT, clear_depth) };
let info = SDL_GPUTextureCreateInfo
{
r#type: SDL_GPU_TEXTURETYPE_2D,
format,
usage: SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET,
width,
height,
layer_count_or_depth: 1,
num_levels: 1,
sample_count: SDL_GPU_SAMPLECOUNT_1,
props,
};
let texture = unsafe { SDL_CreateGPUTexture(self.device, &info) };
unsafe { SDL_DestroyProperties(props) };
if texture.is_null()
{
return Err(NeHeError::sdl("SDL_CreateGPUTexture"));
}
self.depth_texture = texture;
self.depth_texture_size = (width, height);
Ok(())
}
pub fn load_shaders(&self, name: &str,
vertex_uniforms: u32,
vertex_storage: u32,
fragment_samplers: u32)
-> Result<(*mut SDL_GPUShader, *mut SDL_GPUShader), NeHeError>
{
let base = unsafe { CStr::from_ptr(SDL_GetBasePath()) };
let path = Path::new(from_utf8(base.to_bytes()).unwrap())
.join("Data/Shaders").join(name);
let mut info = ShaderProgramCreateInfo
{
format: SDL_GPU_SHADERFORMAT_INVALID,
vertex_uniforms, vertex_storage, fragment_samplers
};
let available_formats = unsafe { SDL_GetGPUShaderFormats(self.device) };
if available_formats & (SDL_GPU_SHADERFORMAT_METALLIB | SDL_GPU_SHADERFORMAT_MSL) != 0
{
if available_formats & SDL_GPU_SHADERFORMAT_METALLIB == SDL_GPU_SHADERFORMAT_METALLIB
{
if let Ok(lib) = fs::read(path.appending_ext("metallib"))
{
info.format = SDL_GPU_SHADERFORMAT_METALLIB;
return Ok((
self.load_shader_blob(&lib, &info, SDL_GPU_SHADERSTAGE_VERTEX, "VertexMain")?,
self.load_shader_blob(&lib, &info, SDL_GPU_SHADERSTAGE_FRAGMENT, "FragmentMain")?
));
}
}
if available_formats & SDL_GPU_SHADERFORMAT_MSL == SDL_GPU_SHADERFORMAT_MSL
{
let src = fs::read(path.appending_ext("metal"))?;
info.format = SDL_GPU_SHADERFORMAT_MSL;
return Ok((
self.load_shader_blob(&src, &info, SDL_GPU_SHADERSTAGE_VERTEX, "VertexMain")?,
self.load_shader_blob(&src, &info, SDL_GPU_SHADERSTAGE_FRAGMENT, "FragmentMain")?
));
}
unreachable!();
}
else if available_formats & SDL_GPU_SHADERFORMAT_SPIRV == SDL_GPU_SHADERFORMAT_SPIRV
{
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
Ok((
self.load_shader(path.appending_ext("vtx.spv"), &info, SDL_GPU_SHADERSTAGE_VERTEX, "main")?,
self.load_shader(path.appending_ext("frg.spv"), &info, SDL_GPU_SHADERSTAGE_FRAGMENT, "main")?
))
}
else if available_formats & SDL_GPU_SHADERFORMAT_DXIL == SDL_GPU_SHADERFORMAT_DXIL
{
info.format = SDL_GPU_SHADERFORMAT_DXIL;
Ok((
self.load_shader(path.appending_ext("vtx.dxb"), &info, SDL_GPU_SHADERSTAGE_VERTEX, "VertexMain")?,
self.load_shader(path.appending_ext("pxl.dxb"), &info, SDL_GPU_SHADERSTAGE_FRAGMENT, "PixelMain")?
))
}
else { Err(NeHeError::Fatal("No supported shader formats")) }
}
fn load_shader(&self, filepath: PathBuf,
info: &ShaderProgramCreateInfo, stage: SDL_GPUShaderStage,
main: &str) -> Result<&mut SDL_GPUShader, NeHeError>
{
let data = fs::read(filepath)?;
self.load_shader_blob(&data, info, stage, main)
}
fn load_shader_blob(&self, code: &Vec<u8>,
info: &ShaderProgramCreateInfo, stage: SDL_GPUShaderStage,
main: &str) -> Result<&mut SDL_GPUShader, NeHeError>
{
let Ok(entrypoint) = CString::new(main) else
{
return Err(NeHeError::Fatal("Null dereference when converting entrypoint string"));
};
let info = SDL_GPUShaderCreateInfo
{
code_size: code.len(),
code: code.as_ptr(),
entrypoint: entrypoint.as_ptr(),
format: info.format,
stage,
num_samplers: if stage == SDL_GPU_SHADERSTAGE_FRAGMENT { info.fragment_samplers } else { 0 },
num_storage_textures: 0,
num_storage_buffers: if stage == SDL_GPU_SHADERSTAGE_VERTEX { info.vertex_storage } else { 0 },
num_uniform_buffers: if stage == SDL_GPU_SHADERSTAGE_VERTEX { info.vertex_uniforms } else { 0 },
props: 0,
};
unsafe { SDL_CreateGPUShader(self.device, &info).as_mut() }
.ok_or(NeHeError::sdl("SDL_CreateGPUShader"))
}
pub fn copy_pass(&self, lambda: impl FnOnce(&mut NeHeCopyPass) -> Result<(), NeHeError>) -> Result<(), NeHeError>
{
let mut pass = NeHeCopyPass::new(&self);
lambda(&mut pass)?;
pass.submit()
}
}
struct ShaderProgramCreateInfo
{
format: SDL_GPUShaderFormat,
vertex_uniforms: u32,
vertex_storage: u32,
fragment_samplers: u32,
}
trait PathExt
{
fn appending_ext(&self, ext: &str) -> Self;
}
impl PathExt for PathBuf
{
fn appending_ext(&self, ext: &str) -> PathBuf
{
let mut path = self.as_os_str().to_owned();
path.push(".");
path.push(ext);
path.into()
}
}

View File

@@ -0,0 +1,274 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
use crate::context::NeHeContext;
use crate::error::NeHeError;
use sdl3_sys::filesystem::SDL_GetBasePath;
use sdl3_sys::gpu::*;
use sdl3_sys::pixels::*;
use sdl3_sys::surface::{SDL_ConvertSurface, SDL_DestroySurface, SDL_FlipSurface, SDL_LoadBMP, SDL_Surface, SDL_FLIP_VERTICAL};
use std::cmp::max;
use std::ffi::{CStr, CString};
use std::ptr;
pub struct NeHeCopyPass<'a>
{
ctx: &'a NeHeContext,
copies: Vec<Copy>,
}
impl NeHeCopyPass<'_>
{
pub fn create_buffer<E>(&mut self, usage: SDL_GPUBufferUsageFlags, elements: &[E])
-> Result<*mut SDL_GPUBuffer, NeHeError>
{
let size = (size_of::<E>() * elements.len()) as u32;
// Create data buffer
let info = SDL_GPUBufferCreateInfo { usage, size, props: 0 };
let buffer = unsafe { SDL_CreateGPUBuffer(self.ctx.device, &info).as_mut() }
.ok_or(NeHeError::sdl("SDL_CreateGPUBuffer"))?;
// Create transfer buffer
let xfer_info = SDL_GPUTransferBufferCreateInfo { usage: SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, size, props: 0 };
let Some(transfer_buffer) = (unsafe { SDL_CreateGPUTransferBuffer(self.ctx.device, &xfer_info).as_mut() }) else
{
let err = NeHeError::sdl("SDL_CreateGPUTransferBuffer");
unsafe { SDL_ReleaseGPUBuffer(self.ctx.device, buffer); }
return Err(err);
};
// Map transfer buffer and copy the data
unsafe
{
let map = SDL_MapGPUTransferBuffer(self.ctx.device, transfer_buffer, false);
if map.is_null()
{
let err = NeHeError::sdl("SDL_MapGPUTransferBuffer");
SDL_ReleaseGPUTransferBuffer(self.ctx.device, transfer_buffer);
SDL_ReleaseGPUBuffer(self.ctx.device, buffer);
return Err(err);
}
ptr::copy_nonoverlapping(elements.as_ptr(), map as *mut E, elements.len());
SDL_UnmapGPUTransferBuffer(self.ctx.device, transfer_buffer);
}
self.copies.push(Copy { transfer_buffer, payload: CopyPayload::Buffer { buffer, size } });
Ok(buffer)
}
pub fn load_texture(&mut self, resource_path: &str, flip_vertical: bool, gen_mipmaps: bool)
-> Result<&mut SDL_GPUTexture, NeHeError>
{
// Build path to resource: "{baseDir}/{resourcePath}"
let path = unsafe
{
let mut path = CString::from(CStr::from_ptr(SDL_GetBasePath())).into_bytes();
path.extend_from_slice(resource_path.as_bytes());
CString::from_vec_with_nul_unchecked(path)
};
// Load image into a surface
let image = unsafe { SDL_LoadBMP(path.as_ptr()).as_mut() }
.ok_or(NeHeError::sdl("SDL_LoadBMP"))?;
// Flip surface if requested
if flip_vertical && !unsafe { SDL_FlipSurface(image, SDL_FLIP_VERTICAL) }
{
let err = NeHeError::sdl("SDL_FlipSurface");
unsafe { SDL_DestroySurface(image); }
return Err(err);
}
// Upload texture to the GPU
let result = self.create_texture_from_surface(image, gen_mipmaps);
unsafe { SDL_DestroySurface(image); }
return result;
}
fn create_texture_from_pixels(&mut self, pixels: &[u8], create_info: SDL_GPUTextureCreateInfo, gen_mipmaps: bool)
-> Result<&mut SDL_GPUTexture, NeHeError>
{
assert!(pixels.len() <= u32::MAX as usize);
let texture = unsafe { SDL_CreateGPUTexture(self.ctx.device, &create_info).as_mut() }
.ok_or(NeHeError::sdl("SDL_CreateGPUTexture"))?;
// Create and copy image data to a transfer buffer
let xfer_info = SDL_GPUTransferBufferCreateInfo
{
usage: SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD,
size: pixels.len() as u32,
props: 0
};
let Some(transfer_buffer) = (unsafe { SDL_CreateGPUTransferBuffer(self.ctx.device, &xfer_info).as_mut() }) else
{
let err = NeHeError::sdl("SDL_CreateGPUTransferBuffer");
unsafe { SDL_ReleaseGPUTexture(self.ctx.device, texture); }
return Err(err);
};
// Map transfer buffer and copy the data
unsafe
{
let map = SDL_MapGPUTransferBuffer(self.ctx.device, transfer_buffer, false);
if map.is_null()
{
let err = NeHeError::sdl("SDL_MapGPUTransferBuffer");
SDL_ReleaseGPUTransferBuffer(self.ctx.device, transfer_buffer);
SDL_ReleaseGPUTexture(self.ctx.device, texture);
return Err(err);
}
ptr::copy_nonoverlapping(pixels.as_ptr(), map as *mut u8, pixels.len());
SDL_UnmapGPUTransferBuffer(self.ctx.device, transfer_buffer);
}
self.copies.push(Copy { transfer_buffer, payload: CopyPayload::Texture
{
texture,
size: (create_info.width, create_info.height),
gen_mipmaps
} });
Ok(texture)
}
pub fn create_texture_from_surface(&mut self, surface: &mut SDL_Surface, gen_mipmaps: bool)
-> Result<&mut SDL_GPUTexture, NeHeError>
{
let mut info: SDL_GPUTextureCreateInfo = unsafe { std::mem::zeroed() };
info.r#type = SDL_GPU_TEXTURETYPE_2D;
info.format = SDL_GPU_TEXTUREFORMAT_INVALID;
info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
info.width = surface.w as u32;
info.height = surface.h as u32;
info.layer_count_or_depth = 1;
info.num_levels = 1;
let needs_convert: bool;
(needs_convert, info.format) = match surface.format
{
SDL_PIXELFORMAT_RGBA32 => (false, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM),
SDL_PIXELFORMAT_RGBA64 => (false, SDL_GPU_TEXTUREFORMAT_R16G16B16A16_UNORM),
SDL_PIXELFORMAT_RGB565 => (false, SDL_GPU_TEXTUREFORMAT_B5G6R5_UNORM),
SDL_PIXELFORMAT_ARGB1555 => (false, SDL_GPU_TEXTUREFORMAT_B5G5R5A1_UNORM),
SDL_PIXELFORMAT_BGRA4444 => (false, SDL_GPU_TEXTUREFORMAT_B4G4R4A4_UNORM),
SDL_PIXELFORMAT_BGRA32 => (false, SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM),
SDL_PIXELFORMAT_RGBA64_FLOAT => (false, SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT),
SDL_PIXELFORMAT_RGBA128_FLOAT => (false, SDL_GPU_TEXTUREFORMAT_R32G32B32A32_FLOAT),
_ => (true, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM)
};
if gen_mipmaps
{
info.usage |= SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
info.num_levels = max(info.width, info.height).ilog2() + 1; // floor(log₂(max(𝑤,)) + 1
}
if needs_convert
{
// Convert pixel format if required
let conv = unsafe { SDL_ConvertSurface(surface, SDL_PIXELFORMAT_ABGR8888).as_mut() }
.ok_or(NeHeError::sdl("SDL_ConvertSurface"))?;
let result = self.create_texture_from_pixels(
unsafe { std::slice::from_raw_parts(
conv.pixels as *const u8,
conv.pitch as usize * conv.h as usize) },
info,
gen_mipmaps);
unsafe { SDL_DestroySurface(conv); }
result
}
else
{
self.create_texture_from_pixels(
unsafe { std::slice::from_raw_parts(
surface.pixels as *const u8,
surface.pitch as usize * surface.h as usize) },
info,
gen_mipmaps)
}
}
}
impl NeHeCopyPass<'_>
{
pub(in crate::context) fn new(ctx: &'_ NeHeContext) -> NeHeCopyPass<'_>
{
NeHeCopyPass { ctx, copies: Vec::new() }
}
pub(in crate::context) fn submit(&mut self, ) -> Result<(), NeHeError>
{
let cmd = unsafe { SDL_AcquireGPUCommandBuffer(self.ctx.device).as_mut() }
.ok_or(NeHeError::sdl("SDL_AcquireGPUCommandBuffer"))?;
// Begin the copy pass
let pass = unsafe { SDL_BeginGPUCopyPass(cmd) };
// Upload data into GPU buffer(s)
for copy in self.copies.iter()
{
match copy.payload
{
CopyPayload::Buffer { buffer, size } =>
{
let source = SDL_GPUTransferBufferLocation { transfer_buffer: copy.transfer_buffer, offset: 0 };
let destination = SDL_GPUBufferRegion { buffer, offset: 0, size };
unsafe { SDL_UploadToGPUBuffer(pass, &source, &destination, false); }
}
CopyPayload::Texture { texture, size, .. } =>
{
let mut source: SDL_GPUTextureTransferInfo = unsafe { std::mem::zeroed() };
source.transfer_buffer = copy.transfer_buffer;
source.offset = 0;
let mut destination: SDL_GPUTextureRegion = unsafe { std::mem::zeroed() };
destination.texture = texture;
destination.w = size.0;
destination.h = size.1;
destination.d = 1; // info.layer_count_or_depth
unsafe { SDL_UploadToGPUTexture(pass, &source, &destination, false); }
}
}
}
// End the copy pass
unsafe { SDL_EndGPUCopyPass(pass); }
// Generate mipmaps if needed
self.copies.iter().for_each(|copy| match copy.payload
{
CopyPayload::Texture { texture, gen_mipmaps, .. } if gen_mipmaps
=> unsafe { SDL_GenerateMipmapsForGPUTexture(cmd, texture) },
_ => ()
});
// Submit the command buffer
unsafe { SDL_SubmitGPUCommandBuffer(cmd); }
Ok(())
}
}
impl Drop for NeHeCopyPass<'_>
{
fn drop(&mut self)
{
for copy in self.copies.iter().rev()
{
unsafe { SDL_ReleaseGPUTransferBuffer(self.ctx.device, copy.transfer_buffer) };
}
}
}
struct Copy
{
transfer_buffer: *mut SDL_GPUTransferBuffer,
payload: CopyPayload,
}
enum CopyPayload
{
Buffer { buffer: *mut SDL_GPUBuffer, size: u32 },
Texture { texture: *mut SDL_GPUTexture, size: (u32, u32), gen_mipmaps: bool },
}

45
src/rust/nehe/error.rs Normal file
View File

@@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
use std::{fmt, io};
use std::error::Error;
use std::ffi::CStr;
use sdl3_sys::error::SDL_GetError;
#[derive(Debug)]
pub enum NeHeError
{
Fatal(&'static str),
SDLError(&'static str, &'static CStr),
IOError(io::Error),
}
impl NeHeError
{
pub fn sdl(fname: &'static str) -> Self
{
unsafe { NeHeError::SDLError(fname, CStr::from_ptr(SDL_GetError())) }
}
}
impl fmt::Display for NeHeError
{
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result
{
match self
{
NeHeError::Fatal(msg) => fmt.write_str(msg),
NeHeError::SDLError(a, b) => write!(fmt, "{}: {}", a, b.to_string_lossy()),
NeHeError::IOError(e) => write!(fmt, "std::io: {}", e),
}
}
}
impl From<io::Error> for NeHeError
{
fn from(err: io::Error) -> Self { Self::IOError(err) }
}
impl Error for NeHeError {}

4
src/rust/nehe/lib.rs Normal file
View File

@@ -0,0 +1,4 @@
pub mod application;
pub mod context;
pub mod error;
pub mod matrix;

158
src/rust/nehe/matrix.rs Normal file
View File

@@ -0,0 +1,158 @@
/*
* SPDX-FileCopyrightText: (C) 2025 a dinosaur
* SPDX-License-Identifier: Zlib
*/
use core::ops::Mul;
#[derive(PartialEq, Clone, Copy)]
pub struct Mtx([f32; 16]);
#[allow(dead_code)]
impl Mtx
{
pub(crate) const fn new(
m00: f32, m01: f32, m02: f32, m03: f32,
m10: f32, m11: f32, m12: f32, m13: f32,
m20: f32, m21: f32, m22: f32, m23: f32,
m30: f32, m31: f32, m32: f32, m33: f32) -> Self
{
Self([
m00, m01, m02, m03,
m10, m11, m12, m13,
m20, m21, m22, m23,
m30, m31, m32, m33,
])
}
pub const IDENTITY: Self = Self::new(
0.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0);
pub const fn translation(x: f32, y: f32, z: f32) -> Self
{
Self::new(
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
x, y, z, 1.0)
}
const fn make_rotation(c: f32, s: f32, x: f32, y: f32, z: f32) -> [f32; 9]
{
let rc = 1.0 - c;
let (rcx, rcy, rcz) = (x * rc, y * rc, z * rc);
let (sx, sy, sz) = (x * s, y * s, z * s);
[
rcx * x + c, rcy * x + sz, rcz * x - sy,
rcx * y - sz, rcy * y + c, rcz * y + sx,
rcx * z + sy, rcy * z - sx, rcz * z + c,
]
}
fn make_gl_rotation(angle: f32, x: f32, y: f32, z: f32) -> [f32; 9]
{
// Treat inputs like glRotatef
let theta = angle.to_radians();
let axis_mag = (x * x + y * y + z * z).sqrt();
let (nx, ny, nz) =
if (axis_mag - 1.0).abs() > f32::EPSILON
{ (x / axis_mag, y / axis_mag, z / axis_mag) } else
{ (x, y, z) };
Self::make_rotation(theta.cos(), theta.sin(), nx, ny, nz)
}
pub(crate) fn rotation(angle: f32, x: f32, y: f32, z: f32) -> Self
{
let r = Self::make_gl_rotation(angle, x, y, z);
Self::new(
r[0], r[1], r[2], 0.0,
r[3], r[4], r[5], 0.0,
r[6], r[7], r[8], 0.0,
0.0, 0.0, 0.0, 1.0)
}
pub const fn perspective(fovy: f32, aspect: f32, near: f32, far: f32) -> Self
{
let h = 1.0 / (fovy.to_radians() * 0.5);
let w = h / aspect;
let invcliprng = 1.0 / (far - near);
let zh = -(far + near) * invcliprng;
let zl = -(2.0 * far * near) * invcliprng;
/*
[w 0 0 0]
[0 h 0 0]
[0 0 zh zl]
[0 0 -1 0]
*/
Self::new(
w, 0.0, 0.0, 0.0,
0.0, h, 0.0, 0.0,
0.0, 0.0, zh, -1.0,
0.0, 0.0, zl, 0.0)
}
pub const fn translate(&mut self, x: f32, y: f32, z: f32)
{
/*
m = { [1 0 0 x]
[0 1 0 y]
[0 0 1 z]
[0 0 0 1] } * m
*/
let m = self.0;
self.0[12] += x * m[0] + y * m[4] + z * m[8];
self.0[13] += x * m[1] + y * m[5] + z * m[9];
self.0[14] += x * m[2] + y * m[6] + z * m[10];
self.0[15] += x * m[3] + y * m[7] + z * m[11];
}
pub fn rotate(&mut self, angle: f32, x: f32, y: f32, z: f32)
{
let r = Self::make_gl_rotation(angle, x, y, z);
// Partial matrix multiplication
let mut tmp = [0f32; 12]; // Set up temporary
tmp.copy_from_slice(&self.0[..12]);
self.0[0] = r[0] * tmp[0] + r[1] * tmp[4] + r[2] * tmp[8];
self.0[1] = r[0] * tmp[1] + r[1] * tmp[5] + r[2] * tmp[9];
self.0[2] = r[0] * tmp[2] + r[1] * tmp[6] + r[2] * tmp[10];
self.0[3] = r[0] * tmp[3] + r[1] * tmp[7] + r[2] * tmp[11];
self.0[4] = r[3] * tmp[0] + r[4] * tmp[4] + r[5] * tmp[8];
self.0[5] = r[3] * tmp[1] + r[4] * tmp[5] + r[5] * tmp[9];
self.0[6] = r[3] * tmp[2] + r[4] * tmp[6] + r[5] * tmp[10];
self.0[7] = r[3] * tmp[3] + r[4] * tmp[7] + r[5] * tmp[11];
self.0[8] = r[6] * tmp[0] + r[7] * tmp[4] + r[8] * tmp[8];
self.0[9] = r[6] * tmp[1] + r[7] * tmp[5] + r[8] * tmp[9];
self.0[10] = r[6] * tmp[2] + r[7] * tmp[6] + r[8] * tmp[10];
self.0[11] = r[6] * tmp[3] + r[7] * tmp[7] + r[8] * tmp[11];
}
#[inline(always)]
#[must_use]
pub const fn as_ptr(&self) -> *const f32 { self.0.as_ptr() }
}
impl Default for Mtx
{
fn default() -> Self { Self::IDENTITY }
}
impl Mul for Mtx
{
type Output = Self;
fn mul(self, rhs: Self) -> Self
{
Self(core::array::from_fn(|i|
{
let (row, col) = (i & 0x3, i >> 2);
(0..4).fold(0f32, |a, j| a +
self.0[j * 4 + row] *
rhs.0[col * 4 + j])
}))
}
}