fb_scroll_rows() hardcoded the pixel distance it physically shifts the framebuffer by as char_rows * 16 * scale -- the bitmap-font (font_8x16.c) cell height -- regardless of which glyph mode vt100.c actually had active. In TTF mode (the REPL's default, cell height 24px via VT100_TTF_CELL_H_PX) this meant every scroll_up(1) call physically shifted the framebuffer by only 16px while the text model (g_vt.rows, py_of()) placed each row 24px apart. That 8px-per-scroll shortfall compounds with every subsequent scroll: a few scrolls barely show it, but enough scrolls -- or scrolling quickly, which is just many scrolls in a short span -- accumulates into visible pixel overlap between rows, with newer lines drawn on top of the tail end of older ones. fb_scroll_rect() (the box-confined scroll added later for 4.4t) already carried a doc comment calling this out explicitly, describing its own explicit pixel_rows parameter as the fix for fb_scroll_rows()'s "fixed 16px-row assumption" -- fb_scroll_rows() itself was just never updated to match. Fixed by changing fb_scroll_rows()'s parameter from an implicit char_rows count to an explicit pixel_rows count (matching fb_scroll_rect()'s existing convention), and having its one caller (vt100.c's scroll_up()) pass lines * cell_h() -- the real active cell height -- instead of a raw line count for the callee to guess at. Verified: booted amd64 to the REPL (TTF mode active per sk_repl()'s own console_fb_enable_ttf() call), let boot chatter + WORDS output scroll the screen through thousands of accumulated scroll_up() calls, then measured every visible line's y-position via a QMP screendump. Spacing held at a perfectly consistent 24px (TTF cell height) top to bottom with zero drift -- the old hardcoded-16px bug could not have produced that after this many scrolls. Re-verified boot to ok> on all three architectures (amd64/aarch64/riscv64) per repo acceptance policy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
369 lines
13 KiB
C
369 lines
13 KiB
C
/*
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
|
||
Copyright (c) 2023–2025 Robert A. James
|
||
All rights reserved.
|
||
|
||
Licensed under the StarForth License, Version 1.0.
|
||
*/
|
||
|
||
/**
|
||
* framebuffer.c — UEFI GOP framebuffer pixel driver
|
||
*
|
||
* Supports BGRX32 and RGBX32 pixel formats.
|
||
* Pixel scale factor allows 1×/2×/4× rendering for hi-res displays.
|
||
*
|
||
* Hi-res mode:
|
||
* 1080p (1920×1080) @ scale=1 → 240 cols × 67 rows (8×16 cells)
|
||
* 1080p (1920×1080) @ scale=2 → 120 cols × 33 rows (16×32 cells)
|
||
* 4K (3840×2160) @ scale=2 → 240 cols × 67 rows (16×32 cells)
|
||
* 4K (3840×2160) @ scale=4 → 120 cols × 33 rows (32×64 cells)
|
||
*/
|
||
|
||
#include "framebuffer.h"
|
||
#include <stdint.h>
|
||
|
||
/* Font API from font_8x16.c */
|
||
extern const uint8_t *font_8x16_glyph(uint8_t ch);
|
||
|
||
/* ANSI 16-color palette (indices 0–15, packed 0x00RRGGBB) */
|
||
const uint32_t FB_ANSI_PALETTE[16] __attribute__((visibility("hidden"))) = {
|
||
/* 0 Black */ FB_RGB(0x00, 0x00, 0x00),
|
||
/* 1 Red */ FB_RGB(0xAA, 0x00, 0x00),
|
||
/* 2 Green */ FB_RGB(0x00, 0xAA, 0x00),
|
||
/* 3 Yellow */ FB_RGB(0xAA, 0x55, 0x00),
|
||
/* 4 Blue */ FB_RGB(0x00, 0x00, 0xAA),
|
||
/* 5 Magenta */ FB_RGB(0xAA, 0x00, 0xAA),
|
||
/* 6 Cyan */ FB_RGB(0x00, 0xAA, 0xAA),
|
||
/* 7 White */ FB_RGB(0xAA, 0xAA, 0xAA),
|
||
/* 8 Br.Black */ FB_RGB(0x55, 0x55, 0x55),
|
||
/* 9 Br.Red */ FB_RGB(0xFF, 0x55, 0x55),
|
||
/* 10 Br.Green */ FB_RGB(0x55, 0xFF, 0x55),
|
||
/* 11 Br.Yellow */ FB_RGB(0xFF, 0xFF, 0x55),
|
||
/* 12 Br.Blue */ FB_RGB(0x55, 0x55, 0xFF),
|
||
/* 13 Br.Magenta */ FB_RGB(0xFF, 0x55, 0xFF),
|
||
/* 14 Br.Cyan */ FB_RGB(0x55, 0xFF, 0xFF),
|
||
/* 15 Br.White */ FB_RGB(0xFF, 0xFF, 0xFF),
|
||
};
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* Module state
|
||
* --------------------------------------------------------------------- */
|
||
|
||
typedef struct {
|
||
volatile uint32_t *base; /* GOP framebuffer base (32-bit pixels) */
|
||
uint32_t stride; /* pixels_per_scanline */
|
||
uint32_t width; /* horizontal resolution in pixels */
|
||
uint32_t height; /* vertical resolution in pixels */
|
||
FbPixelFormat fmt; /* pixel format (BGRX vs RGBX) */
|
||
uint32_t scale; /* pixel scale: 1, 2, or 4 (hi-res) */
|
||
int ready;
|
||
} FbState;
|
||
|
||
static FbState g_fb;
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* Internal helpers
|
||
* --------------------------------------------------------------------- */
|
||
|
||
/*
|
||
* Convert our 0x00RRGGBB to the hardware 32-bit word.
|
||
*
|
||
* GOP format names describe increasing byte address in memory (little-endian
|
||
* uint32 store), e.g. PixelRedGreenBlueReserved8BitPerColor means byte0=R,
|
||
* byte1=G, byte2=B, byte3=X -- which as a little-endian uint32 puts R in
|
||
* bits[7:0], G in bits[15:8], B in bits[23:16]. The two branches below were
|
||
* previously swapped relative to that (found via FABRIC.md item 4.3.1's
|
||
* orientation test: corners rendered with R and B channels exchanged).
|
||
*/
|
||
static uint32_t pack_pixel(uint32_t rgb)
|
||
{
|
||
uint32_t r = FB_R(rgb);
|
||
uint32_t g = FB_G(rgb);
|
||
uint32_t b = FB_B(rgb);
|
||
if (g_fb.fmt == FB_PIXEL_RGBX32) {
|
||
/* RGBX byte order R,G,B: byte0=R@[7:0] byte1=G@[15:8] byte2=B@[23:16] */
|
||
return (b << 16) | (g << 8) | r;
|
||
}
|
||
/* BGRX (default), byte order B,G,R: byte0=B@[7:0] byte1=G@[15:8] byte2=R@[23:16] */
|
||
return (r << 16) | (g << 8) | b;
|
||
}
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* Lifecycle
|
||
* --------------------------------------------------------------------- */
|
||
|
||
void fb_init(const FramebufferInfo *info, FbPixelFormat fmt)
|
||
{
|
||
if (!info || !info->base || info->width == 0 || info->height == 0) {
|
||
return;
|
||
}
|
||
|
||
g_fb.base = (volatile uint32_t *)info->base;
|
||
g_fb.stride = info->pixels_per_scanline;
|
||
g_fb.width = info->width;
|
||
g_fb.height = info->height;
|
||
g_fb.fmt = fmt;
|
||
g_fb.ready = 1;
|
||
|
||
/*
|
||
* Auto-select scale factor based on resolution.
|
||
* ≥ 2560 wide → 2× (gives readable 16px cells on QHD/4K)
|
||
* ≥ 3840 wide → 4× (gives readable 32px cells on 4K at density)
|
||
* Otherwise → 1× (1080p and below)
|
||
*/
|
||
if (info->width >= 3840) {
|
||
g_fb.scale = 4;
|
||
} else if (info->width >= 2560) {
|
||
g_fb.scale = 2;
|
||
} else {
|
||
g_fb.scale = 1;
|
||
}
|
||
}
|
||
|
||
int fb_is_available(void) { return g_fb.ready; }
|
||
uint32_t fb_width(void) { return g_fb.width; }
|
||
uint32_t fb_height(void) { return g_fb.height; }
|
||
|
||
/** Effective character cell dimensions in pixels (accounts for scale). */
|
||
uint32_t fb_cell_w(void) { return 8u * g_fb.scale; }
|
||
uint32_t fb_cell_h(void) { return 16u * g_fb.scale; }
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* Pixel-level primitives
|
||
* --------------------------------------------------------------------- */
|
||
|
||
void fb_put_pixel(uint32_t x, uint32_t y, uint32_t rgb)
|
||
{
|
||
if (!g_fb.ready || x >= g_fb.width || y >= g_fb.height) return;
|
||
g_fb.base[y * g_fb.stride + x] = pack_pixel(rgb);
|
||
}
|
||
|
||
void fb_fill_rect(uint32_t x, uint32_t y, uint32_t w, uint32_t h,
|
||
uint32_t rgb)
|
||
{
|
||
uint32_t px, py;
|
||
uint32_t packed;
|
||
uint32_t x_end, y_end;
|
||
|
||
if (!g_fb.ready) return;
|
||
|
||
packed = pack_pixel(rgb);
|
||
x_end = x + w;
|
||
y_end = y + h;
|
||
if (x_end > g_fb.width) x_end = g_fb.width;
|
||
if (y_end > g_fb.height) y_end = g_fb.height;
|
||
|
||
for (py = y; py < y_end; py++) {
|
||
volatile uint32_t *row = g_fb.base + py * g_fb.stride + x;
|
||
for (px = 0; px < (x_end - x); px++) {
|
||
row[px] = packed;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* Glyph rendering
|
||
* --------------------------------------------------------------------- */
|
||
|
||
/**
|
||
* Draw one 8×16 glyph at pixel position (px, py).
|
||
* Scales by g_fb.scale so the same font works at any resolution.
|
||
*/
|
||
void fb_draw_glyph(uint32_t px, uint32_t py, uint8_t ch,
|
||
uint32_t fg, uint32_t bg)
|
||
{
|
||
const uint8_t *glyph;
|
||
uint32_t row, col, sr, sc;
|
||
uint32_t pfg, pbg;
|
||
|
||
if (!g_fb.ready) return;
|
||
|
||
glyph = font_8x16_glyph(ch);
|
||
pfg = pack_pixel(fg);
|
||
pbg = pack_pixel(bg);
|
||
|
||
for (row = 0; row < 16u; row++) {
|
||
uint8_t bits = glyph[row];
|
||
for (col = 0; col < 8u; col++) {
|
||
uint32_t on = (bits >> (7u - col)) & 1u;
|
||
uint32_t packed = on ? pfg : pbg;
|
||
uint32_t base_x = px + col * g_fb.scale;
|
||
/* font_8x16_data stores each glyph's scanlines bottom-to-top
|
||
* (FABRIC.md item 4.4f) -- row 0 is the glyph's bottom row, so
|
||
* it maps to the cell's last pixel row, not its first. */
|
||
uint32_t base_y = py + (15u - row) * g_fb.scale;
|
||
|
||
/* pixel block for scale > 1 */
|
||
for (sr = 0; sr < g_fb.scale; sr++) {
|
||
uint32_t fy = base_y + sr;
|
||
if (fy >= g_fb.height) continue;
|
||
volatile uint32_t *line =
|
||
g_fb.base + fy * g_fb.stride + base_x;
|
||
for (sc = 0; sc < g_fb.scale; sc++) {
|
||
if (base_x + sc < g_fb.width) {
|
||
line[sc] = packed;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* Boot diagnostic: orientation test pattern
|
||
* --------------------------------------------------------------------- */
|
||
|
||
/**
|
||
* fb_draw_orientation_test — one-time boot diagnostic (FABRIC.md item 4.3.1).
|
||
*
|
||
* Fills each raster corner with a distinct solid color so a screendump
|
||
* immediately reveals whether the image is flipped or rotated. Not part of
|
||
* the Console drawing fabric itself -- diagnostic-only, analogous to item
|
||
* 4.2's self-test scaffolding.
|
||
*
|
||
* Corner colors (raster/top-left-origin coordinates, pre-Cartesian-flip):
|
||
* top-left RED
|
||
* top-right GREEN
|
||
* bottom-left BLUE
|
||
* bottom-right YELLOW
|
||
*/
|
||
void fb_draw_orientation_test(void)
|
||
{
|
||
uint32_t block_w, block_h;
|
||
|
||
if (!g_fb.ready) return;
|
||
|
||
block_w = g_fb.width / 8u;
|
||
block_h = g_fb.height / 8u;
|
||
if (block_w == 0) block_w = 1;
|
||
if (block_h == 0) block_h = 1;
|
||
|
||
fb_fill_rect(0, 0, block_w, block_h, FB_RGB(0xFF, 0x00, 0x00)); /* top-left: RED */
|
||
fb_fill_rect(g_fb.width - block_w, 0, block_w, block_h, FB_RGB(0x00, 0xFF, 0x00)); /* top-right: GREEN */
|
||
fb_fill_rect(0, g_fb.height - block_h, block_w, block_h, FB_RGB(0x00, 0x00, 0xFF)); /* bottom-left: BLUE */
|
||
fb_fill_rect(g_fb.width - block_w, g_fb.height - block_h, block_w, block_h,
|
||
FB_RGB(0xFF, 0xFF, 0x00)); /* bottom-right: YELLOW */
|
||
}
|
||
|
||
/* -----------------------------------------------------------------------
|
||
* Scrolling
|
||
* --------------------------------------------------------------------- */
|
||
|
||
/**
|
||
* Scroll the framebuffer up by `pixel_rows` pixel rows.
|
||
* The vacated rows at the bottom are filled with bg.
|
||
*
|
||
* Takes a pixel-row count directly rather than a character-row count
|
||
* scaled by a hardcoded 16px cell height: the caller (vt100.c's
|
||
* scroll_up()) knows the actual active glyph cell height (16 × scale for
|
||
* bitmap mode, 24 for TTF mode), and a mismatch between the height this
|
||
* function scrolls by and the height the text model actually draws rows
|
||
* at compounds by the gap on every single scroll -- a few scrolls barely
|
||
* show it, but enough scrolls (or scrolling quickly) accumulates visible
|
||
* pixel overlap between rows.
|
||
*
|
||
* Copies through non-volatile pointers (FABRIC.md item 4.4g performance
|
||
* fix, 2026-08-11): the GOP framebuffer is mapped write-back, not
|
||
* cache-disabled MMIO (vmm.c:350-363 -- "QEMU's VGA emulation is coherent
|
||
* and UC- mapping causes #GP"), so it behaves as ordinary RAM and does not
|
||
* need per-word volatile access. `g_fb.base` stays `volatile` for
|
||
* single-pixel writes elsewhere in this file (draw calls that must not be
|
||
* reordered relative to other framebuffer state), but that qualifier forced
|
||
* this copy into one un-batchable 4-byte access per element with no
|
||
* compiler vectorization -- catastrophic once boot output exceeds one
|
||
* screen (~10^10 volatile accesses for an 18,000-line boot transcript
|
||
* under TCG). Casting away volatile here for the bulk copy/fill lets the
|
||
* compiler batch these loops normally.
|
||
*/
|
||
void fb_scroll_rows(uint32_t pixel_rows, uint32_t bg)
|
||
{
|
||
uint32_t pixel_rows_to_scroll;
|
||
uint32_t src_y, dst_y;
|
||
uint32_t x;
|
||
uint32_t packed_bg;
|
||
uint32_t *fb = (uint32_t *)(uintptr_t)g_fb.base;
|
||
|
||
if (!g_fb.ready || pixel_rows == 0) return;
|
||
|
||
pixel_rows_to_scroll = pixel_rows;
|
||
if (pixel_rows_to_scroll >= g_fb.height) {
|
||
fb_fill_rect(0, 0, g_fb.width, g_fb.height, bg);
|
||
return;
|
||
}
|
||
|
||
packed_bg = pack_pixel(bg);
|
||
|
||
/* Copy pixel rows upward */
|
||
for (dst_y = 0; dst_y + pixel_rows_to_scroll < g_fb.height; dst_y++) {
|
||
src_y = dst_y + pixel_rows_to_scroll;
|
||
uint32_t *src = fb + src_y * g_fb.stride;
|
||
uint32_t *dst = fb + dst_y * g_fb.stride;
|
||
for (x = 0; x < g_fb.width; x++) {
|
||
dst[x] = src[x];
|
||
}
|
||
}
|
||
|
||
/* Clear the newly exposed rows at the bottom */
|
||
for (dst_y = g_fb.height - pixel_rows_to_scroll;
|
||
dst_y < g_fb.height; dst_y++) {
|
||
uint32_t *row = fb + dst_y * g_fb.stride;
|
||
for (x = 0; x < g_fb.width; x++) {
|
||
row[x] = packed_bg;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* fb_scroll_rect — box-confined scroll (FABRIC.md item 4.4t).
|
||
*
|
||
* Same non-volatile bulk-copy rationale as fb_scroll_rows() above, but
|
||
* bounded to a caller-supplied rectangle instead of the whole framebuffer --
|
||
* needed once REPL text is confined to the 4.4o CANVAS box, since a
|
||
* whole-framebuffer scroll would drag pixels from outside the box (e.g. a
|
||
* future 4.4r border) into the text area. Takes pixel_rows directly rather
|
||
* than char_rows * a hardcoded cell height, since TTF mode's cell height
|
||
* (24px) differs from bitmap mode's (16px * scale).
|
||
*/
|
||
void fb_scroll_rect(uint32_t x, uint32_t y, uint32_t w, uint32_t h,
|
||
uint32_t pixel_rows, uint32_t bg)
|
||
{
|
||
uint32_t src_y, dst_y, px;
|
||
uint32_t packed_bg;
|
||
uint32_t *fb = (uint32_t *)(uintptr_t)g_fb.base;
|
||
uint32_t x_end, y_end;
|
||
|
||
if (!g_fb.ready || pixel_rows == 0 || w == 0 || h == 0) return;
|
||
|
||
x_end = x + w;
|
||
y_end = y + h;
|
||
if (x_end > g_fb.width) x_end = g_fb.width;
|
||
if (y_end > g_fb.height) y_end = g_fb.height;
|
||
if (x >= x_end || y >= y_end) return;
|
||
|
||
if (pixel_rows >= (y_end - y)) {
|
||
fb_fill_rect(x, y, x_end - x, y_end - y, bg);
|
||
return;
|
||
}
|
||
|
||
packed_bg = pack_pixel(bg);
|
||
|
||
/* Copy pixel rows upward, bounded to [x, x_end) */
|
||
for (dst_y = y; dst_y + pixel_rows < y_end; dst_y++) {
|
||
src_y = dst_y + pixel_rows;
|
||
uint32_t *src = fb + src_y * g_fb.stride + x;
|
||
uint32_t *dst = fb + dst_y * g_fb.stride + x;
|
||
for (px = 0; px < (x_end - x); px++) {
|
||
dst[px] = src[px];
|
||
}
|
||
}
|
||
|
||
/* Clear the newly exposed rows at the bottom of the rect */
|
||
for (dst_y = y_end - pixel_rows; dst_y < y_end; dst_y++) {
|
||
uint32_t *row = fb + dst_y * g_fb.stride + x;
|
||
for (px = 0; px < (x_end - x); px++) {
|
||
row[px] = packed_bg;
|
||
}
|
||
}
|
||
}
|