mirror of
https://github.com/ScrelliCopter/NeHe-SDL_GPU.git
synced 2025-06-19 21:49:17 +10:00
rust: Implement lessons 1-7
This commit is contained in:
107
src/rust/nehe/application.rs
Normal file
107
src/rust/nehe/application.rs
Normal 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(())
|
||||
}
|
||||
24
src/rust/nehe/application/config.rs
Normal file
24
src/rust/nehe/application/config.rs
Normal 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
259
src/rust/nehe/context.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
274
src/rust/nehe/context/copypass.rs
Normal file
274
src/rust/nehe/context/copypass.rs
Normal 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
45
src/rust/nehe/error.rs
Normal 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
4
src/rust/nehe/lib.rs
Normal 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
158
src/rust/nehe/matrix.rs
Normal 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])
|
||||
}))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user