diff --git a/Cargo.toml b/Cargo.toml index 24a552a5..079bfe7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ repository = "https://github.com/jdahlstrom/retrofire" documentation = "https://crates.io/crates/retrofire" [workspace.lints] +rust.mismatched_lifetime_syntaxes = "deny" clippy.manual_range_contains = "allow" clippy.collapsible_if = "allow" @@ -73,7 +74,6 @@ lto = "thin" opt-level = 1 split-debuginfo = "unpacked" - [[bench]] name = "fill" harness = false diff --git a/core/src/render/tex.rs b/core/src/render/tex.rs index 5a95c203..e16cf816 100644 --- a/core/src/render/tex.rs +++ b/core/src/render/tex.rs @@ -144,6 +144,10 @@ impl Atlas { Self { layout, texture } } + pub fn grid(glyph_dims: Dims, texture: Texture>) -> Self { + Self::new(Layout::Grid { sub_dims: glyph_dims }, texture) + } + /// Returns the top-left and bottom-right pixel coordinates /// of the sub-texture with index `i`. fn rect(&self, i: u32) -> [Point2u; 2] { @@ -167,13 +171,13 @@ impl Atlas { self.texture.data.slice(p0..p1).into() } - /// Returns the texture coordinates of the sub-texture with index `i`. + /// Returns the texture coordinates of a sub-texture. /// /// The coordinates are the top-left, top-right, bottom-left, and /// bottom-right corners of the texture, in that order. /// - /// Note that currently this method does not check `i` is actually a valid - /// index and may return coordinates with values greater than one. + /// Note that currently this method does not check whether `i` is actually + /// a valid index, and may return coordinates with values greater than one. // TODO Error handling, more readable result type pub fn coords(&self, i: u32) -> [TexCoord; 4] { let tex_w = self.texture.width(); @@ -183,6 +187,12 @@ impl Atlas { .map(|p| (p.x() as f32 / tex_w, p.y() as f32 / tex_h)); [uv(x0, y0), uv(x1, y0), uv(x0, y1), uv(x1, y1)] } + + /// Returns the width and height of a sub-texture, in pixels. + pub fn dims(&self, _: u32) -> Dims { + let Layout::Grid { sub_dims } = self.layout; + sub_dims + } } // diff --git a/core/src/render/text.rs b/core/src/render/text.rs index e36aa116..0dedd3d6 100644 --- a/core/src/render/text.rs +++ b/core/src/render/text.rs @@ -1,12 +1,19 @@ -use core::fmt; +use core::fmt::{self, Write}; +use core::ops::Range; #[cfg(feature = "std")] use std::io; -use crate::geom::{Mesh, tri, vertex}; -use crate::math::{Color3, Point2, Vec2, pt2, vec2, vec3}; +use crate::geom::{Mesh, Tri, Vertex, Vertex3, tri, vertex}; +use crate::math::{ + Color3, Color4, Point2, Point2u, ProjMat3, ProjVec3, Vec2, color::gray, + orthographic, pt2, pt3, vec2, vec3, viewport, +}; use crate::util::buf::Buf2; -use super::tex::{Atlas, Layout, SamplerClamp, TexCoord}; +use super::{ + Batch, Context, Frag, FragmentShader, Model, Target, VertexShader, + tex::{Atlas, Layout, SamplerClamp, TexCoord}, +}; /// Text represented as texture-mapped geometry, one quad per glyph. #[derive(Clone)] @@ -18,6 +25,42 @@ pub struct Text { cursor: Point2, } +pub struct Console { + text: Text, + left_top: Point2u, + right_bot: Point2u, +} + +pub struct TextShader<'a>(&'a Text); + +impl VertexShader, ProjMat3> for TextShader<'_> { + type Output = Vertex; + + fn shade_vertex( + &self, + v: Vertex3, + tf: ProjMat3, + ) -> Self::Output { + vertex(tf.apply(&v.pos), v.attrib) + } +} + +impl FragmentShader for TextShader<'_> { + fn shade_fragment(&self, f: Frag) -> Option { + let c = self.0.sample(f.var); + (c != gray(0)).then_some(c.to_rgba()) + } +} + +pub type TextBatch<'a> = Batch< + Tri, + Vertex3, + ProjMat3, + TextShader<'a>, + (), + Context, +>; + // // Inherent impls // @@ -64,7 +107,7 @@ impl Text { fn write_char(&mut self, idx: u32) { let Self { font, geom, cursor, .. } = self; - let Layout::Grid { sub_dims: (gw, gh) } = font.layout; + let (gw, gh) = font.dims(idx); let (glyph_w, glyph_h) = (gw as f32, gh as f32); let [tl, tr, bl, br] = font.coords(idx); @@ -90,6 +133,60 @@ impl Text { *cursor += vec2(glyph_w, 0.0); } + + fn newline(&mut self) { + let Layout::Grid { sub_dims } = self.font.layout; + // TODO variable line height support + self.cursor = pt2(0.0, self.cursor.y() + sub_dims.1 as f32) + } +} + +impl Console { + pub fn new(font: Atlas, bounds: Range) -> Self { + Self { + text: Text::new(font), + left_top: bounds.start, + right_bot: bounds.end, + } + } + + pub fn print(&mut self, s: &str) { + _ = self.text.write_str(s); + } + pub fn println(&mut self, s: &str) { + self.print(s); + self.print("\n"); + } + + pub fn write_fmt(&mut self, args: fmt::Arguments) { + _ = self.text.write_fmt(args) + } + + pub fn clear(&mut self) { + self.text.clear(); + } + + pub fn batch(&self) -> TextBatch<'_> { + let Self { + left_top: lt, right_bot: rb, .. + } = self; + + let [w, h] = (*rb - *lt).0; + let projection = + orthographic(pt3(0.0, 0.0, 0.0), pt3(w as f32, h as f32, 0.0)); + + let viewport = viewport(*lt..*rb); + + Batch::new() + .mesh(&self.text.geom) + .uniform(projection.to()) + .shader(TextShader(&self.text)) + .viewport(viewport) + } + + pub fn render(&self, target: impl Target, ctx: &Context) { + self.batch().target(target).context(ctx).render(); + } } // @@ -110,18 +207,9 @@ impl io::Write for Text { /// /// [1]: https://en.wikipedia.org/wiki/Mojibake fn write(&mut self, buf: &[u8]) -> io::Result { - /*let (rows, cols) = buf - .split(|&b| b == b'\n') - .fold((0, 0), |(rs, cs), row| (rs + 1, cs.max(row.len() as u32))); - if rows == 0 || cols == 0 { - return Ok(0); - }*/ - let Layout::Grid { sub_dims } = self.font.layout; - let glyph_h = sub_dims.1 as f32; - for &b in buf { match b { - b'\n' => self.cursor = pt2(0.0, self.cursor.y() + glyph_h), + b'\n' => self.newline(), _ => self.write_char(b.into()), } } @@ -140,20 +228,9 @@ impl fmt::Write for Text { /// of each `char` as an index into the font. As such, the font should have /// enough glyphs to cover all the characters used. fn write_str(&mut self, s: &str) -> fmt::Result { - /*let (rows, cols) = s - .split(|c| c == '\n') - .fold((0, 0), |(rs, cs), row| { - (rs + 1, cs.max(row.chars().count() as u32)) - }); - if rows == 0 || cols == 0 { - return Ok(()); - }*/ - let Layout::Grid { sub_dims } = self.font.layout; - let glyph_h = sub_dims.1 as f32; - for c in s.chars() { match c { - '\n' => self.cursor = pt2(0.0, self.cursor.y() + glyph_h), + '\n' => self.newline(), _ => self.write_char(c.into()), } } diff --git a/demos/assets/font_6x10.pbm b/demos/assets/font_6x10.pbm new file mode 100644 index 00000000..299c1b92 Binary files /dev/null and b/demos/assets/font_6x10.pbm differ diff --git a/demos/assets/font_8x12.pbm b/demos/assets/font_8x12.pbm new file mode 100644 index 00000000..8b9a2f8f Binary files /dev/null and b/demos/assets/font_8x12.pbm differ diff --git a/demos/src/bin/crates.rs b/demos/src/bin/crates.rs index 555659b3..c9f7380b 100644 --- a/demos/src/bin/crates.rs +++ b/demos/src/bin/crates.rs @@ -8,7 +8,8 @@ use re::core::render::{ clip::Status::*, scene::Obj, shader, - tex::SamplerClamp, + tex::{Atlas, SamplerClamp}, + text::Console, }; // Try also Rgb565 or Rgba4444 use re::core::util::{pixfmt::Rgba8888, pnm::read_pnm}; @@ -20,6 +21,7 @@ fn main() { let mut win = Window::builder() .title("retrofire//crates") .pixel_fmt(Rgba8888) + .vsync(false) .build() .expect("should create window"); @@ -56,6 +58,11 @@ fn main() { let floor = floor(); let crates = crates(); + static FONT: &[u8] = include_bytes!("../../assets/font_6x10.pbm"); + let font = Atlas::grid((6, 10), read_pnm(FONT).expect("font exists").into()); + + let mut con = Console::new(font, pt2(20, 20)..pt2(400, 200)); + win.run(|frame| { // // Camera @@ -132,6 +139,10 @@ fn main() { frame.ctx.stats.borrow_mut().objs.o += 1; } + con.clear(); + write!(con, "{}", frame.ctx.stats.borrow()); + con.render(frame.buf, frame.ctx); + Continue(()) }) .expect("should run"); diff --git a/front/src/sdl2.rs b/front/src/sdl2.rs index d340c3bc..28b86aad 100644 --- a/front/src/sdl2.rs +++ b/front/src/sdl2.rs @@ -192,10 +192,11 @@ impl, const N: usize> Window { let mut tex = tc.create_texture_streaming(PF::SDL_FMT, w, h)?; let mut zbuf = Buf2::new(dims); - let mut ctx = self.ctx.clone(); + let ctx = self.ctx.clone(); let start = Instant::now(); let mut last = Instant::now(); + let stats = RefCell::new(Stats::start()); 'main: loop { self.events.clear(); for e in self.ev_pump.poll_iter() { @@ -208,6 +209,11 @@ impl, const N: usize> Window { } } + let mut frame_ctx = Context { + stats: stats.clone(), + ..ctx.clone() + }; + let cf = tex.with_lock(None, |bytes, pitch| { let bytes = bytes.as_chunks_mut().0; let pitch = pitch / N; @@ -232,11 +238,13 @@ impl, const N: usize> Window { dt: replace(&mut last, Instant::now()).elapsed(), buf: &RefCell::new(buf), win: self, - ctx: &mut ctx, + ctx: &mut frame_ctx, }; frame_fn(frame) })?; + *ctx.stats.borrow_mut() += frame_ctx.stats.into_inner(); + self.present(&tex)?; ctx.stats.borrow_mut().frames += 1.0;