From ea1d70c8c28e3b96da6264021fa1c62e28fcd8e4 Mon Sep 17 00:00:00 2001 From: HampusM Date: Fri, 19 Sep 2025 16:36:57 +0200 Subject: feat: add OpenGL bindings crate --- opengl-bindings/Cargo.toml | 65 +++++++ opengl-bindings/build.rs | 107 +++++++++++ opengl-bindings/src/buffer.rs | 167 ++++++++++++++++ opengl-bindings/src/data_types.rs | 37 ++++ opengl-bindings/src/debug.rs | 161 ++++++++++++++++ opengl-bindings/src/lib.rs | 119 ++++++++++++ opengl-bindings/src/misc.rs | 190 +++++++++++++++++++ opengl-bindings/src/shader.rs | 366 ++++++++++++++++++++++++++++++++++++ opengl-bindings/src/texture.rs | 236 +++++++++++++++++++++++ opengl-bindings/src/vertex_array.rs | 260 +++++++++++++++++++++++++ 10 files changed, 1708 insertions(+) create mode 100644 opengl-bindings/Cargo.toml create mode 100644 opengl-bindings/build.rs create mode 100644 opengl-bindings/src/buffer.rs create mode 100644 opengl-bindings/src/data_types.rs create mode 100644 opengl-bindings/src/debug.rs create mode 100644 opengl-bindings/src/lib.rs create mode 100644 opengl-bindings/src/misc.rs create mode 100644 opengl-bindings/src/shader.rs create mode 100644 opengl-bindings/src/texture.rs create mode 100644 opengl-bindings/src/vertex_array.rs (limited to 'opengl-bindings') diff --git a/opengl-bindings/Cargo.toml b/opengl-bindings/Cargo.toml new file mode 100644 index 0000000..8251642 --- /dev/null +++ b/opengl-bindings/Cargo.toml @@ -0,0 +1,65 @@ +[package] +name = "opengl-bindings" +version = "0.1.0" +edition = "2021" + +[dependencies] +glutin = "0.32.3" +thiserror = "1.0.49" +safer-ffi = "0.1.13" +bitflags = "2.4.0" +util-macros = { path = "../util-macros" } + +[build-dependencies] +gl_generator = "=0.14.0" +toml = "0.8.12" +anyhow = "1.0.100" + +[package.metadata.build] +gl_commands = [ + "CreateBuffers", + "NamedBufferData", + "NamedBufferSubData", + "CreateVertexArrays", + "DrawArrays", + "DrawElements", + "VertexArrayElementBuffer", + "VertexArrayVertexBuffer", + "EnableVertexArrayAttrib", + "VertexArrayAttribFormat", + "VertexArrayAttribBinding", + "BindVertexArray", + "TextureStorage2D", + "TextureSubImage2D", + "DeleteTextures", + "GenerateTextureMipmap", + "TextureParameteri", + "CreateTextures", + "BindTextureUnit", + "DeleteShader", + "CreateShader", + "ShaderSource", + "CompileShader", + "GetShaderiv", + "GetShaderInfoLog", + "LinkProgram", + "GetProgramiv", + "CreateProgram", + "AttachShader", + "UseProgram", + "GetUniformLocation", + "ProgramUniform1f", + "ProgramUniform1i", + "ProgramUniform3f", + "ProgramUniformMatrix4fv", + "GetProgramInfoLog", + "DeleteProgram", + "Viewport", + "Clear", + "PolygonMode", + "Enable", + "Disable", + "GetIntegerv", + "DebugMessageCallback", + "DebugMessageControl" +] diff --git a/opengl-bindings/build.rs b/opengl-bindings/build.rs new file mode 100644 index 0000000..060472c --- /dev/null +++ b/opengl-bindings/build.rs @@ -0,0 +1,107 @@ +use std::collections::HashSet; +use std::env; +use std::fs::File; +use std::path::{Path, PathBuf}; + +use anyhow::anyhow; +use gl_generator::{Api, Fallbacks, Profile, Registry, StructGenerator}; + +fn main() -> Result<(), anyhow::Error> +{ + println!("cargo::rerun-if-changed=build.rs"); + println!("cargo::rerun-if-changed=Cargo.toml"); + + let dest = env::var("OUT_DIR")?; + + let mut file = File::create(Path::new(&dest).join("bindings.rs"))?; + + let mut registry = Registry::new(Api::Gl, (4, 6), Profile::Core, Fallbacks::All, []); + + let mut build_metadata = get_build_metadata()?; + + filter_gl_commands(&mut registry, &mut build_metadata)?; + + registry.write_bindings(StructGenerator, &mut file)?; + + Ok(()) +} + +fn filter_gl_commands( + registry: &mut Registry, + build_metadata: &mut BuildMetadata, +) -> Result<(), anyhow::Error> +{ + registry + .cmds + .retain(|command| build_metadata.gl_commands.remove(&command.proto.ident)); + + if !build_metadata.gl_commands.is_empty() { + return Err(anyhow!( + "Invalid GL commands: [{}]", + build_metadata + .gl_commands + .iter() + .cloned() + .collect::>() + .join(", ") + )); + } + + Ok(()) +} + +fn get_build_metadata() -> Result +{ + let manifest_path = PathBuf::from(std::env::var("CARGO_MANIFEST_PATH")?); + + let manifest = std::fs::read_to_string(manifest_path)?.parse::()?; + + let package = match manifest + .get("package") + .ok_or_else(|| anyhow!("Manifest does not have a package table"))? + { + toml::Value::Table(package) => Ok(package), + _ => Err(anyhow!("Manifest package must be a table")), + }?; + + let metadata = match package + .get("metadata") + .ok_or_else(|| anyhow!("Manifest does not have a package.metadata table"))? + { + toml::Value::Table(metadata) => Ok(metadata), + _ => Err(anyhow!("Manifest package.metadata must be a table")), + }?; + + let build_metadata = match metadata + .get("build") + .ok_or_else(|| anyhow!("Manifest does not have a package.metadata.build table"))? + { + toml::Value::Table(build_metadata) => Ok(build_metadata), + _ => Err(anyhow!("Manifest package.metadata.build must be a table")), + }?; + + let gl_command_values = match build_metadata.get("gl_commands").ok_or_else(|| { + anyhow!("Manifest does not have a package.metadata.build.gl_commands array") + })? { + toml::Value::Array(gl_commands) => Ok(gl_commands), + _ => Err(anyhow!( + "Manifest package.metadata.build.gl_commands must be a array" + )), + }?; + + let gl_commands = gl_command_values + .iter() + .map(|gl_command_val| match gl_command_val { + toml::Value::String(gl_command) => Ok(gl_command.clone()), + _ => Err(anyhow!("GL command must be a string")), + }) + .collect::, _>>()?; + + Ok(BuildMetadata { gl_commands }) +} + +#[derive(Debug)] +struct BuildMetadata +{ + gl_commands: HashSet, +} diff --git a/opengl-bindings/src/buffer.rs b/opengl-bindings/src/buffer.rs new file mode 100644 index 0000000..c64ec8d --- /dev/null +++ b/opengl-bindings/src/buffer.rs @@ -0,0 +1,167 @@ +use std::marker::PhantomData; +use std::mem::size_of_val; +use std::ptr::null; + +use safer_ffi::layout::ReprC; + +use crate::CurrentContextWithFns; + +#[derive(Debug)] +pub struct Buffer +{ + buf: crate::sys::types::GLuint, + _pd: PhantomData, +} + +impl Buffer +{ + #[must_use] + pub fn new(current_context: &CurrentContextWithFns<'_>) -> Self + { + let mut buffer = crate::sys::types::GLuint::default(); + + unsafe { + current_context.fns().CreateBuffers(1, &raw mut buffer); + }; + + Self { buf: buffer, _pd: PhantomData } + } + + /// Stores items in this buffer. + /// + /// # Errors + /// Returns `Err` if the total size (in bytes) is too large. + pub fn store( + &self, + current_context: &CurrentContextWithFns<'_>, + items: &[Item], + usage: Usage, + ) -> Result<(), Error> + { + let total_size = size_of_val(items); + + let total_size: crate::sys::types::GLsizeiptr = + total_size + .try_into() + .map_err(|_| Error::TotalItemsSizeTooLarge { + total_size, + max_total_size: crate::sys::types::GLsizeiptr::MAX as usize, + })?; + + unsafe { + current_context.fns().NamedBufferData( + self.buf, + total_size, + items.as_ptr().cast(), + usage.into_gl(), + ); + } + + Ok(()) + } + + /// Maps the values in the `values` slice into `Item`s which is stored into this + /// buffer. + /// + /// # Errors + /// Returns `Err` if the total size (in bytes) is too large. + pub fn store_mapped( + &self, + current_context: &CurrentContextWithFns<'_>, + values: &[Value], + usage: Usage, + mut map_func: impl FnMut(&Value) -> Item, + ) -> Result<(), Error> + { + let item_size: crate::sys::types::GLsizeiptr = const { + assert!(size_of::() <= crate::sys::types::GLsizeiptr::MAX as usize); + + size_of::().cast_signed() + }; + + let total_size = size_of::() * values.len(); + + let total_size: crate::sys::types::GLsizeiptr = + total_size + .try_into() + .map_err(|_| Error::TotalItemsSizeTooLarge { + total_size, + max_total_size: crate::sys::types::GLsizeiptr::MAX as usize, + })?; + + unsafe { + current_context.fns().NamedBufferData( + self.buf, + total_size, + null(), + usage.into_gl(), + ); + } + + for (index, value) in values.iter().enumerate() { + let item = map_func(value); + + let offset = index * size_of::(); + + let Ok(offset_casted) = crate::sys::types::GLintptr::try_from(offset) else { + unreachable!(); // Reason: The total size can be casted to a GLintptr + // (done above) so offsets should be castable as well + }; + + unsafe { + current_context.fns().NamedBufferSubData( + self.buf, + offset_casted, + item_size, + (&raw const item).cast(), + ); + } + } + + Ok(()) + } + + pub(crate) fn object(&self) -> crate::sys::types::GLuint + { + self.buf + } +} + +/// Buffer usage. +#[derive(Debug)] +pub enum Usage +{ + /// The buffer data is set only once and used by the GPU at most a few times. + Stream, + + /// The buffer data is set only once and used many times. + Static, + + /// The buffer data is changed a lot and used many times. + Dynamic, +} + +impl Usage +{ + fn into_gl(self) -> crate::sys::types::GLenum + { + match self { + Self::Stream => crate::sys::STREAM_DRAW, + Self::Static => crate::sys::STATIC_DRAW, + Self::Dynamic => crate::sys::DYNAMIC_DRAW, + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + #[error( + "Total size of items ({total_size}) is too large. Must be < {max_total_size}" + )] + TotalItemsSizeTooLarge + { + total_size: usize, + max_total_size: usize, + }, +} diff --git a/opengl-bindings/src/data_types.rs b/opengl-bindings/src/data_types.rs new file mode 100644 index 0000000..7ead0ab --- /dev/null +++ b/opengl-bindings/src/data_types.rs @@ -0,0 +1,37 @@ +use safer_ffi::derive_ReprC; +use safer_ffi::layout::ReprC; + +#[derive(Debug, Clone)] +#[derive_ReprC] +#[repr(C)] +pub struct Matrix +{ + /// Items must be layed out this way for it to work with OpenGL shaders. + pub items: [[Value; ROWS]; COLUMNS], +} + +#[derive(Debug, Clone)] +#[derive_ReprC] +#[repr(C)] +pub struct Vec3 +{ + pub x: Value, + pub y: Value, + pub z: Value, +} + +#[derive(Debug, Clone)] +#[derive_ReprC] +#[repr(C)] +pub struct Vec2 +{ + pub x: Value, + pub y: Value, +} + +#[derive(Debug, Clone)] +pub struct Dimens +{ + pub width: Value, + pub height: Value, +} diff --git a/opengl-bindings/src/debug.rs b/opengl-bindings/src/debug.rs new file mode 100644 index 0000000..a9369a4 --- /dev/null +++ b/opengl-bindings/src/debug.rs @@ -0,0 +1,161 @@ +use std::ffi::c_void; +use std::io::{stderr, Write}; +use std::mem::transmute; +use std::panic::catch_unwind; + +use util_macros::FromRepr; + +use crate::CurrentContextWithFns; + +pub fn set_debug_message_callback( + current_context: &CurrentContextWithFns<'_>, + cb: MessageCallback, +) +{ + unsafe { + current_context + .fns() + .DebugMessageCallback(Some(debug_message_cb), cb as *mut c_void); + } +} + +/// Sets debug message parameters. +/// +/// # Errors +/// Returns `Err` if `ids` contains too many ids. +pub fn set_debug_message_control( + current_context: &CurrentContextWithFns<'_>, + source: Option, + ty: Option, + severity: Option, + ids: &[u32], + ids_action: MessageIdsAction, +) -> Result<(), SetDebugMessageControlError> +{ + let ids_len: crate::sys::types::GLsizei = + ids.len() + .try_into() + .map_err(|_| SetDebugMessageControlError::TooManyIds { + id_cnt: ids.len(), + max_id_cnt: crate::sys::types::GLsizei::MAX as usize, + })?; + + unsafe { + current_context.fns().DebugMessageControl( + source.map_or(crate::sys::DONT_CARE, |source| source as u32), + ty.map_or(crate::sys::DONT_CARE, |ty| ty as u32), + severity.map_or(crate::sys::DONT_CARE, |severity| severity as u32), + ids_len, + ids.as_ptr(), + ids_action as u8, + ); + } + + Ok(()) +} + +#[derive(Debug, thiserror::Error)] +pub enum SetDebugMessageControlError +{ + #[error("Too many ids provided ({id_cnt}). Must be < {max_id_cnt}")] + TooManyIds + { + id_cnt: usize, max_id_cnt: usize + }, +} + +pub type MessageCallback = fn( + source: MessageSource, + ty: MessageType, + id: u32, + severity: MessageSeverity, + message: &str, +); + +#[derive(Debug, Clone, Copy)] +#[repr(u8)] // GLboolean = u8 +pub enum MessageIdsAction +{ + Enable = crate::sys::TRUE, + Disable = crate::sys::FALSE, +} + +#[derive(Debug, Clone, Copy, FromRepr)] +#[repr(u32)] // GLenum = u32 +pub enum MessageSource +{ + Api = crate::sys::DEBUG_SOURCE_API, + WindowSystem = crate::sys::DEBUG_SOURCE_WINDOW_SYSTEM, + ShaderCompiler = crate::sys::DEBUG_SOURCE_SHADER_COMPILER, + ThirdParty = crate::sys::DEBUG_SOURCE_THIRD_PARTY, + Application = crate::sys::DEBUG_SOURCE_APPLICATION, + Other = crate::sys::DEBUG_SOURCE_OTHER, +} + +#[derive(Debug, Clone, Copy, FromRepr)] +#[repr(u32)] // GLenum = u32 +pub enum MessageType +{ + DeprecatedBehavior = crate::sys::DEBUG_TYPE_DEPRECATED_BEHAVIOR, + Error = crate::sys::DEBUG_TYPE_ERROR, + Marker = crate::sys::DEBUG_TYPE_MARKER, + Other = crate::sys::DEBUG_TYPE_OTHER, + Performance = crate::sys::DEBUG_TYPE_PERFORMANCE, + PopGroup = crate::sys::DEBUG_TYPE_POP_GROUP, + PushGroup = crate::sys::DEBUG_TYPE_PUSH_GROUP, + Portability = crate::sys::DEBUG_TYPE_PORTABILITY, + UndefinedBehavior = crate::sys::DEBUG_TYPE_UNDEFINED_BEHAVIOR, +} + +#[derive(Debug, Clone, Copy, FromRepr)] +#[repr(u32)] // GLenum = u32 +pub enum MessageSeverity +{ + High = crate::sys::DEBUG_SEVERITY_HIGH, + Medium = crate::sys::DEBUG_SEVERITY_MEDIUM, + Low = crate::sys::DEBUG_SEVERITY_LOW, + Notification = crate::sys::DEBUG_SEVERITY_NOTIFICATION, +} + +extern "system" fn debug_message_cb( + source: crate::sys::types::GLenum, + ty: crate::sys::types::GLenum, + id: crate::sys::types::GLuint, + severity: crate::sys::types::GLenum, + message_length: crate::sys::types::GLsizei, + message: *const crate::sys::types::GLchar, + user_cb: *mut c_void, +) +{ + let user_cb = unsafe { transmute::<*mut c_void, MessageCallback>(user_cb) }; + + let Ok(msg_length) = usize::try_from(message_length) else { + return; + }; + + // Unwinds are catched because unwinding from Rust code into foreign code is UB. + let res = catch_unwind(|| { + let msg_source = MessageSource::from_repr(source).unwrap(); + let msg_type = MessageType::from_repr(ty).unwrap(); + let msg_severity = MessageSeverity::from_repr(severity).unwrap(); + + // SAFETY: The received message should be a valid ASCII string + let message = unsafe { + std::str::from_utf8_unchecked(std::slice::from_raw_parts( + message.cast(), + msg_length, + )) + }; + + user_cb(msg_source, msg_type, id, msg_severity, message); + }); + + if res.is_err() { + // eprintln is not used since it can panic and unwinds are unwanted because + // unwinding from Rust code into foreign code is UB. + stderr() + .write_all(b"ERROR: Panic in debug message callback") + .ok(); + println!(); + } +} diff --git a/opengl-bindings/src/lib.rs b/opengl-bindings/src/lib.rs new file mode 100644 index 0000000..7fd9933 --- /dev/null +++ b/opengl-bindings/src/lib.rs @@ -0,0 +1,119 @@ +#![deny(clippy::all, clippy::pedantic)] +use std::ffi::CString; +use std::process::abort; + +use glutin::context::{NotCurrentContext, PossiblyCurrentContext}; +use glutin::display::GetGlDisplay; +use glutin::prelude::{GlDisplay, NotCurrentGlContext, PossiblyCurrentGlContext}; +use glutin::surface::{Surface, SurfaceTypeTrait}; + +pub mod buffer; +pub mod data_types; +pub mod debug; +pub mod misc; +pub mod shader; +pub mod texture; +pub mod vertex_array; + +pub struct ContextWithFns +{ + context: PossiblyCurrentContext, + fns: Box, +} + +impl ContextWithFns +{ + /// Returns a new `ContextWithFns`. + /// + /// # Errors + /// Returns `Err` if making this context current fails. + pub fn new( + context: NotCurrentContext, + surface: &Surface, + ) -> Result + { + let context = context + .make_current(surface) + .map_err(Error::MakeContextCurrentFailed)?; + + let display = context.display(); + + let gl = sys::Gl::load_with(|symbol| { + let Ok(symbol) = CString::new(symbol) else { + eprintln!("GL symbol contains nul byte"); + abort(); + }; + + display.get_proc_address(&symbol) + }); + + Ok(Self { context, fns: Box::new(gl) }) + } + + /// Attempts to make this context current. + /// + /// # Errors + /// Returns `Err` if making this context current fails. + pub fn make_current( + &self, + surface: &Surface, + ) -> Result, Error> + { + if !self.context.is_current() { + self.context + .make_current(surface) + .map_err(Error::MakeContextCurrentFailed)?; + } + + Ok(CurrentContextWithFns { ctx: self }) + } + + #[must_use] + pub fn context(&self) -> &PossiblyCurrentContext + { + &self.context + } + + #[must_use] + pub fn context_mut(&mut self) -> &mut PossiblyCurrentContext + { + &mut self.context + } +} + +pub struct CurrentContextWithFns<'ctx> +{ + ctx: &'ctx ContextWithFns, +} + +impl CurrentContextWithFns<'_> +{ + #[inline] + pub(crate) fn fns(&self) -> &sys::Gl + { + &self.ctx.fns + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + #[error("Failed to make context current")] + MakeContextCurrentFailed(#[source] glutin::error::Error), +} + +mod sys +{ + #![allow( + clippy::missing_safety_doc, + clippy::missing_transmute_annotations, + clippy::too_many_arguments, + clippy::unused_unit, + clippy::upper_case_acronyms, + clippy::doc_markdown, + clippy::unreadable_literal, + unsafe_op_in_unsafe_fn + )] + + include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +} diff --git a/opengl-bindings/src/misc.rs b/opengl-bindings/src/misc.rs new file mode 100644 index 0000000..bb54c1a --- /dev/null +++ b/opengl-bindings/src/misc.rs @@ -0,0 +1,190 @@ +use bitflags::bitflags; + +use crate::data_types::{Dimens, Vec2}; +use crate::CurrentContextWithFns; + +/// Sets the viewport. +/// +/// The `u32` values in `position` and `size` must fit in `i32`s. +/// +/// # Errors +/// Returns `Err` if any value in `position` or `size` does not fit into a `i32`. +pub fn set_viewport( + current_context: &CurrentContextWithFns<'_>, + position: &Vec2, + size: &Dimens, +) -> Result<(), SetViewportError> +{ + let position = Vec2:: { + x: position.x.try_into().map_err(|_| { + SetViewportError::PositionXValueTooLarge { + value: position.x, + max_value: crate::sys::types::GLint::MAX as u32, + } + })?, + y: position.y.try_into().map_err(|_| { + SetViewportError::PositionYValueTooLarge { + value: position.y, + max_value: crate::sys::types::GLint::MAX as u32, + } + })?, + }; + + let size = Dimens:: { + width: size.width.try_into().map_err(|_| { + SetViewportError::SizeWidthValueTooLarge { + value: size.width, + max_value: crate::sys::types::GLsizei::MAX as u32, + } + })?, + height: size.height.try_into().map_err(|_| { + SetViewportError::SizeHeightValueTooLarge { + value: size.height, + max_value: crate::sys::types::GLsizei::MAX as u32, + } + })?, + }; + + unsafe { + current_context + .fns() + .Viewport(position.x, position.y, size.width, size.height); + } + + Ok(()) +} + +pub fn clear_buffers(current_context: &CurrentContextWithFns<'_>, mask: BufferClearMask) +{ + unsafe { + current_context.fns().Clear(mask.bits()); + } +} + +pub fn set_polygon_mode( + current_context: &CurrentContextWithFns<'_>, + face: impl Into, + mode: impl Into, +) +{ + unsafe { + current_context + .fns() + .PolygonMode(face.into() as u32, mode.into() as u32); + } +} + +pub fn enable(current_context: &CurrentContextWithFns<'_>, capacity: Capability) +{ + unsafe { + current_context.fns().Enable(capacity as u32); + } +} + +pub fn disable(current_context: &CurrentContextWithFns<'_>, capability: Capability) +{ + unsafe { + current_context.fns().Disable(capability as u32); + } +} + +pub fn set_enabled( + current_context: &CurrentContextWithFns<'_>, + capability: Capability, + enabled: bool, +) +{ + if enabled { + enable(current_context, capability); + } else { + disable(current_context, capability); + } +} + +#[must_use] +pub fn get_context_flags(current_context: &CurrentContextWithFns<'_>) -> ContextFlags +{ + let mut context_flags = crate::sys::types::GLint::default(); + + unsafe { + current_context + .fns() + .GetIntegerv(crate::sys::CONTEXT_FLAGS, &raw mut context_flags); + } + + ContextFlags::from_bits_truncate(context_flags.cast_unsigned()) +} + +bitflags! { + #[derive(Debug, Clone, Copy)] + pub struct BufferClearMask: u32 { + const COLOR = crate::sys::COLOR_BUFFER_BIT; + const DEPTH = crate::sys::DEPTH_BUFFER_BIT; + const STENCIL = crate::sys::STENCIL_BUFFER_BIT; + } +} + +#[derive(Debug)] +#[repr(u32)] +pub enum Capability +{ + DepthTest = crate::sys::DEPTH_TEST, + MultiSample = crate::sys::MULTISAMPLE, + DebugOutput = crate::sys::DEBUG_OUTPUT, + DebugOutputSynchronous = crate::sys::DEBUG_OUTPUT_SYNCHRONOUS, +} + +#[derive(Debug)] +#[repr(u32)] +pub enum PolygonMode +{ + Point = crate::sys::POINT, + Line = crate::sys::LINE, + Fill = crate::sys::FILL, +} + +#[derive(Debug)] +#[repr(u32)] +pub enum PolygonModeFace +{ + Front = crate::sys::FRONT, + Back = crate::sys::BACK, + FrontAndBack = crate::sys::FRONT_AND_BACK, +} + +bitflags! { +#[derive(Debug, Clone, Copy)] +pub struct ContextFlags: u32 { + const FORWARD_COMPATIBLE = crate::sys::CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT; + const DEBUG = crate::sys::CONTEXT_FLAG_DEBUG_BIT; + const ROBUST_ACCESS = crate::sys::CONTEXT_FLAG_ROBUST_ACCESS_BIT; +} +} + +#[derive(Debug, thiserror::Error)] +pub enum SetViewportError +{ + #[error("Position X value ({value}) is too large. Must be < {max_value}")] + PositionXValueTooLarge + { + value: u32, max_value: u32 + }, + + #[error("Position Y value ({value}) is too large. Must be < {max_value}")] + PositionYValueTooLarge + { + value: u32, max_value: u32 + }, + + #[error("Size width value ({value}) is too large. Must be < {max_value}")] + SizeWidthValueTooLarge + { + value: u32, max_value: u32 + }, + + #[error("Size height value ({value}) is too large. Must be < {max_value}")] + SizeHeightValueTooLarge + { + value: u32, max_value: u32 + }, +} diff --git a/opengl-bindings/src/shader.rs b/opengl-bindings/src/shader.rs new file mode 100644 index 0000000..5ed66a2 --- /dev/null +++ b/opengl-bindings/src/shader.rs @@ -0,0 +1,366 @@ +use std::ffi::CStr; +use std::ptr::null_mut; + +use safer_ffi::layout::ReprC; + +use crate::data_types::{Matrix, Vec3}; +use crate::CurrentContextWithFns; + +#[derive(Debug)] +pub struct Shader +{ + shader: crate::sys::types::GLuint, +} + +impl Shader +{ + #[must_use] + pub fn new(current_context: &CurrentContextWithFns<'_>, kind: Kind) -> Self + { + let shader = unsafe { + current_context + .fns() + .CreateShader(kind as crate::sys::types::GLenum) + }; + + Self { shader } + } + + /// Sets the source code of this shader. + /// + /// # Errors + /// Returns `Err` if `source` is not ASCII. + pub fn set_source( + &self, + current_context: &CurrentContextWithFns<'_>, + source: &str, + ) -> Result<(), Error> + { + if !source.is_ascii() { + return Err(Error::SourceNotAscii); + } + + let length: crate::sys::types::GLint = + source.len().try_into().map_err(|_| Error::SourceTooLarge { + length: source.len(), + max_length: crate::sys::types::GLint::MAX as usize, + })?; + + unsafe { + current_context.fns().ShaderSource( + self.shader, + 1, + &source.as_ptr().cast(), + &raw const length, + ); + } + + Ok(()) + } + + /// Compiles this shader. + /// + /// # Errors + /// Returns `Err` if compiling fails. + pub fn compile( + &self, + current_context: &CurrentContextWithFns<'_>, + ) -> Result<(), Error> + { + unsafe { + current_context.fns().CompileShader(self.shader); + } + + let mut compile_success = crate::sys::types::GLint::default(); + + unsafe { + current_context.fns().GetShaderiv( + self.shader, + crate::sys::COMPILE_STATUS, + &raw mut compile_success, + ); + } + + if compile_success == 0 { + let info_log = self.get_info_log(current_context); + + return Err(Error::CompileFailed { log: info_log }); + } + + Ok(()) + } + + pub fn delete(self, current_context: &CurrentContextWithFns<'_>) + { + unsafe { + current_context.fns().DeleteShader(self.shader); + } + } + + fn get_info_log(&self, current_context: &CurrentContextWithFns<'_>) -> String + { + const BUF_SIZE: crate::sys::types::GLsizei = 512; + + let mut buf = vec![crate::sys::types::GLchar::default(); BUF_SIZE as usize]; + + unsafe { + current_context.fns().GetShaderInfoLog( + self.shader, + BUF_SIZE, + null_mut(), + buf.as_mut_ptr(), + ); + } + + let info_log = unsafe { CStr::from_ptr(buf.as_ptr()) }; + + unsafe { String::from_utf8_unchecked(info_log.to_bytes().to_vec()) } + } +} + +/// Shader kind. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum Kind +{ + Vertex = crate::sys::VERTEX_SHADER, + Fragment = crate::sys::FRAGMENT_SHADER, +} + +/// Shader program +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct Program +{ + program: crate::sys::types::GLuint, +} + +impl Program +{ + #[must_use] + pub fn new(current_context: &CurrentContextWithFns<'_>) -> Self + { + let program = unsafe { current_context.fns().CreateProgram() }; + + Self { program } + } + + pub fn attach(&self, current_context: &CurrentContextWithFns<'_>, shader: &Shader) + { + unsafe { + current_context + .fns() + .AttachShader(self.program, shader.shader); + } + } + + /// Links this program. + /// + /// # Errors + /// Returns `Err` if linking fails. + pub fn link(&self, current_context: &CurrentContextWithFns<'_>) -> Result<(), Error> + { + unsafe { + current_context.fns().LinkProgram(self.program); + } + + let mut link_success = crate::sys::types::GLint::default(); + + unsafe { + current_context.fns().GetProgramiv( + self.program, + crate::sys::LINK_STATUS, + &raw mut link_success, + ); + } + + if link_success == 0 { + let info_log = self.get_info_log(current_context); + + return Err(Error::LinkFailed { log: info_log }); + } + + Ok(()) + } + + pub fn activate(&self, current_context: &CurrentContextWithFns<'_>) + { + unsafe { + current_context.fns().UseProgram(self.program); + } + } + + pub fn set_uniform( + &self, + current_context: &CurrentContextWithFns<'_>, + name: &CStr, + var: &impl UniformVariable, + ) + { + let location = UniformLocation(unsafe { + current_context + .fns() + .GetUniformLocation(self.program, name.as_ptr().cast()) + }); + + var.set(current_context, self, location); + } + + pub fn delete(self, current_context: &CurrentContextWithFns<'_>) + { + unsafe { + current_context.fns().DeleteProgram(self.program); + } + } + + fn get_info_log(&self, current_context: &CurrentContextWithFns<'_>) -> String + { + const BUF_SIZE: crate::sys::types::GLsizei = 512; + + let mut buf = vec![crate::sys::types::GLchar::default(); BUF_SIZE as usize]; + + unsafe { + current_context.fns().GetProgramInfoLog( + self.program, + BUF_SIZE, + null_mut(), + buf.as_mut_ptr(), + ); + } + + let info_log = unsafe { CStr::from_ptr(buf.as_ptr()) }; + + unsafe { String::from_utf8_unchecked(info_log.to_bytes().to_vec()) } + } +} + +pub trait UniformVariable: ReprC + sealed::Sealed +{ + fn set( + &self, + current_context: &CurrentContextWithFns<'_>, + program: &Program, + uniform_location: UniformLocation, + ); +} + +impl UniformVariable for f32 +{ + fn set( + &self, + current_context: &CurrentContextWithFns<'_>, + program: &Program, + uniform_location: UniformLocation, + ) + { + unsafe { + current_context.fns().ProgramUniform1f( + program.program, + uniform_location.0, + *self, + ); + } + } +} + +impl sealed::Sealed for f32 {} + +impl UniformVariable for i32 +{ + fn set( + &self, + current_context: &CurrentContextWithFns<'_>, + program: &Program, + uniform_location: UniformLocation, + ) + { + unsafe { + current_context.fns().ProgramUniform1i( + program.program, + uniform_location.0, + *self, + ); + } + } +} + +impl sealed::Sealed for i32 {} + +impl UniformVariable for Vec3 +{ + fn set( + &self, + current_context: &CurrentContextWithFns<'_>, + program: &Program, + uniform_location: UniformLocation, + ) + { + unsafe { + current_context.fns().ProgramUniform3f( + program.program, + uniform_location.0, + self.x, + self.y, + self.z, + ); + } + } +} + +impl sealed::Sealed for Vec3 {} + +impl UniformVariable for Matrix +{ + fn set( + &self, + current_context: &CurrentContextWithFns<'_>, + program: &Program, + uniform_location: UniformLocation, + ) + { + unsafe { + current_context.fns().ProgramUniformMatrix4fv( + program.program, + uniform_location.0, + 1, + crate::sys::FALSE, + self.items.as_ptr().cast::(), + ); + } + } +} + +impl sealed::Sealed for Matrix {} + +#[derive(Debug)] +pub struct UniformLocation(crate::sys::types::GLint); + +/// Shader error. +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + #[error("All characters in source are not within the ASCII range")] + SourceNotAscii, + + #[error("Source is too large. Length ({length}) must be < {max_length}")] + SourceTooLarge + { + length: usize, max_length: usize + }, + + #[error("Failed to compile shader")] + CompileFailed + { + log: String + }, + + #[error("Failed to link shader program")] + LinkFailed + { + log: String + }, +} + +mod sealed +{ + pub trait Sealed {} +} diff --git a/opengl-bindings/src/texture.rs b/opengl-bindings/src/texture.rs new file mode 100644 index 0000000..1859beb --- /dev/null +++ b/opengl-bindings/src/texture.rs @@ -0,0 +1,236 @@ +use crate::data_types::Dimens; +use crate::CurrentContextWithFns; + +#[derive(Debug)] +pub struct Texture +{ + texture: crate::sys::types::GLuint, +} + +impl Texture +{ + #[must_use] + pub fn new(current_context: &CurrentContextWithFns<'_>) -> Self + { + let mut texture = crate::sys::types::GLuint::default(); + + unsafe { + current_context.fns().CreateTextures( + crate::sys::TEXTURE_2D, + 1, + &raw mut texture, + ); + }; + + Self { texture } + } + + pub fn bind_to_texture_unit( + &self, + current_context: &CurrentContextWithFns<'_>, + texture_unit: u32, + ) + { + unsafe { + current_context + .fns() + .BindTextureUnit(texture_unit, self.texture); + } + } + + /// Allocates the texture storage, stores pixel data & generates a mipmap. + /// + /// # Errors + /// Returns `Err` if any value in `size` does not fit into a `i32`. + pub fn generate( + &self, + current_context: &CurrentContextWithFns<'_>, + size: &Dimens, + data: &[u8], + pixel_data_format: PixelDataFormat, + ) -> Result<(), GenerateError> + { + let size = Dimens:: { + width: size.width.try_into().map_err(|_| { + GenerateError::SizeWidthValueTooLarge { + value: size.width, + max_value: crate::sys::types::GLsizei::MAX as u32, + } + })?, + height: size.height.try_into().map_err(|_| { + GenerateError::SizeHeightValueTooLarge { + value: size.height, + max_value: crate::sys::types::GLsizei::MAX as u32, + } + })?, + }; + + self.alloc_image(current_context, pixel_data_format, &size, data); + + unsafe { + current_context.fns().GenerateTextureMipmap(self.texture); + } + + Ok(()) + } + + pub fn set_wrap( + &self, + current_context: &CurrentContextWithFns<'_>, + wrapping: Wrapping, + ) + { + unsafe { + current_context.fns().TextureParameteri( + self.texture, + crate::sys::TEXTURE_WRAP_S, + wrapping as i32, + ); + + current_context.fns().TextureParameteri( + self.texture, + crate::sys::TEXTURE_WRAP_T, + wrapping as i32, + ); + } + } + + pub fn set_magnifying_filter( + &self, + current_context: &CurrentContextWithFns<'_>, + filtering: Filtering, + ) + { + unsafe { + current_context.fns().TextureParameteri( + self.texture, + crate::sys::TEXTURE_MAG_FILTER, + filtering as i32, + ); + } + } + + pub fn set_minifying_filter( + &self, + current_context: &CurrentContextWithFns<'_>, + filtering: Filtering, + ) + { + unsafe { + current_context.fns().TextureParameteri( + self.texture, + crate::sys::TEXTURE_MIN_FILTER, + filtering as i32, + ); + } + } + + pub fn delete(self, current_context: &CurrentContextWithFns<'_>) + { + unsafe { + current_context + .fns() + .DeleteTextures(1, &raw const self.texture); + } + } + + fn alloc_image( + &self, + current_context: &CurrentContextWithFns<'_>, + pixel_data_format: PixelDataFormat, + size: &Dimens, + data: &[u8], + ) + { + unsafe { + current_context.fns().TextureStorage2D( + self.texture, + 1, + pixel_data_format.to_sized_internal_format(), + size.width, + size.height, + ); + + current_context.fns().TextureSubImage2D( + self.texture, + 0, + 0, + 0, + size.width, + size.height, + pixel_data_format.to_format(), + crate::sys::UNSIGNED_BYTE, + data.as_ptr().cast(), + ); + } + } +} + +const fn try_cast_u32_to_i32(val: u32) -> i32 +{ + assert!(val <= i32::MAX as u32); + + val.cast_signed() +} + +/// Texture wrapping. +#[derive(Debug, Clone, Copy)] +#[repr(i32)] +pub enum Wrapping +{ + Repeat = const { try_cast_u32_to_i32(crate::sys::REPEAT) }, + MirroredRepeat = const { try_cast_u32_to_i32(crate::sys::MIRRORED_REPEAT) }, + ClampToEdge = const { try_cast_u32_to_i32(crate::sys::CLAMP_TO_EDGE) }, + ClampToBorder = const { try_cast_u32_to_i32(crate::sys::CLAMP_TO_BORDER) }, +} + +#[derive(Debug, Clone, Copy)] +#[repr(i32)] +pub enum Filtering +{ + Nearest = const { try_cast_u32_to_i32(crate::sys::NEAREST) }, + Linear = const { try_cast_u32_to_i32(crate::sys::LINEAR) }, +} + +/// Texture pixel data format. +#[derive(Debug, Clone, Copy)] +pub enum PixelDataFormat +{ + Rgb8, + Rgba8, +} + +impl PixelDataFormat +{ + fn to_sized_internal_format(self) -> crate::sys::types::GLenum + { + match self { + Self::Rgb8 => crate::sys::RGB8, + Self::Rgba8 => crate::sys::RGBA8, + } + } + + fn to_format(self) -> crate::sys::types::GLenum + { + match self { + Self::Rgb8 => crate::sys::RGB, + Self::Rgba8 => crate::sys::RGBA, + } + } +} + +/// Error generating texture. +#[derive(Debug, thiserror::Error)] +pub enum GenerateError +{ + #[error("Size width value ({value}) is too large. Must be < {max_value}")] + SizeWidthValueTooLarge + { + value: u32, max_value: u32 + }, + #[error("Size height value ({value}) is too large. Must be < {max_value}")] + SizeHeightValueTooLarge + { + value: u32, max_value: u32 + }, +} diff --git a/opengl-bindings/src/vertex_array.rs b/opengl-bindings/src/vertex_array.rs new file mode 100644 index 0000000..9942fe7 --- /dev/null +++ b/opengl-bindings/src/vertex_array.rs @@ -0,0 +1,260 @@ +use std::ffi::{c_int, c_void}; +use std::mem::size_of; + +use safer_ffi::layout::ReprC; + +use crate::buffer::Buffer; +use crate::CurrentContextWithFns; + +#[derive(Debug)] +pub struct VertexArray +{ + array: crate::sys::types::GLuint, +} + +impl VertexArray +{ + #[must_use] + pub fn new(current_context: &CurrentContextWithFns<'_>) -> Self + { + let mut array = 0; + + unsafe { + current_context.fns().CreateVertexArrays(1, &raw mut array); + } + + Self { array } + } + + /// Draws the currently bound vertex array. + /// + /// # Errors + /// Returns `Err` if: + /// - `start_index` is too large + /// - `cnt` is too large + pub fn draw_arrays( + current_context: &CurrentContextWithFns<'_>, + primitive_kind: PrimitiveKind, + start_index: u32, + cnt: u32, + ) -> Result<(), DrawError> + { + let start_index: crate::sys::types::GLint = + start_index + .try_into() + .map_err(|_| DrawError::StartIndexValueTooLarge { + value: start_index, + max_value: crate::sys::types::GLint::MAX as u32, + })?; + + let cnt: crate::sys::types::GLsizei = + cnt.try_into().map_err(|_| DrawError::CountValueTooLarge { + value: cnt, + max_value: crate::sys::types::GLsizei::MAX as u32, + })?; + + unsafe { + current_context + .fns() + .DrawArrays(primitive_kind.into_gl(), start_index, cnt); + } + + Ok(()) + } + + /// Draws the currently bound vertex array. + /// + /// # Errors + /// Returns `Err` if `cnt` is too large. + pub fn draw_elements( + current_context: &CurrentContextWithFns<'_>, + primitive_kind: PrimitiveKind, + start_index: u32, + cnt: u32, + ) -> Result<(), DrawError> + { + let cnt: crate::sys::types::GLsizei = + cnt.try_into().map_err(|_| DrawError::CountValueTooLarge { + value: cnt, + max_value: crate::sys::types::GLsizei::MAX as u32, + })?; + + unsafe { + current_context.fns().DrawElements( + primitive_kind.into_gl(), + cnt, + crate::sys::UNSIGNED_INT, + // TODO: Make this not sometimes UB. DrawElements expects a actual + // pointer to a memory location when no VBO is bound. + // See: https://stackoverflow.com/q/21706113 + std::ptr::without_provenance::(start_index as usize), + ); + } + + Ok(()) + } + + pub fn bind_element_buffer( + &self, + current_context: &CurrentContextWithFns<'_>, + element_buffer: &Buffer, + ) + { + unsafe { + current_context + .fns() + .VertexArrayElementBuffer(self.array, element_buffer.object()); + } + } + + pub fn bind_vertex_buffer( + &self, + current_context: &CurrentContextWithFns<'_>, + binding_index: u32, + vertex_buffer: &Buffer, + offset: isize, + ) + { + let vertex_size = const { cast_usize_to_c_int(size_of::()) }; + + unsafe { + current_context.fns().VertexArrayVertexBuffer( + self.array, + binding_index, + vertex_buffer.object(), + offset, + vertex_size, + ); + } + } + + pub fn enable_attrib( + &self, + current_context: &CurrentContextWithFns<'_>, + attrib_index: u32, + ) + { + unsafe { + current_context.fns().EnableVertexArrayAttrib( + self.array, + attrib_index as crate::sys::types::GLuint, + ); + } + } + + pub fn set_attrib_format( + &self, + current_context: &CurrentContextWithFns<'_>, + attrib_index: u32, + data_type: DataType, + normalized: bool, + offset: u32, + ) + { + unsafe { + current_context.fns().VertexArrayAttribFormat( + self.array, + attrib_index, + data_type.size(), + data_type as u32, + if normalized { + crate::sys::TRUE + } else { + crate::sys::FALSE + }, + offset, + ); + } + } + + /// Associate a vertex attribute and a vertex buffer binding. + pub fn set_attrib_vertex_buf_binding( + &self, + current_context: &CurrentContextWithFns<'_>, + attrib_index: u32, + vertex_buf_binding_index: u32, + ) + { + unsafe { + current_context.fns().VertexArrayAttribBinding( + self.array, + attrib_index, + vertex_buf_binding_index, + ); + } + } + + pub fn bind(&self, current_context: &CurrentContextWithFns<'_>) + { + unsafe { current_context.fns().BindVertexArray(self.array) } + } +} + +#[derive(Debug)] +pub enum PrimitiveKind +{ + Triangles, +} + +impl PrimitiveKind +{ + fn into_gl(self) -> crate::sys::types::GLenum + { + match self { + Self::Triangles => crate::sys::TRIANGLES, + } + } +} + +#[derive(Debug, Clone, Copy)] +#[repr(u32)] +pub enum DataType +{ + Float = crate::sys::FLOAT, +} + +impl DataType +{ + fn size(self) -> crate::sys::types::GLint + { + match self { + Self::Float => const { cast_usize_to_c_int(size_of::()) }, + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum DrawError +{ + #[error("Start index value {value} is too large. Must be < {max_value}")] + StartIndexValueTooLarge + { + value: u32, max_value: u32 + }, + + #[error("Count value {value} is too large. Must be < {max_value}")] + CountValueTooLarge + { + value: u32, max_value: u32 + }, +} + +const fn cast_usize_to_c_int(num: usize) -> c_int +{ + assert!(num <= c_int::MAX.cast_unsigned() as usize); + + c_int::from_ne_bytes(shorten_byte_array(num.to_ne_bytes())) +} + +const fn shorten_byte_array( + src: [u8; SRC_LEN], +) -> [u8; DST_LEN] +{ + assert!(DST_LEN < SRC_LEN); + + let mut ret = [0; DST_LEN]; + + ret.copy_from_slice(src.split_at(DST_LEN).0); + + ret +} -- cgit v1.2.3-18-g5258