Files
StarForth/src/starkernel/hal/framebuffer.c
T
Robert Allan JamesandClaude Opus 5 f81e9c92bc Fix framebuffer console: scroll drift causes progressive line overlap in TTF mode
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>
2026-09-02 17:25:06 -04:00

369 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
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;
}
}
}