]> git.nega.tv - josh/narcissus/commitdiff
Add support for basic text drawing
authorJoshua Simmons <josh@nega.tv>
Sun, 26 Feb 2023 18:23:07 +0000 (19:23 +0100)
committerJoshua Simmons <josh@nega.tv>
Sun, 26 Feb 2023 18:23:07 +0000 (19:23 +0100)
Move pipelines out of main.rs and into their own module.
Add new text pipeline.
Add new text shader.
Update main.rs to draw nice text. :)

15 files changed:
bins/narcissus/Cargo.toml
bins/narcissus/src/fonts.rs [new file with mode: 0644]
bins/narcissus/src/main.rs
bins/narcissus/src/pipelines/basic.rs [new file with mode: 0644]
bins/narcissus/src/pipelines/mod.rs [new file with mode: 0644]
bins/narcissus/src/pipelines/text.rs [new file with mode: 0644]
bins/narcissus/src/shaders/basic.frag.glsl
bins/narcissus/src/shaders/basic.frag.spv
bins/narcissus/src/shaders/basic.vert.glsl
bins/narcissus/src/shaders/basic.vert.spv
bins/narcissus/src/shaders/build.sh
bins/narcissus/src/shaders/text.frag.glsl [new file with mode: 0644]
bins/narcissus/src/shaders/text.frag.spv [new file with mode: 0644]
bins/narcissus/src/shaders/text.vert.glsl [new file with mode: 0644]
bins/narcissus/src/shaders/text.vert.spv [new file with mode: 0644]

index 4768c83d5bdc1c7eacafc740dfc6f1be64b334c8..c17ab4cc1bb70161767939e0de5df49c86ee52f9 100644 (file)
@@ -7,6 +7,7 @@ edition = "2021"
 
 [dependencies]
 narcissus-core = { path = "../../libs/narcissus-core" }
+narcissus-font = { path = "../../libs/narcissus-font" }
 narcissus-maths = { path = "../../libs/narcissus-maths" }
 narcissus-image = { path = "../../libs/narcissus-image" }
 narcissus-app = { path = "../../libs/narcissus-app" }
diff --git a/bins/narcissus/src/fonts.rs b/bins/narcissus/src/fonts.rs
new file mode 100644 (file)
index 0000000..0b107ce
--- /dev/null
@@ -0,0 +1,36 @@
+use narcissus_font::{Font, FontCollection};
+
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
+pub enum FontFamily {
+    NotoSansJapanese,
+    RobotoRegular,
+}
+
+pub struct Fonts<'a> {
+    noto_sans_japanese: Font<'a>,
+    roboto_regular: Font<'a>,
+}
+
+impl<'a> Fonts<'a> {
+    pub fn new() -> Self {
+        // Safety: Safe because Roboto-Regular.ttf is a valid ttf font embedded in the application.
+        let roboto_regular =
+            unsafe { Font::from_bytes(include_bytes!("fonts/Roboto-Regular.ttf")) };
+        let noto_sans_japanese =
+            unsafe { Font::from_bytes(include_bytes!("fonts/NotoSansJP-Medium.otf")) };
+        Self {
+            noto_sans_japanese,
+            roboto_regular,
+        }
+    }
+}
+
+impl<'a> FontCollection<'a> for Fonts<'a> {
+    type Family = FontFamily;
+    fn font(&self, family: Self::Family) -> &Font<'a> {
+        match family {
+            FontFamily::NotoSansJapanese => &self.noto_sans_japanese,
+            FontFamily::RobotoRegular => &self.roboto_regular,
+        }
+    }
+}
index 1f18062feeb729466c69dffea8cd451d6d13440a..f8a02260f4a42f0d919c5066912d9c7e0c11f083 100644 (file)
@@ -1,25 +1,34 @@
 use std::{path::Path, time::Instant};
 
 use narcissus_app::{create_app, Event, Key, WindowDesc};
-use narcissus_core::{cstr, default, include_bytes_align, obj, rand::Pcg64};
+use narcissus_core::{default, obj, rand::Pcg64};
+use narcissus_font::{CachedGlyph, CachedGlyphIndex, FontCollection, GlyphCache, Oversample};
 use narcissus_gpu::{
-    create_device, Access, Bind, BindGroupLayoutDesc, BindGroupLayoutEntryDesc, BindingType,
-    BlendMode, Buffer, BufferDesc, BufferImageCopy, BufferUsageFlags, ClearValue, CompareOp,
-    CullingMode, Device, Extent2d, Extent3d, FrontFace, GraphicsPipelineDesc,
-    GraphicsPipelineLayout, Image, ImageAspectFlags, ImageBarrier, ImageDesc, ImageDimension,
+    create_device, Access, Bind, Buffer, BufferDesc, BufferImageCopy, BufferUsageFlags, ClearValue,
+    Device, Extent2d, Extent3d, Image, ImageAspectFlags, ImageBarrier, ImageDesc, ImageDimension,
     ImageFormat, ImageLayout, ImageUsageFlags, IndexType, LoadOp, MemoryLocation, Offset2d,
-    Offset3d, PolygonMode, RenderingAttachment, RenderingDesc, SamplerAddressMode, SamplerDesc,
-    SamplerFilter, Scissor, ShaderDesc, ShaderStageFlags, StoreOp, ThreadToken, Topology,
-    TypedBind, Viewport,
+    Offset3d, RenderingAttachment, RenderingDesc, SamplerAddressMode, SamplerDesc, SamplerFilter,
+    Scissor, StoreOp, ThreadToken, TypedBind, Viewport,
 };
 use narcissus_image as image;
 use narcissus_maths::{
     sin_cos_pi_f32, vec2, vec3, vec4, Affine3, Deg, HalfTurn, Mat3, Mat4, Point3, Vec2, Vec3,
 };
 
+use crate::{
+    fonts::{FontFamily, Fonts},
+    pipelines::{BasicPipeline, TextPipeline},
+};
+
+mod fonts;
+mod pipelines;
+
 const MAX_SHARKS: usize = 262_144;
 const NUM_SHARKS: usize = 50;
 
+const MAX_GLYPH_INSTANCES: usize = 262_144;
+const MAX_GLYPHS: usize = 1024;
+
 /// Marker trait indicates it's safe to convert a given type directly to an array of bytes.
 ///
 /// # Safety
@@ -28,19 +37,44 @@ const NUM_SHARKS: usize = 50;
 unsafe trait Blittable: Sized {}
 
 #[allow(unused)]
-struct Uniforms {
+#[repr(C)]
+struct BasicUniforms {
     clip_from_model: Mat4,
 }
 
-unsafe impl Blittable for Uniforms {}
+unsafe impl Blittable for BasicUniforms {}
 
 #[allow(unused)]
+#[repr(C)]
 struct Vertex {
     position: [f32; 4],
     normal: [f32; 4],
     texcoord: [f32; 4],
 }
 
+#[allow(unused)]
+#[repr(C)]
+struct TextUniforms {
+    screen_width: u32,
+    screen_height: u32,
+    atlas_width: u32,
+    atlas_height: u32,
+}
+
+unsafe impl Blittable for TextUniforms {}
+
+#[allow(unused)]
+#[repr(C)]
+struct GlyphInstance {
+    cached_glyph_index: CachedGlyphIndex,
+    x: f32,
+    y: f32,
+    color: u32,
+}
+
+unsafe impl Blittable for CachedGlyph {}
+unsafe impl Blittable for GlyphInstance {}
+
 unsafe impl Blittable for Vertex {}
 unsafe impl Blittable for u8 {}
 unsafe impl Blittable for u16 {}
@@ -297,75 +331,11 @@ pub fn main() {
 
     let thread_token = ThreadToken::new();
 
-    let vert_spv = include_bytes_align!(4, "shaders/basic.vert.spv");
-    let frag_spv = include_bytes_align!(4, "shaders/basic.frag.spv");
-
-    let uniform_bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDesc {
-        entries: &[BindGroupLayoutEntryDesc {
-            slot: 0,
-            stages: ShaderStageFlags::ALL,
-            binding_type: BindingType::UniformBuffer,
-            count: 1,
-        }],
-    });
-
-    let storage_bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDesc {
-        entries: &[
-            BindGroupLayoutEntryDesc {
-                slot: 0,
-                stages: ShaderStageFlags::ALL,
-                binding_type: BindingType::StorageBuffer,
-                count: 1,
-            },
-            BindGroupLayoutEntryDesc {
-                slot: 1,
-                stages: ShaderStageFlags::ALL,
-                binding_type: BindingType::StorageBuffer,
-                count: 1,
-            },
-            BindGroupLayoutEntryDesc {
-                slot: 2,
-                stages: ShaderStageFlags::ALL,
-                binding_type: BindingType::Sampler,
-                count: 1,
-            },
-            BindGroupLayoutEntryDesc {
-                slot: 3,
-                stages: ShaderStageFlags::ALL,
-                binding_type: BindingType::Image,
-                count: 1,
-            },
-        ],
-    });
+    let basic_pipeline = BasicPipeline::new(device.as_ref());
+    let text_pipeline = TextPipeline::new(device.as_ref());
 
-    let pipeline = device.create_graphics_pipeline(&GraphicsPipelineDesc {
-        vertex_shader: ShaderDesc {
-            entry: cstr!("main"),
-            code: vert_spv,
-        },
-        fragment_shader: ShaderDesc {
-            entry: cstr!("main"),
-            code: frag_spv,
-        },
-        bind_group_layouts: &[uniform_bind_group_layout, storage_bind_group_layout],
-        layout: GraphicsPipelineLayout {
-            color_attachment_formats: &[ImageFormat::BGRA8_SRGB],
-            depth_attachment_format: Some(ImageFormat::DEPTH_F32),
-            stencil_attachment_format: None,
-        },
-        topology: Topology::Triangles,
-        polygon_mode: PolygonMode::Fill,
-        culling_mode: CullingMode::Back,
-        front_face: FrontFace::CounterClockwise,
-        blend_mode: BlendMode::Opaque,
-        depth_bias: None,
-        depth_compare_op: CompareOp::GreaterOrEqual,
-        depth_test_enable: true,
-        depth_write_enable: true,
-        stencil_test_enable: false,
-        stencil_back: default(),
-        stencil_front: default(),
-    });
+    let fonts = Fonts::new();
+    let mut glyph_cache = GlyphCache::new(&fonts, 512, 512, Oversample::X2, Oversample::X2);
 
     let blåhaj_image = load_image("bins/narcissus/data/blåhaj.png");
     let (blåhaj_vertices, blåhaj_indices) = load_obj("bins/narcissus/data/blåhaj.obj");
@@ -390,19 +360,19 @@ pub fn main() {
         blåhaj_image.as_slice(),
     );
 
-    let mut uniforms = MappedBuffer::new(
+    let mut basic_uniforms = MappedBuffer::new(
         device.as_ref(),
         BufferUsageFlags::UNIFORM,
-        std::mem::size_of::<Uniforms>(),
+        std::mem::size_of::<BasicUniforms>(),
     );
 
-    let mut transforms = MappedBuffer::new(
+    let mut basic_transforms = MappedBuffer::new(
         device.as_ref(),
         BufferUsageFlags::STORAGE,
         std::mem::size_of::<Affine3>() * MAX_SHARKS,
     );
 
-    let sampler = device.create_sampler(&SamplerDesc {
+    let basic_sampler = device.create_sampler(&SamplerDesc {
         filter: SamplerFilter::Point,
         address_mode: SamplerAddressMode::Clamp,
         compare_op: None,
@@ -411,6 +381,33 @@ pub fn main() {
         max_lod: 1000.0,
     });
 
+    let mut text_uniforms = MappedBuffer::new(
+        device.as_ref(),
+        BufferUsageFlags::UNIFORM,
+        std::mem::size_of::<TextUniforms>(),
+    );
+
+    let mut glyph_instance_buffer = MappedBuffer::new(
+        device.as_ref(),
+        BufferUsageFlags::STORAGE,
+        std::mem::size_of::<GlyphInstance>() * MAX_GLYPH_INSTANCES,
+    );
+
+    let mut cached_glyph_buffer = MappedBuffer::new(
+        device.as_ref(),
+        BufferUsageFlags::STORAGE,
+        std::mem::size_of::<CachedGlyph>() * MAX_GLYPHS,
+    );
+
+    let text_sampler = device.create_sampler(&SamplerDesc {
+        filter: SamplerFilter::Bilinear,
+        address_mode: SamplerAddressMode::Clamp,
+        compare_op: None,
+        mip_lod_bias: 0.0,
+        min_lod: 0.0,
+        max_lod: 0.0,
+    });
+
     let mut depth_width = 0;
     let mut depth_height = 0;
     let mut depth_image = default();
@@ -514,7 +511,7 @@ pub fn main() {
             transform.matrix *= Mat3::from_axis_rotation(Vec3::Y, HalfTurn::new(0.002 * direction))
         }
 
-        transforms.write_slice(&shark_transforms);
+        basic_transforms.write_slice(&shark_transforms);
 
         let (s, c) = sin_cos_pi_f32(frame_start * 0.2);
         let camera_height = c * 8.0;
@@ -526,52 +523,150 @@ pub fn main() {
             Mat4::perspective_rev_inf_zo(Deg::new(45.0).into(), width as f32 / height as f32, 0.01);
         let clip_from_model = clip_from_camera * camera_from_model;
 
-        uniforms.write(Uniforms { clip_from_model });
+        basic_uniforms.write(BasicUniforms { clip_from_model });
 
-        device.cmd_set_pipeline(&mut cmd_buffer, pipeline);
+        // Do some Font Shit.'
+        let line0 = "Snarfe, Blåhaj! And the Quick Brown Fox jumped Over the Lazy doge. ½½½½ Snarfe, Blåhaj! And the Quick Brown Fox jumped Over the Lazy doge. ½½½½";
+        let line1 = "加盟国は、国際連合と協力して";
 
-        device.cmd_set_bind_group(
-            &frame,
-            &mut cmd_buffer,
-            uniform_bind_group_layout,
-            0,
-            &[Bind {
-                binding: 0,
-                array_element: 0,
-                typed: TypedBind::UniformBuffer(&[uniforms.buffer()]),
-            }],
-        );
+        let mut glyph_instances = Vec::new();
+        let mut glyphs = Vec::new();
 
-        device.cmd_set_bind_group(
-            &frame,
-            &mut cmd_buffer,
-            storage_bind_group_layout,
-            1,
-            &[
-                Bind {
-                    binding: 0,
-                    array_element: 0,
-                    typed: TypedBind::StorageBuffer(&[blåhaj_vertex_buffer]),
-                },
-                Bind {
-                    binding: 1,
-                    array_element: 0,
-                    typed: TypedBind::StorageBuffer(&[transforms.buffer()]),
-                },
-                Bind {
-                    binding: 2,
-                    array_element: 0,
-                    typed: TypedBind::Sampler(&[sampler]),
-                },
-                Bind {
-                    binding: 3,
-                    array_element: 0,
-                    typed: TypedBind::Image(&[(ImageLayout::Optimal, blåhaj_image)]),
-                },
-            ],
-        );
+        let mut y = 0.0;
+
+        let mut rng = Pcg64::new();
+
+        for line in 0..100 {
+            let font_family = if line & 1 == 0 {
+                FontFamily::RobotoRegular
+            } else {
+                FontFamily::NotoSansJapanese
+            };
+            let font = fonts.font(font_family);
+
+            let v_metrics = font.vertical_metrics();
+            let font_scale = font.scale_for_pixel_height(if line & 1 == 0 { 25.0 } else { 40.0 });
+
+            y += v_metrics.ascent * font_scale - v_metrics.descent * font_scale
+                + v_metrics.line_gap * font_scale;
+            y = y.trunc();
+
+            let mut x = 0.0;
+
+            glyphs.clear();
+
+            let text = if line & 1 == 0 { line0 } else { line1 };
+
+            glyphs.extend(text.chars().map(|c| {
+                font.glyph_id(c)
+                    .unwrap_or_else(|| font.glyph_id('□').unwrap())
+            }));
+
+            let mut prev_glyph_index = None;
+            for glyph_index in glyphs.iter().copied() {
+                let cached_glyph_index =
+                    glyph_cache.cache_glyph(font_family, glyph_index, font_scale);
+
+                if let Some(prev_glyph_index) = prev_glyph_index {
+                    x += font.kerning_advance(prev_glyph_index, glyph_index) * font_scale;
+                }
+
+                const COLOR_SERIES: [u32; 4] = [0xfffac228, 0xfff57d15, 0xffd44842, 0xff9f2a63];
+
+                glyph_instances.push(GlyphInstance {
+                    cached_glyph_index,
+                    x,
+                    y,
+                    color: COLOR_SERIES[rng.next_bound_u64(4) as usize],
+                });
+
+                let h_metrics = font.horizontal_metrics(glyph_index);
+                x += h_metrics.advance_width * font_scale;
+                prev_glyph_index = Some(glyph_index);
+            }
+        }
+
+        let atlas_width = glyph_cache.width() as u32;
+        let atlas_height = glyph_cache.height() as u32;
 
-        device.cmd_set_index_buffer(&mut cmd_buffer, blåhaj_index_buffer, 0, IndexType::U16);
+        let (cached_glyphs, texture) = glyph_cache.update_atlas();
+
+        text_uniforms.write(TextUniforms {
+            screen_width: width,
+            screen_height: height,
+            atlas_width,
+            atlas_height,
+        });
+        cached_glyph_buffer.write_slice(cached_glyphs);
+        glyph_instance_buffer.write_slice(&glyph_instances);
+
+        // upload atlas
+        let glyph_atlas = {
+            let width = atlas_width;
+            let height = atlas_height;
+            let data = texture;
+
+            let buffer =
+                create_buffer_with_data(device.as_ref(), BufferUsageFlags::TRANSFER_SRC, data);
+
+            let image = device.create_image(&ImageDesc {
+                location: MemoryLocation::Device,
+                usage: ImageUsageFlags::SAMPLED | ImageUsageFlags::TRANSFER_DST,
+                dimension: ImageDimension::Type2d,
+                format: ImageFormat::R8_UNORM,
+                initial_layout: ImageLayout::Optimal,
+                width,
+                height,
+                depth: 1,
+                layer_count: 1,
+                mip_levels: 1,
+            });
+
+            device.cmd_barrier(
+                &mut cmd_buffer,
+                None,
+                &[ImageBarrier::layout_optimal(
+                    &[Access::None],
+                    &[Access::TransferWrite],
+                    image,
+                    ImageAspectFlags::COLOR,
+                )],
+            );
+
+            device.cmd_copy_buffer_to_image(
+                &mut cmd_buffer,
+                buffer,
+                image,
+                ImageLayout::Optimal,
+                &[BufferImageCopy {
+                    buffer_offset: 0,
+                    buffer_row_length: 0,
+                    buffer_image_height: 0,
+                    image_subresource: default(),
+                    image_offset: Offset3d { x: 0, y: 0, z: 0 },
+                    image_extent: Extent3d {
+                        width,
+                        height,
+                        depth: 1,
+                    },
+                }],
+            );
+
+            device.cmd_barrier(
+                &mut cmd_buffer,
+                None,
+                &[ImageBarrier::layout_optimal(
+                    &[Access::TransferWrite],
+                    &[Access::FragmentShaderSampledImageRead],
+                    image,
+                    ImageAspectFlags::COLOR,
+                )],
+            );
+
+            device.destroy_buffer(&frame, buffer);
+
+            image
+        };
 
         device.cmd_begin_rendering(
             &mut cmd_buffer,
@@ -619,17 +714,115 @@ pub fn main() {
             }],
         );
 
-        device.cmd_draw_indexed(
-            &mut cmd_buffer,
-            blåhaj_indices.len() as u32,
-            shark_transforms.len() as u32,
-            0,
-            0,
-            0,
-        );
+        // Render basic stuff.
+        {
+            device.cmd_set_pipeline(&mut cmd_buffer, basic_pipeline.pipeline);
+
+            device.cmd_set_bind_group(
+                &frame,
+                &mut cmd_buffer,
+                basic_pipeline.uniforms_bind_group_layout,
+                0,
+                &[Bind {
+                    binding: 0,
+                    array_element: 0,
+                    typed: TypedBind::UniformBuffer(&[basic_uniforms.buffer()]),
+                }],
+            );
+
+            device.cmd_set_bind_group(
+                &frame,
+                &mut cmd_buffer,
+                basic_pipeline.storage_bind_group_layout,
+                1,
+                &[
+                    Bind {
+                        binding: 0,
+                        array_element: 0,
+                        typed: TypedBind::StorageBuffer(&[blåhaj_vertex_buffer]),
+                    },
+                    Bind {
+                        binding: 1,
+                        array_element: 0,
+                        typed: TypedBind::StorageBuffer(&[basic_transforms.buffer()]),
+                    },
+                    Bind {
+                        binding: 2,
+                        array_element: 0,
+                        typed: TypedBind::Sampler(&[basic_sampler]),
+                    },
+                    Bind {
+                        binding: 3,
+                        array_element: 0,
+                        typed: TypedBind::Image(&[(ImageLayout::Optimal, blåhaj_image)]),
+                    },
+                ],
+            );
+
+            device.cmd_set_index_buffer(&mut cmd_buffer, blåhaj_index_buffer, 0, IndexType::U16);
+
+            device.cmd_draw_indexed(
+                &mut cmd_buffer,
+                blåhaj_indices.len() as u32,
+                shark_transforms.len() as u32,
+                0,
+                0,
+                0,
+            );
+        }
+
+        // Render text stuff.
+        {
+            device.cmd_set_pipeline(&mut cmd_buffer, text_pipeline.pipeline);
+
+            device.cmd_set_bind_group(
+                &frame,
+                &mut cmd_buffer,
+                text_pipeline.uniforms_bind_group_layout,
+                0,
+                &[Bind {
+                    binding: 0,
+                    array_element: 0,
+                    typed: TypedBind::UniformBuffer(&[text_uniforms.buffer()]),
+                }],
+            );
+
+            device.cmd_set_bind_group(
+                &frame,
+                &mut cmd_buffer,
+                basic_pipeline.storage_bind_group_layout,
+                1,
+                &[
+                    Bind {
+                        binding: 0,
+                        array_element: 0,
+                        typed: TypedBind::StorageBuffer(&[cached_glyph_buffer.buffer()]),
+                    },
+                    Bind {
+                        binding: 1,
+                        array_element: 0,
+                        typed: TypedBind::StorageBuffer(&[glyph_instance_buffer.buffer()]),
+                    },
+                    Bind {
+                        binding: 2,
+                        array_element: 0,
+                        typed: TypedBind::Sampler(&[text_sampler]),
+                    },
+                    Bind {
+                        binding: 3,
+                        array_element: 0,
+                        typed: TypedBind::Image(&[(ImageLayout::Optimal, glyph_atlas)]),
+                    },
+                ],
+            );
+
+            device.cmd_draw(&mut cmd_buffer, 4, glyph_instances.len() as u32, 0, 0);
+        }
 
         device.cmd_end_rendering(&mut cmd_buffer);
 
+        device.destroy_image(&frame, glyph_atlas);
+
         device.submit(&frame, cmd_buffer);
 
         device.end_frame(frame);
diff --git a/bins/narcissus/src/pipelines/basic.rs b/bins/narcissus/src/pipelines/basic.rs
new file mode 100644 (file)
index 0000000..47fa486
--- /dev/null
@@ -0,0 +1,92 @@
+use narcissus_core::{cstr, default, include_bytes_align};
+use narcissus_gpu::{
+    BindGroupLayout, BindGroupLayoutDesc, BindGroupLayoutEntryDesc, BindingType, BlendMode,
+    CompareOp, CullingMode, Device, FrontFace, GraphicsPipelineDesc, GraphicsPipelineLayout,
+    ImageFormat, Pipeline, PolygonMode, ShaderDesc, ShaderStageFlags, Topology,
+};
+
+const VERT_SPV: &'static [u8] = include_bytes_align!(4, "../shaders/basic.vert.spv");
+const FRAG_SPV: &'static [u8] = include_bytes_align!(4, "../shaders/basic.frag.spv");
+
+pub struct BasicPipeline {
+    pub uniforms_bind_group_layout: BindGroupLayout,
+    pub storage_bind_group_layout: BindGroupLayout,
+    pub pipeline: Pipeline,
+}
+
+impl BasicPipeline {
+    pub fn new(device: &dyn Device) -> Self {
+        let uniforms_bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDesc {
+            entries: &[BindGroupLayoutEntryDesc {
+                slot: 0,
+                stages: ShaderStageFlags::ALL,
+                binding_type: BindingType::UniformBuffer,
+                count: 1,
+            }],
+        });
+
+        let storage_bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDesc {
+            entries: &[
+                BindGroupLayoutEntryDesc {
+                    slot: 0,
+                    stages: ShaderStageFlags::ALL,
+                    binding_type: BindingType::StorageBuffer,
+                    count: 1,
+                },
+                BindGroupLayoutEntryDesc {
+                    slot: 1,
+                    stages: ShaderStageFlags::ALL,
+                    binding_type: BindingType::StorageBuffer,
+                    count: 1,
+                },
+                BindGroupLayoutEntryDesc {
+                    slot: 2,
+                    stages: ShaderStageFlags::ALL,
+                    binding_type: BindingType::Sampler,
+                    count: 1,
+                },
+                BindGroupLayoutEntryDesc {
+                    slot: 3,
+                    stages: ShaderStageFlags::ALL,
+                    binding_type: BindingType::Image,
+                    count: 1,
+                },
+            ],
+        });
+
+        let pipeline = device.create_graphics_pipeline(&GraphicsPipelineDesc {
+            vertex_shader: ShaderDesc {
+                entry: cstr!("main"),
+                code: VERT_SPV,
+            },
+            fragment_shader: ShaderDesc {
+                entry: cstr!("main"),
+                code: FRAG_SPV,
+            },
+            bind_group_layouts: &[uniforms_bind_group_layout, storage_bind_group_layout],
+            layout: GraphicsPipelineLayout {
+                color_attachment_formats: &[ImageFormat::BGRA8_SRGB],
+                depth_attachment_format: Some(ImageFormat::DEPTH_F32),
+                stencil_attachment_format: None,
+            },
+            topology: Topology::Triangles,
+            polygon_mode: PolygonMode::Fill,
+            culling_mode: CullingMode::Back,
+            front_face: FrontFace::CounterClockwise,
+            blend_mode: BlendMode::Opaque,
+            depth_bias: None,
+            depth_compare_op: CompareOp::GreaterOrEqual,
+            depth_test_enable: true,
+            depth_write_enable: true,
+            stencil_test_enable: false,
+            stencil_back: default(),
+            stencil_front: default(),
+        });
+
+        Self {
+            uniforms_bind_group_layout,
+            storage_bind_group_layout,
+            pipeline,
+        }
+    }
+}
diff --git a/bins/narcissus/src/pipelines/mod.rs b/bins/narcissus/src/pipelines/mod.rs
new file mode 100644 (file)
index 0000000..ecc2c49
--- /dev/null
@@ -0,0 +1,5 @@
+mod basic;
+mod text;
+
+pub use basic::BasicPipeline;
+pub use text::TextPipeline;
diff --git a/bins/narcissus/src/pipelines/text.rs b/bins/narcissus/src/pipelines/text.rs
new file mode 100644 (file)
index 0000000..203011b
--- /dev/null
@@ -0,0 +1,92 @@
+use narcissus_core::{cstr, default, include_bytes_align};
+use narcissus_gpu::{
+    BindGroupLayout, BindGroupLayoutDesc, BindGroupLayoutEntryDesc, BindingType, BlendMode,
+    CompareOp, CullingMode, Device, FrontFace, GraphicsPipelineDesc, GraphicsPipelineLayout,
+    ImageFormat, Pipeline, PolygonMode, ShaderDesc, ShaderStageFlags, Topology,
+};
+
+const VERT_SPV: &'static [u8] = include_bytes_align!(4, "../shaders/text.vert.spv");
+const FRAG_SPV: &'static [u8] = include_bytes_align!(4, "../shaders/text.frag.spv");
+
+pub struct TextPipeline {
+    pub uniforms_bind_group_layout: BindGroupLayout,
+    pub storage_bind_group_layout: BindGroupLayout,
+    pub pipeline: Pipeline,
+}
+
+impl TextPipeline {
+    pub fn new(device: &dyn Device) -> Self {
+        let uniforms_bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDesc {
+            entries: &[BindGroupLayoutEntryDesc {
+                slot: 0,
+                stages: ShaderStageFlags::ALL,
+                binding_type: BindingType::UniformBuffer,
+                count: 1,
+            }],
+        });
+
+        let storage_bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDesc {
+            entries: &[
+                BindGroupLayoutEntryDesc {
+                    slot: 0,
+                    stages: ShaderStageFlags::ALL,
+                    binding_type: BindingType::StorageBuffer,
+                    count: 1,
+                },
+                BindGroupLayoutEntryDesc {
+                    slot: 1,
+                    stages: ShaderStageFlags::ALL,
+                    binding_type: BindingType::StorageBuffer,
+                    count: 1,
+                },
+                BindGroupLayoutEntryDesc {
+                    slot: 2,
+                    stages: ShaderStageFlags::ALL,
+                    binding_type: BindingType::Sampler,
+                    count: 1,
+                },
+                BindGroupLayoutEntryDesc {
+                    slot: 3,
+                    stages: ShaderStageFlags::ALL,
+                    binding_type: BindingType::Image,
+                    count: 1,
+                },
+            ],
+        });
+
+        let pipeline = device.create_graphics_pipeline(&GraphicsPipelineDesc {
+            vertex_shader: ShaderDesc {
+                entry: cstr!("main"),
+                code: VERT_SPV,
+            },
+            fragment_shader: ShaderDesc {
+                entry: cstr!("main"),
+                code: FRAG_SPV,
+            },
+            bind_group_layouts: &[uniforms_bind_group_layout, storage_bind_group_layout],
+            layout: GraphicsPipelineLayout {
+                color_attachment_formats: &[ImageFormat::BGRA8_SRGB],
+                depth_attachment_format: Some(ImageFormat::DEPTH_F32),
+                stencil_attachment_format: None,
+            },
+            topology: Topology::TriangleStrip,
+            polygon_mode: PolygonMode::Fill,
+            culling_mode: CullingMode::None,
+            front_face: FrontFace::CounterClockwise,
+            blend_mode: BlendMode::Premultiplied,
+            depth_bias: None,
+            depth_compare_op: CompareOp::Always,
+            depth_test_enable: false,
+            depth_write_enable: false,
+            stencil_test_enable: false,
+            stencil_back: default(),
+            stencil_front: default(),
+        });
+
+        Self {
+            uniforms_bind_group_layout,
+            storage_bind_group_layout,
+            pipeline,
+        }
+    }
+}
index 5b23cbd99bb2b0ed457361ccfe08c7d8016d970f..c09e33056ca227a0e5f6ec91c95e19369e303c02 100644 (file)
@@ -11,4 +11,4 @@ void main() {
     float NdotL = max(dot(normal, vec3(0.0, 1.0, 0.0)), 0.1f);
     vec3 rgb = texture(sampler2D(tex, texSampler), vec2(texcoord.x, texcoord.y)).rgb;
     outColor = vec4(rgb * NdotL, 1.0);
-}
\ No newline at end of file
+}
index a297aed4d56b8d95aa8742e01b35bd86ea3a47cb..aabdd66eaf0b8deb0faff787824003c68c2fc62e 100644 (file)
Binary files a/bins/narcissus/src/shaders/basic.frag.spv and b/bins/narcissus/src/shaders/basic.frag.spv differ
index f7f36ac7ae8591075cfe9c8c43b913bca9347e56..569def4a29c848421858ac5d5d0575e0d6ce284e 100644 (file)
@@ -1,9 +1,5 @@
 #version 460
 
-layout(set = 0, binding = 0) uniform uniformBuffer {
-    mat4 viewProj;
-};
-
 struct VertexData {
     vec4 position;
     vec4 normal;
@@ -14,6 +10,10 @@ struct TransformData {
     vec4 transform[3];
 };
 
+layout(set = 0, binding = 0) uniform uniformBuffer {
+    mat4 viewProj;
+};
+
 layout(std430, set = 1, binding = 0) readonly buffer vertexBuffer {
     VertexData vertices[];
 };
@@ -22,8 +22,8 @@ layout(std430, set = 1, binding = 1) readonly buffer transformBuffer {
     TransformData transforms[];
 };
 
-layout(location = 0) out vec2 texcoord;
-layout(location = 1) out vec3 normal;
+layout(location = 0) out vec2 outTexcoord;
+layout(location = 1) out vec3 outNormal;
 
 void main() {
     TransformData td = transforms[gl_InstanceIndex];
@@ -39,6 +39,6 @@ void main() {
     vec4 posClip = transpose(viewProj) * vec4(posWorld, 1.0);
     gl_Position = posClip;
 
-    normal = vd.normal.xyz;
-    texcoord = vec2(vd.texcoord.x, 1.0 - vd.texcoord.y);
+    outNormal = vd.normal.xyz;
+    outTexcoord = vec2(vd.texcoord.x, 1.0 - vd.texcoord.y);
 }
index f9260ed64f9501b8e21323e0c4a02fb9c386bb7f..f22e6089d9b0ae3f67572dd1cfddd5812c048728 100644 (file)
Binary files a/bins/narcissus/src/shaders/basic.vert.spv and b/bins/narcissus/src/shaders/basic.vert.spv differ
index e870edb2996a694397e5942c9dac4bc713089aae..4d379fc0536f101f4bb73f8debff39a74da2ea84 100755 (executable)
@@ -3,5 +3,7 @@
 pushd "${0%/*}"
 glslc --target-env=vulkan1.3 -O -fshader-stage=vert -o basic.vert.spv basic.vert.glsl
 glslc --target-env=vulkan1.3 -O -fshader-stage=frag -o basic.frag.spv basic.frag.glsl
+glslc --target-env=vulkan1.3 -O -fshader-stage=vert -o text.vert.spv text.vert.glsl
+glslc --target-env=vulkan1.3 -O -fshader-stage=frag -o text.frag.spv text.frag.glsl
 echo "built shaders"
 popd
diff --git a/bins/narcissus/src/shaders/text.frag.glsl b/bins/narcissus/src/shaders/text.frag.glsl
new file mode 100644 (file)
index 0000000..a946192
--- /dev/null
@@ -0,0 +1,13 @@
+#version 460
+
+layout(set = 1, binding = 2) uniform sampler texSampler;
+layout(set = 1, binding = 3) uniform texture2D tex;
+
+layout(location = 0) in vec2 texcoord;
+layout(location = 1) in vec4 color;
+layout(location = 0) out vec4 outColor;
+
+void main() {
+    float coverage = texture(sampler2D(tex, texSampler), vec2(texcoord.x, texcoord.y)).r;
+    outColor = color * coverage;
+}
diff --git a/bins/narcissus/src/shaders/text.frag.spv b/bins/narcissus/src/shaders/text.frag.spv
new file mode 100644 (file)
index 0000000..d15569a
Binary files /dev/null and b/bins/narcissus/src/shaders/text.frag.spv differ
diff --git a/bins/narcissus/src/shaders/text.vert.glsl b/bins/narcissus/src/shaders/text.vert.glsl
new file mode 100644 (file)
index 0000000..b8fba72
--- /dev/null
@@ -0,0 +1,67 @@
+#version 460
+
+struct CachedGlyph {
+    uint x0;
+    uint x1;
+    uint y0;
+    uint y1;
+
+    float offset_x0;
+    float offset_x1;
+    float offset_y0;
+    float offset_y1;
+};
+
+struct GlyphInstance {
+    uint index;
+    float x;
+    float y;
+    uint color;
+};
+
+layout(set = 0, binding = 0) uniform uniformBuffer {
+    uint screenWidth;
+    uint screenHeight;
+    uint atlasWidth;
+    uint atlasHeight;
+};
+
+layout(std430, set = 1, binding = 0) readonly buffer glyphBuffer {
+    CachedGlyph cachedGlyphs[];
+};
+
+layout(std430, set = 1, binding = 1) readonly buffer glyphInstanceBuffer {
+    GlyphInstance glyphInstances[];
+};
+
+layout(location = 0) out vec2 outTexcoord;
+layout(location = 1) out flat vec4 outColor;
+
+void main() {
+    GlyphInstance gi = glyphInstances[gl_InstanceIndex];
+    CachedGlyph cg = cachedGlyphs[gi.index];
+
+    vec2 positions[4] = {
+        vec2(cg.offset_x0, cg.offset_y0),
+        vec2(cg.offset_x0, cg.offset_y1),
+        vec2(cg.offset_x1, cg.offset_y0),
+        vec2(cg.offset_x1, cg.offset_y1)
+    };
+
+    vec2 halfScreenSize = vec2(screenWidth, screenHeight) / 2.0;
+    vec2 glyphPosition = vec2(gi.x, gi.y);
+    vec2 vertexPosition = positions[gl_VertexIndex] + glyphPosition;
+    vec2 position = vertexPosition / halfScreenSize - 1.0;
+    gl_Position = vec4(position, 0.0, 1.0);
+
+    vec2 texcoords[4] = {
+        vec2(cg.x0, cg.y0),
+        vec2(cg.x0, cg.y1),
+        vec2(cg.x1, cg.y0),
+        vec2(cg.x1, cg.y1)
+    };
+    outTexcoord = texcoords[gl_VertexIndex] / vec2(atlasWidth, atlasHeight);
+
+    vec4 color = unpackUnorm4x8(gi.color).bgra;
+    outColor = color;
+}
diff --git a/bins/narcissus/src/shaders/text.vert.spv b/bins/narcissus/src/shaders/text.vert.spv
new file mode 100644 (file)
index 0000000..5a948e7
Binary files /dev/null and b/bins/narcissus/src/shaders/text.vert.spv differ