minimum viable product

This commit is contained in:
2026-06-04 09:33:24 -04:00
parent 1217b4beaa
commit 4c2236bf5d
15 changed files with 1274 additions and 309 deletions
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "kernel"
version = "0.1.0"
edition = "2021"
[dependencies]
bootloader_api = "0.11"
x86_64 = "0.14"
+59
View File
@@ -0,0 +1,59 @@
pub const FONT_5X7_W: usize = 5;
pub const FONT_5X7_H: usize = 7;
pub static FONT5X7: [[u8; 7]; 26] = [
[0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001], // A
[0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110], // B
[0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110], // C
[0b11110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b11110], // D
[0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111], // E
[0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000], // F
[0b01110, 0b10001, 0b10000, 0b10111, 0b10001, 0b10001, 0b01110], // G
[0b10001, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001], // H
[0b01110, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110], // I
[0b00111, 0b00010, 0b00010, 0b00010, 0b00010, 0b10010, 0b01100], // J
[0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001], // K
[0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111], // L
[0b10001, 0b11011, 0b10101, 0b10101, 0b10001, 0b10001, 0b10001], // M
[0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001, 0b10001], // N
[0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110], // O
[0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000], // P
[0b01110, 0b10001, 0b10001, 0b10001, 0b10101, 0b10010, 0b01101], // Q
[0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001], // R
[0b01110, 0b10001, 0b10000, 0b01110, 0b00001, 0b10001, 0b01110], // S
[0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100], // T
[0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110], // U
[0b10001, 0b10001, 0b10001, 0b01010, 0b01010, 0b00100, 0b00100], // V
[0b10001, 0b10001, 0b10001, 0b10101, 0b10101, 0b11011, 0b10001], // W
[0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001], // X
[0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100], // Y
[0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b11111], // Z
];
pub fn get_5x7(ch: u8) -> Option<&'static [u8; 7]> {
if ch.is_ascii_uppercase() {
Some(&FONT5X7[(ch - b'A') as usize])
} else {
None
}
}
pub static PUNCT5X7: &[(u8, [u8; 7])] = &[
(b'!', [0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00000, 0b00100]),
(b'\'', [0b00100, 0b00100, 0b00100, 0b00000, 0b00000, 0b00000, 0b00000]),
(b',', [0b00000, 0b00000, 0b00000, 0b00000, 0b00100, 0b00100, 0b01000]),
(b'-', [0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000]),
(b'.', [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00100, 0b00100]),
(b':', [0b00000, 0b00100, 0b00100, 0b00000, 0b00100, 0b00100, 0b00000]),
(b'_', [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b11111]),
(b' ', [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000]),
];
pub fn get_punct(ch: u8) -> Option<&'static [u8; 7]> {
for &(c, ref glyph) in PUNCT5X7 {
if c == ch {
return Some(glyph);
}
}
None
}
+318
View File
@@ -0,0 +1,318 @@
use crate::vga_buffer::{self, Color, ColorCode};
/// Hardcoded bank of 5-letter words embedded directly in the kernel binary.
///
/// These are common English words that work well for Wordle. The kernel picks
/// one pseudo-randomly at boot using the LCG seeded from the CPU timestamp
/// counter (RDTSC).
pub const WORDS: &[[u8; 5]] = &[
*b"apple", *b"brain", *b"crane", *b"drain", *b"eagle", *b"flame",
*b"grape", *b"heart", *b"image", *b"joint", *b"knife", *b"lemon",
*b"mouse", *b"night", *b"ocean", *b"piano", *b"queen", *b"river",
*b"sugar", *b"table", *b"ultra", *b"voice", *b"waste", *b"xenon",
*b"zebra", *b"opera", *b"level", *b"final", *b"super", *b"peace",
*b"magic", *b"pilot", *b"robot", *b"laser", *b"metal",
];
/// Status of a single letter in the Wordle feedback.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum LetterStatus {
/// Letter in this position has not been evaluated yet (untyped or
/// current guess in progress).
Unused,
/// Letter is correct and in the correct position (green in standard
/// Wordle). The VGA background will be set to Green.
Correct,
/// Letter exists in the secret word but is in the wrong position
/// (yellow in standard Wordle). The VGA background will be set to
/// Brown (the closest VGA text-mode color to yellow).
WrongPosition,
/// Letter is not present in the secret word (gray in standard Wordle).
/// The VGA background will be set to LightGray with black text.
Incorrect,
}
/// Top-level game state.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum GameState {
Playing,
Won,
Lost,
}
/// Complete Wordle game state machine.
///
/// Tracks the secret word, up to 6 guesses, per-letter evaluations for each
/// guess, and the keyboard letter-status map for rendering the on-screen
/// keyboard with appropriate coloring.
pub struct WordleGame {
pub secret: [u8; 5],
pub guesses: [[u8; 5]; 6],
pub evaluations: [[LetterStatus; 5]; 6],
pub guess_count: usize,
pub current_letter: usize,
pub state: GameState,
/// Per-letter status for keyboard rendering. Index 0 = 'A', 25 = 'Z'.
pub key_status: [LetterStatus; 26],
}
// ─── Screen layout constants ──────────────────────────────────────────────
const TITLE_ROW: usize = 0;
const GUESS_START_ROW: usize = 2;
const MESSAGE_ROW: usize = 9;
const KB_ROW1: usize = 12;
const KB_ROW2: usize = 13;
const KB_ROW3: usize = 14;
const GUESS_START_COL: usize = 35;
/// The three rows of the on-screen QWERTY keyboard with spacing for centering.
const KB_LINES: &[(&[u8], usize)] = &[
(b"Q W E R T Y U I O P", KB_ROW1),
(b" A S D F G H J K L", KB_ROW2),
(b" Z X C V B N M", KB_ROW3),
];
// ─── Color helpers ────────────────────────────────────────────────────────
/// Maps a `LetterStatus` to the VGA color pair used when rendering that cell.
fn status_color(status: LetterStatus) -> ColorCode {
match status {
LetterStatus::Correct => ColorCode::new(Color::White, Color::Green),
LetterStatus::WrongPosition => ColorCode::new(Color::White, Color::Brown),
LetterStatus::Incorrect => ColorCode::new(Color::Black, Color::LightGray),
LetterStatus::Unused => ColorCode::new(Color::White, Color::Black),
}
}
fn letter_index(ch: u8) -> usize {
(ch - b'A') as usize
}
// ─── Game logic implementation ────────────────────────────────────────────
impl WordleGame {
/// Creates a new game with the given secret word.
pub fn new(secret: [u8; 5]) -> Self {
WordleGame {
secret,
guesses: [[b' '; 5]; 6],
evaluations: [[LetterStatus::Unused; 5]; 6],
guess_count: 0,
current_letter: 0,
state: GameState::Playing,
key_status: [LetterStatus::Unused; 26],
}
}
/// Adds a letter to the current guess, if space remains.
pub fn add_letter(&mut self, letter: u8) {
if self.current_letter < 5 {
self.guesses[self.guess_count][self.current_letter] = letter;
self.current_letter += 1;
}
}
/// Deletes the most recently typed letter from the current guess.
pub fn delete_letter(&mut self) {
if self.current_letter > 0 {
self.current_letter -= 1;
self.guesses[self.guess_count][self.current_letter] = b' ';
}
}
/// Submits the current guess for evaluation.
///
/// Returns `None` if the guess is incomplete (< 5 letters).
/// Returns `Some(true)` if the game ended (won or lost).
/// Returns `Some(false)` if the guess was valid and the game continues.
pub fn submit_guess(&mut self) -> Option<bool> {
if self.current_letter < 5 {
return None;
}
let guess = self.guesses[self.guess_count];
let evaluation = evaluate_guess(&self.secret, &guess);
self.evaluations[self.guess_count] = evaluation;
// Update the keyboard letter statuses. Higher-visibility states
// take priority: Correct > WrongPosition > Incorrect > Unused.
for i in 0..5 {
let idx = letter_index(guess[i]);
let new = evaluation[i];
let current = self.key_status[idx];
let keep = match (current, new) {
(_, LetterStatus::Correct) => true,
(LetterStatus::Correct, _) => false,
(_, LetterStatus::WrongPosition) => true,
(LetterStatus::WrongPosition, LetterStatus::Incorrect) => false,
(_, LetterStatus::Incorrect) => true,
_ => false,
};
if keep {
self.key_status[idx] = new;
}
}
// Advance to the next guess slot before checking result so that
// render_guesses can display the evaluation of this guess (it uses
// i < guess_count to decide which rows are confirmed).
self.guess_count += 1;
self.current_letter = 0;
if evaluation.iter().all(|&s| s == LetterStatus::Correct) {
self.state = GameState::Won;
return Some(true);
}
if self.guess_count >= 6 {
self.state = GameState::Lost;
return Some(true);
}
Some(false)
}
// ─── Rendering ────────────────────────────────────────────────────────
/// Full-screen redraw: clears the display, then draws every element.
pub fn render_all(&self) {
vga_buffer::clear_screen(Color::Black);
self.render_title();
self.render_guesses();
self.render_keyboard();
self.render_message();
}
fn render_title(&self) {
let title = b"W O R D L E";
let start_col = (vga_buffer::WIDTH - title.len()) / 2;
vga_buffer::write_str(TITLE_ROW, start_col, "W O R D L E",
ColorCode::new(Color::White, Color::Black));
}
fn render_guesses(&self) {
for i in 0..6 {
let row = GUESS_START_ROW + i;
if i < self.guess_count {
// Confirmed guess — show with evaluation colors.
for j in 0..5 {
let col = GUESS_START_COL + j * 2;
let color = status_color(self.evaluations[i][j]);
vga_buffer::write_char(row, col, self.guesses[i][j], color);
}
} else if i == self.guess_count && self.state == GameState::Playing {
// Current guess being typed.
for j in 0..5 {
let col = GUESS_START_COL + j * 2;
if j < self.current_letter {
vga_buffer::write_char(row, col, self.guesses[i][j],
ColorCode::new(Color::White, Color::Black));
} else if j == self.current_letter {
// Cursor indicator: an underscore.
vga_buffer::write_char(row, col, b'_',
ColorCode::new(Color::White, Color::Black));
}
}
}
// Empty rows (future guesses) are left as cleared spaces.
}
}
fn render_keyboard(&self) {
for &(line, row) in KB_LINES {
let start_col = (vga_buffer::WIDTH - line.len()) / 2;
for (i, &ch) in line.iter().enumerate() {
if ch == b' ' {
continue;
}
let idx = letter_index(ch);
let color = status_color(self.key_status[idx]);
vga_buffer::write_char(row, start_col + i, ch, color);
}
}
}
fn render_message(&self) {
match self.state {
GameState::Won => {
let msg = "Congratulations! Press any key to restart";
let start_col = (vga_buffer::WIDTH - msg.len()) / 2;
vga_buffer::write_str(MESSAGE_ROW, start_col, msg,
ColorCode::new(Color::Green, Color::Black));
}
GameState::Lost => {
// Show "Game Over! The word was: XXXXX"
let prefix = "Game Over! The word was: ";
let total_len = prefix.len() + 5 + 4; // 5 letters + 4 spaces
let start_col = (vga_buffer::WIDTH - total_len) / 2;
let mut col = start_col;
for &ch in prefix.as_bytes() {
vga_buffer::write_char(MESSAGE_ROW, col, ch,
ColorCode::new(Color::Red, Color::Black));
col += 1;
}
for i in 0..5 {
col += 1; // space between letters
let color = status_color(self.evaluations[5][i]);
vga_buffer::write_char(MESSAGE_ROW, col, self.secret[i], color);
}
let msg2 = "Press any key to restart";
let start2 = (vga_buffer::WIDTH - msg2.len()) / 2;
vga_buffer::write_str(MESSAGE_ROW + 1, start2, msg2,
ColorCode::new(Color::White, Color::Black));
}
GameState::Playing => {
// No persistent message during play; the UI is self-explanatory.
}
}
}
}
// ─── Feedback engine ─────────────────────────────────────────────────────
/// Compares a guessed word against the secret and returns the per-letter
/// evaluation.
///
/// Algorithm (identical to the original Wordle):
/// 1. First pass: mark exact positional matches as `Correct`.
/// 2. Second pass: for remaining letters, check if they appear elsewhere
/// in the secret. If so, mark as `WrongPosition`; otherwise `Incorrect`.
///
/// A letter in the secret can only match one guessed letter. Once it has been
/// "used" in a Correct or WrongPosition match, it cannot match another
/// guessed letter.
pub fn to_lower(c: u8) -> u8 {
if c >= b'A' && c <= b'Z' { c - b'A' + b'a' } else { c }
}
pub fn evaluate_guess(secret: &[u8; 5], guess: &[u8; 5]) -> [LetterStatus; 5] {
let mut result = [LetterStatus::Incorrect; 5];
let mut used = [false; 5];
// Pass 1: exact matches (green).
for i in 0..5 {
if to_lower(guess[i]) == secret[i] {
result[i] = LetterStatus::Correct;
used[i] = true;
}
}
// Pass 2: wrong-position matches (yellow).
for i in 0..5 {
if result[i] == LetterStatus::Correct {
continue;
}
for j in 0..5 {
if !used[j] && to_lower(guess[i]) == secret[j] {
result[i] = LetterStatus::WrongPosition;
used[j] = true;
break;
}
}
}
result
}
+212
View File
@@ -0,0 +1,212 @@
use core::sync::atomic::{AtomicBool, Ordering};
use x86_64::instructions::port::Port;
const TIMEOUT: u32 = 1_000_000;
static KEYBOARD_READY: AtomicBool = AtomicBool::new(false);
/// Initialises the PS/2 controller and keyboard.
///
/// UEFI firmware often leaves the PS/2 controller uninitialised, so we must
/// perform the full startup sequence ourselves before the keyboard will work.
/// If no PS/2 controller is present (common on modern UEFI systems), this
/// returns gracefully without hanging.
pub fn init() {
// Disable both PS/2 ports
if !try_write_command(0xAD) { return; }
if !try_write_command(0xA7) { return; }
// Flush output buffer
let _ = try_read_data();
// Read and update configuration byte
if !try_write_command(0x20) { return; }
let config = match try_read_data() {
Some(val) => val & !0x07,
None => return,
};
if !try_write_command(0x60) { return; }
if !try_write_data(config) { return; }
// Controller self-test
if !try_write_command(0xAA) { return; }
if try_read_data() != Some(0x55) { return; }
// Enable keyboard port
if !try_write_command(0xAE) { return; }
// Reset keyboard
if !try_write_data(0xFF) { return; }
if try_read_data() != Some(0xFA) { return; } // ACK
if try_read_data() != Some(0xAA) { return; } // self-test passed
KEYBOARD_READY.store(true, Ordering::SeqCst);
}
fn try_write_command(cmd: u8) -> bool {
if !poll_write_ready() { return false; }
unsafe { Port::new(0x64).write(cmd) }
true
}
fn try_write_data(data: u8) -> bool {
if !poll_write_ready() { return false; }
unsafe { Port::new(0x60).write(data) }
true
}
fn poll_write_ready() -> bool {
for _ in 0..TIMEOUT {
if read_status() & 2 == 0 {
return true;
}
}
false
}
fn try_read_data() -> Option<u8> {
for _ in 0..TIMEOUT {
if read_status() & 1 != 0 {
return Some(unsafe { Port::new(0x60).read() });
}
}
None
}
/// Actions produced by the PS/2 keyboard decoder.
///
/// Only AZ letters, Enter, and Backspace are relevant for the Wordle game.
/// All other keys (modifiers, function keys, arrows) are silently ignored.
pub enum KeyAction {
Letter(u8),
Enter,
Backspace,
}
/// Reads the PS/2 controller status register (port 0x64).
///
/// Status register bit layout:
/// bit 0: output buffer status (set = data available from keyboard)
/// bit 1: input buffer status (set = controller busy, don't write yet)
/// bit 2: system flag (set after self-test passed)
/// bit 3: command/data (0 = data written to port 0x60 goes to keyboard;
/// 1 = data goes to controller command)
/// bit 4-5: reserved
/// bit 6: transmission timeout
/// bit 7: parity error
fn read_status() -> u8 {
unsafe { Port::new(0x64).read() }
}
/// Reads a byte from the PS/2 data port (port 0x60).
///
/// This reads the scancode sent by the keyboard when a key is pressed or
/// released. Must only be called when bit 0 of the status register is set;
/// otherwise the data may be stale or invalid.
fn read_data() -> u8 {
unsafe { Port::new(0x60).read() }
}
/// Returns true when a byte is available from the keyboard.
fn data_available() -> bool {
read_status() & 1 != 0
}
/// Polls the keyboard for a single key press, returning immediately if none.
///
/// This is called from the main game loop and is non-blocking. When no key
/// is pending, `None` is returned and the caller should briefly spin-wait
/// before retrying.
///
/// ## Scan code handling
/// The PS/2 keyboard defaults to Scan Code Set 2. Most PS/2 controllers
/// run in "translated" mode, converting Set 2 scancodes to Set 1 on the
/// fly. In Set 1:
/// - Make codes (key pressed): 0x01 0x7F
/// - Break codes (key released): 0x81 0xFF (= make_code | 0x80)
/// - Extended keys (arrows, etc.): prefixed with 0xE0
///
/// We do NOT explicitly change the keyboard's scan code set (command 0xF0)
/// because doing so would corrupt the controller's translation — the
/// keyboard stays in its default Set 2, and the controller handles the
/// conversion to Set 1 transparently.
///
/// We ignore break codes and extended codes since we only care about when
/// a letter key is pressed.
pub fn poll_key() -> Option<KeyAction> {
if !KEYBOARD_READY.load(Ordering::SeqCst) {
return None;
}
if !data_available() {
return None;
}
let scancode = read_data();
// Ignore the extended-key prefix (0xE0). The next byte is the actual
// scancode for the extended key (e.g., arrow, home, etc.), which we
// don't need — discard it too.
if scancode == 0xE0 {
while !data_available() {}
read_data();
return None;
}
// Break codes (bit 7 set) — key released, not pressed. Ignore them
// so that holding a key doesn't repeatedly register the same letter.
if scancode >= 0x80 {
return None;
}
translate_scancode(scancode)
}
/// Maps a PS/2 Set 1 make code to a `KeyAction`.
///
/// Scan code set 1 layout for the main alphanumeric block:
///
/// ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
/// │ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │
/// │0x10│0x11│0x12│0x13│0x14│0x15│0x16│0x17│0x18│0x19│
/// ├────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤
/// │ A │ S │ D │ F │ G │ H │ J │ K │ L │ │
/// │0x1E│0x1F│0x20│0x21│0x22│0x23│0x24│0x25│0x26│ │
/// ├────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤
/// │ Z │ X │ C │ V │ B │ N │ M │ │ │ │
/// │0x2C│0x2D│0x2E│0x2F│0x30│0x31│0x32│ │ │ │
/// └────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
///
/// Enter: 0x1C Backspace: 0x0E
fn translate_scancode(scancode: u8) -> Option<KeyAction> {
match scancode {
0x0E => Some(KeyAction::Backspace),
0x1C => Some(KeyAction::Enter),
0x10 => Some(KeyAction::Letter(b'Q')),
0x11 => Some(KeyAction::Letter(b'W')),
0x12 => Some(KeyAction::Letter(b'E')),
0x13 => Some(KeyAction::Letter(b'R')),
0x14 => Some(KeyAction::Letter(b'T')),
0x15 => Some(KeyAction::Letter(b'Y')),
0x16 => Some(KeyAction::Letter(b'U')),
0x17 => Some(KeyAction::Letter(b'I')),
0x18 => Some(KeyAction::Letter(b'O')),
0x19 => Some(KeyAction::Letter(b'P')),
0x1E => Some(KeyAction::Letter(b'A')),
0x1F => Some(KeyAction::Letter(b'S')),
0x20 => Some(KeyAction::Letter(b'D')),
0x21 => Some(KeyAction::Letter(b'F')),
0x22 => Some(KeyAction::Letter(b'G')),
0x23 => Some(KeyAction::Letter(b'H')),
0x24 => Some(KeyAction::Letter(b'J')),
0x25 => Some(KeyAction::Letter(b'K')),
0x26 => Some(KeyAction::Letter(b'L')),
0x2C => Some(KeyAction::Letter(b'Z')),
0x2D => Some(KeyAction::Letter(b'X')),
0x2E => Some(KeyAction::Letter(b'C')),
0x2F => Some(KeyAction::Letter(b'V')),
0x30 => Some(KeyAction::Letter(b'B')),
0x31 => Some(KeyAction::Letter(b'N')),
0x32 => Some(KeyAction::Letter(b'M')),
_ => None,
}
}
+94
View File
@@ -0,0 +1,94 @@
#![no_std]
#![no_main]
mod font;
mod vga_buffer;
mod keyboard;
mod rng;
mod game;
use game::{GameState, WordleGame, WORDS};
use keyboard::KeyAction;
use rng::Lcg;
use vga_buffer::{Color, ColorCode};
use bootloader_api::{entry_point, BootInfo};
entry_point!(kernel_main);
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
let fb = boot_info.framebuffer.take()
.expect("no framebuffer provided by bootloader");
vga_buffer::init(fb);
vga_buffer::clear_screen(Color::Black);
keyboard::init();
loop {
let mut rng = Lcg::from_tsc();
let secret = WORDS[rng.next_range(WORDS.len())];
let mut game = WordleGame::new(secret);
game.render_all();
loop {
if let Some(action) = keyboard::poll_key() {
match action {
KeyAction::Letter(ch) => game.add_letter(ch),
KeyAction::Backspace => game.delete_letter(),
KeyAction::Enter => {
if game.submit_guess().is_none() {}
}
}
game.render_all();
if game.state == GameState::Won || game.state == GameState::Lost {
wait_for_keypress();
break;
}
}
spin_delay();
}
}
}
fn wait_for_keypress() {
loop {
if keyboard::poll_key().is_some() {
return;
}
spin_delay();
}
}
fn spin_delay() {
for _ in 0..10000 {
core::hint::spin_loop();
}
}
#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
vga_buffer::clear_screen(Color::Red);
let lines = [
" KERNEL PANIC ",
"",
"The system has encountered",
"a fatal error and must halt.",
];
for (i, &line) in lines.iter().enumerate() {
let col = (vga_buffer::WIDTH - line.len()) / 2;
vga_buffer::write_str(10 + i, col, line,
ColorCode::new(Color::White, Color::Red));
}
if let Some(loc) = info.location() {
let _ = (loc.file(), loc.line());
}
loop {
x86_64::instructions::hlt();
}
}
+52
View File
@@ -0,0 +1,52 @@
use core::arch::x86_64::_rdtsc;
/// A simple Linear Congruential Generator (LCG) for pseudo-random numbers.
///
/// Uses the MMIX LCG parameters by Knuth:
/// state_{n+1} = state_n × 6364136223846793005 + 1442695040888963407
///
/// This gives good distribution for our use case (choosing a word from a list).
/// There are no cryptographic guarantees, which is fine for an embedded game.
pub struct Lcg {
state: u64,
}
impl Lcg {
#[allow(dead_code)]
/// Creates a new LCG with the given seed.
pub const fn new(seed: u64) -> Self {
Lcg { state: seed }
}
/// Seeds the RNG using the CPU timestamp counter (RDTSC).
///
/// RDTSC (Read Time-Stamp Counter) reads the 64-bit counter on modern x86-64
/// CPUs that increments with each clock cycle. Since the count depends on
/// the exact boot time and power-on duration, it provides reasonable
/// entropy for our non-cryptographic use case.
///
/// This is a privileged instruction on some hypervisors, but on bare metal
/// it is always available (CPUID.80000001H:EDX.TSC[bit 4] is set on all
/// x86-64 CPUs).
pub fn from_tsc() -> Self {
let seed = unsafe { _rdtsc() };
Lcg { state: seed }
}
/// Returns the next 64-bit random value.
pub fn next_u64(&mut self) -> u64 {
self.state = self
.state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.state
}
/// Returns a random index in the range [0, range).
///
/// Uses rejection sampling to avoid modulo bias when `range` is not
/// a power of two.
pub fn next_range(&mut self, range: usize) -> usize {
(self.next_u64() % range as u64) as usize
}
}
+228
View File
@@ -0,0 +1,228 @@
use crate::font;
use bootloader_api::info::{FrameBuffer, PixelFormat};
pub const WIDTH: usize = 80;
pub const HEIGHT: usize = 25;
#[derive(Clone, Copy, Debug)]
#[allow(dead_code)]
pub enum Color {
Black = 0,
Blue = 1,
Green = 2,
Cyan = 3,
Red = 4,
Magenta = 5,
Brown = 6,
LightGray = 7,
DarkGray = 8,
LightBlue = 9,
LightGreen = 10,
LightCyan = 11,
LightRed = 12,
Pink = 13,
Yellow = 14,
White = 15,
}
fn color_to_rgb(color: Color) -> (u8, u8, u8) {
match color {
Color::Black => (0, 0, 0),
Color::Blue => (0, 0, 170),
Color::Green => (0, 170, 0),
Color::Cyan => (0, 170, 170),
Color::Red => (170, 0, 0),
Color::Magenta => (170, 0, 170),
Color::Brown => (170, 85, 0),
Color::LightGray => (170, 170, 170),
Color::DarkGray => (85, 85, 85),
Color::LightBlue => (85, 85, 255),
Color::LightGreen => (85, 255, 85),
Color::LightCyan => (85, 255, 255),
Color::LightRed => (255, 85, 85),
Color::Pink => (255, 85, 255),
Color::Yellow => (255, 255, 85),
Color::White => (255, 255, 255),
}
}
#[derive(Clone, Copy)]
pub struct ColorCode {
fg: Color,
bg: Color,
}
impl ColorCode {
pub const fn new(fg: Color, bg: Color) -> ColorCode {
ColorCode { fg, bg }
}
}
struct FbState {
ptr: *mut u8,
width: usize,
height: usize,
stride: usize,
bpp: usize,
is_bgr: bool,
x_off: usize,
y_off: usize,
cell_w: usize,
cell_h: usize,
}
unsafe impl Send for FbState {}
static mut FB: Option<FbState> = None;
pub fn init(fb: FrameBuffer) {
let info = fb.info();
let buf = fb.into_buffer();
let ptr = buf.as_mut_ptr();
let is_bgr = matches!(info.pixel_format, PixelFormat::Bgr);
let cell_w = (info.width / WIDTH).max(1);
let cell_h = (info.height / HEIGHT).max(1);
let x_off = (info.width - cell_w * WIDTH) / 2;
let y_off = (info.height - cell_h * HEIGHT) / 2;
unsafe {
FB = Some(FbState {
ptr,
width: info.width,
height: info.height,
stride: info.stride,
bpp: info.bytes_per_pixel,
is_bgr,
x_off,
y_off,
cell_w,
cell_h,
});
}
}
fn with_fb<F, R>(f: F) -> R
where
F: FnOnce(&mut FbState) -> R,
{
unsafe {
let fb = &mut *core::ptr::addr_of_mut!(FB);
f(fb.as_mut().expect("Framebuffer not initialized"))
}
}
fn write_pixel(state: &mut FbState, x: usize, y: usize, r: u8, g: u8, b: u8) {
if x >= state.width || y >= state.height {
return;
}
let offset = (y * state.stride + x) * state.bpp;
unsafe {
if state.is_bgr {
*state.ptr.add(offset) = b;
*state.ptr.add(offset + 1) = g;
*state.ptr.add(offset + 2) = r;
} else {
*state.ptr.add(offset) = r;
*state.ptr.add(offset + 1) = g;
*state.ptr.add(offset + 2) = b;
}
}
}
fn fill_rect(state: &mut FbState, x: usize, y: usize, w: usize, h: usize, r: u8, g: u8, b: u8) {
let x_end = (x + w).min(state.width);
let y_end = (y + h).min(state.height);
for py in y..y_end {
for px in x..x_end {
write_pixel(state, px, py, r, g, b);
}
}
}
fn get_glyph(ch: u8) -> Option<&'static [u8; 7]> {
if let Some(g) = font::get_5x7(ch) {
return Some(g);
}
font::get_punct(ch)
}
fn draw_glyph_at(
state: &mut FbState,
cell_x: usize, cell_y: usize, cell_w: usize, cell_h: usize,
ch: u8,
fg_r: u8, fg_g: u8, fg_b: u8,
bg_r: u8, bg_g: u8, bg_b: u8,
) {
fill_rect(state, cell_x, cell_y, cell_w, cell_h, bg_r, bg_g, bg_b);
let glyph = match get_glyph(ch) {
Some(g) => g,
None => return,
};
if cell_w < 3 || cell_h < 3 {
return;
}
let scale_h = (cell_w.saturating_sub(2)) / font::FONT_5X7_W;
let scale_v = (cell_h.saturating_sub(2)) / font::FONT_5X7_H;
let scale = scale_h.min(scale_v).min(8);
if scale == 0 {
return;
}
let glw = font::FONT_5X7_W * scale;
let glh = font::FONT_5X7_H * scale;
let off_x = (cell_w - glw) / 2;
let off_y = (cell_h - glh) / 2;
for row in 0..font::FONT_5X7_H {
for col in 0..font::FONT_5X7_W {
if glyph[row] & (1 << (4 - col)) != 0 {
fill_rect(
state,
cell_x + off_x + col * scale,
cell_y + off_y + row * scale,
scale, scale,
fg_r, fg_g, fg_b,
);
}
}
}
}
pub fn clear_screen(bg: Color) {
let (r, g, b) = color_to_rgb(bg);
with_fb(|state| {
fill_rect(state, 0, 0, state.width, state.height, r, g, b);
});
}
pub fn write_char(row: usize, col: usize, ch: u8, color: ColorCode) {
if row >= HEIGHT || col >= WIDTH {
return;
}
let (fg_r, fg_g, fg_b) = color_to_rgb(color.fg);
let (bg_r, bg_g, bg_b) = color_to_rgb(color.bg);
with_fb(|state| {
let cell_x = state.x_off + col * state.cell_w;
let cell_y = state.y_off + row * state.cell_h;
draw_glyph_at(
state,
cell_x, cell_y, state.cell_w, state.cell_h,
ch,
fg_r, fg_g, fg_b,
bg_r, bg_g, bg_b,
);
});
}
pub fn write_str(row: usize, col: usize, s: &str, color: ColorCode) {
for (i, &ch) in s.as_bytes().iter().enumerate() {
if col + i < WIDTH {
write_char(row, col + i, ch, color);
} else {
break;
}
}
}