tty: the grid was 80x24 on a 160x50 screen, and wrapping was eager

Three symptoms, two causes, both in the terminal layer.

**The grid and TIOCGWINSZ disagreed about the size.** vt_init() sizes every
grid from gfx_cols()/gfx_rows(), and it runs at main.c:75 -- while the display
driver attaches its scanout from the PCI bus walk at main.c:99. So the grids
were the 80x24 fallback, and TIOCGWINSZ, which computed its own answer live,
said 160x50.

That one disagreement produced all three things visible in the report: text
stopped two thirds of the way down the screen, `ls` laid out 160 columns into
an 80-column grid so words split across the wrap ("vers" / "ion"), and zsh's
cursor tracking was wrong on every line.

Fixed in two places, because either alone would leave the trap set:
gfx_attach_scanout() now calls vt_resize(), so the grids learn the real screen
the moment there is one; and TIOCGWINSZ reports v->cols/v->rows rather than
measuring again. One number, from the terminal being asked.

**Wrapping was eager.** A glyph printed in the last column moved the cursor to
the next line immediately. Real terminals set a pending-wrap flag and wrap when
the *next* glyph arrives -- which is why a line exactly as wide as the screen
takes one row and not two, and why a program can write the bottom-right cell
without scrolling.

zsh's partial-line marker depends on that rule directly: it writes exactly
COLUMNS-1 spaces and expects to still be on the line. With eager wrap it left a
reverse-video '#' above every prompt, which is the hashtag in the report. Both
are gone.

Verified by screendump: `ls /proc` now fits one line with "version" intact, no
markers, and output fills the screen to the last row. schfs-check passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
Nexoniarz 2026-09-13 12:49:13 +02:00
commit a5b0d5a0ef
5 changed files with 137 additions and 25 deletions

View file

@ -21,6 +21,7 @@
#include "device.h"
#include "display.h"
#include "gfx.h"
#include "vt.h"
#define BACKSPACE 0x100
#define CHAR_HEIGHT 16
@ -47,6 +48,12 @@ gfx_attach_scanout(const struct fb_info *info)
{
scanout = *info;
fb = info->ptr;
// Tell the terminals how big the screen actually is. They were sized before
// this driver existed -- the PCI bus is walked long after the terminals are
// set up -- so until this call every grid is the 80x24 fallback. See
// vt_resize().
vt_resize();
}
const struct fb_info *

View file

@ -118,18 +118,29 @@ termios_defaults(struct termios *t)
t->c_ospeed = 15;
}
void
vt_init(void)
// The screen size, clamped to what a grid can hold.
//
// 80x24 when there is no framebuffer yet -- which is the usual case when
// vt_init runs, because the display driver is found on the PCI bus and the
// terminals are set up before the bus is walked. vt_resize() corrects it the
// moment a scanout arrives.
static void
measure(int *out_cols, int *out_rows)
{
int cols = gfx_cols(), rows = gfx_rows();
// No framebuffer (serial-only boot): 80x24 is a stated default rather than
// a measurement of nothing. The grids still work and still scroll; nobody
// is looking at them.
if (cols <= 0) cols = 80;
if (rows <= 0) rows = 24;
if (cols > VT_MAXCOLS) cols = VT_MAXCOLS;
if (rows > VT_MAXROWS) rows = VT_MAXROWS;
*out_cols = cols;
*out_rows = rows;
}
void
vt_init(void)
{
int cols, rows;
measure(&cols, &rows);
for (int i = 0; i < NVT; i++) {
struct vt *v = &vts[i];
@ -153,6 +164,42 @@ vt_init(void)
vt_ready = 1;
}
// The real screen has arrived. Re-measure and re-lay the grids.
//
// This exists because of the order the kernel comes up in: terminals are
// initialised before the PCI bus is walked, so the display driver attaches its
// scanout *after* vt_init has already sized every grid from a framebuffer that
// was not there yet. Without this the grids stayed 80x24 on a 160x50 screen --
// text stopped two thirds of the way down, `ls` formatted for a width the grid
// did not have and split words across the wrap, and zsh's cursor tracking was
// wrong on every line.
//
// Content is not preserved. Reflowing a grid into a different width is a real
// problem and there is nothing here worth reflowing: this runs during boot,
// before console_takeover(), when every grid is still blank.
void
vt_resize(void)
{
int cols, rows;
if (!vt_ready)
return; // vt_init has not run; it will measure itself
measure(&cols, &rows);
if (cols == vts[0].cols && rows == vts[0].rows)
return; // same screen, nothing to do
for (int i = 0; i < NVT; i++) {
struct vt *v = &vts[i];
v->cols = cols;
v->rows = rows;
if (v->cx >= cols) v->cx = cols - 1;
if (v->cy >= rows) v->cy = rows - 1;
for (int r = 0; r < rows; r++)
blank_row(v, r);
}
vt_repaint(vt_current());
}
struct vt *
vt_get(int i)
{
@ -219,6 +266,7 @@ vt_cursor_set(struct vt *v, int col, int row)
if (row >= v->rows) row = v->rows - 1;
v->cx = col;
v->cy = row;
v->wrap_pending = 0; // an explicit move is not a wrap
}
void
@ -239,21 +287,38 @@ put_blank_at(struct vt *v, int col, int row)
show(v, col, row);
}
// Take the deferred wrap, if one is owed. Called before anything that needs
// the cursor to be at a real position.
static void
wrap_now(struct vt *v)
{
if (!v->wrap_pending)
return;
v->wrap_pending = 0;
v->cx = 0;
v->cy++;
while (v->cy >= v->rows)
vt_scroll(v);
}
void
vt_putc(struct vt *v, int glyph)
{
switch (glyph) {
case '\n':
v->wrap_pending = 0;
v->cx = 0;
v->cy++;
break;
case '\r':
v->wrap_pending = 0;
v->cx = 0;
break;
case '\b':
case 0x7f:
v->wrap_pending = 0;
if (v->cx > 0) {
v->cx--;
put_blank_at(v, v->cx, v->cy);
@ -265,10 +330,12 @@ vt_putc(struct vt *v, int glyph)
// rather than skipping over stale characters. Without a case here '\t'
// reaches the font as glyph 9, which in CP437 is a hollow circle: pressing
// Tab printed a circle.
wrap_now(v);
do {
put_blank_at(v, v->cx, v->cy);
v->cx++;
} while (v->cx % 8 != 0 && v->cx < v->cols);
if (v->cx >= v->cols) { v->cx = v->cols - 1; v->wrap_pending = 1; }
break;
default: {
@ -282,21 +349,25 @@ vt_putc(struct vt *v, int glyph)
return;
if (glyph < 0 || glyph > 255)
glyph = '?';
wrap_now(v); // the previous glyph filled the line
struct vtcell *k = cell_at(v, v->cx, v->cy);
k->ch = (uint8)glyph;
k->fg = v->fg;
k->bg = v->bg;
k->attr = (uint8)v->attr;
show(v, v->cx, v->cy);
v->cx++;
// Filling the last column does not move to the next line. It owes a wrap,
// and pays it when the next glyph arrives -- which is what makes a line
// that is exactly as wide as the screen take one row instead of two, and
// what lets a program write to the bottom-right cell without scrolling.
if (v->cx + 1 >= v->cols)
v->wrap_pending = 1;
else
v->cx++;
break;
}
}
if (v->cx >= v->cols) {
v->cx = 0;
v->cy++;
}
while (v->cy >= v->rows)
vt_scroll(v);
}
@ -339,6 +410,7 @@ vt_clear(struct vt *v)
for (int r = 0; r < v->rows; r++)
blank_row(v, r);
v->cx = v->cy = 0;
v->wrap_pending = 0;
vt_repaint(v);
}

View file

@ -1061,22 +1061,24 @@ addr_t sys_ioctl(void)
if (request == TIOCGWINSZ) {
struct winsize *ws = (struct winsize*)user_ptr(argp, sizeof(*ws));
if (!ws) return -EFAULT;
// The console's real geometry, not a VT100's.
// The terminal's own grid, and nothing else.
//
// This used to answer 24x80 unconditionally -- the size of a terminal
// SchOS has never drawn. Every program that lays out a screen asks
// this one question first: a line editor to decide where to wrap, `ls`
// to pick a column count, fastfetch to place its logo beside the text.
// At 1024x768 with the 8x16 font the console is 128x48, so all of them
// were formatting for a screen a third the area of the real one, and
// each looked like a separate bug in a separate program.
int cols = gfx_cols(), rows = gfx_rows();
// Serial-only boot, with no framebuffer attached: 80x24 is then a
// stated default rather than a wrong measurement.
ws->ws_col = cols > 0 ? (uint16)cols : 80;
ws->ws_row = rows > 0 ? (uint16)rows : 24;
ws->ws_xpixel = (uint16)gfx_width();
ws->ws_ypixel = (uint16)gfx_height();
// SchOS has never drawn -- and was then fixed to compute the answer
// live from gfx_cols()/gfx_rows(). That was a second source of truth,
// and it disagreed with the first: the grids are sized once at
// vt_init(), which runs before the display driver is found on the PCI
// bus, so they were 80x24 while this reported 160x50. Programs
// formatted for a screen the grid did not have -- `ls` split words
// across the wrap, text stopped two thirds of the way down, and zsh's
// cursor tracking was wrong on every line.
//
// One number, from the terminal being asked. They cannot disagree.
struct vt *v = vt_for_minor(f->ip->minor);
ws->ws_col = (uint16)v->cols;
ws->ws_row = (uint16)v->rows;
ws->ws_xpixel = (uint16)(v->cols * gfx_char_w());
ws->ws_ypixel = (uint16)(v->rows * gfx_char_h());
return 0;
}
// The console's size is a property of the framebuffer, so a request to