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

@ -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);
}