commit 55275d7b052c49e6bd545103ed6d77c7635ec712 Author: Héctor Ramón Jiménez Date: Mon May 18 15:46:14 2020 +0200 Initial plumbing 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 0000000..3e54746 Binary files /dev/null and b/examples/Inconsolata-Regular.ttf differ 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); +}