phosphor-crt 0.1.0

A real-time plotter of waveforms, imitating oscillscope CRTs
Documentation
use crate::gradient::{Gradient, RgbColor};
use wgpu::{
    BindGroup, BindGroupEntry, BindGroupLayout, BindGroupLayoutEntry, BindingType, Device,
    Extent3d, Queue, Texture, TextureFormat,
};

#[derive(Debug)]
pub(crate) struct LutTextureResources {
    texture: Texture,
    bind_group: BindGroup,
    bind_group_layout: BindGroupLayout,
    pending_gradient_update: Option<Gradient>,
}

impl LutTextureResources {
    pub(crate) fn bind_group(&self) -> &BindGroup {
        &self.bind_group
    }
}

impl LutTextureResources {
    const SIZE: u32 = 64;

    pub(crate) fn new(device: &Device) -> Self {
        let texture = device.create_texture(&wgpu::TextureDescriptor {
            size: Extent3d {
                width: Self::SIZE,
                ..Default::default()
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D1,
            format: TextureFormat::Rgba8Unorm,
            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
            label: Some("lut_texture"),
            view_formats: &[],
        });
        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            address_mode_u: wgpu::AddressMode::ClampToEdge,
            address_mode_v: wgpu::AddressMode::ClampToEdge,
            address_mode_w: wgpu::AddressMode::ClampToEdge,
            mag_filter: wgpu::FilterMode::Linear,
            min_filter: wgpu::FilterMode::Linear,
            mipmap_filter: wgpu::FilterMode::Nearest,
            ..Default::default()
        });

        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            entries: &[
                BindGroupLayoutEntry {
                    binding: 0,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: BindingType::Texture {
                        multisampled: false,
                        view_dimension: wgpu::TextureViewDimension::D1,
                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
                    },
                    count: None,
                },
                BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    // This should match the filterable field of the
                    // corresponding Texture entry above.
                    ty: BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                    count: None,
                },
            ],
            label: Some("texture_bind_group_layout"),
        });

        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            layout: &bind_group_layout,
            entries: &[
                BindGroupEntry {
                    binding: 0,
                    resource: wgpu::BindingResource::TextureView(&view),
                },
                BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::Sampler(&sampler),
                },
            ],
            label: Some("texture_bind_group"),
        });

        let lut = Gradient::new([(0., RgbColor::BLACK), (1., RgbColor::WHITE)]);

        LutTextureResources {
            texture,
            bind_group,
            bind_group_layout,
            pending_gradient_update: Some(lut),
        }
    }

    fn texel_copy_texture_info(&self) -> wgpu::TexelCopyTextureInfo {
        wgpu::TexelCopyTextureInfo {
            texture: &self.texture,
            mip_level: 0,
            origin: wgpu::Origin3d::ZERO,
            aspect: wgpu::TextureAspect::All,
        }
    }

    fn texel_copy_buffer_layout(&self) -> wgpu::TexelCopyBufferLayout {
        let format = self.texture.format();
        assert_eq!(
            format.block_dimensions(),
            (1, 1),
            "invalid texture dimension"
        );
        let bytes_per_row = Self::SIZE * format.block_copy_size(None).unwrap();
        assert_eq!(bytes_per_row, Self::SIZE * 4, "invalid texture row size");
        wgpu::TexelCopyBufferLayout {
            offset: 0,
            bytes_per_row: Some(bytes_per_row),
            rows_per_image: None,
        }
    }

    pub(crate) fn prepare(&mut self, queue: &Queue) {
        let Some(lut) = &self.pending_gradient_update.take() else {
            return;
        };
        let gradient_sampled: Vec<u8> = lut
            .linear_eval(LutTextureResources::SIZE as usize)
            .into_iter()
            .map(Into::<RgbColor<u8>>::into)
            .flat_map(|c| [c.r, c.g, c.b, 255u8])
            .collect();
        queue.write_texture(
            self.texel_copy_texture_info(),
            &gradient_sampled,
            self.texel_copy_buffer_layout(),
            Extent3d {
                width: LutTextureResources::SIZE,
                ..Default::default()
            },
        );
    }

    pub(crate) fn bind_group_layout(&self) -> &BindGroupLayout {
        &self.bind_group_layout
    }

    pub(crate) fn update_lut(&mut self, gradient: Gradient) {
        self.pending_gradient_update = Some(gradient)
    }
}