"Ą Ę Ś Ł Ż" drew as "?????" and the obvious diagnosis -- the font has no glyphs for them -- was wrong twice over. `struct vtcell.ch` was **one byte** holding a CP437 index, and the console mapped UTF-8 down to it on the way in. U+0104 does not fit in a byte. No font could have fixed that; the cell had to get wider first. So: `ch` is a uint32 codepoint, the mapping on the way in is gone, and the cell draws through kernel/dev/ttf.c against JetBrains Mono linked into the image. Cell size comes from the font's measured advance instead of a hardcoded 8x16. The grid costs 864 KB of BSS instead of 432 KB, which is the honest price of holding a codepoint per cell for six terminals at 240x75. The rasteriser needed three fixes before it could be trusted with this. **21 KB of locals on a 4 KB kernel stack.** `ttf_render` held an outline (512 points as three arrays) and a raster (1024 edges) as automatics. KSTACKSIZE is 4096. They walked off the bottom of the stack into whatever was underneath, which surfaced as user processes faulting on addresses they never touched and then a panic inside freevm() on an overwritten page directory. Nothing pointed anywhere near a font. **And then again, recursively.** load_outline() held a 4736-byte outline as a local *and recursed* for composite glyphs. Composite glyphs are exactly Ą, Ę, Ś, Ł, Ż -- so the second crash arrived the moment the first fix let Unicode reach the screen, looking like a fresh bug. Now a static outline per recursion level with a depth cap of 4 (the font's deepest real nesting is 2), and the leaf parse split into load_simple() so its 512-byte flag array is not multiplied by the depth. Worst chain measured from the object file: ~2.3 KB. Neither showed in tools/ttftest, which has an 8 MB stack. That is the standing hazard of one source file built for host and kernel: the host is most forgiving about exactly the resource the kernel is strictest about. **Component transforms were being skipped**, on the stated theory that a wrong transform is worse than none. It is not. `‘` in this font is the comma glyph with a scale of -1 -- a 180-degree rotation -- and the offset that ships with it only lands correctly after the rotation. Without it the mark went twelve pixels into a ten-pixel cell and was clipped away entirely, so the character read as missing from the font rather than as misplaced. F2Dot14 2x2 now, with SCALED_COMPONENT_OFFSET honoured. The glyph cache was ASCII-only, so every accented letter re-rasterised on every repaint: a screenful of Polish did hundreds of bezier flattenings per scroll where the same screenful of English did none. 95 reserved ASCII slots plus 224 shared slots keyed by codepoint, 64 KB pool. Verified 13680 cached renders byte-identical to direct ones across collisions and a pool flush. tools/ttftest speaks UTF-8 now. Iterating bytes meant it could only ever ask for ASCII, and ASCII is entirely simple glyphs -- it could not reach the code path that mattered. Also: resolvconf's success line moves behind /proc/bootmode, same rule as vgpud. A quiet boot opened with "resolvconf: nameserver 10.0.2.3" where the one line was supposed to be "Welcome to SchOS!". Failures stay loud. Verified on screen, not just on the host: Ą Ę Ś Ł Ż ć ń ó ź, Ω λ π Œ Ð Æ ß, ∞ ‘x’ → ≤ all render, panic=0 across four boots. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
337 lines
16 KiB
Markdown
337 lines
16 KiB
Markdown
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
# Virtual terminals
|
|
|
|
Six of them, `/dev/tty1` through `/dev/tty6`.
|
|
|
|
| | |
|
|
|---|---|
|
|
| **AltGr+F1 .. F6** | go to that terminal |
|
|
| **AltGr+-** / **AltGr+=** | previous / next terminal *in use* |
|
|
| `chvt N` | the same, from a script or a serial console |
|
|
|
|
The chord is settable — `chvt -c` — for the reasons below. Each one is an independent terminal: its own screen, its own
|
|
cursor, its own line discipline, its own foreground process group and its own
|
|
session. Two shells on two terminals are two unrelated processes that happen to
|
|
share a keyboard and a screen.
|
|
|
|
A seventh is deliberately not bound. That is where a display server goes,
|
|
and it will claim switching with `VT_SETMODE` rather than a chord.
|
|
|
|
---
|
|
|
|
## What makes switching possible
|
|
|
|
A backing store, and nothing else.
|
|
|
|
Before this, the console drew straight into the framebuffer and scrolled by
|
|
moving pixels. That works exactly as long as nothing else ever wants the
|
|
screen: there was nowhere to put terminal 2 while terminal 1 was visible, and
|
|
nothing to repaint terminal 1 from when it came back.
|
|
|
|
So each terminal owns a grid of cells and the framebuffer shows whichever grid
|
|
is active. `vt_switch()` is three lines — set the active index, repaint from
|
|
the grid, return — because nothing has to be *saved* on the way out. The
|
|
outgoing terminal's grid has been current all along.
|
|
|
|
The rule the whole layer is built on:
|
|
|
|
> **A terminal does not know whether it is visible.**
|
|
|
|
Writes land in the grid, the cursor moves, the grid scrolls, all of it
|
|
unconditionally. Drawing is a separate question, asked once, at the bottom of
|
|
`kernel/dev/vt.c`, answered by comparing against `vt_active`. Every alternative
|
|
spreads "am I on screen" through the escape parser, and then a terminal
|
|
switched away from mid-sequence comes back wrong.
|
|
|
|
## A cell
|
|
|
|
Eight bytes:
|
|
|
|
| | |
|
|
|---|---|
|
|
| `ch` | the Unicode codepoint, 32 bits |
|
|
| `fg`, `bg` | palette indices, 0-15 ANSI plus two theme defaults |
|
|
| `attr` | reverse video; room for underline and the rest |
|
|
| `pad` | to eight, so the grid indexes without a multiply |
|
|
|
|
`ch` was one byte holding a CP437 glyph, not a codepoint: the console mapped
|
|
UTF-8 down to CP437 on the way in, because that is what `include/font.h`
|
|
contained. Widening it to the codepoint is what made Ą, Ę, Ś, Ω and the rest
|
|
possible, and no font change could have done it alone — a byte cannot *hold*
|
|
U+0104 whatever is drawing it. Mapping on the way in also meant the decision
|
|
was made once, at write time, and a later font could not revisit it.
|
|
|
|
The cost is real: the grid went from four bytes per cell to eight, and six
|
|
terminals at 240x75 is 864 KB of BSS rather than 432 KB. Paid once at link
|
|
time, which is the only way to pay it — there is no contiguous multi-page
|
|
allocator to ask.
|
|
|
|
The colours stay indices rather than RGB, for the same reason they always
|
|
were: a cell holding two 32-bit colours would be sixteen bytes.
|
|
|
|
## What is per-terminal
|
|
|
|
Everything that used to be a single global:
|
|
|
|
| Was | Now |
|
|
|---|---|
|
|
| `int tty_fg_pgrp = 1;` | `v->fg_pgrp`, one per terminal |
|
|
| one `struct termios tio` | `v->tio` |
|
|
| one 256-byte input ring | `v->in` |
|
|
| one escape-parser state | `v->esc_*` |
|
|
| one UTF-8 decoder | `v->u8_*` |
|
|
| one cursor, in pixels, in `gfx.c` | `v->cx`, `v->cy`, in cells |
|
|
|
|
`TIOCSCTTY` used to be `return 0` — it claimed to make a terminal the caller's
|
|
controlling terminal and did nothing, because there was one terminal and
|
|
nothing to choose between. It records `proc->ctty` now, which is what
|
|
`/dev/tty` resolves through.
|
|
|
|
## The devices
|
|
|
|
| Path | Minor | What |
|
|
|---|---|---|
|
|
| `/dev/console` | 1 | the **active** terminal, so kernel messages follow a switch |
|
|
| `/dev/tty` | 10 | the caller's **controlling** terminal, via `proc->ctty` |
|
|
| `/dev/tty1`..`tty6` | 11-16 | that terminal, whether or not it is on screen |
|
|
|
|
`/dev/tty` was an alias for `/dev/console` with the same major and minor. They
|
|
are different questions and now have different answers.
|
|
|
|
## The ioctls
|
|
|
|
Linux's numbers, because they do Linux's job.
|
|
|
|
| | |
|
|
|---|---|
|
|
| `VT_ACTIVATE` | switch. Argument is a 1-based number **by value**, not a pointer |
|
|
| `VT_WAITACTIVE` | returns immediately: `vt_switch` repaints synchronously, so there is nothing left to wait for |
|
|
| `VT_GETSTATE` | active terminal and a bitmask of those in use |
|
|
| `VT_OPENQRY` | the first terminal nobody has open |
|
|
| `VT_GETMODE`/`VT_SETMODE` | accepted and ignored — the kernel drives switching and there is no process mode to set |
|
|
|
|
## The keyboard
|
|
|
|
`kbd.c` recognises Ctrl+Alt+Shift+F1..F6 and returns `KEY_VT_SWITCH + n`, which is above
|
|
0xFF on purpose: every value that fits in a byte is already either a character
|
|
or a UTF-8 continuation byte, and a chord that collided with one would be typed
|
|
into the terminal instead of switching away from it.
|
|
|
|
What the chord *means* is decided in `console.c`, where the terminals are. The
|
|
keyboard driver names it and stops.
|
|
|
|
### The chord is a setting, and that is the point
|
|
|
|
**There is no combination that is free for everyone on every host**, and a
|
|
guest cannot find out what its host takes: a key the host swallowed is
|
|
indistinguishable from a key nobody pressed. Three parties have a claim on
|
|
every candidate.
|
|
|
|
| | |
|
|
|---|---|
|
|
| **The host takes it, and the guest never sees it** | |
|
|
| `Alt+F4`, `Alt+F2` | window manager |
|
|
| `Ctrl+Alt+F1..F6` | display manager — and it matches **loosely**, so adding Shift does not get past it |
|
|
| `Ctrl+Alt+`digit | QEMU's own console tabs |
|
|
| **Applications want it, and lose it permanently if the kernel eats it** | |
|
|
| `Alt+`anything | Meta. zsh binds Meta-digit as `digit-argument`; so does every readline editor |
|
|
| `Ctrl+`letter | control characters |
|
|
| `Ctrl+Shift+`digit | types a character — `Ctrl+Shift+2` is `^@` |
|
|
| `F1..F6` alone | help, and whatever a full-screen program wants |
|
|
| **The keyboard layout wants it** | |
|
|
| `AltGr+`letter | Polish diacritics — ą ć ę ł ń ó ś ź ż — and on some setups AltGr arrives as Ctrl+Alt |
|
|
|
|
So the kernel ships a default and lets it be moved:
|
|
|
|
```sh
|
|
chvt # report the active terminal and the current chord
|
|
chvt -c altgr+F # AltGr+F1..F6 (the default)
|
|
chvt -c ctrl+shift+F # Ctrl+Shift+F1..F6
|
|
chvt -c alt+1 # Alt+1..6
|
|
chvt -c off # no chord; chvt N is then the only way
|
|
```
|
|
|
|
`off` is a legitimate setting, not a failure mode. On a machine whose host
|
|
grabs everything worth binding, `chvt N` from a shell is the honest answer.
|
|
|
|
The default is **AltGr+F1..F6**, and the `AltGr+`letter row above needs reading
|
|
carefully: what a Polish layout wants is AltGr with a **letter**. AltGr with a
|
|
**function key** produces nothing on any layout, no window manager binds it,
|
|
and no application asks for it. It is the one combination all three parties
|
|
leave alone.
|
|
|
|
That distinction is only expressible because Right Alt is tracked as its own
|
|
modifier bit. It used to share `ALT`, which is harmless for typing — neither is
|
|
used to look up a character — and made this impossible to say.
|
|
|
|
Verified rather than argued: with the default set, `AltGr+F2` switches,
|
|
`AltGr+F1` returns to a pixel-identical tty1, and `AltGr+a` still reaches the
|
|
shell, so ą is untouched.
|
|
|
|
Whatever the modifiers are, they also bind **minus** and **equals** to the
|
|
previous and next terminal. Those step only through terminals that are *in
|
|
use* — the same `opened` count `VT_GETSTATE` reports — because six terminals
|
|
with two shells on them means four blank screens in between, and cycling past
|
|
them is worse than not having the key.
|
|
|
|
Which chords reach a guest at all was **measured, not assumed**: a temporary
|
|
probe in `kbdgetc` logged every modified keypress, and QEMU's `sendkey` fired
|
|
each candidate at it. Everything arrived, `Ctrl+Alt+F2` included — so that
|
|
conflict is the host's display manager, not QEMU, and no amount of extra
|
|
modifiers gets around a grab that matches loosely.
|
|
|
|
The modifier test is an **equality**, not a mask:
|
|
|
|
```c
|
|
if (vt_chord_mods != 0 &&
|
|
(shift & (CTL | SHIFT | ALT | RALT)) == vt_chord_mods && ...)
|
|
```
|
|
|
|
A mask fires on any superset, so a chord aimed at the host would switch
|
|
terminals behind your back. Right Alt is tracked as its own bit (`RALT`) for
|
|
the same reason; it used to share `ALT`, which is harmless for typing — neither
|
|
is used to look up a character — and wrong for chords.
|
|
|
|
`include/posix.h`'s `VTC_*` bits mirror `include/kbd.h`'s modifier bits, and a
|
|
`_Static_assert` in `kbd.c` holds them together. A mismatch would be silent:
|
|
a chord set from userspace would simply never match, and the only symptom
|
|
would be a key that does nothing.
|
|
|
|
### Taking the chord back
|
|
|
|
Three layers, in increasing order of how much a program is claiming:
|
|
|
|
| | |
|
|
|---|---|
|
|
| **`VT_SETMODE(VT_PROCESS)`** | the terminal's owner handles switching itself. The kernel stops acting on the chord entirely. This is what a display server uses. |
|
|
| **`EVIOCGRAB` on `/dev/input/event0`** | an exclusive keyboard grab. Checked in `kbdgetc` *before* the chord is even recognised, so a grabbing program receives every scancode including this one. |
|
|
| nothing | the kernel switches, which is the default and right for a plain terminal |
|
|
|
|
`VT_SETMODE` used to be accepted and ignored — a program could ask for the
|
|
chord, be told yes, and still not get it. It works now.
|
|
|
|
What `VT_PROCESS` does **not** do yet is the release handshake: Linux signals
|
|
the owning process on every switch request and blocks until it answers
|
|
`VT_RELDISP`, which is how a display server releases the GPU before the screen
|
|
changes hands. Nothing here has anything to release, so the kernel simply
|
|
stops switching rather than negotiating. `struct vt_mode` already carries
|
|
`relsig`/`acqsig` for when Schcom needs it.
|
|
|
|
Input goes to whichever terminal is on screen. That is what makes a terminal a
|
|
terminal: the keyboard follows the eyes.
|
|
|
|
## Two rules that are easy to get wrong
|
|
|
|
**The grid is the only source of truth for the size.** `TIOCGWINSZ` reports
|
|
`v->cols`/`v->rows`, not a fresh `gfx_cols()`/`gfx_rows()`. When it computed
|
|
its own answer there were two, and they disagreed: the grids are sized at
|
|
`vt_init()`, which runs before the display driver is found on the PCI bus, so
|
|
they were 80x24 while `TIOCGWINSZ` said 160x50. Text stopped two thirds of the
|
|
way down the screen, `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 --
|
|
three symptoms, one disagreement. `vt_resize()` now corrects the grids when a
|
|
scanout arrives, and the ioctl reads them.
|
|
|
|
**A glyph in the last column does not wrap.** It sets `wrap_pending`, and the
|
|
wrap happens when the *next* glyph arrives. Every real terminal behaves this
|
|
way and none of them document it clearly. Wrapping immediately makes a line
|
|
exactly as wide as the screen take two rows instead of one, and makes it
|
|
impossible to write the bottom-right cell without scrolling -- and zsh's
|
|
partial-line marker depends on the rule directly: it writes exactly
|
|
`COLUMNS - 1` spaces and expects the cursor to still be on that line. With
|
|
immediate wrap, a reverse-video `#` appeared above every prompt.
|
|
|
|
Anything that moves the cursor deliberately -- CR, LF, backspace, an escape
|
|
sequence -- clears the debt.
|
|
|
|
## Ctrl-C used to kill the shell
|
|
|
|
Worth its own note, because the cause was one character and the effect was a
|
|
machine you could not use.
|
|
|
|
When Ctrl-C arrives, `consoleread()` discards the partial line and breaks off
|
|
the read in progress. It returned **`-1`** to do so — and `-1` is `-EPERM`. So a
|
|
program that handles `SIGINT` by carrying on saw a permanent error instead of
|
|
an interrupted call, and exited:
|
|
|
|
```
|
|
zsh: error on TTY read: operation not permitted
|
|
schinitd: restarting os.schos.console-shell
|
|
```
|
|
|
|
and then the next Ctrl-C did the same thing to the replacement. A shell that
|
|
cannot survive Ctrl-C is a shell nobody can use, and the supervisor faithfully
|
|
restarting it made it a loop.
|
|
|
|
It returns **`EINTR`** now, which is what a signal-interrupted read is required
|
|
to give: the whole convention — retry, or return to the caller — is built on
|
|
receiving that and not something else.
|
|
|
|
## Editing a line you have already typed
|
|
|
|
Two bugs, both in the same half-second of using a shell, and both worth
|
|
recording because the symptom pointed away from the cause.
|
|
|
|
**Arrow keys corrupted the next two keystrokes.** They arrive from the keyboard
|
|
as single keycodes -- `KEY_UP` is `0xE2` -- and were pushed into the input
|
|
buffer exactly like that. No terminal has ever worked that way, and `0xE2` is
|
|
the worst byte to have picked: it is a UTF-8 lead byte for a three-byte
|
|
character, so a reader decoding UTF-8 swallowed the *next two keys* waiting for
|
|
continuations that never came. `consoleintr()` translates them to the DEC/xterm
|
|
sequences now -- `ESC[A`, `ESC[D`, `ESC[5~` and the rest -- which is what every
|
|
terminfo entry and every line editor already expects.
|
|
|
|
**Then the end of the line vanished as you typed into it.** With the arrows
|
|
working, editing mid-line left the buffer perfectly correct and the screen
|
|
wrong, which is a maddening pair. The cause is one line in `vt_putc()`:
|
|
backspace *erased the cell it moved onto*.
|
|
|
|
That is what a line-discipline erase does. It is not what the BS control
|
|
character does — a program that wants to rub a character out sends `\b \b`,
|
|
backspace, space, backspace, and the separation matters because programs also
|
|
use plain `\b` simply to go back. zsh does, on every mid-line edit:
|
|
|
|
```
|
|
echo alpha \b\b zha \b\b
|
|
```
|
|
|
|
Move back two, rewrite the tail, move back two again to leave the cursor where
|
|
the user expects it. Against a destructive backspace that last pair deleted the
|
|
`ha` it had just drawn.
|
|
|
|
`\b` moves and nothing else now; `0x7f` is ignored, as it is on a real
|
|
terminal; and `consputc()` spells its own erase out as `\b \b`, exactly as it
|
|
already did for the serial line. `ESC[@`, `ESC[P` and `ESC[X` — insert, delete
|
|
and erase characters — were added at the same time, because `TERM=linux`
|
|
declares them and a terminal that drops them silently is the same class of bug
|
|
waiting for a different editor.
|
|
|
|
## What it does not do
|
|
|
|
- **No scrollback.** The grid is exactly the visible screen. Shift+PageUp does
|
|
nothing because there is nothing above the top row to show.
|
|
- **No reflow on resize.** The geometry is read once at `vt_init()`. Nothing
|
|
resizes a framebuffer console today; a compositor will, and that is when this
|
|
has to grow.
|
|
- **No `VT_RELDISP` handshake.** `VT_PROCESS` stops the kernel switching, but
|
|
the kernel does not signal the owning process and wait for an answer. A
|
|
display server that needs to release the GPU *before* the screen changes
|
|
hands will need that, and `struct vt_mode` already has the fields.
|
|
- **No terminal past 6.** Linux allows 63. Six is what the chords reach and
|
|
what the grids are sized for.
|
|
- **Only 240x75 of any screen.** Larger framebuffers work; the grid is clamped
|
|
and the remainder stays background. Raising `VT_MAXCOLS`/`VT_MAXROWS` is the
|
|
fix, and costs BSS.
|
|
- **No UTF-8 the font lacks.** The console draws through the TrueType
|
|
rasteriser now, so the limit is the font's `cmap` rather than a 256-entry
|
|
table — Latin, Latin Extended, Greek, Cyrillic and the common punctuation and
|
|
maths symbols all render. Anything genuinely absent still draws as `?`, which
|
|
is deliberate: drawing a *different* character would be worse.
|
|
- **No shaping, and no wide characters.** One codepoint is one cell of one
|
|
advance. Combining marks land on top of their own cell rather than the
|
|
preceding one, and CJK draws at half the width it wants. Both need a
|
|
width table and a cell that can span two columns.
|
|
- **Typed input above U+07FF is unreliable.** Two-byte UTF-8 arrives intact;
|
|
three-byte sequences typed at the shell come through mangled. The console's
|
|
own decoder is not the problem — the same characters `cat`ed from a file
|
|
render correctly — so this belongs with keyboard and locale handling.
|