diff options
Diffstat (limited to 'src/win.rs')
-rw-r--r-- | src/win.rs | 866 |
1 files changed, 866 insertions, 0 deletions
diff --git a/src/win.rs b/src/win.rs new file mode 100644 index 0000000..781156d --- /dev/null +++ b/src/win.rs @@ -0,0 +1,866 @@ +// all OpenGL calls are done through the Window. +// this is because OpenGL is not thread safe. +use crate::sdl; +use gl::types::{GLchar, GLenum, GLint, GLsizei, GLuint}; +#[allow(unused_imports)] +use std::ffi::{c_char, c_int, c_uint, c_void, CStr, CString}; +use std::sync::Mutex; +use std::{fmt, mem}; +use mem::size_of; + +pub type AudioCallback = fn(sample_rate: u32, samples: &mut [f32]); + +struct AudioData { + callback: AudioCallback, + device: sdl::SDL_AudioDeviceID, + sample_rate: u32, +} + +pub struct Window { + sdlwin: *mut sdl::SDL_Window, + glctx: sdl::SDL_GLContext, + used_program: GLuint, + audio_data: Option<Box<AudioData>>, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Key { + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T, + U, + V, + W, + X, + Y, + Z, + N0, + N1, + N2, + N3, + N4, + N5, + N6, + N7, + N8, + N9, + Up, + Left, + Right, + Down, + Space, + Enter, + Escape, +} + +impl Key { + fn from_sdl(scancode: sdl::SDL_Scancode) -> Option<Self> { + use sdl::scancode::*; + Some(match scancode { + A => Key::A, + B => Key::B, + C => Key::C, + D => Key::D, + E => Key::E, + F => Key::F, + G => Key::G, + H => Key::H, + I => Key::I, + J => Key::J, + K => Key::K, + L => Key::L, + M => Key::M, + N => Key::N, + O => Key::O, + P => Key::P, + Q => Key::Q, + R => Key::R, + S => Key::S, + T => Key::T, + U => Key::U, + V => Key::V, + W => Key::W, + X => Key::X, + Y => Key::Y, + Z => Key::Z, + RETURN => Key::Enter, + SPACE => Key::Space, + N0 => Key::N0, + N1 => Key::N1, + N2 => Key::N2, + N3 => Key::N3, + N4 => Key::N4, + N5 => Key::N5, + N6 => Key::N6, + N7 => Key::N7, + N8 => Key::N8, + N9 => Key::N9, + UP => Key::Up, + LEFT => Key::Left, + RIGHT => Key::Right, + DOWN => Key::Down, + ESCAPE => Key::Escape, + _ => return None, + }) + } + + fn to_sdl(self) -> sdl::SDL_Scancode { + use sdl::scancode::*; + match self { + Key::A => A, + Key::B => B, + Key::C => C, + Key::D => D, + Key::E => E, + Key::F => F, + Key::G => G, + Key::H => H, + Key::I => I, + Key::J => J, + Key::K => K, + Key::L => L, + Key::M => M, + Key::N => N, + Key::O => O, + Key::P => P, + Key::Q => Q, + Key::R => R, + Key::S => S, + Key::T => T, + Key::U => U, + Key::V => V, + Key::W => W, + Key::X => X, + Key::Y => Y, + Key::Z => Z, + Key::Enter => RETURN, + Key::Space => SPACE, + Key::N0 => N0, + Key::N1 => N1, + Key::N2 => N2, + Key::N3 => N3, + Key::N4 => N4, + Key::N5 => N5, + Key::N6 => N6, + Key::N7 => N7, + Key::N8 => N8, + Key::N9 => N9, + Key::Up => UP, + Key::Left => LEFT, + Key::Right => RIGHT, + Key::Down => DOWN, + Key::Escape => ESCAPE, + } + } +} + +#[derive(Debug)] +pub enum Event { + Quit, + KeyDown(Key), + KeyUp(Key), +} + +pub fn display_error_message(message: &str) { + let result = unsafe { + sdl::show_simple_message_box(sdl::SDL_MESSAGEBOX_ERROR, "Error", message, 0 as _) + }; + if result.is_err() { + eprintln!("{}", message); + } +} + +#[derive(Clone, Copy)] +pub struct ColorF32 { + pub r: f32, + pub g: f32, + pub b: f32, + pub a: f32, +} + +#[derive(Clone, Copy)] +pub struct ColorU8 { + pub r: u8, + pub g: u8, + pub b: u8, + pub a: u8, +} + +impl fmt::Display for ColorU8 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "#{:02x}{:02x}{:02x}{:02x}", + self.r, self.g, self.b, self.a + ) + } +} + +impl fmt::Debug for ColorU8 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self) + } +} + +impl ColorU8 { + pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { + ColorU8 { r, g, b, a } + } + + pub const fn rgb(r: u8, g: u8, b: u8) -> Self { + ColorU8 { r, g, b, a: 255 } + } + + pub fn tint(&mut self, other: ColorU8) { + self.r = ((self.r as u32 * other.r as u32) >> 8) as u8; + self.g = ((self.g as u32 * other.g as u32) >> 8) as u8; + self.b = ((self.b as u32 * other.b as u32) >> 8) as u8; + self.a = ((self.a as u32 * other.a as u32) >> 8) as u8; + } + + pub fn from_bytes(bytes: &[u8]) -> Self { + assert!(bytes.len() >= 4); + Self { + r: bytes[0], + g: bytes[1], + b: bytes[2], + a: bytes[3], + } + } + + pub fn as_bytes(self, bytes: &mut [u8]) { + assert!(bytes.len() >= 4); + bytes[0] = self.r; + bytes[1] = self.g; + bytes[2] = self.b; + bytes[3] = self.a; + } + + pub fn as_tuple(self) -> (u8, u8, u8, u8) { + (self.r, self.g, self.b, self.a) + } +} + +impl From<u32> for ColorU8 { + fn from(color: u32) -> ColorU8 { + ColorU8::new( + (color >> 24) as u8, + (color >> 16) as u8, + (color >> 8) as u8, + color as u8, + ) + } +} + +impl ColorF32 { + pub const BLACK: Self = Self::rgb(0.0, 0.0, 0.0); + + pub const fn rgb(r: f32, g: f32, b: f32) -> Self { + ColorF32 { r, g, b, a: 1.0 } + } + + pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self { + ColorF32 { r, g, b, a } + } +} + +pub struct Shader { + id: GLuint, +} + +impl Shader { + fn new(r#type: GLenum, source: &str) -> Result<Self, String> { + let id = unsafe { gl::CreateShader(r#type) }; + let result = Self::new_with_id(id, r#type, source); + if result.is_err() { + unsafe { gl::DeleteShader(id) }; + } + result + } + + fn new_with_id(id: GLuint, r#type: GLenum, source: &str) -> Result<Self, String> { + if id == 0 { + return Err(format!("couldn't create shader (GL error {})", unsafe { + gl::GetError() + })); + } + + { + //set source + // @TODO(eventually): support for older versions of GLSL + let header = if r#type == gl::FRAGMENT_SHADER { "#version 130 +#define IN in +#define OUT out +#define gl_FragColor o_color +out vec4 o_color; +#line 1 +" } else { +"#version 130 +#define IN in +#define OUT out +#define ATTRIBUTE in +#line 1 +" +}; + let hdrptr = header.as_bytes().as_ptr() as *const GLchar; + let srcptr = source.as_bytes().as_ptr() as *const GLchar; + let sources = [hdrptr, srcptr]; + let lengths = [header.len() as GLint, source.len() as GLint]; + + let sources_ptr = &sources[0] as *const *const GLchar; + let lengths_ptr = &lengths[0] as *const GLint; + + unsafe { gl::ShaderSource(id, sources.len() as _, sources_ptr, lengths_ptr) }; + } + + unsafe { gl::CompileShader(id) }; + { + //check log + let mut log = [0u8; 1024]; + let mut len: GLsizei = 0; + let logp = &mut log as *mut u8 as *mut GLchar; + let lenp = &mut len as *mut GLsizei; + unsafe { gl::GetShaderInfoLog(id, log.len() as GLsizei, lenp, logp) }; + if len > 0 { + eprintln!("{}", String::from_utf8_lossy(&log[..len as usize])); + } + } + { + let mut status: GLint = 0; + unsafe { gl::GetShaderiv(id, gl::COMPILE_STATUS, (&mut status) as _) }; + if status == 0 { + return Err(format!("failed to compile")); + } + } + + Ok(Self { id }) + } +} + +impl Drop for Shader { + fn drop(&mut self) { + unsafe { gl::DeleteShader(self.id) }; + } +} + +pub struct Program { + id: GLuint, +} + +impl Program { + fn new(shaders: &[Shader]) -> Result<Self, String> { + let id = unsafe { gl::CreateProgram() }; + let result = Self::new_with_id(id, shaders); + if result.is_err() { + unsafe { gl::DeleteShader(id) }; + } + result + } + + fn new_with_id(id: GLuint, shaders: &[Shader]) -> Result<Self, String> { + for shader in shaders { + unsafe { gl::AttachShader(id, shader.id) }; + } + unsafe { gl::LinkProgram(id) }; + { + // check log + let mut log = [0u8; 1024]; + let mut len: GLsizei = 0; + let logp = &mut log as *mut u8 as *mut GLchar; + let lenp = &mut len as *mut GLsizei; + unsafe { gl::GetProgramInfoLog(id, log.len() as GLsizei, lenp, logp) }; + if len > 0 { + eprintln!("{}", String::from_utf8_lossy(&log[..len as usize])); + } + } + + { + let mut status: GLint = 0; + unsafe { gl::GetProgramiv(id, gl::LINK_STATUS, (&mut status) as _) }; + if status == 0 { + return Err(format!("failed to link")); + } + } + + Ok(Self { id }) + } +} + +impl Drop for Program { + fn drop(&mut self) { + unsafe { gl::DeleteProgram(self.id) }; + } +} + +pub struct Buffer { + id: GLuint, + stride: u32, + count: u32, +} + +impl Buffer { + fn new() -> Self { + let mut id = 0; + unsafe { gl::CreateBuffers(1, &mut id as *mut GLuint) }; + Self { + id, + stride: 0, + count: 0, + } + } + + fn bind(&self) { + unsafe { gl::BindBuffer(gl::ARRAY_BUFFER, self.id) }; + } + + fn set_data<T>(&mut self, data: &[T]) { + unsafe { + gl::BindBuffer(gl::ARRAY_BUFFER, self.id); + } + self.count = data.len() as u32; + self.stride = mem::size_of::<T>() as u32; + + unsafe { + gl::BufferData( + gl::ARRAY_BUFFER, + (self.count * self.stride) as _, + data.as_ptr() as _, + gl::STATIC_DRAW, + ); + } + } +} + +impl Drop for Buffer { + fn drop(&mut self) { + unsafe { gl::DeleteBuffers(1, &self.id as *const GLuint) }; + } +} + +pub struct VertexArray { + buffer: Buffer, + id: GLuint, + program: GLuint, +} + +impl VertexArray { + fn new(buffer: Buffer, program: &Program) -> Self { + let mut id: GLuint = 0; + + unsafe { gl::GenVertexArrays(1, &mut id as *mut GLuint) }; + + Self { id, buffer, program: program.id } + } + + fn bind(&self) { + unsafe { gl::BindVertexArray(self.id) }; + } + + fn attribnf(&mut self, n: u8, name: &str, offset: usize) -> bool { + let Ok(cstring) = CString::new(name) else { return false }; + let cstr = cstring.as_ptr() as *const GLchar; + let loc = unsafe { gl::GetAttribLocation(self.program, cstr) }; + let Ok(loc) = loc.try_into() else { return false }; + + if offset + usize::from(n) * size_of::<f32>() > self.buffer.stride as usize { + // offset too large + return false; + } + + self.bind(); + self.buffer.bind(); + unsafe { + gl::VertexAttribPointer(loc, n.into(), gl::FLOAT, 0, self.buffer.stride as _, offset as _) + }; + unsafe { gl::EnableVertexAttribArray(loc) }; + true + } + + fn draw(&self) { + self.bind(); + unsafe { gl::DrawArrays(gl::TRIANGLES, 0, self.buffer.count as i32) }; + } +} + +impl Drop for VertexArray { + fn drop(&mut self) { + unsafe { gl::DeleteVertexArrays(1, &self.id as *const GLuint) }; + } +} + +#[cfg(debug_assertions)] +extern "system" fn gl_message_callback( + _source: GLenum, + _type: GLenum, + _id: c_uint, + severity: GLenum, + _length: GLsizei, + message: *const c_char, + _user_param: *mut c_void, +) { + let message = String::from_utf8_lossy(unsafe { CStr::from_ptr(message) }.to_bytes()); + if severity == gl::DEBUG_SEVERITY_NOTIFICATION { + return; + } + println!("Message from opengl: {message}"); +} + +pub struct Texture { + id: GLuint, + params: TextureParams, +} + +impl Drop for Texture { + fn drop(&mut self) { + unsafe { gl::DeleteTextures(1, (&self.id) as *const GLuint) }; + } +} + +#[derive(Copy, Clone)] +pub enum TextureFilter { + Nearest, + Linear, +} + +impl TextureFilter { + fn to_gl(self) -> GLint { + use TextureFilter::*; + match self { + Nearest => gl::NEAREST as _, + Linear => gl::LINEAR as _, + } + } +} + +#[derive(Clone)] +pub struct TextureParams { + pub min_filter: TextureFilter, + pub mag_filter: TextureFilter, +} + +impl Default for TextureParams { + fn default() -> Self { + Self { + min_filter: TextureFilter::Nearest, + mag_filter: TextureFilter::Linear, + } + } +} + +impl Window { + pub fn new(title: &str, width: i32, height: i32, shown: bool) -> Result<Self, String> { + { + static WINDOW_CREATED: Mutex<bool> = Mutex::new(false); + let guard = WINDOW_CREATED.lock(); + match guard { + Err(_) => return Err("couldn't lock mutex.".to_string()), + Ok(x) if *x => return Err("window already created".to_string()), + Ok(mut x) => *x = true, + } + } + + unsafe { + sdl::set_hint("SDL_NO_SIGNAL_HANDLERS", "1"); // don't replace Ctrl+C, TERM with quit event + sdl::init()?; + #[cfg(debug_assertions)] + { + sdl::gl_set_context_version(4, 3); + sdl::gl_set_attribute(sdl::SDL_GL_CONTEXT_FLAGS, sdl::SDL_GL_CONTEXT_DEBUG_FLAG); + } + #[cfg(not(debug_assertions))] + sdl::gl_set_context_version(3, 0); + } + let mut flags = sdl::SDL_WINDOW_OPENGL; + if !shown { + flags |= sdl::SDL_WINDOW_HIDDEN; + } + let sdlwin = unsafe { sdl::create_window(title, width, height, flags) }?; + let ctx = unsafe { sdl::gl_create_context(sdlwin) }?; + gl::load_with(|name| unsafe { sdl::gl_get_proc_address(name) }); + unsafe { + sdl::gl_set_swap_interval(1); + let mut flags: GLint = 0; + gl::GetIntegerv(gl::CONTEXT_FLAGS, (&mut flags) as _); + #[cfg(debug_assertions)] + if (flags as GLuint & gl::CONTEXT_FLAG_DEBUG_BIT) != 0 { + gl::DebugMessageCallback(Some(gl_message_callback), 0 as _); + gl::DebugMessageControl( + gl::DONT_CARE, + gl::DONT_CARE, + gl::DONT_CARE, + 0, + 0 as _, + gl::TRUE, + ); + gl::Enable(gl::DEBUG_OUTPUT); + gl::Enable(gl::DEBUG_OUTPUT_SYNCHRONOUS); + } + } + + Ok(Window { + sdlwin, + glctx: ctx, + used_program: 0, + audio_data: None, + }) + } + + pub fn show(&mut self) { + unsafe { sdl::show_window(self.sdlwin) }; + } + + pub fn create_program( + &mut self, + source_vshader: &str, + source_fshader: &str, + ) -> Result<Program, String> { + let vshader = Shader::new(gl::VERTEX_SHADER, source_vshader)?; + let fshader = Shader::new(gl::FRAGMENT_SHADER, source_fshader)?; + Program::new(&[vshader, fshader]) + } + + pub fn create_buffer(&mut self) -> Buffer { + Buffer::new() + } + + pub fn set_buffer_data<T>(&mut self, buffer: &mut Buffer, data: &[T]) { + buffer.set_data(data); + } + + pub fn create_vertex_array(&mut self, buffer: Buffer, program: &Program) -> VertexArray { + VertexArray::new(buffer, program) + } + + fn array_attribnf(&mut self, array: &mut VertexArray, n: u8, name: &str, offset: usize) -> bool { + array.attribnf(n, name, offset) + } + + pub fn array_attrib2f(&mut self, array: &mut VertexArray, name: &str, offset: usize) -> bool { + self.array_attribnf(array, 2, name, offset) + } + + pub fn array_attrib3f(&mut self, array: &mut VertexArray, name: &str, offset: usize) -> bool { + self.array_attribnf(array, 3, name, offset) + } + + pub fn array_attrib4f(&mut self, array: &mut VertexArray, name: &str, offset: usize) -> bool { + self.array_attribnf(array, 4, name, offset) + } + + pub fn size(&mut self) -> (i32, i32) { + let mut x = 0; + let mut y = 0; + unsafe { sdl::get_window_size(self.sdlwin, &mut x, &mut y) }; + (x, y) + } + + pub fn viewport(&mut self, x: i32, y: i32, w: i32, h: i32) { + unsafe { gl::Viewport(x, y, w, h) }; + } + + pub fn next_event(&mut self) -> Option<Event> { + loop { + let sdl = unsafe { sdl::poll_event() }?; + let r#type = unsafe { sdl.r#type }; + match r#type { + sdl::SDL_QUIT => return Some(Event::Quit), + sdl::SDL_KEYDOWN | sdl::SDL_KEYUP => { + let scancode = unsafe { sdl.key }.keysym.scancode; + if let Some(k) = Key::from_sdl(scancode) { + if r#type == sdl::SDL_KEYDOWN { + return Some(Event::KeyDown(k)); + } else { + return Some(Event::KeyUp(k)); + } + } + } + _ => {} + } + } + } + + pub fn clear_screen(&mut self, color: ColorF32) { + unsafe { + gl::ClearColor(color.r, color.g, color.b, color.a); + gl::Clear(gl::COLOR_BUFFER_BIT); + } + } + + pub fn create_rgba_texture(&mut self, params: &TextureParams) -> Texture { + let mut id: GLuint = 0; + unsafe { + gl::GenTextures(1, (&mut id) as *mut GLuint); + } + Texture { id, params: params.clone() } + } + + pub fn set_texture_data( + &mut self, + texture: &mut Texture, + data: &[u8], + width: usize, + height: usize, + ) -> Result<(), String> { + let width = width as GLsizei; + let height = height as GLsizei; + let expected_len = 4 * width * height; + if data.len() as GLsizei != expected_len { + return Err(format!( + "bad data length (expected {}, got {})", + expected_len, + data.len() + )); + } + let params = &texture.params; + unsafe { + gl::BindTexture(gl::TEXTURE_2D, texture.id); + gl::TexImage2D( + gl::TEXTURE_2D, + 0, + gl::RGBA as _, + width, + height, + 0, + gl::RGBA, + gl::UNSIGNED_BYTE, + data.as_ptr() as _, + ); + gl::TexParameteri( + gl::TEXTURE_2D, + gl::TEXTURE_MIN_FILTER, + params.min_filter.to_gl(), + ); + gl::TexParameteri( + gl::TEXTURE_2D, + gl::TEXTURE_MAG_FILTER, + params.mag_filter.to_gl(), + ); + } + Ok(()) + } + + pub fn set_audio_callback(&mut self, callback: AudioCallback) -> Result<(), String> { + if self.audio_data.is_some() { + return Err("audio callback already set.".into()); + } + + extern "C" fn sdl_callback(userdata: *mut c_void, stream: *mut u8, len: c_int) { + let data = unsafe { (userdata as *const AudioData).as_ref() }.unwrap(); + // this should never panick, since SDL shouldn't pass us a negative length. + let samples: usize = (len / 4).try_into().unwrap(); + let slice = unsafe { std::slice::from_raw_parts_mut(stream as *mut f32, samples) }; + slice.fill(0.0); + (data.callback)(data.sample_rate, slice); + } + + let mut data = Box::new(AudioData { + callback, + device: 0, + sample_rate: 0, + }); + + let desired = sdl::SDL_AudioSpec::new( + sdl_callback, + &*data as *const AudioData as *mut c_void, + 2, + sdl::AUDIO_F32, + 44100, + 4096, + ); + + let (obtained, id) = unsafe { + sdl::open_audio_device( + None, + false, + &desired, + sdl::SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | sdl::SDL_AUDIO_ALLOW_SAMPLES_CHANGE, + )? + }; + data.sample_rate = obtained.freq.try_into().unwrap(); + data.device = id; + + self.audio_data = Some(data); + + unsafe { sdl::pause_audio_device(id, false) }; + + Ok(()) + } + + pub fn use_program(&mut self, program: &Program) { + if self.used_program != program.id { + unsafe { gl::UseProgram(program.id) }; + self.used_program = program.id; + } + } + + fn get_uniform_location(&self, name: &str) -> Option<GLint> { + if self.used_program == 0 { + return None; + } + let cstring = CString::new(name).ok()?; + let cstr = cstring.as_ptr() as *const GLchar; + let loc = unsafe { gl::GetUniformLocation(self.used_program, cstr) }; + if loc == -1 { + None + } else { + Some(loc) + } + } + + pub fn active_texture(&mut self, slot: u32, texture: &Texture) { + unsafe { + gl::ActiveTexture(gl::TEXTURE0 + slot); + gl::BindTexture(gl::TEXTURE_2D, texture.id); + } + } + + pub fn uniform_texture(&mut self, name: &str, slot: u32) { + let loc = self.get_uniform_location(name).unwrap_or(-1); + unsafe { + gl::Uniform1i(loc, slot as i32); + } + } + + pub fn draw_array(&mut self, array: &VertexArray) { + array.draw(); + } + + #[allow(dead_code)] + pub fn is_key_down(&mut self, key: Key) -> bool { + let kbd_state = unsafe { sdl::get_keyboard_state() }; + kbd_state[key.to_sdl() as usize] != 0 + } + + pub fn swap(&mut self) { + unsafe { sdl::gl_swap_window(self.sdlwin) }; + } +} + +impl Drop for Window { + fn drop(&mut self) { + unsafe { + if let Some(audio_data) = &self.audio_data { + sdl::close_audio_device(audio_data.device); + } + sdl::gl_delete_context(self.glctx); + sdl::destroy_window(self.sdlwin); + } + } +} |