[oden] Text is mildly functional

This commit is contained in:
John Doty 2023-08-31 17:18:37 -07:00
parent 8914b1795f
commit ecce7b64eb
5 changed files with 204 additions and 114 deletions

15
game/log.ts Normal file
View file

@ -0,0 +1,15 @@
import { print } from "./graphics";
const lines: string[] = [];
export function log(...args: unknown[]) {
// const line = args.join(" ");
lines.push(args.join(" "));
}
export function draw_log() {
for (const line of lines) {
print(line);
break;
}
}

View file

@ -1,4 +1,4 @@
import { cls, print } from "./graphics";
import { cls } from "./graphics";
import { since_start } from "./time";
import { new_v2 } from "./vector";
import { load_world, World, Level, draw_level } from "./level";
@ -10,6 +10,7 @@ import {
is_actor_type,
spawn_actor,
} from "./actor";
import { log, draw_log } from "./log";
/// A nice looping frame counter.
let clock = 0;
@ -25,7 +26,7 @@ let actors: Actor[] = [];
function start_load_assets() {
// Start this load, but then...
load_world("./overworld.ldtk").then((w) => {
print("World loaded at", since_start());
log("World loaded at", since_start());
world = w;
// Assume we start at 0,0
@ -43,7 +44,7 @@ function start_load_assets() {
const props = new_actor_props(entity.id, new_v2(x, y), w);
actors.push(spawn_actor(entity.type, props));
} else {
print("WARNING: Ignoring entity of type", entity.type);
log("WARNING: Ignoring entity of type", entity.type);
}
}
});
@ -52,7 +53,7 @@ function start_load_assets() {
// TODO: Build a system whereby the signatures of the fundamental functions can be checked.
export function init() {
print("Hello world!");
log("Hello world!");
start_load_assets();
}
@ -62,7 +63,7 @@ interface Snapshot {
}
export function suspend(): Snapshot {
print("Suspend! ", actors.length, "actors");
log("Suspend! ", actors.length, "actors");
return {
clock,
actors: actors.map((a) => {
@ -75,7 +76,7 @@ export function resume(snapshot: Snapshot | undefined) {
if (snapshot) {
clock = snapshot.clock || 0;
actors = snapshot.actors.map((s) => spawn_actor(s.type, s.props));
print("Resume! ", actors.length, "actors");
log("Resume! ", actors.length, "actors");
}
}
@ -109,5 +110,6 @@ export function draw() {
actor.draw(clock);
}
// print("FRAME TIME:", since_last_frame());
// log("FRAME TIME:", since_last_frame());
draw_log();
}

View file

@ -629,7 +629,7 @@ impl State {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: true,
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
},
@ -695,7 +695,7 @@ impl State {
});
let inconsolata = include_bytes!("./Inconsolata-Regular.ttf") as &[u8];
let inconsolata = text::FontCache::new(&device, inconsolata, 16.0);
let inconsolata = text::FontCache::new(&device, inconsolata, 32.0);
let mut font_textures = HashMap::new();
{
@ -787,6 +787,7 @@ impl State {
// Reset instance buffers.
self.sprite_instance_buffers.clear();
self.circle_instance_buffers.clear();
self.text_instance_buffers.clear();
let mut builder = FrameBuilder::new(self)?;
for command in commands {
@ -871,18 +872,25 @@ impl State {
.copy_instance_buffers(&self.queue);
self.circle_instance_buffers
.copy_instance_buffers(&self.queue);
self.text_instance_buffers
.copy_instance_buffers(&self.queue);
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum DrawMode {
Sprites,
None,
Sprites(u32),
Circles,
Text,
Text(u32),
}
trait DrawModeInstance: Sized {
const MODE: DrawMode;
// const MODE: DrawMode;
fn assert_compatible_mode(mode: DrawMode);
fn new_vertex_buffer(state: &mut State) -> (u32, VertexBufferHandle);
fn get_vertex_buffer<'a>(
state: &'a mut State,
@ -891,7 +899,19 @@ trait DrawModeInstance: Sized {
}
impl DrawModeInstance for SpriteInstance {
const MODE: DrawMode = DrawMode::Sprites;
// const MODE: DrawMode = DrawMode::Sprites;
fn assert_compatible_mode(mode: DrawMode) {
let compat = match mode {
DrawMode::Sprites(_) => true,
_ => false,
};
assert!(compat, "{mode:?} is not compatible with SpriteInstance");
}
fn new_vertex_buffer(state: &mut State) -> (u32, VertexBufferHandle) {
state.sprite_instance_buffers.get_a_buffer(&state.device)
}
fn get_vertex_buffer<'a>(
state: &'a mut State,
@ -902,7 +922,19 @@ impl DrawModeInstance for SpriteInstance {
}
impl DrawModeInstance for CircleInstance {
const MODE: DrawMode = DrawMode::Circles;
// const MODE: DrawMode = DrawMode::Circles;
fn assert_compatible_mode(mode: DrawMode) {
let compat = match mode {
DrawMode::Circles => true,
_ => false,
};
assert!(compat, "{mode:?} is not compatible with CircleInstance");
}
fn new_vertex_buffer(state: &mut State) -> (u32, VertexBufferHandle) {
state.circle_instance_buffers.get_a_buffer(&state.device)
}
fn get_vertex_buffer<'a>(
state: &'a mut State,
@ -913,7 +945,19 @@ impl DrawModeInstance for CircleInstance {
}
impl DrawModeInstance for GlyphInstance {
const MODE: DrawMode = DrawMode::Text;
// const MODE: DrawMode = DrawMode::Text;
fn assert_compatible_mode(mode: DrawMode) {
let compat = match mode {
DrawMode::Text(_) => true,
_ => false,
};
assert!(compat, "{mode:?} is not compatible with GlyphInstance");
}
fn new_vertex_buffer(state: &mut State) -> (u32, VertexBufferHandle) {
state.text_instance_buffers.get_a_buffer(&state.device)
}
fn get_vertex_buffer<'a>(
state: &'a mut State,
@ -926,7 +970,6 @@ impl DrawModeInstance for GlyphInstance {
#[derive(Debug)]
struct DrawCall {
mode: DrawMode,
texture_id: Option<u32>,
vertex_buffer: VertexBufferHandle,
draw_start: u32,
draw_end: u32,
@ -936,7 +979,6 @@ impl DrawCall {
pub fn new(mode: DrawMode, vertex_buffer: VertexBufferHandle, draw_start: u32) -> Self {
DrawCall {
mode,
texture_id: None,
vertex_buffer,
draw_start,
draw_end: draw_start,
@ -947,16 +989,6 @@ impl DrawCall {
DrawCall::new(self.mode, self.vertex_buffer.clone(), self.draw_end)
}
pub fn switch_textures(&self, id: u32) -> DrawCall {
let mut next = self.new_at_buffer_tail();
next.texture_id = Some(id);
next
}
pub fn empty(&self) -> bool {
self.draw_start == self.draw_end
}
pub fn allocate_capacity(&mut self, capacity: u32) -> Option<VertexBufferHandle> {
// TODO: FIX: CAPACITY == 1 ALWAYS??
if self.vertex_buffer.capacity >= (self.draw_end + capacity) as usize {
@ -970,14 +1002,13 @@ impl DrawCall {
pub fn draw<'a>(&self, state: &'a State, pass: &mut wgpu::RenderPass<'a>) {
if self.draw_end > self.draw_start {
let vb = match self.mode {
DrawMode::Sprites => {
match self.texture_id {
Some(id) => {
let bind_group = state.sprite_textures.get(&id).unwrap();
pass.set_bind_group(1, bind_group, &[]);
}
None => (),
};
DrawMode::None => return,
DrawMode::Sprites(id) => {
let bind_group = state
.sprite_textures
.get(&id)
.expect("sprite mode has unknown sprite texture");
pass.set_bind_group(1, bind_group, &[]);
&state
.sprite_instance_buffers
@ -985,14 +1016,13 @@ impl DrawCall {
.buffer
}
DrawMode::Text => {
match self.texture_id {
Some(id) => {
let bind_group = state.font_textures.get(&id).unwrap();
pass.set_bind_group(1, bind_group, &[]);
}
None => (),
}
DrawMode::Text(id) => {
let bind_group = state
.font_textures
.get(&id)
.expect("text mode has unknown font texture");
pass.set_bind_group(1, bind_group, &[]);
&state.text_instance_buffers.get(&self.vertex_buffer).buffer
}
@ -1023,6 +1053,7 @@ struct FrameBuilder<'a> {
output: wgpu::SurfaceTexture,
mode: DrawMode,
last_texture: Option<u32>,
fill_color: [f32; 4],
stroke_color: [f32; 4],
target: Rc<wgpu::TextureView>,
@ -1054,7 +1085,8 @@ impl<'a> FrameBuilder<'a> {
encoder,
output,
mode: DrawMode::Sprites,
mode: DrawMode::None,
last_texture: None,
stroke_color: [0.0, 0.0, 0.0, 1.0],
fill_color: [1.0, 1.0, 1.0, 1.0],
target: last_view,
@ -1152,59 +1184,16 @@ impl<'a> FrameBuilder<'a> {
}
fn use_texture(&mut self, texture_id: u32) {
match self.draw_calls.last_mut() {
Some(call) => match call.texture_id {
Some(id) => {
if id == texture_id {
return;
} else if call.empty() {
call.texture_id = Some(texture_id);
} else {
let next = call.switch_textures(texture_id);
self.draw_calls.push(next);
return;
}
}
None => {
call.texture_id = Some(texture_id);
}
},
None => {
let (draw_start, buffer) = self.new_instance_buffer();
let mut call = DrawCall::new(self.mode, buffer, draw_start);
call.texture_id = Some(texture_id);
self.draw_calls.push(call);
}
}
self.last_texture = Some(texture_id);
}
fn new_instance_buffer(&mut self) -> (u32, VertexBufferHandle) {
match self.mode {
DrawMode::Sprites => self
.state
.sprite_instance_buffers
.get_a_buffer(&self.state.device),
fn get_instance_buffer<T: DrawModeInstance>(&mut self, mode: DrawMode) -> &mut VertexBuffer<T> {
T::assert_compatible_mode(mode);
DrawMode::Circles => self
.state
.circle_instance_buffers
.get_a_buffer(&self.state.device),
DrawMode::Text => self
.state
.text_instance_buffers
.get_a_buffer(&self.state.device),
}
}
fn get_instance_buffer<T: DrawModeInstance>(&mut self) -> &mut VertexBuffer<T> {
// TODO: Re-work the mode shuffle. Keep the most recent vertex buffer
// around from the
self.mode = T::MODE;
self.mode = mode;
match self.draw_calls.last_mut() {
Some(call) => {
if call.mode == T::MODE {
if call.mode == self.mode {
match call.allocate_capacity(1) {
Some(vb) => return T::get_vertex_buffer(self.state, &vb),
None => {}
@ -1214,7 +1203,7 @@ impl<'a> FrameBuilder<'a> {
None => {}
};
let (draw_start, buffer) = self.new_instance_buffer();
let (draw_start, buffer) = T::new_vertex_buffer(self.state);
let mut call = DrawCall::new(self.mode, buffer, draw_start);
let vb = call.allocate_capacity(1).unwrap();
self.draw_calls.push(call);
@ -1222,15 +1211,19 @@ impl<'a> FrameBuilder<'a> {
}
fn push_sprite(&mut self, si: SpriteInstance) {
let vertex_buffer = self.get_instance_buffer();
vertex_buffer.vec.push(si);
if let Some(id) = self.last_texture {
let vertex_buffer = self.get_instance_buffer(DrawMode::Sprites(id));
vertex_buffer.vec.push(si);
} else {
eprintln!("WARNING: sprite drawn with no active texture");
}
}
fn push_circle(&mut self, cc: script::graphics::CircleCommand) {
let stroke_color = self.stroke_color.clone();
let fill_color = self.fill_color.clone();
let vertex_buffer = self.get_instance_buffer();
let vertex_buffer = self.get_instance_buffer(DrawMode::Circles);
vertex_buffer.vec.push(CircleInstance {
center: cc.center,
radius: cc.radius,
@ -1241,14 +1234,14 @@ impl<'a> FrameBuilder<'a> {
}
fn push_text(&mut self, pc: script::graphics::PrintCommand) {
println!("{}", pc.text);
// println!("{}", pc.text);
let mut cursor_x = 0.0;
let cursor_y = 0.0;
self.mode = DrawMode::Text; // ?
self.use_texture(0); // ?
let font_id: u32 = 0;
// self.mode = ; // ?
let font = self.state.fonts.get_mut(&font_id).unwrap();
let mut glyphs = vec![];
for char in pc.text.chars() {
@ -1257,17 +1250,22 @@ impl<'a> FrameBuilder<'a> {
glyphs.push(GlyphInstance {
src_top_left: [glyph.x, glyph.y],
src_dims: [glyph.w, glyph.h],
dest_top_left: [cursor_x, cursor_y],
dest_dims: [glyph.w, glyph.h],
dest_top_left: [
cursor_x + (glyph.adjust_x / 2.0),
cursor_y + (glyph.adjust_y / 2.0),
],
dest_dims: [glyph.w / 2.0, glyph.h / 2.0],
color,
});
cursor_x += glyph.w;
cursor_x += glyph.advance_width / 2.0;
}
for glyph in glyphs {
// TODO: Get new instance buffer when we don't have space for
// this glyph instead of doing this over and over.
self.get_instance_buffer().vec.push(glyph);
self.get_instance_buffer(DrawMode::Text(font_id))
.vec
.push(glyph);
}
}
@ -1303,9 +1301,12 @@ impl<'a> FrameBuilder<'a> {
};
if need_set_pipeline {
match call.mode {
DrawMode::Sprites => pass.set_pipeline(&self.state.sprite_pipeline),
DrawMode::None => {
continue; // Skip this draw call I guess.
}
DrawMode::Sprites(_) => pass.set_pipeline(&self.state.sprite_pipeline),
DrawMode::Circles => pass.set_pipeline(&self.state.circle_pipeline),
DrawMode::Text => pass.set_pipeline(&self.state.text_pipeline),
DrawMode::Text(_) => pass.set_pipeline(&self.state.text_pipeline),
}
last_mode = Some(call.mode);
}

View file

@ -46,6 +46,8 @@ pub struct FontCache {
font: Font,
size: f32,
texture: wgpu::Texture,
cell_width: u16,
cell_height: u16,
pub view: wgpu::TextureView,
pub sampler: wgpu::Sampler,
@ -69,8 +71,11 @@ struct GlyphCell {
pub struct Glyph {
pub x: f32,
pub y: f32,
pub adjust_x: f32,
pub adjust_y: f32,
pub w: f32,
pub h: f32,
pub advance_width: f32,
}
impl FontCache {
@ -90,7 +95,7 @@ impl FontCache {
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 4,
sample_count: 1, // 4 for multisample?
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING
@ -124,6 +129,8 @@ impl FontCache {
char_width = metrics.width.max(char_width);
char_height = metrics.height.max(char_height);
eprintln!("For this font, width={char_width} height={char_height}");
// Allocate the individual cells in the texture atlas; this records
// the state of what's in the cell and whatnot.
let mut cells = vec![];
@ -151,6 +158,9 @@ impl FontCache {
font,
size,
texture,
cell_width: char_width.try_into().unwrap(),
cell_height: char_height.try_into().unwrap(),
view,
sampler,
cells,
@ -182,9 +192,33 @@ impl FontCache {
SlotState::Empty => {
let (metrics, bitmap) = self.font.rasterize_indexed(index, self.size);
// eprintln!("Rasterizing '{c}' (index {index}): {metrics:?}");
// For a good time, call
// {
// eprintln!();
// let mut i = 0;
// for _ in (0..metrics.height) {
// for _ in (0..metrics.width) {
// let bv = bitmap[i];
// let rc = if bv == 0 {
// ' '
// } else if bv < 25 {
// '.'
// } else {
// 'X'
// };
// eprint!("{rc}");
// i += 1;
// }
// eprintln!();
// }
// eprintln!();
// }
let mut texture = self.texture.as_image_copy();
texture.origin.x = cell.x.into();
texture.origin.y = cell.y.into();
// eprintln!(" Rendering to {}, {}", texture.origin.x, texture.origin.y);
queue.write_texture(
texture,
@ -207,14 +241,44 @@ impl FontCache {
};
Glyph {
x: cell.x as f32 + metrics.bounds.xmin,
y: cell.y as f32 + metrics.bounds.ymin,
w: metrics.bounds.width,
h: metrics.bounds.height,
x: cell.x as f32,
y: cell.y as f32,
adjust_x: metrics.xmin as f32,
adjust_y: (self.cell_height as f32)
+ floor(-metrics.bounds.height - metrics.bounds.ymin), // PositiveYDown,
w: self.cell_width as f32,
h: self.cell_height as f32,
advance_width: metrics.advance_width,
}
}
}
fn floor(x: f32) -> f32 {
let mut ui = x.to_bits();
let e = (((ui >> 23) as i32) & 0xff) - 0x7f;
if e >= 23 {
return x;
}
if e >= 0 {
let m: u32 = 0x007fffff >> e;
if (ui & m) == 0 {
return x;
}
if ui >> 31 != 0 {
ui += m;
}
ui &= !m;
} else {
if ui >> 31 == 0 {
ui = 0;
} else if ui << 1 != 0 {
return -1.0;
}
}
f32::from_bits(ui)
}
// pub fn inconsolata() {
// let font = include_bytes!("./Inconsolata-Regular.ttf") as &[u8];
// let font = fontdue::Font::from_bytes(font, fontdue::FontSettings::default()).unwrap();

View file

@ -34,13 +34,21 @@ struct VertexOutput {
// Fragment shader
// ----------------------------------------------------------------------------
@group(1) @binding(0) var t_diffuse : texture_multisampled_2d<f32>;
@group(1) @binding(0) var t_diffuse : texture_2d<f32>;
@group(1) @binding(1) var s_diffuse : sampler;
@fragment fn fs_main(in : VertexOutput)->@location(0) vec4<f32> {
// TODO: Should we be sampling here for the shader?
let tc = vec2(u32(in.tex_coords.x), u32(in.tex_coords.y));
return textureLoad(t_diffuse, tc, 0);
// let tc = vec2(u32(in.tex_coords.x), u32(in.tex_coords.y));
// let c = textureLoad(t_diffuse, tc, 0);
let tc = in.tex_coords / vec2<f32>(textureDimensions(t_diffuse));
let c = textureSample(t_diffuse, s_diffuse, tc);
return vec4<f32>(c.r,c.r,c.r,c.r);
//return vec4<f32>(1.0,1.0,1.0,1.0);
}