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

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])
}))
}
}