From 1b4a78f5cbadb674e7571a336fcd75d27351e3d7 Mon Sep 17 00:00:00 2001 From: a dinosaur Date: Thu, 12 Jun 2025 20:09:07 +1000 Subject: [PATCH] rust: Implement lessons 1-7 --- Cargo.toml | 38 +++ build.rs | 53 ++++ src/rust/lesson1.rs | 43 ++++ src/rust/lesson2.rs | 199 +++++++++++++++ src/rust/lesson3.rs | 209 +++++++++++++++ src/rust/lesson4.rs | 220 ++++++++++++++++ src/rust/lesson5.rs | 266 +++++++++++++++++++ src/rust/lesson6.rs | 278 ++++++++++++++++++++ src/rust/lesson7.rs | 381 ++++++++++++++++++++++++++++ src/rust/nehe/application.rs | 107 ++++++++ src/rust/nehe/application/config.rs | 24 ++ src/rust/nehe/context.rs | 259 +++++++++++++++++++ src/rust/nehe/context/copypass.rs | 274 ++++++++++++++++++++ src/rust/nehe/error.rs | 45 ++++ src/rust/nehe/lib.rs | 4 + src/rust/nehe/matrix.rs | 158 ++++++++++++ 16 files changed, 2558 insertions(+) create mode 100644 Cargo.toml create mode 100644 build.rs create mode 100644 src/rust/lesson1.rs create mode 100644 src/rust/lesson2.rs create mode 100644 src/rust/lesson3.rs create mode 100644 src/rust/lesson4.rs create mode 100644 src/rust/lesson5.rs create mode 100644 src/rust/lesson6.rs create mode 100644 src/rust/lesson7.rs create mode 100644 src/rust/nehe/application.rs create mode 100644 src/rust/nehe/application/config.rs create mode 100644 src/rust/nehe/context.rs create mode 100644 src/rust/nehe/context/copypass.rs create mode 100644 src/rust/nehe/error.rs create mode 100644 src/rust/nehe/lib.rs create mode 100644 src/rust/nehe/matrix.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..65074a4 --- /dev/null +++ b/Cargo.toml @@ -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" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..4ea0492 --- /dev/null +++ b/build.rs @@ -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(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", + ]); +} diff --git a/src/rust/lesson1.rs b/src/rust/lesson1.rs new file mode 100644 index 0000000..8cc6c09 --- /dev/null +++ b/src/rust/lesson1.rs @@ -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> +{ + run::()?; + Ok(ExitCode::SUCCESS) +} diff --git a/src/rust/lesson2.rs b/src/rust/lesson2.rs new file mode 100644 index 0000000..8882a7f --- /dev/null +++ b/src/rust/lesson2.rs @@ -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::() 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::() 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::() as u32); + SDL_DrawGPUIndexedPrimitives(pass, 6, 1, 3, 0, 0); + + SDL_EndGPURenderPass(pass); + } + } +} + +pub fn main() -> Result> +{ + run::()?; + Ok(ExitCode::SUCCESS) +} diff --git a/src/rust/lesson3.rs b/src/rust/lesson3.rs new file mode 100644 index 0000000..469aa00 --- /dev/null +++ b/src/rust/lesson3.rs @@ -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::() 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::() 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::() as u32); + SDL_DrawGPUIndexedPrimitives(pass, 6, 1, 3, 0, 0); + + SDL_EndGPURenderPass(pass); + } + } +} + +pub fn main() -> Result> +{ + run::()?; + Ok(ExitCode::SUCCESS) +} diff --git a/src/rust/lesson4.rs b/src/rust/lesson4.rs new file mode 100644 index 0000000..46d2d84 --- /dev/null +++ b/src/rust/lesson4.rs @@ -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::() 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::() 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::() 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> +{ + run::()?; + Ok(ExitCode::SUCCESS) +} diff --git a/src/rust/lesson5.rs b/src/rust/lesson5.rs new file mode 100644 index 0000000..53512a4 --- /dev/null +++ b/src/rust/lesson5.rs @@ -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::() 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::() 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::() 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> +{ + run::()?; + Ok(ExitCode::SUCCESS) +} diff --git a/src/rust/lesson6.rs b/src/rust/lesson6.rs new file mode 100644 index 0000000..b407a81 --- /dev/null +++ b/src/rust/lesson6.rs @@ -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::() 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::() 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> +{ + run::()?; + Ok(ExitCode::SUCCESS) +} diff --git a/src/rust/lesson7.rs b/src/rust/lesson7.rs new file mode 100644 index 0000000..8f8d867 --- /dev/null +++ b/src/rust/lesson7.rs @@ -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::() 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::() as u32); + SDL_PushGPUVertexUniformData(cmd, 1, addr_of!(self.light) as *const c_void, size_of::() 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::() 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> +{ + run::()?; + Ok(ExitCode::SUCCESS) +} diff --git a/src/rust/nehe/application.rs b/src/rust/nehe/application.rs new file mode 100644 index 0000000..f2fed41 --- /dev/null +++ b/src/rust/nehe/application.rs @@ -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() -> 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(()) +} diff --git a/src/rust/nehe/application/config.rs b/src/rust/nehe/application/config.rs new file mode 100644 index 0000000..c9389f0 --- /dev/null +++ b/src/rust/nehe/application/config.rs @@ -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) {} +} diff --git a/src/rust/nehe/context.rs b/src/rust/nehe/context.rs new file mode 100644 index 0000000..d0bf341 --- /dev/null +++ b/src/rust/nehe/context.rs @@ -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 + { + // 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, + 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() + } +} diff --git a/src/rust/nehe/context/copypass.rs b/src/rust/nehe/context/copypass.rs new file mode 100644 index 0000000..bed67e8 --- /dev/null +++ b/src/rust/nehe/context/copypass.rs @@ -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, +} + +impl NeHeCopyPass<'_> +{ + pub fn create_buffer(&mut self, usage: SDL_GPUBufferUsageFlags, elements: &[E]) + -> Result<*mut SDL_GPUBuffer, NeHeError> + { + let size = (size_of::() * 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 }, +} diff --git a/src/rust/nehe/error.rs b/src/rust/nehe/error.rs new file mode 100644 index 0000000..49a89cd --- /dev/null +++ b/src/rust/nehe/error.rs @@ -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 for NeHeError +{ + fn from(err: io::Error) -> Self { Self::IOError(err) } +} + +impl Error for NeHeError {} diff --git a/src/rust/nehe/lib.rs b/src/rust/nehe/lib.rs new file mode 100644 index 0000000..18fecf9 --- /dev/null +++ b/src/rust/nehe/lib.rs @@ -0,0 +1,4 @@ +pub mod application; +pub mod context; +pub mod error; +pub mod matrix; diff --git a/src/rust/nehe/matrix.rs b/src/rust/nehe/matrix.rs new file mode 100644 index 0000000..610381c --- /dev/null +++ b/src/rust/nehe/matrix.rs @@ -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]) + })) + } +}