crow 0.7.2

A pixel perfect 2D rendering engine
Documentation
use std::{ffi::c_void, ptr};

use gl::types::*;

use image::RgbaImage;

use crate::{backend::Backend, DrawConfig, NewTextureError, UnwrapBug};

#[derive(Debug)]
pub struct RawTexture {
    pub id: GLuint,
    pub framebuffer_id: GLuint,
    pub depth_id: GLuint,
    pub dimensions: (u32, u32),
    pub has_framebuffer: bool,
}

impl Drop for RawTexture {
    fn drop(&mut self) {
        // SAFETY: `n` is `1` for all functions
        if self.has_framebuffer {
            unsafe { gl::DeleteFramebuffers(1, &self.framebuffer_id) }
            unsafe { gl::DeleteRenderbuffers(1, &self.depth_id) }
        }
        unsafe { gl::DeleteTextures(1, &self.id) }
    }
}

impl RawTexture {
    fn internal_new(
        backend: &mut Backend,
        dimensions: (u32, u32),
        data: *const c_void,
    ) -> Result<RawTexture, NewTextureError> {
        let (max_width, max_height) = backend.constants().max_texture_size;
        if (dimensions.0 == 0 || dimensions.1 == 0)
            || (dimensions.0 > max_width || dimensions.1 > max_height)
        {
            return Err(NewTextureError::InvalidTextureSize {
                width: dimensions.0,
                height: dimensions.1,
            });
        }

        info!(
            "Creating RawTexture with dimensions: {}x{}",
            dimensions.0, dimensions.1
        );

        let mut id = 0;
        unsafe {
            // SAFETY: `n` is one.
            gl::GenTextures(1, &mut id);
            backend.state.update_texture(id);

            // TODO: consider using `gl::CLAMP_TO_BORDER` with an invisible border instead.

            // SAFETY:
            // `gl::TEXTURE_2D` is a valid target
            // `gl::TEXTUREWRAP_(S|T)` and `gl::TEXTURE_(MIN|MAG)_FILTER` are valid `pname`
            // `gl::CLAMP_TO_EDGE` is a valid `param` for `gl::TEXTURE_WRAP_(S|T)`
            // `gl::NEAREST` is a valid `param` for `gl::TEXTURE_(MIN|MAG)_FILTER`
            gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as _);
            gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as _);
            gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::NEAREST as _);
            gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::NEAREST as _);

            // SAFETY:
            // `gl::TEXTURE_2D` is a valid `target`
            // `gl::UNSIGNED_BYTE` is a valid `type` constant
            // `width` and `height` are both in the range `0..=GL_MAX_TEXTURE_SIZE`
            // `gl::RGBA8` is a valid sized `internalformat`
            // `level` and `border` are 0
            // We never bind something to `GL_PIXEL_UNPACK_BUFFER`
            gl::TexImage2D(
                gl::TEXTURE_2D,
                0,
                gl::RGBA8 as _,
                dimensions.0 as _,
                dimensions.1 as _,
                0,
                gl::RGBA,
                gl::UNSIGNED_BYTE,
                data,
            );
        }

        Ok(Self {
            id,
            framebuffer_id: 0,
            depth_id: 0,
            dimensions,
            has_framebuffer: false,
        })
    }

    pub fn new(
        backend: &mut Backend,
        dimensions: (u32, u32),
    ) -> Result<RawTexture, NewTextureError> {
        Self::internal_new(backend, dimensions, ptr::null())
    }

    pub fn from_image(
        backend: &mut Backend,
        image: RgbaImage,
    ) -> Result<RawTexture, NewTextureError> {
        let dimensions = image.dimensions();
        // open gl presents images upside down,
        // we therefore flip it to get the desired output.
        let reversed_data: Vec<u8> = image
            .into_raw()
            .chunks(dimensions.0 as usize * 4)
            .rev()
            .flat_map(|row| row.iter())
            .copied()
            .collect();

        Self::internal_new(backend, dimensions, reversed_data.as_ptr() as *const _)
    }

    pub fn add_framebuffer(&mut self, backend: &mut Backend) {
        assert!(!self.has_framebuffer);
        let mut buffer = 0;
        let mut depth = 0;

        unsafe {
            // SAFETY: `n` is 1
            gl::GenFramebuffers(1, &mut buffer);

            backend.state.update_framebuffer(buffer);

            // SAFETY:
            // `gl::FRAMEBUFFER` is a valid `target`
            // We just bound `buffer` to `target` meaning that buffer is not zero
            // `gl::COLOR_ATTACHMENT0` is a valid `attachment`
            // `self.id` is a valid `texture` which supports the `level` zero.
            // `self.id` is a `gl::TEXTURE_2D`
            gl::FramebufferTexture(gl::FRAMEBUFFER, gl::COLOR_ATTACHMENT0, self.id, 0);

            // SAFETY: `n` is 1
            gl::GenRenderbuffers(1, &mut depth);

            // SAFETY:
            // `target` is `gl::RENDERBUFFER`
            // `depth` was returned from `gl::GenRenderbuffers`
            gl::BindRenderbuffer(gl::RENDERBUFFER, depth);

            // SAFETY:
            // `target` is `gl::RENDERBUFFER`
            // `width` and `height` in the range `0..=gl::MAX_RENDERBUFFER_SIZE`
            // `gl::DEPTH_COMPONENT16` is a depth-renderable format
            gl::RenderbufferStorage(
                gl::RENDERBUFFER,
                gl::DEPTH_COMPONENT16,
                self.dimensions.0 as _,
                self.dimensions.1 as _,
            );
            // check if GL is out of memory
            let gl_error = gl::GetError();
            match gl_error {
                gl::NO_ERROR => (),
                gl::OUT_OF_MEMORY => {
                    // TODO: OpenGl is now in an undefined state,
                    // consider aborting instead, as it is possible
                    // to catch a panic
                    panic!("OpenGl is out of memory and in an invalid state");
                }
                e => bug!("unexpected error: {}", e),
            }

            // SAFETY:
            // `gl::FRAMEBUFFER` is a valid `target`
            // We just bound `buffer` to `target` meaning that buffer is not zero
            // `gl::DEPTH_ATTACHMENT` is a valid `attachment`
            // the `renderbuffertarget` is `gl::RENDERBUFFER`
            // `depth` has type `gl::RENDERBUFFER` and was returned from `gl::GenRenderbuffers`
            gl::FramebufferRenderbuffer(
                gl::FRAMEBUFFER,
                gl::DEPTH_ATTACHMENT,
                gl::RENDERBUFFER,
                depth,
            );

            // SAFETY:
            // `gl::COLOR_ATTACHMENT0` is an accepted value
            // the current framebuffer is not the default
            // `n` is one
            // `gl::COLOR_ATTACHMENT0` has been added to the current framebuffer
            gl::DrawBuffers(1, &gl::COLOR_ATTACHMENT0);

            // ATTACHMENT COMPLETENESS:
            // the source object still exists and did not change its type
            // image size is not zero or greater than GL_MAX_FRAMEBUFFER_(WIDTH|HEIGHT)
            //      [if the extension `ARB_framebuffer_no_attachments` is used]
            // no samples are attached
            // FRAMEBUFFER COMPLETENESS:
            // all attachments are ATTACHMENT COMPLETE
            // exactly one image is attached
            // the draw buffer has an image attached

            // SAFETY:
            // `gl::FRAMEBUFFER` is a valid `target`
            if gl::CheckFramebufferStatus(gl::FRAMEBUFFER) != gl::FRAMEBUFFER_COMPLETE {
                bug!("incomplete framebuffer");
            }

            // SAFETY:
            // no undefined bit is set in `mask`
            // `glBegin` and `glEnd` are never used
            gl::Clear(gl::DEPTH_BUFFER_BIT);
        }

        self.depth_id = depth;
        self.framebuffer_id = buffer;

        self.has_framebuffer = true;
    }

    pub fn clone_as_target(previous: &Self, backend: &mut Backend) -> Self {
        let mut clone = Self::new(backend, previous.dimensions).unwrap_bug();
        clone.add_framebuffer(backend);
        backend.clear_color(clone.framebuffer_id, (0.0, 0.0, 0.0, 0.0));
        backend.draw(
            clone.framebuffer_id,
            previous.dimensions,
            1,
            previous,
            (0, 0),
            previous.dimensions,
            (0, 0),
            &DrawConfig::default(),
        );

        clone
    }
}