text rendering

This commit is contained in:
Vinzenz Schroeter 2024-05-10 18:24:26 +02:00
parent 05c7b4cb71
commit ae12c82c2e
9 changed files with 447 additions and 166 deletions

56
src/font.rs Normal file
View file

@ -0,0 +1,56 @@
use font_kit::canvas::*;
use font_kit::hinting::HintingOptions;
use pathfinder_geometry::transform2d::Transform2F;
use pathfinder_geometry::vector::{vec2f, vec2i};
use servicepoint2::{PixelGrid, TILE_SIZE};
pub struct BitmapFont {
bitmaps: [PixelGrid; u8::MAX as usize],
}
impl BitmapFont {
pub fn load_file(file: &str) -> BitmapFont {
let font = font_kit::font::Font::from_path(file, 0).expect("could not load font");
let mut bitmaps =
core::array::from_fn(|_| PixelGrid::new(TILE_SIZE as usize, TILE_SIZE as usize));
for char_code in u8::MIN..u8::MAX {
let char = char_code as char;
let glyph_id = match font.glyph_for_char(char) {
None => continue,
Some(val) => val,
};
let size = 8f32;
let transform = Transform2F::default();
let mut canvas = Canvas::new(vec2i(size as i32, size as i32), Format::A8);
font.rasterize_glyph(
&mut canvas,
glyph_id,
size,
Transform2F::from_translation(vec2f(0f32, size)) * transform,
HintingOptions::None,
RasterizationOptions::GrayscaleAa,
)
.unwrap();
assert_eq!(canvas.pixels.len(), 64);
assert_eq!(canvas.stride, 8);
for y in 0..TILE_SIZE as usize {
for x in 0..TILE_SIZE as usize {
let index = x + y * TILE_SIZE as usize;
let canvas_val = canvas.pixels[index] != 0;
bitmaps[char_code as usize].set(x, y, canvas_val);
}
}
}
return BitmapFont { bitmaps };
}
pub fn get_bitmap(&self, char_code: u8) -> &PixelGrid {
&self.bitmaps[char_code as usize]
}
}

View file

@ -1,8 +1,8 @@
use crate::protocol::{PIXEL_HEIGHT, PIXEL_WIDTH};
use crate::DISPLAY;
use log::{trace, warn};
use pixels::wgpu::TextureFormat;
use pixels::{Pixels, PixelsBuilder, SurfaceTexture};
use servicepoint2::{PIXEL_HEIGHT, PIXEL_WIDTH};
use winit::application::ApplicationHandler;
use winit::dpi::{LogicalSize, Size};
use winit::event::WindowEvent;

View file

@ -1,17 +1,17 @@
#![deny(clippy::all)]
mod font;
mod gui;
mod upd_loop;
mod protocol;
mod upd_loop;
use std::default::Default;
use std::sync::mpsc;
use crate::gui::App;
use crate::upd_loop::start_udp_thread;
use crate::upd_loop::UdpThread;
use clap::Parser;
use log::info;
use protocol::PIXEL_COUNT;
use servicepoint2::PIXEL_COUNT;
use winit::event_loop::{ControlFlow, EventLoop};
#[derive(Parser, Debug)]
@ -28,8 +28,7 @@ fn main() {
let cli = Cli::parse();
info!("starting with args: {:?}", &cli);
let (stop_udp_tx, stop_udp_rx) = mpsc::channel();
let thread = start_udp_thread(cli.bind, stop_udp_rx);
let thread = UdpThread::start_new(cli.bind);
let event_loop = EventLoop::new().expect("could not create event loop");
event_loop.set_control_flow(ControlFlow::Poll);
@ -39,6 +38,5 @@ fn main() {
.run_app(&mut app)
.expect("could not run event loop");
stop_udp_tx.send(()).expect("could not cancel thread");
thread.join().expect("could not join threads");
thread.stop_and_wait();
}

View file

@ -1,27 +1,38 @@
use num_derive::{FromPrimitive, ToPrimitive};
use std::mem::size_of;
use num_derive::{FromPrimitive, ToPrimitive};
#[repr(u16)]
#[derive(Debug, FromPrimitive, ToPrimitive, Default)]
pub enum DisplayCommand {
pub enum DisplayCommandCode {
#[default]
CmdClear = 0x0002,
CmdCp437data = 0x0003,
CmdCharBrightness = 0x0005,
CmdBrightness = 0x0007,
CmdHardReset = 0x000b,
CmdFadeOut = 0x000d,
CmdBitmapLegacy = 0x0010,
CmdBitmapLinear = 0x0012,
CmdBitmapLinearWin = 0x0013,
CmdBitmapLinearAnd = 0x0014,
CmdBitmapLinearOr = 0x0015,
CmdBitmapLinearXor = 0x0016,
Clear = 0x0002,
Cp437data = 0x0003,
CharBrightness = 0x0005,
Brightness = 0x0007,
HardReset = 0x000b,
FadeOut = 0x000d,
BitmapLegacy = 0x0010,
BitmapLinear = 0x0012,
BitmapLinearWin = 0x0013,
BitmapLinearAnd = 0x0014,
BitmapLinearOr = 0x0015,
BitmapLinearXor = 0x0016,
}
impl DisplayCommandCode {
pub fn from_primitive(value: u16) -> Option<Self> {
num::FromPrimitive::from_u16(value)
}
pub fn to_primitive(&self) -> u16 {
num::ToPrimitive::to_u16(self).unwrap()
}
}
#[repr(C)]
#[derive(Debug, Default)]
pub struct HdrWindow {
pub command: DisplayCommand,
pub command: DisplayCommandCode,
pub x: u16,
pub y: u16,
pub w: u16,
@ -49,27 +60,22 @@ pub enum DisplaySubcommand {
SubCmdBitmapCompressZs = 0x7a73,
}
pub const TILE_SIZE: u16 = 8;
pub const TILE_WIDTH: u16 = 56;
pub const TILE_HEIGHT: u16 = 20;
pub const PIXEL_WIDTH: u16 = TILE_WIDTH * TILE_SIZE;
pub const PIXEL_HEIGHT: u16 = TILE_HEIGHT * TILE_SIZE;
pub const PIXEL_COUNT: usize = PIXEL_WIDTH as usize * PIXEL_HEIGHT as usize;
#[derive(Debug)]
pub enum ReadHeaderError {
BufferTooSmall,
WrongCommandEndianness(u16, DisplayCommand),
WrongCommandEndianness(u16, DisplayCommandCode),
InvalidCommand(u16),
}
pub fn read_header(buffer: &[u8]) -> Result<HdrWindow, ReadHeaderError> {
assert_eq!(size_of::<HdrWindow>(), 10, "invalid struct size");
if buffer.len() < size_of::<HdrWindow>() {
return Err(ReadHeaderError::BufferTooSmall);
}
let command_u16 = read_beu16(&buffer[0..=1]);
return match num::FromPrimitive::from_u16(command_u16) {
return match DisplayCommandCode::from_primitive(command_u16) {
Some(command) => Ok(HdrWindow {
command,
x: read_beu16(&buffer[2..=3]),
@ -78,8 +84,7 @@ pub fn read_header(buffer: &[u8]) -> Result<HdrWindow, ReadHeaderError> {
h: read_beu16(&buffer[8..=9]),
}),
None => {
let maybe_command: Option<DisplayCommand> =
num::FromPrimitive::from_u16(u16::swap_bytes(command_u16));
let maybe_command = DisplayCommandCode::from_primitive(u16::swap_bytes(command_u16));
return match maybe_command {
None => Err(ReadHeaderError::InvalidCommand(command_u16)),
Some(command) => Err(ReadHeaderError::WrongCommandEndianness(

View file

@ -1,164 +1,179 @@
use crate::protocol::{
read_header, DisplayCommand, HdrWindow, ReadHeaderError, PIXEL_WIDTH, TILE_SIZE,
};
use crate::font::BitmapFont;
use crate::protocol::{read_header, DisplayCommandCode, HdrWindow, ReadHeaderError};
use crate::DISPLAY;
use log::{error, info, warn};
use log::{debug, error, info, warn};
use servicepoint2::{PixelGrid, PIXEL_WIDTH, TILE_SIZE};
use std::io::ErrorKind;
use std::mem::size_of;
use std::net::UdpSocket;
use std::sync::mpsc::Receiver;
use std::net::{ToSocketAddrs, UdpSocket};
use std::sync::mpsc;
use std::sync::mpsc::Sender;
use std::thread;
use std::thread::JoinHandle;
use std::time::Duration;
pub fn start_udp_thread(bind: String, stop_receiver: Receiver<()>) -> JoinHandle<()> {
assert_eq!(size_of::<HdrWindow>(), 10, "invalid struct size");
pub struct UdpThread {
thread: JoinHandle<()>,
stop_tx: Sender<()>,
}
impl UdpThread {
pub fn start_new(bind: impl ToSocketAddrs) -> Self {
let (stop_tx, stop_rx) = mpsc::channel();
return thread::spawn(move || {
let socket = UdpSocket::bind(bind).expect("could not bind socket");
socket
.set_nonblocking(true)
.expect("could not enter non blocking mode");
let mut buf = [0; 8985];
let font = BitmapFont::load_file("Web437_IBM_BIOS.woff");
while stop_receiver.try_recv().is_err() {
let (amount, _) = match socket.recv_from(&mut buf) {
Err(err) if err.kind() == ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(1));
continue;
let thread = thread::spawn(move || {
let mut buf = [0; 8985];
while stop_rx.try_recv().is_err() {
let (amount, _) = match socket.recv_from(&mut buf) {
Err(err) if err.kind() == ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(1));
continue;
}
Ok(result) => result,
other => other.unwrap(),
};
if amount == buf.len() {
warn!(
"the received package may have been truncated to a length of {}",
amount
);
}
Ok(result) => result,
other => other.unwrap(),
};
if amount == buf.len() {
warn!(
"the received package may have been truncated to a length of {}",
amount
);
Self::handle_package(&mut buf[..amount], &font);
}
});
handle_package(&mut buf[..amount]);
}
});
}
return Self { stop_tx, thread };
}
fn handle_package(received: &mut [u8]) {
let header = match read_header(&received[..10]) {
Err(ReadHeaderError::BufferTooSmall) => {
error!("received a packet that is too small");
return;
}
Err(ReadHeaderError::InvalidCommand(command_u16)) => {
error!("received invalid command {}", command_u16);
return;
}
Err(ReadHeaderError::WrongCommandEndianness(command_u16, command_swapped)) => {
error!(
pub fn stop_and_wait(self) {
self.stop_tx.send(()).expect("could not send stop packet");
self.thread.join().expect("could not wait on udp thread");
}
fn handle_package(received: &mut [u8], font: &BitmapFont) {
let header = match read_header(&received[..10]) {
Err(ReadHeaderError::BufferTooSmall) => {
error!("received a packet that is too small");
return;
}
Err(ReadHeaderError::InvalidCommand(command_u16)) => {
error!("received invalid command {}", command_u16);
return;
}
Err(ReadHeaderError::WrongCommandEndianness(command_u16, command_swapped)) => {
error!(
"The reversed byte order of {} matches command {:?}, you are probably sending the wrong endianness",
command_u16, command_swapped
);
return;
}
Ok(value) => value,
};
return;
}
Ok(value) => value,
};
let payload = &received[10..];
let payload = &received[10..];
info!(
"received from {:?} (and {} bytes of payload)",
header,
payload.len()
);
info!(
"received from {:?} (and {} bytes of payload)",
header,
payload.len()
);
match header.command {
DisplayCommand::CmdClear => {
info!("clearing display");
for v in unsafe { DISPLAY.iter_mut() } {
*v = false;
match header.command {
DisplayCommandCode::Clear => {
info!("clearing display");
for v in unsafe { DISPLAY.iter_mut() } {
*v = false;
}
}
DisplayCommandCode::HardReset => {
warn!("display shutting down");
return;
}
DisplayCommandCode::BitmapLinearWin => {
Self::print_bitmap_linear_win(&header, payload);
}
DisplayCommandCode::Cp437data => {
Self::print_cp437_data(&header, payload, font);
}
_ => {
error!("command {:?} not implemented yet", header.command);
}
}
DisplayCommand::CmdHardReset => {
warn!("display shutting down");
}
fn check_payload_size(buf: &[u8], expected: usize) -> bool {
let actual = buf.len();
if actual == expected {
return true;
}
error!(
"expected a payload length of {} but got {}",
expected, actual
);
return false;
}
fn print_bitmap_linear_win(header: &HdrWindow, payload: &[u8]) {
if !Self::check_payload_size(payload, header.w as usize * header.h as usize) {
return;
}
DisplayCommand::CmdBitmapLinearWin => {
print_bitmap_linear_win(&header, payload);
}
DisplayCommand::CmdCp437data => {
print_cp437_data(&header, payload);
}
_ => {
error!("command {:?} not implemented yet", header.command);
}
}
}
fn check_payload_size(buf: &[u8], expected: usize) -> bool {
let actual = buf.len();
if actual == expected {
return true;
let pixel_grid = PixelGrid::load(
header.w as usize * TILE_SIZE as usize,
header.h as usize,
payload,
);
Self::print_pixel_grid(
header.x as usize * TILE_SIZE as usize,
header.y as usize,
&pixel_grid,
);
}
error!(
"expected a payload length of {} but got {}",
expected, actual
);
return false;
}
fn print_cp437_data(header: &HdrWindow, payload: &[u8], font: &BitmapFont) {
if !UdpThread::check_payload_size(payload, (header.w * header.h) as usize) {
return;
}
fn print_bitmap_linear_win(header: &HdrWindow, payload: &[u8]) {
if !check_payload_size(payload, header.w as usize * header.h as usize) {
return;
for char_y in 0usize..header.h as usize {
for char_x in 0usize..header.w as usize {
let char_code = payload[char_y * header.w as usize + char_x];
let tile_x = char_x + header.x as usize;
let tile_y = char_y + header.y as usize;
let bitmap = font.get_bitmap(char_code);
Self::print_pixel_grid(
tile_x * TILE_SIZE as usize,
tile_y * TILE_SIZE as usize,
bitmap,
);
}
}
}
info!(
"top left is offset {} tiles in x-direction and {} pixels in y-direction",
header.x, header.y
);
for y in 0..header.h {
for byte_x in 0..header.w {
let byte_index = (y * header.w + byte_x) as usize;
let byte = payload[byte_index];
for pixel_x in 0u8..8u8 {
let bit_index = 7 - pixel_x;
let bitmask = 1 << bit_index;
let is_set = byte & bitmask != 0;
let x = byte_x * TILE_SIZE + pixel_x as u16;
let translated_x = (x + header.x) as usize;
let translated_y = (y + header.y) as usize;
let index = translated_y * PIXEL_WIDTH as usize + translated_x;
fn print_pixel_grid(offset_x: usize, offset_y: usize, pixels: &PixelGrid) {
debug!("printing {}x{} grid at {offset_x} {offset_y}", pixels.width, pixels.height);
for inner_y in 0..pixels.height {
for inner_x in 0..pixels.width {
let is_set = pixels.get(inner_x, inner_y);
let display_index =
(offset_x + inner_x) + ((offset_y + inner_y) * PIXEL_WIDTH as usize);
unsafe {
DISPLAY[index] = is_set;
DISPLAY[display_index] = is_set;
}
}
}
}
}
// TODO: actually convert from CP437
fn print_cp437_data(header: &HdrWindow, payload: &[u8]) {
if !check_payload_size(payload, (header.w * header.h) as usize) {
return;
}
info!("top left is offset by ({} | {}) tiles", header.x, header.y);
let mut str = String::new();
for y in 0..header.h {
for x in 0..header.w {
let byte_index = (y * header.w + x) as usize;
str.push(payload[byte_index] as char);
}
str.push('\n');
}
info!("{}", str);
}