klyff 0.1.3

Text rendering library for games with MSDF support
Documentation
use super::screen_size_uniform::ScreenSizeUniform;
use crate::{MeshEncoder, TextureAtlas, pipelines::MaterialEncoder};

/// Renders MSDF glyphs encoded by [`MeshEncoder`] + [`MaterialEncoder`].
pub struct MsdfTextPipeline {
    pipeline: wgpu::RenderPipeline,
}

impl MsdfTextPipeline {
    pub fn new(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self {
        Self::new_and_modify(device, surface_format, |_| {})
    }

    /// Builds a pipeline and modify the [`wgpu::RenderPipelineDescriptor`] before creating the
    /// pipeline.
    ///
    /// Useful for specifying custom depth, stencil settings, or other fields, however,
    /// you must be careful when modifying fields related to shader entry point or pipeline layout
    /// as it may lead to a runtime panic.
    ///
    /// It is not recommended that you change the shader module. If you want to use a custom shader,
    /// then maintaining your own [`wgpu::RenderPipeline`] will be easier and more flexible, since
    /// otherwise you will have to make sure the shader has correct pipeline layout. `klyff` makes
    /// NO guarantee about backward compatibility about custom shader's compatibility when passed
    /// in through this method.
    pub fn new_and_modify(
        device: &wgpu::Device,
        surface_format: wgpu::TextureFormat,
        modifer: impl FnOnce(&mut wgpu::RenderPipelineDescriptor<'_>),
    ) -> Self {
        let shader = device.create_shader_module(wgpu::include_wgsl!("msdf.wgsl"));
        let atlas_bgl = TextureAtlas::bind_group_layout(device);
        let uniform_bgl = ScreenSizeUniform::bind_group_layout(device);
        let storage_bgl = MaterialEncoder::storage_bind_group_layout(device);

        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("klyff text pipeline layout"),
            bind_group_layouts: &[&atlas_bgl, &uniform_bgl, &storage_bgl],
            push_constant_ranges: &[],
        });

        let mut desc = wgpu::RenderPipelineDescriptor {
            label: Some("klyff msdf pipeline"),
            layout: Some(&pipeline_layout),
            vertex: wgpu::VertexState {
                module: &shader,
                entry_point: Some("vs_main"),
                buffers: &[
                    MeshEncoder::msdf_vertex_buffer_layout(),
                    MaterialEncoder::material_vertex_buffer_layout(),
                ],
                compilation_options: Default::default(),
            },
            fragment: Some(wgpu::FragmentState {
                module: &shader,
                entry_point: Some("fs_main"),
                targets: &[Some(wgpu::ColorTargetState {
                    format: surface_format,
                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
                    write_mask: wgpu::ColorWrites::all(),
                })],
                compilation_options: Default::default(),
            }),
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList,
                strip_index_format: None,
                front_face: wgpu::FrontFace::Ccw,
                cull_mode: None,
                unclipped_depth: false,
                polygon_mode: wgpu::PolygonMode::Fill,
                conservative: false,
            },
            depth_stencil: None,
            multisample: wgpu::MultisampleState::default(),
            multiview: None,
            cache: None,
        };

        modifer(&mut desc);

        Self {
            pipeline: device.create_render_pipeline(&desc),
        }
    }

    /// Records the draw calls to render the glyphs encoded in `mesh` + `material` into `pass`.
    pub fn render<'a>(
        &'a self,
        pass: &mut wgpu::RenderPass<'a>,
        atlas: &'a TextureAtlas,
        uniform: &'a ScreenSizeUniform,
        mesh: &'a MeshEncoder,
        material: &'a MaterialEncoder,
    ) {
        if mesh.msdf_index_count() == 0 {
            return;
        }
        pass.set_pipeline(&self.pipeline);
        pass.set_bind_group(0, atlas.bind_group(), &[]);
        pass.set_bind_group(1, uniform.uniform_bind_group(), &[]);
        pass.set_bind_group(2, material.storage_bind_group(), &[]);
        pass.set_vertex_buffer(0, mesh.msdf_vertex_buffer().slice(..));
        pass.set_vertex_buffer(1, material.material_vertex_buffer().slice(..));
        pass.set_index_buffer(
            mesh.msdf_index_buffer().slice(..),
            wgpu::IndexFormat::Uint16,
        );
        pass.draw_indexed(0..mesh.msdf_index_count(), 0, 0..1);
    }
}

/// Renders rasterized (colour-bitmap) glyphs encoded by [`MeshEncoder`].
pub struct RasterizedTextPipeline(wgpu::RenderPipeline);

impl RasterizedTextPipeline {
    pub fn new(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self {
        Self::new_and_modify(device, surface_format, |_| {})
    }

    /// Builds a pipeline and modify the [`wgpu::RenderPipelineDescriptor`] before creating the
    /// pipeline.
    ///
    /// Useful for specifying custom depth, stencil settings, or other fields, however,
    /// you must be careful when modifying fields related to shader entry point or pipeline layout
    /// as it may lead to a runtime panic.
    ///
    /// It is not recommended that you change the shader module. If you want to use a custom shader,
    /// then maintaining your own [`wgpu::RenderPipeline`] will be easier and more flexible, since
    /// otherwise you will have to make sure the shader has correct pipeline layout. `klyff` makes
    /// NO guarantee about backward compatibility about custom shader's compatibility when passed
    /// in through this method.
    pub fn new_and_modify(
        device: &wgpu::Device,
        surface_format: wgpu::TextureFormat,
        modify: impl FnOnce(&mut wgpu::RenderPipelineDescriptor),
    ) -> Self {
        let shader = device.create_shader_module(wgpu::include_wgsl!("rasterized.wgsl"));
        let atlas_bgl = TextureAtlas::bind_group_layout(device);
        let uniform_bgl = ScreenSizeUniform::bind_group_layout(device);

        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("klyff text pipeline layout for rasterized glyphs"),
            bind_group_layouts: &[&atlas_bgl, &uniform_bgl],
            push_constant_ranges: &[],
        });

        let mut desc = wgpu::RenderPipelineDescriptor {
            label: Some("klyff text pipeline for rasterized glyphs"),
            layout: Some(&pipeline_layout),
            vertex: wgpu::VertexState {
                module: &shader,
                entry_point: Some("vs_main"),
                buffers: &[MeshEncoder::rasterized_vertex_buffer_layout()],
                compilation_options: Default::default(),
            },
            fragment: Some(wgpu::FragmentState {
                module: &shader,
                entry_point: Some("fs_main"),
                targets: &[Some(wgpu::ColorTargetState {
                    format: surface_format,
                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
                    write_mask: wgpu::ColorWrites::ALL,
                })],
                compilation_options: Default::default(),
            }),
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList,
                strip_index_format: None,
                front_face: wgpu::FrontFace::Ccw,
                cull_mode: None,
                unclipped_depth: false,
                polygon_mode: wgpu::PolygonMode::Fill,
                conservative: false,
            },
            depth_stencil: None,
            multisample: wgpu::MultisampleState::default(),
            multiview: None,
            cache: None,
        };
        modify(&mut desc);
        Self(device.create_render_pipeline(&desc))
    }

    /// Records the draw calls to render the glyphs encoded in `mesh`.
    pub fn render<'a>(
        &'a self,
        pass: &mut wgpu::RenderPass<'a>,
        atlas: &'a TextureAtlas,
        uniform: &'a ScreenSizeUniform,
        mesh: &'a MeshEncoder,
    ) {
        if mesh.rasterized_index_count() == 0 {
            return;
        }
        pass.set_pipeline(&self.0);
        pass.set_bind_group(0, atlas.bind_group(), &[]);
        pass.set_bind_group(1, uniform.uniform_bind_group(), &[]);
        pass.set_vertex_buffer(0, mesh.rasterized_vertex_buffer().slice(..));
        pass.set_index_buffer(
            mesh.rasterized_index_buffer().slice(..),
            wgpu::IndexFormat::Uint16,
        );
        pass.draw_indexed(0..mesh.rasterized_index_count(), 0, 0..1);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    async fn request_device() -> Option<(wgpu::Device, wgpu::Queue)> {
        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
        let adapter = instance
            .request_adapter(&wgpu::RequestAdapterOptions {
                power_preference: wgpu::PowerPreference::None,
                compatible_surface: None,
                force_fallback_adapter: false,
            })
            .await
            .ok()?;
        adapter
            .request_device(&wgpu::DeviceDescriptor::default())
            .await
            .ok()
    }

    #[test]
    fn msdf_pipeline_compiles() {
        let Some((device, _)) = pollster::block_on(request_device()) else {
            return;
        };

        MsdfTextPipeline::new(&device, wgpu::TextureFormat::Bgra8UnormSrgb);
    }

    #[test]
    fn rasterized_pipeline_compiles() {
        let Some((device, _)) = pollster::block_on(request_device()) else {
            return;
        };
        RasterizedTextPipeline::new(&device, wgpu::TextureFormat::Bgra8UnormSrgb);
    }
}