rust/lib-hedgewars-engine/src/render/gl.rs
author alfadur
Fri, 13 Nov 2020 20:54:00 +0300
changeset 15762 84c07aa94b61
parent 15761 e7eb0cd5b0e4
child 15764 b10bbfb2b354
permissions -rw-r--r--
start drawing gears

use integral_geometry::{Rect, Size};

use std::{ffi, ffi::CString, mem, num::NonZeroU32, ptr, slice};

#[derive(Default)]
pub struct PipelineState {
    blending: bool,
}

impl PipelineState {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn with_blend(mut self) -> Self {
        unsafe {
            gl::Enable(gl::BLEND);
            gl::BlendFunc(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA);
        }
        self.blending = true;
        self
    }
}

impl Drop for PipelineState {
    fn drop(&mut self) {
        if self.blending {
            unsafe { gl::Disable(gl::BLEND) }
        }
    }
}

#[derive(Debug)]
pub struct Texture2D {
    handle: Option<NonZeroU32>,
    size: Size,
}

impl Drop for Texture2D {
    fn drop(&mut self) {
        if let Some(handle) = self.handle {
            unsafe {
                gl::DeleteTextures(1, &handle.get());
            }
        }
    }
}

fn new_texture() -> Option<NonZeroU32> {
    let mut handle = 0;
    unsafe {
        gl::GenTextures(1, &mut handle);
    }
    NonZeroU32::new(handle)
}

fn tex_params(filter: TextureFilter) {
    unsafe {
        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as i32);
        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as i32);
        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, filter as i32);
        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, filter as i32);
    }
}

#[derive(Clone, Copy, Debug)]
pub enum TextureFormat {
    Rgba = gl::RGBA as isize,
}

#[derive(Clone, Copy, Debug)]
pub enum TextureInternalFormat {
    Rgba8 = gl::RGBA as isize,
}

#[derive(Clone, Copy, Debug)]
pub enum TextureDataType {
    UnsignedByte = gl::UNSIGNED_BYTE as isize,
}

#[derive(Clone, Copy, Debug)]
pub enum TextureFilter {
    Nearest = gl::NEAREST as isize,
    Linear = gl::LINEAR as isize,
}

#[inline]
fn get_u32(value: Option<NonZeroU32>) -> u32 {
    value.map_or(0, |v| v.get())
}

fn is_out_of_bounds(data: &[u8], data_stride: Option<NonZeroU32>, texture_size: Size) -> bool {
    let data_stride = get_u32(data_stride);
    data_stride == 0 && texture_size.area() * 4 > data.len()
        || data_stride != 0
            && texture_size.width > data_stride as usize
            && (texture_size.height * data_stride as usize) * 4 > data.len()
}

impl Texture2D {
    pub fn new(size: Size, internal_format: TextureInternalFormat, filter: TextureFilter) -> Self {
        if let Some(handle) = new_texture() {
            unsafe {
                gl::BindTexture(gl::TEXTURE_2D, handle.get());
                gl::TexImage2D(
                    gl::TEXTURE_2D,
                    0,
                    internal_format as i32,
                    size.width as i32,
                    size.height as i32,
                    0,
                    TextureFormat::Rgba as u32,
                    TextureDataType::UnsignedByte as u32,
                    std::ptr::null(),
                )
            }

            tex_params(filter);
            Self {
                handle: Some(handle),
                size,
            }
        } else {
            Self { handle: None, size }
        }
    }

    pub fn with_data(
        data: &[u8],
        data_stride: Option<NonZeroU32>,
        size: Size,
        internal_format: TextureInternalFormat,
        format: TextureFormat,
        data_type: TextureDataType,
        filter: TextureFilter,
    ) -> Self {
        if is_out_of_bounds(data, data_stride, size) {
            return Self { handle: None, size };
        }

        if let Some(handle) = new_texture() {
            unsafe {
                gl::BindTexture(gl::TEXTURE_2D, handle.get());
                gl::PixelStorei(gl::UNPACK_ROW_LENGTH, get_u32(data_stride) as i32);
                gl::TexImage2D(
                    gl::TEXTURE_2D,
                    0,
                    internal_format as i32,
                    size.width as i32,
                    size.height as i32,
                    0,
                    format as u32,
                    data_type as u32,
                    data.as_ptr() as *const _,
                )
            }

            tex_params(filter);
            Self {
                handle: Some(handle),
                size,
            }
        } else {
            Self { handle: None, size }
        }
    }

    pub fn update(
        &self,
        region: Rect,
        data: &[u8],
        data_stride: Option<NonZeroU32>,
        format: TextureFormat,
        data_type: TextureDataType,
    ) {
        if is_out_of_bounds(data, data_stride, self.size) {
            return;
        }

        if let Some(handle) = self.handle {
            unsafe {
                gl::BindTexture(gl::TEXTURE_2D, handle.get());
                gl::PixelStorei(gl::UNPACK_ROW_LENGTH, get_u32(data_stride) as i32);
                gl::TexSubImage2D(
                    gl::TEXTURE_2D,
                    0,
                    region.left(),
                    region.top(),
                    region.width() as i32,
                    region.height() as i32,
                    format as u32,
                    data_type as u32,
                    data.as_ptr() as *const _,
                );
            }
        }
    }

    pub fn retrieve(&self, data: &mut [u8]) {
        if self.size.area() * 4 > data.len() {
            return;
        }

        if let Some(handle) = self.handle {
            unsafe {
                gl::BindTexture(gl::TEXTURE_2D, handle.get());
                gl::GetTexImage(
                    gl::TEXTURE_2D,
                    0,
                    TextureFormat::Rgba as u32,
                    TextureDataType::UnsignedByte as u32,
                    data.as_mut_ptr() as *mut _,
                );
            }
        }
    }
}

#[derive(Clone, Copy, Debug)]
#[repr(u32)]
pub enum BufferType {
    Array = gl::ARRAY_BUFFER,
    ElementArray = gl::ELEMENT_ARRAY_BUFFER,
}

#[derive(Clone, Copy, Debug)]
#[repr(u32)]
pub enum BufferUsage {
    DynamicDraw = gl::DYNAMIC_DRAW,
}

#[derive(Debug)]
pub struct Buffer {
    pub handle: Option<NonZeroU32>,
    pub buffer_type: BufferType,
    pub usage: BufferUsage,
}

impl Buffer {
    pub fn empty(buffer_type: BufferType, usage: BufferUsage) -> Buffer {
        let mut buffer = 0;

        unsafe {
            gl::GenBuffers(1, &mut buffer);
        }

        Buffer {
            handle: NonZeroU32::new(buffer),
            buffer_type: buffer_type,
            usage,
        }
    }

    fn with_data(buffer_type: BufferType, usage: BufferUsage, data: &[u8]) -> Buffer {
        let mut buffer = 0;

        unsafe {
            gl::GenBuffers(1, &mut buffer);
            if buffer != 0 {
                gl::BindBuffer(buffer_type as u32, buffer);
                gl::BufferData(
                    buffer_type as u32,
                    data.len() as isize,
                    data.as_ptr() as _,
                    usage as u32,
                );
            }
        }

        Buffer {
            handle: NonZeroU32::new(buffer),
            buffer_type,
            usage,
        }
    }

    pub fn ty(&self) -> BufferType {
        self.buffer_type
    }

    pub fn handle(&self) -> Option<NonZeroU32> {
        self.handle
    }

    pub fn write_typed<T>(&self, data: &[T]) {
        if let Some(handle) = self.handle {
            unsafe {
                gl::BindBuffer(self.buffer_type as u32, handle.get());
                gl::BufferData(
                    self.buffer_type as u32,
                    (data.len() * mem::size_of::<T>()) as isize,
                    data.as_ptr() as *const _,
                    self.usage as u32,
                );
            }
        }
    }

    pub fn write(&self, data: &[u8]) {
        if let Some(handle) = self.handle {
            unsafe {
                gl::BindBuffer(self.buffer_type as u32, handle.get());
                gl::BufferData(
                    self.buffer_type as u32,
                    data.len() as isize,
                    data.as_ptr() as *const _,
                    self.usage as u32,
                );
            }
        }
    }
}

impl Drop for Buffer {
    fn drop(&mut self) {
        if let Some(handle) = self.handle {
            let handle = handle.get();
            unsafe {
                gl::DeleteBuffers(1, &handle);
            }
        }
    }
}

#[derive(Debug)]
pub enum VariableBinding<'a> {
    Attribute(&'a str, u32),
    Uniform(&'a str, u32),
    UniformBlock(&'a str, u32),
    Sampler(&'a str, u32),
}

#[derive(Debug)]
pub struct Shader {
    pub program: u32,
}

impl Drop for Shader {
    fn drop(&mut self) {
        unsafe {
            gl::DeleteProgram(self.program);
        }
    }
}

impl Shader {
    pub fn new<'a>(
        vs: &str,
        ps: Option<&str>,
        bindings: &[VariableBinding<'a>],
    ) -> Result<Self, String> {
        unsafe fn compile_shader(shader_type: u32, shader_code: &str) -> Result<u32, String> {
            let shader = gl::CreateShader(shader_type);
            let len = shader_code.len() as i32;
            let code_strings = shader_code.as_ptr() as *const i8;
            gl::ShaderSource(shader, 1, &code_strings, &len);
            gl::CompileShader(shader);

            let mut success = 0i32;
            gl::GetShaderiv(shader, gl::COMPILE_STATUS, &mut success as _);

            if success == gl::FALSE as i32 {
                let mut log_size = 0i32;
                gl::GetShaderiv(shader, gl::INFO_LOG_LENGTH, &mut log_size as _);

                let mut log = vec![0u8; log_size as usize];
                gl::GetShaderInfoLog(shader, log_size, ptr::null_mut(), log.as_mut_ptr() as _);

                gl::DeleteShader(shader);
                Err(String::from_utf8_unchecked(log))
            } else {
                Ok(shader)
            }
        }

        let vs = unsafe { compile_shader(gl::VERTEX_SHADER, vs)? };
        let ps = if let Some(ps) = ps {
            Some(unsafe { compile_shader(gl::FRAGMENT_SHADER, ps)? })
        } else {
            None
        };

        unsafe {
            let program = gl::CreateProgram();

            gl::AttachShader(program, vs);
            if let Some(ps) = ps {
                gl::AttachShader(program, ps);
            }

            for bind in bindings {
                match bind {
                    &VariableBinding::Attribute(ref name, id) => {
                        let c_str = CString::new(name.as_bytes()).unwrap();
                        gl::BindAttribLocation(
                            program,
                            id,
                            c_str.to_bytes_with_nul().as_ptr() as *const _,
                        );
                    }
                    _ => {}
                }
            }

            gl::LinkProgram(program);

            let mut success = 0i32;
            gl::GetProgramiv(program, gl::LINK_STATUS, &mut success);
            if success == gl::FALSE as i32 {
                let mut log_size = 0i32;
                gl::GetProgramiv(program, gl::INFO_LOG_LENGTH, &mut log_size as _);

                let mut log = vec![0u8; log_size as usize];
                gl::GetProgramInfoLog(program, log_size, ptr::null_mut(), log.as_mut_ptr() as _);

                gl::DeleteProgram(program);
                return Err(String::from_utf8_unchecked(log));
            }

            gl::UseProgram(program);

            for bind in bindings {
                match bind {
                    VariableBinding::Uniform(name, id) => {
                        let c_str = CString::new(name.as_bytes()).unwrap();
                        let index = gl::GetUniformLocation(
                            program,
                            c_str.to_bytes_with_nul().as_ptr() as *const _,
                        );

                        // TODO: impl for block?
                    }
                    VariableBinding::UniformBlock(name, id) => {
                        let c_str = CString::new(name.as_bytes()).unwrap();
                        let index = gl::GetUniformBlockIndex(
                            program,
                            c_str.to_bytes_with_nul().as_ptr() as *const _,
                        );

                        gl::UniformBlockBinding(program, index, *id);
                    }
                    VariableBinding::Sampler(name, id) => {
                        let c_str = CString::new(name.as_bytes()).unwrap();
                        let index = gl::GetUniformLocation(
                            program,
                            c_str.to_bytes_with_nul().as_ptr() as *const _,
                        );

                        gl::Uniform1i(index, *id as i32);
                    }
                    _ => {}
                }
            }

            Ok(Shader { program })
        }
    }

    pub fn bind(&self) {
        unsafe {
            gl::UseProgram(self.program);
        }
    }

    pub fn set_matrix(&self, name: &str, matrix: *const f32) {
        unsafe {
            let c_str = CString::new(name).unwrap();
            let index = gl::GetUniformLocation(
                self.program,
                c_str.to_bytes_with_nul().as_ptr() as *const _,
            );

            gl::UniformMatrix4fv(index, 1, gl::FALSE, matrix);
        }
    }

    pub fn bind_texture_2d(&self, index: u32, texture: &Texture2D) {
        self.bind();

        if let Some(handle) = texture.handle {
            unsafe {
                gl::ActiveTexture(gl::TEXTURE0 + index);
                gl::BindTexture(gl::TEXTURE_2D, handle.get());
            }
        }
    }
}

pub enum InputFormat {
    Float(u32, bool),
    Integer(u32),
}

pub struct InputElement {
    pub shader_slot: u32,
    pub buffer_slot: u32,
    pub format: InputFormat,
    pub components: u32,
    pub stride: u32,
    pub offset: u32,
}

// TODO:
pub struct InputLayout {
    pub elements: Vec<InputElement>,
}

pub struct LayoutGuard {
    vao: u32,
}

impl Drop for LayoutGuard {
    fn drop(&mut self) {
        unsafe {
            gl::DeleteVertexArrays(1, [self.vao].as_ptr());
            gl::BindVertexArray(0);
        }
    }
}

impl InputLayout {
    pub fn new(elements: Vec<InputElement>) -> Self {
        InputLayout { elements }
    }

    pub fn bind(
        &mut self,
        buffers: &[(u32, &Buffer)],
        index_buffer: Option<&Buffer>,
    ) -> LayoutGuard {
        let mut vao = 0;

        unsafe {
            gl::GenVertexArrays(1, &mut vao);
            gl::BindVertexArray(vao);
        }

        for &(slot, ref buffer) in buffers {
            if let Some(handle) = buffer.handle() {
                unsafe {
                    gl::BindBuffer(buffer.ty() as u32, handle.get());
                }
            }

            for attr in self.elements.iter().filter(|a| a.buffer_slot == slot) {
                unsafe {
                    gl::EnableVertexAttribArray(attr.shader_slot);
                    match attr.format {
                        InputFormat::Float(fmt, normalized) => {
                            gl::VertexAttribPointer(
                                attr.shader_slot,
                                attr.components as i32,
                                fmt,
                                if normalized { gl::TRUE } else { gl::FALSE },
                                attr.stride as i32,
                                attr.offset as *const _,
                            );
                        }
                        InputFormat::Integer(fmt) => {
                            gl::VertexAttribIPointer(
                                attr.shader_slot,
                                attr.components as i32,
                                fmt,
                                attr.stride as i32,
                                attr.offset as *const _,
                            );
                        }
                    }
                }
            }
        }

        if let Some(buf) = index_buffer {
            if let Some(handle) = buf.handle() {
                unsafe {
                    gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, handle.get());
                }
            }
        }

        LayoutGuard { vao }
    }
}