From 55275d7b052c49e6bd545103ed6d77c7635ec712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 18 May 2020 15:46:14 +0200 Subject: [PATCH] Initial plumbing --- .gitignore | 2 + Cargo.toml | 15 ++ examples/Inconsolata-Regular.ttf | Bin 0 -> 92124 bytes examples/hello.rs | 89 +++++++++ rustfmt.toml | 1 + src/GLYPH_BRUSH_LICENSE | 201 +++++++++++++++++++ src/builder.rs | 79 ++++++++ src/lib.rs | 322 +++++++++++++++++++++++++++++++ src/pipeline.rs | 221 +++++++++++++++++++++ src/pipeline/cache.rs | 23 +++ src/region.rs | 7 + src/shader/fragment.frag | 18 ++ src/shader/vertex.vert | 46 +++++ 13 files changed, 1024 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 examples/Inconsolata-Regular.ttf create mode 100644 examples/hello.rs create mode 100644 rustfmt.toml create mode 100644 src/GLYPH_BRUSH_LICENSE create mode 100644 src/builder.rs create mode 100644 src/lib.rs create mode 100644 src/pipeline.rs create mode 100644 src/pipeline/cache.rs create mode 100644 src/region.rs create mode 100644 src/shader/fragment.frag create mode 100644 src/shader/vertex.vert diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..faf5b54 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "glow_glyph" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" + +[dependencies] +glow = "0.4" +glyph_brush = "0.6" +log = "0.4" +bytemuck = "1.2" + +[dev-dependencies] +glutin = "0.24" +env_logger = "0.7" diff --git a/examples/Inconsolata-Regular.ttf b/examples/Inconsolata-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..3e547460b79225335bbc935a679a59da6dc99eae GIT binary patch literal 92124 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 literal 0 HcmV?d00001 diff --git a/examples/hello.rs b/examples/hello.rs new file mode 100644 index 0000000..d75f269 --- /dev/null +++ b/examples/hello.rs @@ -0,0 +1,89 @@ +use glow::HasContext; +use glow_glyph::{GlyphBrushBuilder, Scale, Section}; + +fn main() -> Result<(), String> { + env_logger::init(); + + // Open window and create a surface + let event_loop = glutin::event_loop::EventLoop::new(); + + let window_builder = + glutin::window::WindowBuilder::new().with_resizable(false); + + let context = glutin::ContextBuilder::new() + .with_srgb(true) + .build_windowed(window_builder, &event_loop) + .expect("Open window"); + + let context = + unsafe { context.make_current().expect("Make OpenGL context current") }; + + let mut size = context.window().inner_size(); + + // Initialize OpenGL + let gl = glow::Context::from_loader_function(|s| { + context.get_proc_address(s) as *const _ + }); + + // Prepare glyph_brush + let inconsolata: &[u8] = include_bytes!("Inconsolata-Regular.ttf"); + let mut glyph_brush = GlyphBrushBuilder::using_font_bytes(inconsolata) + .expect("Load fonts") + .build(&gl); + + // Render loop + context.window().request_redraw(); + + unsafe { + gl.enable(glow::FRAMEBUFFER_SRGB); + gl.clear_color(0.4, 0.4, 0.4, 1.0); + } + + event_loop.run(move |event, _, control_flow| { + match event { + glutin::event::Event::WindowEvent { + event: glutin::event::WindowEvent::CloseRequested, + .. + } => *control_flow = glutin::event_loop::ControlFlow::Exit, + glutin::event::Event::WindowEvent { + event: glutin::event::WindowEvent::Resized(new_size), + .. + } => { + context.resize(new_size); + + size = new_size; + } + glutin::event::Event::RedrawRequested { .. } => { + unsafe { gl.clear(glow::COLOR_BUFFER_BIT) } + + glyph_brush.queue(Section { + text: "Hello wgpu_glyph!", + screen_position: (30.0, 30.0), + color: [0.0, 0.0, 0.0, 1.0], + scale: Scale { x: 40.0, y: 40.0 }, + bounds: (size.width as f32, size.height as f32), + ..Section::default() + }); + + glyph_brush.queue(Section { + text: "Hello wgpu_glyph!", + screen_position: (30.0, 90.0), + color: [1.0, 1.0, 1.0, 1.0], + scale: Scale { x: 40.0, y: 40.0 }, + bounds: (size.width as f32, size.height as f32), + ..Section::default() + }); + + // Draw the text! + glyph_brush + .draw_queued(&gl, size.width, size.height) + .expect("Draw queued"); + + context.swap_buffers().expect("Swap buffers"); + } + _ => { + *control_flow = glutin::event_loop::ControlFlow::Wait; + } + } + }) +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..d979d31 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width=80 diff --git a/src/GLYPH_BRUSH_LICENSE b/src/GLYPH_BRUSH_LICENSE new file mode 100644 index 0000000..0e13673 --- /dev/null +++ b/src/GLYPH_BRUSH_LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 Alex Butler + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/builder.rs b/src/builder.rs new file mode 100644 index 0000000..1994d98 --- /dev/null +++ b/src/builder.rs @@ -0,0 +1,79 @@ +use core::hash::BuildHasher; + +use glyph_brush::delegate_glyph_brush_builder_fns; +use glyph_brush::{rusttype, DefaultSectionHasher}; +use rusttype::{Error, Font, SharedBytes}; + +use super::GlyphBrush; + +/// Builder for a [`GlyphBrush`](struct.GlyphBrush.html). +pub struct GlyphBrushBuilder<'a, H = DefaultSectionHasher> { + inner: glyph_brush::GlyphBrushBuilder<'a, H>, +} + +impl<'a, H> From> for GlyphBrushBuilder<'a, H> { + fn from(inner: glyph_brush::GlyphBrushBuilder<'a, H>) -> Self { + GlyphBrushBuilder { inner } + } +} +impl<'a> GlyphBrushBuilder<'a> { + /// Specifies the default font data used to render glyphs. + /// Referenced with `FontId(0)`, which is default. + #[inline] + pub fn using_font_bytes>>(font_0_data: B) -> Result { + let font = Font::from_bytes(font_0_data)?; + + Ok(Self::using_font(font)) + } + + #[inline] + pub fn using_fonts_bytes(font_data: V) -> Result + where + B: Into>, + V: Into>, + { + let fonts = font_data + .into() + .into_iter() + .map(Font::from_bytes) + .collect::, Error>>()?; + + Ok(Self::using_fonts(fonts)) + } + + /// Specifies the default font used to render glyphs. + /// Referenced with `FontId(0)`, which is default. + #[inline] + pub fn using_font(font_0: Font<'a>) -> Self { + Self::using_fonts(vec![font_0]) + } + + pub fn using_fonts>>>(fonts: V) -> Self { + GlyphBrushBuilder { + inner: glyph_brush::GlyphBrushBuilder::using_fonts(fonts), + } + } +} + +impl<'a, H: BuildHasher> GlyphBrushBuilder<'a, H> { + delegate_glyph_brush_builder_fns!(inner); + + /// Sets the section hasher. `GlyphBrush` cannot handle absolute section + /// hash collisions so use a good hash algorithm. + /// + /// This hasher is used to distinguish sections, rather than for hashmap + /// internal use. + /// + /// Defaults to [seahash](https://docs.rs/seahash). + pub fn section_hasher(self, section_hasher: T) -> GlyphBrushBuilder<'a, T> { + GlyphBrushBuilder { + inner: self.inner.section_hasher(section_hasher), + } + } + + /// Builds a `GlyphBrush` using the given `wgpu::Device` that can render + /// text for texture views with the given `render_format`. + pub fn build(self, gl: &glow::Context) -> GlyphBrush<'a, H> { + GlyphBrush::::new(gl, self.inner) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d064e24 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,322 @@ +//! A fast text renderer for [`wgpu`]. Powered by [`glyph_brush`]. +//! +//! [`wgpu`]: https://github.com/gfx-rs/wgpu +//! [`glyph_brush`]: https://github.com/alexheretic/glyph-brush/tree/master/glyph-brush +#![deny(unused_results)] +mod builder; +mod pipeline; +mod region; + +pub use region::Region; + +use pipeline::{Instance, Pipeline}; + +pub use builder::GlyphBrushBuilder; +pub use glyph_brush::{ + rusttype::{self, Font, Point, PositionedGlyph, Rect, Scale, SharedBytes}, + BuiltInLineBreaker, FontId, FontMap, GlyphCruncher, GlyphPositioner, HorizontalAlign, Layout, + LineBreak, LineBreaker, OwnedSectionText, OwnedVariedSection, PositionedGlyphIter, Section, + SectionGeometry, SectionText, VariedSection, VerticalAlign, +}; + +use core::hash::BuildHasher; +use std::borrow::Cow; + +use glyph_brush::{BrushAction, BrushError, Color, DefaultSectionHasher}; +use log::{log_enabled, warn}; + +/// Object allowing glyph drawing, containing cache state. Manages glyph positioning cacheing, +/// glyph draw caching & efficient GPU texture cache updating and re-sizing on demand. +/// +/// Build using a [`GlyphBrushBuilder`](struct.GlyphBrushBuilder.html). +pub struct GlyphBrush<'font, H = DefaultSectionHasher> { + pipeline: Pipeline, + glyph_brush: glyph_brush::GlyphBrush<'font, Instance, H>, +} + +impl<'font, H: BuildHasher> GlyphBrush<'font, H> { + /// Queues a section/layout to be drawn by the next call of + /// [`draw_queued`](struct.GlyphBrush.html#method.draw_queued). Can be + /// called multiple times to queue multiple sections for drawing. + /// + /// Benefits from caching, see [caching behaviour](#caching-behaviour). + #[inline] + pub fn queue<'a, S>(&mut self, section: S) + where + S: Into>>, + { + self.glyph_brush.queue(section) + } + + /// Queues a section/layout to be drawn by the next call of + /// [`draw_queued`](struct.GlyphBrush.html#method.draw_queued). Can be + /// called multiple times to queue multiple sections for drawing. + /// + /// Used to provide custom `GlyphPositioner` logic, if using built-in + /// [`Layout`](enum.Layout.html) simply use + /// [`queue`](struct.GlyphBrush.html#method.queue) + /// + /// Benefits from caching, see [caching behaviour](#caching-behaviour). + #[inline] + pub fn queue_custom_layout<'a, S, G>(&mut self, section: S, custom_layout: &G) + where + G: GlyphPositioner, + S: Into>>, + { + self.glyph_brush.queue_custom_layout(section, custom_layout) + } + + /// Queues pre-positioned glyphs to be processed by the next call of + /// [`draw_queued`](struct.GlyphBrush.html#method.draw_queued). Can be + /// called multiple times. + #[inline] + pub fn queue_pre_positioned( + &mut self, + glyphs: Vec<(PositionedGlyph<'font>, Color, FontId)>, + bounds: Rect, + z: f32, + ) { + self.glyph_brush.queue_pre_positioned(glyphs, bounds, z) + } + + /// Retains the section in the cache as if it had been used in the last + /// draw-frame. + /// + /// Should not be necessary unless using multiple draws per frame with + /// distinct transforms, see [caching behaviour](#caching-behaviour). + #[inline] + pub fn keep_cached_custom_layout<'a, S, G>(&mut self, section: S, custom_layout: &G) + where + S: Into>>, + G: GlyphPositioner, + { + self.glyph_brush + .keep_cached_custom_layout(section, custom_layout) + } + + /// Retains the section in the cache as if it had been used in the last + /// draw-frame. + /// + /// Should not be necessary unless using multiple draws per frame with + /// distinct transforms, see [caching behaviour](#caching-behaviour). + #[inline] + pub fn keep_cached<'a, S>(&mut self, section: S) + where + S: Into>>, + { + self.glyph_brush.keep_cached(section) + } + + fn process_queued(&mut self, context: &glow::Context) { + let pipeline = &mut self.pipeline; + + let mut brush_action; + + loop { + brush_action = self.glyph_brush.process_queued( + |rect, tex_data| { + let offset = [rect.min.x as u16, rect.min.y as u16]; + let size = [rect.width() as u16, rect.height() as u16]; + + pipeline.update_cache(context, offset, size, tex_data); + }, + Instance::from, + ); + + match brush_action { + Ok(_) => break, + Err(BrushError::TextureTooSmall { suggested }) => { + // TODO: Obtain max texture dimensions using `wgpu` + // This is currently not possible I think. Ask! + let max_image_dimension = 2048; + + let (new_width, new_height) = if (suggested.0 > max_image_dimension + || suggested.1 > max_image_dimension) + && (self.glyph_brush.texture_dimensions().0 < max_image_dimension + || self.glyph_brush.texture_dimensions().1 < max_image_dimension) + { + (max_image_dimension, max_image_dimension) + } else { + suggested + }; + + if log_enabled!(log::Level::Warn) { + warn!( + "Increasing glyph texture size {old:?} -> {new:?}. \ + Consider building with `.initial_cache_size({new:?})` to avoid \ + resizing", + old = self.glyph_brush.texture_dimensions(), + new = (new_width, new_height), + ); + } + + pipeline.increase_cache_size(context, new_width, new_height); + self.glyph_brush.resize_texture(new_width, new_height); + } + } + } + + match brush_action.unwrap() { + BrushAction::Draw(verts) => { + self.pipeline.upload(context, &verts); + } + BrushAction::ReDraw => {} + }; + } + + /// Returns the available fonts. + /// + /// The `FontId` corresponds to the index of the font data. + #[inline] + pub fn fonts(&self) -> &[Font<'_>] { + self.glyph_brush.fonts() + } + + /// Adds an additional font to the one(s) initially added on build. + /// + /// Returns a new [`FontId`](struct.FontId.html) to reference this font. + pub fn add_font_bytes<'a: 'font, B: Into>>(&mut self, font_data: B) -> FontId { + self.glyph_brush.add_font_bytes(font_data) + } + + /// Adds an additional font to the one(s) initially added on build. + /// + /// Returns a new [`FontId`](struct.FontId.html) to reference this font. + pub fn add_font<'a: 'font>(&mut self, font_data: Font<'a>) -> FontId { + self.glyph_brush.add_font(font_data) + } +} + +impl<'font, H: BuildHasher> GlyphBrush<'font, H> { + fn new(gl: &glow::Context, raw_builder: glyph_brush::GlyphBrushBuilder<'font, H>) -> Self { + let glyph_brush = raw_builder.build(); + let (cache_width, cache_height) = glyph_brush.texture_dimensions(); + GlyphBrush { + pipeline: Pipeline::new(gl, cache_width, cache_height), + glyph_brush, + } + } + + /// Draws all queued sections onto a render target. + /// See [`queue`](struct.GlyphBrush.html#method.queue). + /// + /// It __does not__ submit the encoder command buffer to the device queue. + /// + /// Trims the cache, see [caching behaviour](#caching-behaviour). + /// + /// # Panics + /// Panics if the provided `target` has a texture format that does not match + /// the `render_format` provided on creation of the `GlyphBrush`. + #[inline] + pub fn draw_queued( + &mut self, + context: &glow::Context, + target_width: u32, + target_height: u32, + ) -> Result<(), String> { + self.draw_queued_with_transform( + context, + orthographic_projection(target_width, target_height), + ) + } + + /// Draws all queued sections onto a render target, applying a position + /// transform (e.g. a projection). + /// See [`queue`](struct.GlyphBrush.html#method.queue). + /// + /// It __does not__ submit the encoder command buffer to the device queue. + /// + /// Trims the cache, see [caching behaviour](#caching-behaviour). + /// + /// # Panics + /// Panics if the provided `target` has a texture format that does not match + /// the `render_format` provided on creation of the `GlyphBrush`. + #[inline] + pub fn draw_queued_with_transform( + &mut self, + context: &glow::Context, + transform: [f32; 16], + ) -> Result<(), String> { + self.process_queued(context); + self.pipeline.draw(context, transform, None); + + Ok(()) + } + + /// Draws all queued sections onto a render target, applying a position + /// transform (e.g. a projection) and a scissoring region. + /// See [`queue`](struct.GlyphBrush.html#method.queue). + /// + /// It __does not__ submit the encoder command buffer to the device queue. + /// + /// Trims the cache, see [caching behaviour](#caching-behaviour). + /// + /// # Panics + /// Panics if the provided `target` has a texture format that does not match + /// the `render_format` provided on creation of the `GlyphBrush`. + #[inline] + pub fn draw_queued_with_transform_and_scissoring( + &mut self, + context: &glow::Context, + transform: [f32; 16], + region: Region, + ) -> Result<(), String> { + self.process_queued(context); + self.pipeline.draw(context, transform, Some(region)); + + Ok(()) + } +} + +/// Helper function to generate a generate a transform matrix. +pub fn orthographic_projection(width: u32, height: u32) -> [f32; 16] { + #[cfg_attr(rustfmt, rustfmt_skip)] + [ + 2.0 / width as f32, 0.0, 0.0, 0.0, + 0.0, -2.0 / height as f32, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + -1.0, 1.0, 0.0, 1.0, + ] +} + +impl<'font, H: BuildHasher> GlyphCruncher<'font> for GlyphBrush<'font, H> { + #[inline] + fn pixel_bounds_custom_layout<'a, S, L>( + &mut self, + section: S, + custom_layout: &L, + ) -> Option> + where + L: GlyphPositioner + std::hash::Hash, + S: Into>>, + { + self.glyph_brush + .pixel_bounds_custom_layout(section, custom_layout) + } + + #[inline] + fn glyphs_custom_layout<'a, 'b, S, L>( + &'b mut self, + section: S, + custom_layout: &L, + ) -> PositionedGlyphIter<'b, 'font> + where + L: GlyphPositioner + std::hash::Hash, + S: Into>>, + { + self.glyph_brush + .glyphs_custom_layout(section, custom_layout) + } + + #[inline] + fn fonts(&self) -> &[Font<'font>] { + self.glyph_brush.fonts() + } +} + +impl std::fmt::Debug for GlyphBrush<'_, H> { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "GlyphBrush") + } +} diff --git a/src/pipeline.rs b/src/pipeline.rs new file mode 100644 index 0000000..fa572de --- /dev/null +++ b/src/pipeline.rs @@ -0,0 +1,221 @@ +mod cache; + +use crate::Region; +use cache::Cache; + +use glow::HasContext; +use glyph_brush::rusttype::{point, Rect}; + +pub struct Pipeline { + sampler: ::Sampler, + program: ::Program, + instances: ::Buffer, + cache: Cache, + current_instances: usize, + supported_instances: usize, + current_transform: [f32; 16], +} + +impl Pipeline { + pub fn new( + gl: &glow::Context, + cache_width: u32, + cache_height: u32, + ) -> Pipeline { + let sampler = + unsafe { gl.create_sampler().expect("Create glyph sampler") }; + + let cache = Cache::new(gl, cache_width, cache_height); + + let program = unsafe { + create_program( + gl, + &[ + (glow::VERTEX_SHADER, include_str!("./shader/vertex.vert")), + ( + glow::FRAGMENT_SHADER, + include_str!("./shader/fragment.frag"), + ), + ], + ) + }; + + let instances = + unsafe { gl.create_buffer().expect("Create instance buffer") }; + + Pipeline { + sampler, + program, + cache, + instances, + current_instances: 0, + supported_instances: Instance::INITIAL_AMOUNT, + current_transform: [0.0; 16], + } + } + + pub fn draw( + &mut self, + gl: &glow::Context, + transform: [f32; 16], + region: Option, + ) { + } + + pub fn update_cache( + &mut self, + gl: &glow::Context, + offset: [u16; 2], + size: [u16; 2], + data: &[u8], + ) { + self.cache.update(gl, offset, size, data); + } + + pub fn increase_cache_size( + &mut self, + gl: &glow::Context, + width: u32, + height: u32, + ) { + self.cache = Cache::new(gl, width, height); + } + + pub fn upload(&mut self, gl: &glow::Context, instances: &[Instance]) { + if instances.is_empty() { + self.current_instances = 0; + return; + } + + if instances.len() > self.supported_instances { + // TODO + + self.supported_instances = instances.len(); + } + + // TODO + + self.current_instances = instances.len(); + } +} + +// Helpers +#[cfg_attr(rustfmt, rustfmt_skip)] +const IDENTITY_MATRIX: [f32; 16] = [ + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0, +]; + +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct Instance { + left_top: [f32; 3], + right_bottom: [f32; 2], + tex_left_top: [f32; 2], + tex_right_bottom: [f32; 2], + color: [f32; 4], +} + +unsafe impl bytemuck::Zeroable for Instance {} +unsafe impl bytemuck::Pod for Instance {} + +impl Instance { + const INITIAL_AMOUNT: usize = 50_000; +} + +impl From for Instance { + #[inline] + fn from(vertex: glyph_brush::GlyphVertex) -> Instance { + let glyph_brush::GlyphVertex { + mut tex_coords, + pixel_coords, + bounds, + color, + z, + } = vertex; + + let gl_bounds = bounds; + + let mut gl_rect = Rect { + min: point(pixel_coords.min.x as f32, pixel_coords.min.y as f32), + max: point(pixel_coords.max.x as f32, pixel_coords.max.y as f32), + }; + + // handle overlapping bounds, modify uv_rect to preserve texture aspect + if gl_rect.max.x > gl_bounds.max.x { + let old_width = gl_rect.width(); + gl_rect.max.x = gl_bounds.max.x; + tex_coords.max.x = tex_coords.min.x + + tex_coords.width() * gl_rect.width() / old_width; + } + + if gl_rect.min.x < gl_bounds.min.x { + let old_width = gl_rect.width(); + gl_rect.min.x = gl_bounds.min.x; + tex_coords.min.x = tex_coords.max.x + - tex_coords.width() * gl_rect.width() / old_width; + } + + if gl_rect.max.y > gl_bounds.max.y { + let old_height = gl_rect.height(); + gl_rect.max.y = gl_bounds.max.y; + tex_coords.max.y = tex_coords.min.y + + tex_coords.height() * gl_rect.height() / old_height; + } + + if gl_rect.min.y < gl_bounds.min.y { + let old_height = gl_rect.height(); + gl_rect.min.y = gl_bounds.min.y; + tex_coords.min.y = tex_coords.max.y + - tex_coords.height() * gl_rect.height() / old_height; + } + + Instance { + left_top: [gl_rect.min.x, gl_rect.max.y, z], + right_bottom: [gl_rect.max.x, gl_rect.min.y], + tex_left_top: [tex_coords.min.x, tex_coords.max.y], + tex_right_bottom: [tex_coords.max.x, tex_coords.min.y], + color, + } + } +} + +unsafe fn create_program( + gl: &glow::Context, + shader_sources: &[(u32, &str)], +) -> ::Program { + let program = gl.create_program().expect("Cannot create program"); + + let mut shaders = Vec::with_capacity(shader_sources.len()); + + for (shader_type, shader_source) in shader_sources.iter() { + let shader = gl + .create_shader(*shader_type) + .expect("Cannot create shader"); + + gl.shader_source(shader, shader_source); + gl.compile_shader(shader); + + if !gl.get_shader_compile_status(shader) { + panic!(gl.get_shader_info_log(shader)); + } + + gl.attach_shader(program, shader); + + shaders.push(shader); + } + + gl.link_program(program); + if !gl.get_program_link_status(program) { + panic!(gl.get_program_info_log(program)); + } + + for shader in shaders { + gl.detach_shader(program, shader); + gl.delete_shader(shader); + } + + program +} diff --git a/src/pipeline/cache.rs b/src/pipeline/cache.rs new file mode 100644 index 0000000..b80ccc2 --- /dev/null +++ b/src/pipeline/cache.rs @@ -0,0 +1,23 @@ +use glow::HasContext; + +pub struct Cache { + texture: ::Texture, +} + +impl Cache { + pub fn new(gl: &glow::Context, width: u32, height: u32) -> Cache { + let texture = unsafe { + let handle = gl.create_texture().expect("Create glyph cache texture"); + + gl.tex_storage_2d(handle, 1, glow::R8, width as i32, height as i32); + + handle + }; + + Cache { texture } + } + + pub fn update(&self, gl: &glow::Context, offset: [u16; 2], size: [u16; 2], data: &[u8]) { + // TODO + } +} diff --git a/src/region.rs b/src/region.rs new file mode 100644 index 0000000..8fcdbcd --- /dev/null +++ b/src/region.rs @@ -0,0 +1,7 @@ +/// A region of the screen. +pub struct Region { + pub x: u32, + pub y: u32, + pub width: u32, + pub height: u32, +} diff --git a/src/shader/fragment.frag b/src/shader/fragment.frag new file mode 100644 index 0000000..4a335b1 --- /dev/null +++ b/src/shader/fragment.frag @@ -0,0 +1,18 @@ +#version 450 + +layout(location = 1) uniform sampler2D font_sampler; + +in vec2 f_tex_pos; +in vec4 f_color; + +out vec4 Target0; + +void main() { + float alpha = texture(font_sampler, f_tex_pos).r; + + if (alpha <= 0.0) { + discard; + } + + Target0 = f_color * vec4(1.0, 1.0, 1.0, alpha); +} diff --git a/src/shader/vertex.vert b/src/shader/vertex.vert new file mode 100644 index 0000000..18fc01c --- /dev/null +++ b/src/shader/vertex.vert @@ -0,0 +1,46 @@ +#version 450 + +layout(location = 0) uniform mat4 transform; + +in vec3 left_top; +in vec2 right_bottom; +in vec2 tex_left_top; +in vec2 tex_right_bottom; +in vec4 color; + +out vec2 f_tex_pos; +out vec4 f_color; + +// generate positional data based on vertex ID +void main() { + vec2 pos = vec2(0.0); + float left = left_top.x; + float right = right_bottom.x; + float top = left_top.y; + float bottom = right_bottom.y; + + switch (gl_VertexID) { + case 0: + pos = vec2(left, top); + f_tex_pos = tex_left_top; + break; + + case 1: + pos = vec2(right, top); + f_tex_pos = vec2(tex_right_bottom.x, tex_left_top.y); + break; + + case 2: + pos = vec2(left, bottom); + f_tex_pos = vec2(tex_left_top.x, tex_right_bottom.y); + break; + + case 3: + pos = vec2(right, bottom); + f_tex_pos = tex_right_bottom; + break; + } + + f_color = color; + gl_Position = transform * vec4(pos, left_top.z, 1.0); +}