twgpu 0.4.1

Render Teeworlds and DDNet maps
Documentation
use crate::blit::Blit;
use image::{Rgba, RgbaImage};
use twmap::{Image, Layer, TwMap};
use vek::az::UnwrappedAs;
use wgpu::{
    BindGroupLayoutEntry, BindingType, CommandEncoderDescriptor, Device, Extent3d, Origin3d, Queue,
    ShaderStages, Texture, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat,
    TextureSampleType, TextureUsages, TextureView, TextureViewDescriptor, TextureViewDimension,
};
use wgpu::{CommandEncoder, TexelCopyBufferLayout, TexelCopyTextureInfo};

const LABEL: Option<&str> = Some("Mapres");

// TODO: maybe make texture optional as well
pub struct Mapres {
    pub name: String,
    pub width: u32,
    pub height: u32,
    pub texture: Texture,
    pub array_texture: Option<Texture>,
}

/// Contains all textures used in a map
pub struct MapresStorage {
    /// All mapres contained in the map, in order
    pub mapres: Vec<Mapres>,
    /// White texture used when a layer doesn't specify an image
    pub blank: Mapres,
    _blit: Blit,
}

impl Mapres {
    pub fn texture_layout_entry(
        binding: u32,
        view_dimension: TextureViewDimension,
    ) -> BindGroupLayoutEntry {
        BindGroupLayoutEntry {
            binding,
            visibility: ShaderStages::FRAGMENT,
            ty: BindingType::Texture {
                sample_type: TextureSampleType::Float { filterable: true },
                view_dimension,
                multisampled: false,
            },
            count: None,
        }
    }

    fn from_rgba(
        name: String,
        image: &RgbaImage,
        blit: &mut Blit,
        encoder: &mut CommandEncoder,
        device: &Device,
        queue: &Queue,
    ) -> Self {
        let width = image.width();
        let height = image.height();
        let size = Extent3d {
            width,
            height,
            depth_or_array_layers: 1,
        };
        let texture = device.create_texture(&TextureDescriptor {
            label: Some(&format!("Image {name}")),
            size,
            mip_level_count: size.max_mips(TextureDimension::D2),
            sample_count: 1,
            dimension: TextureDimension::D2,
            format: TextureFormat::Rgba8Unorm,
            usage: TextureUsages::TEXTURE_BINDING
                | TextureUsages::COPY_SRC
                | TextureUsages::COPY_DST
                | TextureUsages::RENDER_ATTACHMENT,
            view_formats: &[],
        });
        queue.write_texture(
            texture.as_image_copy(),
            image.as_raw(),
            TexelCopyBufferLayout {
                offset: 0,
                bytes_per_row: Some(4 * width),
                rows_per_image: Some(height),
            },
            size,
        );
        blit.generate_mipmaps(encoder, &texture, device);
        Self {
            name,
            width,
            height,
            texture,
            array_texture: None,
        }
    }

    /// Generates the array texture for this mapres, if it isn't already generated
    pub fn generate_array_texture(
        &mut self,
        blit: &mut Blit,
        encoder: &mut CommandEncoder,
        device: &Device,
        queue: &Queue,
    ) {
        if self.array_texture.is_some() {
            return;
        }
        assert_eq!(self.width % 16, 0);
        assert_eq!(self.height % 16, 0);
        let tile_width = self.width / 16;
        let tile_height = self.height / 16;
        let tile_row_data_size = tile_width.unwrapped_as::<usize>() * 4;
        let tile_data_size: usize = tile_row_data_size * tile_height.unwrapped_as::<usize>() * 4;
        let size = Extent3d {
            width: tile_width,
            height: tile_height,
            depth_or_array_layers: 256,
        };
        let array_texture = device.create_texture(&TextureDescriptor {
            label: Some(&format!("Array Image {}", self.name)),
            size,
            mip_level_count: size.max_mips(TextureDimension::D2),
            sample_count: 1,
            dimension: TextureDimension::D2,
            format: TextureFormat::Rgba8Unorm,
            usage: TextureUsages::TEXTURE_BINDING
                | TextureUsages::COPY_SRC
                | TextureUsages::COPY_DST
                | TextureUsages::RENDER_ATTACHMENT,
            view_formats: &[],
        });
        let copy_size = Extent3d {
            width: tile_width,
            height: tile_height,
            depth_or_array_layers: 1,
        };

        let top_left_tile = vec![0; tile_data_size];
        queue.write_texture(
            TexelCopyTextureInfo {
                texture: &array_texture,
                mip_level: 0,
                origin: Origin3d { x: 0, y: 0, z: 0 },
                aspect: TextureAspect::All,
            },
            &top_left_tile,
            TexelCopyBufferLayout {
                bytes_per_row: Some(tile_row_data_size.unwrapped_as()),
                ..TexelCopyBufferLayout::default()
            },
            copy_size,
        );

        for y in 0..16 {
            for x in 0..16 {
                if (x, y) == (0, 0) {
                    continue;
                }
                encoder.copy_texture_to_texture(
                    TexelCopyTextureInfo {
                        texture: &self.texture,
                        mip_level: 0,
                        origin: Origin3d {
                            x: x * tile_width,
                            y: y * tile_height,
                            z: 0,
                        },
                        aspect: TextureAspect::All,
                    },
                    TexelCopyTextureInfo {
                        texture: &array_texture,
                        mip_level: 0,
                        origin: Origin3d {
                            x: 0,
                            y: 0,
                            z: y * 16 + x,
                        },
                        aspect: TextureAspect::All,
                    },
                    copy_size,
                );
            }
        }
        blit.generate_mipmaps(encoder, &array_texture, device);
        self.array_texture = Some(array_texture);
    }

    /// Accessor to the array texture of this mapres
    /// Panics it `generate_array_texture` wasn't called previously
    pub fn array_texture(&self) -> &Texture {
        match &self.array_texture {
            None => {
                panic!("Accessed array texture was not generated, use `generate_array_texture`")
            }
            Some(texture) => texture,
        }
    }
}

impl MapresStorage {
    pub fn upload(map: &TwMap, device: &Device, queue: &Queue) -> Self {
        let mut mapres = Vec::new();
        let mut blit = Blit::new(device);

        // There are at max 64 mapres.
        // This array stores, which of the mapres also need to be turned into array textures.
        let mut tilemap_textures = [false; 64];

        for group in &map.groups {
            for layer in &group.layers {
                if let Layer::Tiles(layer) = layer {
                    if let Some(image) = layer.image {
                        tilemap_textures[usize::from(image)] = true;
                    }
                }
            }
        }

        let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor { label: LABEL });
        for image in &map.images {
            let new_mapres = match image {
                Image::External(_) => panic!("External images can't be rendered, embed them!"),
                Image::Embedded(emb) => Mapres::from_rgba(
                    image.name().clone(),
                    emb.image.unwrap_ref(),
                    &mut blit,
                    &mut encoder,
                    device,
                    queue,
                ),
            };
            mapres.push(new_mapres);
        }
        queue.submit([encoder.finish()]);

        for (i, mapres) in mapres.iter_mut().enumerate() {
            if tilemap_textures[i] {
                let mut encoder =
                    device.create_command_encoder(&CommandEncoderDescriptor { label: LABEL });
                mapres.generate_array_texture(&mut blit, &mut encoder, device, queue);
                queue.submit([encoder.finish()]);
            }
        }

        let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor { label: LABEL });
        let mut blank = Mapres::from_rgba(
            "Blank Image".into(),
            &RgbaImage::from_pixel(16, 16, Rgba([255; 4])),
            &mut blit,
            &mut encoder,
            device,
            queue,
        );
        blank.generate_array_texture(&mut blit, &mut encoder, device, queue);
        queue.submit([encoder.finish()]);
        Self {
            mapres,
            blank,
            _blit: blit,
        }
    }

    pub fn view_texture(&self, index: Option<u16>) -> TextureView {
        match index {
            None => self
                .blank
                .texture
                .create_view(&TextureViewDescriptor::default()),
            Some(index) => self.mapres[index.unwrapped_as::<usize>()]
                .texture
                .create_view(&TextureViewDescriptor::default()),
        }
    }

    /// Generates array texture on first use
    pub fn view_array_texture(&self, index: Option<u16>) -> TextureView {
        match index {
            None => self
                .blank
                .array_texture()
                .create_view(&TextureViewDescriptor {
                    array_layer_count: Some(256),
                    base_array_layer: 0,
                    ..TextureViewDescriptor::default()
                }),
            //.create_view(&TextureViewDescriptor::default()),
            Some(index) => self.mapres[index.unwrapped_as::<usize>()]
                .array_texture()
                .create_view(&TextureViewDescriptor {
                    array_layer_count: Some(256),
                    base_array_layer: 0,
                    ..TextureViewDescriptor::default()
                }),
            //.create_view(&TextureViewDescriptor::default()),
        }
    }
}