diff --git a/clks/drivers/video/framebuffer.c b/clks/drivers/video/framebuffer.c index 2a60b4d..c86b2e5 100644 --- a/clks/drivers/video/framebuffer.c +++ b/clks/drivers/video/framebuffer.c @@ -19,7 +19,7 @@ static struct clks_fb_state clks_fb = { .address = CLKS_NULL, .info = {0, 0, 0, 0}, .font = CLKS_NULL, - .external_font = {0, 0, 0, 0, CLKS_NULL}, + .external_font = {0, 0, 0, 0, 0, CLKS_NULL}, .external_font_active = CLKS_FALSE, .glyph_width = 8U, .glyph_height = 8U, @@ -149,6 +149,7 @@ void clks_fb_draw_char(u32 x, u32 y, char ch, u32 fg_rgb, u32 bg_rgb) { u32 col; u32 cols; u32 rows; + u32 row_stride; if (clks_fb.ready == CLKS_FALSE || clks_fb.font == CLKS_NULL) { return; @@ -167,15 +168,26 @@ void clks_fb_draw_char(u32 x, u32 y, char ch, u32 fg_rgb, u32 bg_rgb) { rows = 8U; } - if (cols > 8U) { - cols = 8U; + row_stride = clks_fb.font->bytes_per_row; + + if (row_stride == 0U) { + row_stride = (cols + 7U) / 8U; + } + + if (row_stride == 0U) { + return; + } + + if (((usize)row_stride * (usize)rows) > (usize)clks_fb.font->bytes_per_glyph) { + return; } for (row = 0U; row < rows; row++) { - u8 bits = glyph[row]; + const u8 *row_bits = glyph + ((usize)row * (usize)row_stride); for (col = 0U; col < cols; col++) { - u8 mask = (u8)(1U << (7U - col)); + u8 bits = row_bits[col >> 3U]; + u8 mask = (u8)(0x80U >> (col & 7U)); u32 color = (bits & mask) != 0U ? fg_rgb : bg_rgb; clks_fb_put_pixel(x + col, y + row, color); } @@ -183,7 +195,7 @@ void clks_fb_draw_char(u32 x, u32 y, char ch, u32 fg_rgb, u32 bg_rgb) { } clks_bool clks_fb_load_psf_font(const void *blob, u64 blob_size) { - struct clks_psf_font parsed = {0, 0, 0, 0, CLKS_NULL}; + struct clks_psf_font parsed = {0, 0, 0, 0, 0, CLKS_NULL}; if (clks_psf_parse_font(blob, blob_size, &parsed) == CLKS_FALSE) { return CLKS_FALSE; @@ -201,4 +213,4 @@ u32 clks_fb_cell_width(void) { u32 clks_fb_cell_height(void) { return clks_fb.glyph_height == 0U ? 8U : clks_fb.glyph_height; -} \ No newline at end of file +} diff --git a/clks/drivers/video/psf_font.c b/clks/drivers/video/psf_font.c index adc8597..1bfb629 100644 --- a/clks/drivers/video/psf_font.c +++ b/clks/drivers/video/psf_font.c @@ -10,12 +10,26 @@ #define CLKS_PSF1_GLYPH_BYTES 8U #define CLKS_PSF1_BLOB_SIZE (CLKS_PSF1_HEADER_SIZE + (CLKS_PSF1_GLYPH_COUNT * CLKS_PSF1_GLYPH_BYTES)) +#define CLKS_PSF2_MAGIC 0x864AB572U +#define CLKS_PSF2_HEADER_MIN_SIZE 32U + struct clks_psf1_header { u8 magic[2]; u8 mode; u8 charsize; }; +struct clks_psf2_header { + u32 magic; + u32 version; + u32 headersize; + u32 flags; + u32 length; + u32 charsize; + u32 height; + u32 width; +}; + struct clks_psf_seed_glyph { u8 code; u8 rows[CLKS_PSF1_GLYPH_BYTES]; @@ -74,7 +88,7 @@ static const struct clks_psf_seed_glyph clks_psf_seed_table[] = { }; static u8 clks_psf_default_blob[CLKS_PSF1_BLOB_SIZE]; -static struct clks_psf_font clks_psf_default = {8U, 8U, 0U, 0U, CLKS_NULL}; +static struct clks_psf_font clks_psf_default = {8U, 8U, 0U, 0U, 1U, CLKS_NULL}; static clks_bool clks_psf_default_ready = CLKS_FALSE; static clks_bool clks_psf_parse_psf1(const u8 *blob, usize blob_size, struct clks_psf_font *out_font) { @@ -114,10 +128,70 @@ static clks_bool clks_psf_parse_psf1(const u8 *blob, usize blob_size, struct clk out_font->height = glyph_bytes; out_font->glyph_count = glyph_count; out_font->bytes_per_glyph = glyph_bytes; + out_font->bytes_per_row = 1U; out_font->glyphs = blob + CLKS_PSF1_HEADER_SIZE; return CLKS_TRUE; } +static clks_bool clks_psf_parse_psf2(const u8 *blob, usize blob_size, struct clks_psf_font *out_font) { + const struct clks_psf2_header *hdr; + u32 bytes_per_row; + usize min_bytes_per_glyph; + usize payload_size; + + if (blob == CLKS_NULL || out_font == CLKS_NULL) { + return CLKS_FALSE; + } + + if (blob_size < CLKS_PSF2_HEADER_MIN_SIZE) { + return CLKS_FALSE; + } + + hdr = (const struct clks_psf2_header *)blob; + + if (hdr->magic != CLKS_PSF2_MAGIC) { + return CLKS_FALSE; + } + + if (hdr->headersize < CLKS_PSF2_HEADER_MIN_SIZE || hdr->headersize > blob_size) { + return CLKS_FALSE; + } + + if (hdr->width == 0U || hdr->height == 0U || hdr->length == 0U || hdr->charsize == 0U) { + return CLKS_FALSE; + } + + bytes_per_row = (hdr->width + 7U) / 8U; + + if (bytes_per_row == 0U) { + return CLKS_FALSE; + } + + min_bytes_per_glyph = (usize)bytes_per_row * (usize)hdr->height; + + if (min_bytes_per_glyph > hdr->charsize) { + return CLKS_FALSE; + } + + if ((usize)hdr->length > (((usize)-1) / (usize)hdr->charsize)) { + return CLKS_FALSE; + } + + payload_size = (usize)hdr->length * (usize)hdr->charsize; + + if (payload_size > (blob_size - (usize)hdr->headersize)) { + return CLKS_FALSE; + } + + out_font->width = hdr->width; + out_font->height = hdr->height; + out_font->glyph_count = hdr->length; + out_font->bytes_per_glyph = hdr->charsize; + out_font->bytes_per_row = bytes_per_row; + out_font->glyphs = blob + hdr->headersize; + return CLKS_TRUE; +} + static void clks_psf_seed_default_blob(void) { struct clks_psf1_header *hdr; u32 i; @@ -157,6 +231,7 @@ const struct clks_psf_font *clks_psf_default_font(void) { clks_psf_default.height = 8U; clks_psf_default.glyph_count = 1U; clks_psf_default.bytes_per_glyph = CLKS_PSF1_GLYPH_BYTES; + clks_psf_default.bytes_per_row = 1U; clks_psf_default.glyphs = clks_psf_unknown; } @@ -185,9 +260,26 @@ const u8 *clks_psf_glyph(const struct clks_psf_font *font, u32 codepoint) { } clks_bool clks_psf_parse_font(const void *blob, u64 blob_size, struct clks_psf_font *out_font) { - if (blob_size == 0ULL) { + const u8 *bytes; + u32 magic32; + + if (blob == CLKS_NULL || out_font == CLKS_NULL || blob_size == 0ULL) { return CLKS_FALSE; } - return clks_psf_parse_psf1((const u8 *)blob, (usize)blob_size, out_font); + bytes = (const u8 *)blob; + + if (blob_size >= 4ULL) { + magic32 = + ((u32)bytes[0]) | + (((u32)bytes[1]) << 8U) | + (((u32)bytes[2]) << 16U) | + (((u32)bytes[3]) << 24U); + + if (magic32 == CLKS_PSF2_MAGIC) { + return clks_psf_parse_psf2(bytes, (usize)blob_size, out_font); + } + } + + return clks_psf_parse_psf1(bytes, (usize)blob_size, out_font); } diff --git a/clks/drivers/video/psf_font.h b/clks/drivers/video/psf_font.h index 8089f79..26c43db 100644 --- a/clks/drivers/video/psf_font.h +++ b/clks/drivers/video/psf_font.h @@ -8,6 +8,7 @@ struct clks_psf_font { u32 height; u32 glyph_count; u32 bytes_per_glyph; + u32 bytes_per_row; const u8 *glyphs; }; @@ -15,4 +16,4 @@ const struct clks_psf_font *clks_psf_default_font(void); const u8 *clks_psf_glyph(const struct clks_psf_font *font, u32 codepoint); clks_bool clks_psf_parse_font(const void *blob, u64 blob_size, struct clks_psf_font *out_font); -#endif \ No newline at end of file +#endif diff --git a/clks/include/clks/panic.h b/clks/include/clks/panic.h new file mode 100644 index 0000000..8ceffe8 --- /dev/null +++ b/clks/include/clks/panic.h @@ -0,0 +1,10 @@ +#ifndef CLKS_PANIC_H +#define CLKS_PANIC_H + +#include +#include + +CLKS_NORETURN void clks_panic(const char *reason); +CLKS_NORETURN void clks_panic_exception(const char *name, u64 vector, u64 error_code, u64 rip); + +#endif \ No newline at end of file diff --git a/clks/kernel/interrupts.c b/clks/kernel/interrupts.c index a9cbf14..ec65204 100644 --- a/clks/kernel/interrupts.c +++ b/clks/kernel/interrupts.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -247,11 +248,7 @@ void clks_interrupt_dispatch(struct clks_interrupt_frame *frame) { } if (vector < 32U) { - clks_log(CLKS_LOG_ERROR, "EXC", clks_exception_names[vector]); - clks_log_hex(CLKS_LOG_ERROR, "EXC", "VECTOR", vector); - clks_log_hex(CLKS_LOG_ERROR, "EXC", "ERROR", frame->error_code); - clks_log_hex(CLKS_LOG_ERROR, "EXC", "RIP", frame->rip); - clks_cpu_halt_forever(); + clks_panic_exception(clks_exception_names[vector], vector, frame->error_code, frame->rip); } if (vector == CLKS_IRQ_TIMER) { diff --git a/clks/kernel/kmain.c b/clks/kernel/kmain.c index 4a04587..70d314b 100644 --- a/clks/kernel/kmain.c +++ b/clks/kernel/kmain.c @@ -96,7 +96,7 @@ void clks_kernel_main(void) { clks_tty_init(); } - clks_log(CLKS_LOG_INFO, "BOOT", "CLEONOS Stage23 START"); + clks_log(CLKS_LOG_INFO, "BOOT", "CLEONOS Stage24 START"); if (boot_fb == CLKS_NULL) { clks_log(CLKS_LOG_WARN, "VIDEO", "NO FRAMEBUFFER FROM LIMINE"); diff --git a/clks/kernel/panic.c b/clks/kernel/panic.c new file mode 100644 index 0000000..83d94b5 --- /dev/null +++ b/clks/kernel/panic.c @@ -0,0 +1,239 @@ +#include +#include +#include +#include +#include +#include + +#define CLKS_PANIC_BG 0x00200000U +#define CLKS_PANIC_FG 0x00FFE0E0U + +struct clks_panic_console { + u32 cols; + u32 rows; + u32 row; + u32 col; + u32 cell_w; + u32 cell_h; +}; + +static clks_bool clks_panic_active = CLKS_FALSE; + +static inline void clks_panic_disable_interrupts(void) { +#if defined(CLKS_ARCH_X86_64) + __asm__ volatile("cli"); +#elif defined(CLKS_ARCH_AARCH64) + __asm__ volatile("msr daifset, #0xf"); +#endif +} + +static void clks_panic_u64_to_hex(u64 value, char out[19]) { + int nibble; + + out[0] = '0'; + out[1] = 'X'; + + for (nibble = 0; nibble < 16; nibble++) { + u8 current = (u8)((value >> ((15 - nibble) * 4)) & 0x0FULL); + out[2 + nibble] = (current < 10U) ? (char)('0' + current) : (char)('A' + (current - 10U)); + } + + out[18] = '\0'; +} + +static clks_bool clks_panic_console_init(struct clks_panic_console *console) { + struct clks_framebuffer_info info; + + if (console == CLKS_NULL || clks_fb_ready() == CLKS_FALSE) { + return CLKS_FALSE; + } + + info = clks_fb_info(); + + console->cell_w = clks_fb_cell_width(); + console->cell_h = clks_fb_cell_height(); + + if (console->cell_w == 0U) { + console->cell_w = 8U; + } + + if (console->cell_h == 0U) { + console->cell_h = 8U; + } + + console->cols = info.width / console->cell_w; + console->rows = info.height / console->cell_h; + console->row = 0U; + console->col = 0U; + + if (console->cols == 0U || console->rows == 0U) { + return CLKS_FALSE; + } + + return CLKS_TRUE; +} + +static void clks_panic_console_newline(struct clks_panic_console *console) { + if (console == CLKS_NULL) { + return; + } + + console->col = 0U; + + if (console->row + 1U < console->rows) { + console->row++; + } +} + +static void clks_panic_console_put_char(struct clks_panic_console *console, char ch) { + u32 x; + u32 y; + + if (console == CLKS_NULL) { + return; + } + + if (ch == '\n') { + clks_panic_console_newline(console); + return; + } + + if (ch == '\r') { + console->col = 0U; + return; + } + + if (console->row >= console->rows || console->col >= console->cols) { + return; + } + + x = console->col * console->cell_w; + y = console->row * console->cell_h; + clks_fb_draw_char(x, y, ch, CLKS_PANIC_FG, CLKS_PANIC_BG); + + console->col++; + + if (console->col >= console->cols) { + clks_panic_console_newline(console); + } +} + +static void clks_panic_console_write(struct clks_panic_console *console, const char *text) { + usize i = 0U; + + if (console == CLKS_NULL || text == CLKS_NULL) { + return; + } + + while (text[i] != '\0') { + clks_panic_console_put_char(console, text[i]); + i++; + } +} + +static void clks_panic_serial_write_line(const char *line) { + if (line == CLKS_NULL) { + return; + } + + clks_serial_write(line); + clks_serial_write("\n"); +} + +static CLKS_NORETURN void clks_panic_halt_loop(void) { + clks_cpu_halt_forever(); +} + +CLKS_NORETURN void clks_panic(const char *reason) { + struct clks_panic_console console; + + clks_panic_disable_interrupts(); + + if (clks_panic_active == CLKS_TRUE) { + clks_panic_halt_loop(); + } + + clks_panic_active = CLKS_TRUE; + + clks_panic_serial_write_line("[PANIC] CLeonOS KERNEL PANIC"); + + if (reason != CLKS_NULL) { + clks_panic_serial_write_line(reason); + } + + if (clks_panic_console_init(&console) == CLKS_TRUE) { + clks_fb_clear(CLKS_PANIC_BG); + + clks_panic_console_write(&console, "CLeonOS KERNEL PANIC\n"); + clks_panic_console_write(&console, "====================\n\n"); + + if (reason != CLKS_NULL) { + clks_panic_console_write(&console, "REASON: "); + clks_panic_console_write(&console, reason); + clks_panic_console_write(&console, "\n"); + } + + clks_panic_console_write(&console, "\nSystem halted. Please reboot the VM.\n"); + } + + clks_panic_halt_loop(); +} + +CLKS_NORETURN void clks_panic_exception(const char *name, u64 vector, u64 error_code, u64 rip) { + struct clks_panic_console console; + char hex_buf[19]; + + clks_panic_disable_interrupts(); + + if (clks_panic_active == CLKS_TRUE) { + clks_panic_halt_loop(); + } + + clks_panic_active = CLKS_TRUE; + + clks_panic_serial_write_line("[PANIC] CPU EXCEPTION"); + + if (name != CLKS_NULL) { + clks_panic_serial_write_line(name); + } + + clks_panic_u64_to_hex(vector, hex_buf); + clks_panic_serial_write_line(hex_buf); + clks_panic_u64_to_hex(error_code, hex_buf); + clks_panic_serial_write_line(hex_buf); + clks_panic_u64_to_hex(rip, hex_buf); + clks_panic_serial_write_line(hex_buf); + + if (clks_panic_console_init(&console) == CLKS_TRUE) { + clks_fb_clear(CLKS_PANIC_BG); + + clks_panic_console_write(&console, "CLeonOS KERNEL PANIC\n"); + clks_panic_console_write(&console, "====================\n\n"); + clks_panic_console_write(&console, "TYPE: CPU EXCEPTION\n"); + + if (name != CLKS_NULL) { + clks_panic_console_write(&console, "NAME: "); + clks_panic_console_write(&console, name); + clks_panic_console_write(&console, "\n"); + } + + clks_panic_u64_to_hex(vector, hex_buf); + clks_panic_console_write(&console, "VECTOR: "); + clks_panic_console_write(&console, hex_buf); + clks_panic_console_write(&console, "\n"); + + clks_panic_u64_to_hex(error_code, hex_buf); + clks_panic_console_write(&console, "ERROR: "); + clks_panic_console_write(&console, hex_buf); + clks_panic_console_write(&console, "\n"); + + clks_panic_u64_to_hex(rip, hex_buf); + clks_panic_console_write(&console, "RIP: "); + clks_panic_console_write(&console, hex_buf); + clks_panic_console_write(&console, "\n\n"); + + clks_panic_console_write(&console, "System halted. Please reboot the VM.\n"); + } + + clks_panic_halt_loop(); +} \ No newline at end of file diff --git a/clks/kernel/shell.c b/clks/kernel/shell.c index 7aaf927..9400a5a 100644 --- a/clks/kernel/shell.c +++ b/clks/kernel/shell.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -696,6 +697,7 @@ static clks_bool clks_shell_cmd_help(void) { clks_shell_writeln(" dmesg [n]"); clks_shell_writeln(" shstat"); clks_shell_writeln(" rusttest"); + clks_shell_writeln(" panic"); clks_shell_writeln(" exec "); clks_shell_writeln(" clear"); clks_shell_writeln(" kbdstat"); @@ -1209,6 +1211,11 @@ static clks_bool clks_shell_cmd_rusttest(void) { return CLKS_TRUE; } +static clks_bool clks_shell_cmd_panic(void) { + clks_panic("MANUAL PANIC FROM KERNEL SHELL"); + return CLKS_FALSE; +} + static void clks_shell_execute_line(const char *line) { char line_buf[CLKS_SHELL_LINE_MAX]; char cmd[CLKS_SHELL_CMD_MAX]; @@ -1271,6 +1278,8 @@ static void clks_shell_execute_line(const char *line) { success = clks_shell_cmd_shstat(); } else if (clks_shell_streq(cmd, "rusttest") == CLKS_TRUE) { success = clks_shell_cmd_rusttest(); + } else if (clks_shell_streq(cmd, "panic") == CLKS_TRUE) { + success = clks_shell_cmd_panic(); } else if (clks_shell_streq(cmd, "exec") == CLKS_TRUE || clks_shell_streq(cmd, "run") == CLKS_TRUE) { success = clks_shell_cmd_exec(arg); } else if (clks_shell_streq(cmd, "clear") == CLKS_TRUE) { diff --git a/docs/README.md b/docs/README.md index 6234d3c..3dd2797 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,6 +20,7 @@ - `stage21.md` - `stage22.md` - `stage23.md` +- `stage24.md` ## Notes - Stage docs use a fixed template: goal, implementation, acceptance criteria, build targets, QEMU command, and debugging notes. diff --git a/docs/stage24.md b/docs/stage24.md new file mode 100644 index 0000000..a7fa4a2 --- /dev/null +++ b/docs/stage24.md @@ -0,0 +1,48 @@ +# CLeonOS Stage24 + +## Stage Goal +- Add a kernel shell command `panic`. +- Trigger a deliberate kernel panic from shell command input. +- Show a dedicated panic screen in framebuffer mode for manual panic and CPU exceptions. + +## What Was Implemented +- New panic subsystem: + - `clks/include/clks/panic.h` + - `clks/kernel/panic.c` +- Panic subsystem behavior: + - disable interrupts before panic halt + - write panic summary to serial (`-serial stdio`) + - draw full-screen panic page on framebuffer + - halt forever after panic +- Exception path integration: + - `clks/kernel/interrupts.c` now routes `vector < 32` to `clks_panic_exception(...)` +- Shell integration: + - `clks/kernel/shell.c` adds `panic` command + - `panic` command calls `clks_panic("MANUAL PANIC FROM KERNEL SHELL")` + - `help` output includes `panic` +- Stage banner updated: + - `CLEONOS Stage24 START` + +## Acceptance Criteria +- Kernel boots and prints `CLEONOS Stage24 START`. +- `help` lists `panic`. +- Typing `panic` in kernel shell shows panic page and system halts. +- Triggering CPU exceptions (if any) also enters the same panic screen flow. + +## Build Targets +- `make setup` +- `make userapps` +- `make iso` +- `make run` +- `make debug` + +## QEMU Command +- `qemu-system-x86_64 -M q35 -m 1024M -cdrom build/CLeonOS-x86_64.iso -serial stdio` + +## Common Bugs and Debugging +- `panic` command not found: + - Confirm `help` output contains `panic` and shell dispatch includes `clks_shell_cmd_panic()`. +- Panic screen not visible but serial has panic logs: + - Ensure framebuffer is available from Limine and `clks_fb_ready()` is true. +- Exception still prints old logs instead of panic page: + - Check `clks_interrupt_dispatch()` exception branch routes to `clks_panic_exception(...)`. diff --git a/ramdisk/system/tty.psf b/ramdisk/system/tty.psf index f59610e..d9b4710 100644 Binary files a/ramdisk/system/tty.psf and b/ramdisk/system/tty.psf differ diff --git a/scripts/gen-tty-psf.ps1 b/scripts/gen-tty-psf.ps1 index e796502..b143e3f 100644 --- a/scripts/gen-tty-psf.ps1 +++ b/scripts/gen-tty-psf.ps1 @@ -1,24 +1,35 @@ param( - [string]$OutputPath = "ramdisk/system/tty.psf" + [string]$OutputPath = "ramdisk/system/tty.psf", + [int]$Width = 12, + [int]$Height = 24, + [int]$GlyphCount = 256, + [int]$FontSize = 20, + [int]$OffsetX = 0, + [int]$OffsetY = 0, + [int]$AutoCenter = 1 ) Add-Type -AssemblyName System.Drawing -$width = 8 -$height = 16 -$glyphCount = 256 -$headerSize = 4 -$bytesPerGlyph = $height -$totalSize = $headerSize + ($glyphCount * $bytesPerGlyph) +if ($Width -le 0 -or $Height -le 0) { + throw "Width and Height must be positive" +} -$fontCandidates = @("Consolas", "Lucida Console", "Courier New") +if ($GlyphCount -le 0) { + throw "GlyphCount must be positive" +} + +$fontCandidates = @("Cascadia Mono", "Consolas", "Lucida Console") $font = $null + foreach ($name in $fontCandidates) { try { - $font = New-Object System.Drawing.Font($name, 14, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Pixel) - if ($font.Name -eq $name) { + $candidate = New-Object System.Drawing.Font($name, $FontSize, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Pixel) + if ($candidate.Name -eq $name) { + $font = $candidate break } + $candidate.Dispose() } catch { $font = $null } @@ -28,57 +39,170 @@ if ($null -eq $font) { throw "No usable monospace font found for PSF generation" } -$bytes = New-Object byte[] $totalSize -$bytes[0] = 0x36 -$bytes[1] = 0x04 -$bytes[2] = 0x00 -$bytes[3] = [byte]$bytesPerGlyph +function Set-U32LE { + param( + [byte[]]$Buffer, + [int]$Offset, + [uint32]$Value + ) + + $Buffer[$Offset + 0] = [byte]($Value -band 0xFF) + $Buffer[$Offset + 1] = [byte](($Value -shr 8) -band 0xFF) + $Buffer[$Offset + 2] = [byte](($Value -shr 16) -band 0xFF) + $Buffer[$Offset + 3] = [byte](($Value -shr 24) -band 0xFF) +} + +function Pixel-On { + param([System.Drawing.Color]$Pixel) + return (($Pixel.R + $Pixel.G + $Pixel.B) -ge 384) +} + +function Render-NormalizedGlyph { + param( + [char]$Char, + [System.Drawing.Font]$Font, + [int]$GlyphWidth, + [int]$GlyphHeight, + [int]$ShiftX, + [int]$ShiftY, + [int]$DoCenter + ) + + $workW = [Math]::Max($GlyphWidth * 4, 64) + $workH = [Math]::Max($GlyphHeight * 4, 64) + + $src = New-Object System.Drawing.Bitmap $workW, $workH + $g = [System.Drawing.Graphics]::FromImage($src) + $g.Clear([System.Drawing.Color]::Black) + $g.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::SingleBitPerPixelGridFit + + $fmt = New-Object System.Drawing.StringFormat + $fmt.FormatFlags = [System.Drawing.StringFormatFlags]::NoClip + $fmt.Alignment = [System.Drawing.StringAlignment]::Near + $fmt.LineAlignment = [System.Drawing.StringAlignment]::Near + + $size = $g.MeasureString([string]$Char, $Font, 1000, $fmt) + $drawX = [int][Math]::Floor(($workW - $size.Width) / 2.0) + $drawY = [int][Math]::Floor(($workH - $size.Height) / 2.0) + + $g.DrawString([string]$Char, $Font, [System.Drawing.Brushes]::White, $drawX, $drawY, $fmt) + $g.Dispose() + $fmt.Dispose() + + $minX = $workW + $minY = $workH + $maxX = -1 + $maxY = -1 + + for ($y = 0; $y -lt $workH; $y++) { + for ($x = 0; $x -lt $workW; $x++) { + if (Pixel-On ($src.GetPixel($x, $y))) { + if ($x -lt $minX) { $minX = $x } + if ($y -lt $minY) { $minY = $y } + if ($x -gt $maxX) { $maxX = $x } + if ($y -gt $maxY) { $maxY = $y } + } + } + } + + $dst = New-Object System.Drawing.Bitmap $GlyphWidth, $GlyphHeight + for ($y = 0; $y -lt $GlyphHeight; $y++) { + for ($x = 0; $x -lt $GlyphWidth; $x++) { + $dst.SetPixel($x, $y, [System.Drawing.Color]::Black) + } + } + + if ($maxX -ge $minX -and $maxY -ge $minY) { + $contentW = $maxX - $minX + 1 + $contentH = $maxY - $minY + 1 + + if ($DoCenter -ne 0) { + $baseX = [int][Math]::Floor(($GlyphWidth - $contentW) / 2.0) + $baseY = [int][Math]::Floor(($GlyphHeight - $contentH) / 2.0) + } else { + $baseX = 0 + $baseY = 0 + } + + $targetX = $baseX + $ShiftX + $targetY = $baseY + $ShiftY + + for ($y = $minY; $y -le $maxY; $y++) { + for ($x = $minX; $x -le $maxX; $x++) { + if (-not (Pixel-On ($src.GetPixel($x, $y)))) { + continue + } + + $nx = $targetX + ($x - $minX) + $ny = $targetY + ($y - $minY) + + if ($nx -ge 0 -and $nx -lt $GlyphWidth -and $ny -ge 0 -and $ny -lt $GlyphHeight) { + $dst.SetPixel($nx, $ny, [System.Drawing.Color]::White) + } + } + } + } + + $src.Dispose() + return $dst +} + +$headerSize = 32 +$bytesPerRow = (($Width + 7) -shr 3) +$bytesPerGlyph = $bytesPerRow * $Height +$totalSize = $headerSize + ($GlyphCount * $bytesPerGlyph) + +$bytes = New-Object byte[] $totalSize + +# PSF2 header +Set-U32LE -Buffer $bytes -Offset 0 -Value ([Convert]::ToUInt32("864AB572", 16)) +Set-U32LE -Buffer $bytes -Offset 4 -Value 0 +Set-U32LE -Buffer $bytes -Offset 8 -Value ([uint32]$headerSize) +Set-U32LE -Buffer $bytes -Offset 12 -Value 0 +Set-U32LE -Buffer $bytes -Offset 16 -Value ([uint32]$GlyphCount) +Set-U32LE -Buffer $bytes -Offset 20 -Value ([uint32]$bytesPerGlyph) +Set-U32LE -Buffer $bytes -Offset 24 -Value ([uint32]$Height) +Set-U32LE -Buffer $bytes -Offset 28 -Value ([uint32]$Width) -# default all glyphs to blank, avoids noisy '?' for undefined chars. for ($i = $headerSize; $i -lt $totalSize; $i++) { $bytes[$i] = 0x00 } -$white = [System.Drawing.Brushes]::White -$black = [System.Drawing.Color]::Black - -for ($code = 32; $code -le 126; $code++) { +for ($code = 32; $code -le [Math]::Min(126, $GlyphCount - 1); $code++) { $ch = [char]$code - - $bmp = New-Object System.Drawing.Bitmap $width, $height - $g = [System.Drawing.Graphics]::FromImage($bmp) - $g.Clear($black) - $g.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::SingleBitPerPixelGridFit - - # Small negative offset keeps glyph centered in 8x16 cell for common monospace fonts. - $g.DrawString([string]$ch, $font, $white, -1, -2) + $glyphBmp = Render-NormalizedGlyph -Char $ch -Font $font -GlyphWidth $Width -GlyphHeight $Height -ShiftX $OffsetX -ShiftY $OffsetY -DoCenter $AutoCenter $glyphOffset = $headerSize + ($code * $bytesPerGlyph) - for ($y = 0; $y -lt $height; $y++) { - [byte]$rowBits = 0 + for ($y = 0; $y -lt $Height; $y++) { + $rowOffset = $glyphOffset + ($y * $bytesPerRow) - for ($x = 0; $x -lt $width; $x++) { - $p = $bmp.GetPixel($x, $y) - if (($p.R + $p.G + $p.B) -ge 384) { - $rowBits = [byte]($rowBits -bor (1 -shl (7 - $x))) + for ($x = 0; $x -lt $Width; $x++) { + $p = $glyphBmp.GetPixel($x, $y) + if (Pixel-On $p) { + $byteIndex = ($x -shr 3) + $bitIndex = 7 - ($x -band 7) + $target = $rowOffset + $byteIndex + $bytes[$target] = [byte]($bytes[$target] -bor (1 -shl $bitIndex)) } } - - $bytes[$glyphOffset + $y] = $rowBits } - $g.Dispose() - $bmp.Dispose() + $glyphBmp.Dispose() } $font.Dispose() -$dir = Split-Path -Parent $OutputPath +if ([System.IO.Path]::IsPathRooted($OutputPath)) { + $fullOutput = $OutputPath +} else { + $fullOutput = Join-Path (Resolve-Path '.') $OutputPath +} + +$dir = Split-Path -Parent $fullOutput if (-not [string]::IsNullOrWhiteSpace($dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } -$fullOutput = Join-Path (Resolve-Path '.') $OutputPath [System.IO.File]::WriteAllBytes($fullOutput, $bytes) -Write-Output "Generated $OutputPath ($($bytes.Length) bytes)" \ No newline at end of file +Write-Output "Generated PSF2: $OutputPath (w=$Width, h=$Height, glyphs=$GlyphCount, bytes=$($bytes.Length), center=$AutoCenter)"