mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
tvdos: concurrency and VT
This commit is contained in:
100
CLAUDE.md
100
CLAUDE.md
@@ -436,3 +436,103 @@ The different weights for Mid and Side channels reflect the perceptual importanc
|
||||
- DC frequency underamplification (using 1.0 instead of 4.0/6.0)
|
||||
- Incorrect stereo imaging and extreme side channel distortion
|
||||
- Severe frequency response errors that manifest as "clipping-like" distortion
|
||||
|
||||
## Virtual Consoles (vtmgr)
|
||||
|
||||
Linux-style virtual consoles for TVDOS: up to 6 independent shell sessions,
|
||||
switched with **Alt-1..Alt-6** or the **`chvt N`** builtin, **Alt-0** to exit.
|
||||
Implemented entirely in JS — **no tsvm_core changes**.
|
||||
|
||||
### Architecture
|
||||
|
||||
- **Dispatcher**: `assets/disk0/tvdos/sbin/vtmgr.js`. Launched as the boot shell
|
||||
from `AUTOEXEC.BAT` (replaces the old `fsh` / `command -fancy` tail). Owns the
|
||||
physical keyboard and screen. Each VT runs in its own GraalVM context/thread
|
||||
via the existing `parallel.spawnNewContext` / `attachProgram` / `launch` API
|
||||
(see `VMJSR223Delegate.kt` `class Parallel`). VT 1 spawns at boot; VT 2-6 are
|
||||
lazy-spawned on first switch and re-spawned if their shell exits.
|
||||
- **Concurrency model**: truly concurrent — switching works mid-command, not
|
||||
just at the prompt. Background panes keep running (no `Thread.suspend`; it is
|
||||
unusable on JDK 21). A cooperative gate inside the shimmed `con.getch` parks
|
||||
panes blocked on input; CPU-bound background panes are allowed to run.
|
||||
- **Shared memory**: one `sys.malloc` region holds a control block (active VT,
|
||||
switch request, debounce, spawned-bits) plus, per VT, an input ring buffer and
|
||||
a 7682-byte text-plane buffer mirroring the GPU text-area layout
|
||||
(cursor 2 + fore 2560 + back 2560 + char 2560).
|
||||
- **Compositor** (30 Hz): blits the active VT's text plane to the physical GPU
|
||||
text area via `sys.memcpy`, and pushes that VT's cursor-visibility into the GPU
|
||||
blink bit (MMIO attribute byte 6, addressed at `-1 - (131072*gpuSlot + 6)`).
|
||||
- **Per-pane bootstrap**: each pane re-evals `TVDOS.SYS` (with
|
||||
`_TVDOS_SKIP_AUTOEXEC` + `_TVDOS_IS_VT_PANE` set, and a `_BIOS` stub captured
|
||||
live from the main context) then launches `command -fancy`, all in ONE direct
|
||||
`eval` so the shell launcher shares scope with `_TVDOS`/`files`/`execApp`.
|
||||
The environment (`_TVDOS.variables`: PATH/INCLPATH/HELPPATH/KEYBOARD, fully
|
||||
`$PATH`-expanded) is snapshotted from the main context at vtmgr start and
|
||||
replayed into every pane (env-copy, NOT per-pane AUTOEXEC — AUTOEXEC launches
|
||||
the GUI shell `fsh` which must not run inside a pane). The snapshot is a
|
||||
boot-time baseline; later `set` in one pane does not propagate to others.
|
||||
|
||||
### Output/input shimming (in the pane bootstrap)
|
||||
|
||||
`con` and the global `print`/`println` family are plain JS, so the bootstrap
|
||||
overrides them to read/write the per-VT shared-memory buffers instead of the
|
||||
physical GPU. **`sys` and `graphics` are host objects and CANNOT be overridden
|
||||
from JS** — this is the key constraint that shapes everything below.
|
||||
|
||||
- The shimmed `print` is a faithful JS port of the GPU's TTY interpreter
|
||||
(`GlassTty.acceptChar` + `GraphicsAdapter` handlers): control bytes, the
|
||||
`\x84<decimal>u` "emit char by code" escape (used by `con.prnch`), CSI cursor
|
||||
moves / erase / SGR colours, and the `?25` cursor-visibility private sequence.
|
||||
A swallow-only parser is NOT enough — TVDOS apps drive the screen through
|
||||
these `print` escapes.
|
||||
- `con.move`/`con.getyx` are **1-based** (mirroring `graphics.setCursorYX`'s
|
||||
`cx-1` and `getCursorYX`'s `cx+1`); `con.addch` does NOT advance the cursor
|
||||
(matches `graphics.putSymbol`), while `con.prnch` DOES.
|
||||
- `command.js`'s `shell.execute` reassigns the global print family to
|
||||
`shell.stdio.out.*`, which call `sys.print` (→ physical GPU). `shell.stdio.out`
|
||||
was made to delegate to a `globalThis.__VT_OUT` hook when present (set by the
|
||||
bootstrap); outside a VT the hook is absent and the path is byte-identical.
|
||||
|
||||
### Direct-VRAM apps need a VT-aware base (the `vaddr` pattern)
|
||||
|
||||
Apps that write the text area directly via `graphics.getGpuMemBase()` (rather
|
||||
than `con.*`/`print`) bypass the shims and paint the physical screen, invading
|
||||
whatever VT is visible. They must resolve text-area byte `m` through a
|
||||
VT-aware base:
|
||||
|
||||
```js
|
||||
// physical: backward (byte m at gpuBase - m) — getDev inverts to forward-native
|
||||
// VT pane: forward (byte m at VT_TEXT_PLANE + m, the pane buffer the compositor blits)
|
||||
const VT = (typeof globalThis.VT_TEXT_PLANE !== 'undefined')
|
||||
const VRAM_BASE = VT ? globalThis.VT_TEXT_PLANE : (graphics.getGpuMemBase() - 253950)
|
||||
const VRAM_SGN = VT ? 1 : -1
|
||||
function vaddr(m) { return VRAM_BASE + VRAM_SGN * m }
|
||||
```
|
||||
|
||||
`sys.memcpy`/`sys.pokeBytes` copy forward in the resolved native memory, so this
|
||||
works for both directions. The physical branch is identical to the original
|
||||
arithmetic (no regression outside vtmgr). Applied so far in
|
||||
`assets/disk0/tvdos/bin/taut.js` and `assets/disk0/hopper/include/aa.mjs`
|
||||
(used by `bb.js`). Any future direct-VRAM app needs the same one-line `vaddr`.
|
||||
|
||||
### Files
|
||||
|
||||
- New: `assets/disk0/tvdos/sbin/vtmgr.js` (dispatcher + per-pane bootstrap)
|
||||
- `assets/disk0/tvdos/bin/command.js`: `chvt` builtin, `[N]` prompt prefix for
|
||||
VT 2-6, `shell.stdio.out` → `__VT_OUT` delegation
|
||||
- `assets/disk0/tvdos/TVDOS.SYS`: boot block skips AUTOEXEC when
|
||||
`_TVDOS_SKIP_AUTOEXEC` is set (so pane re-init doesn't recurse)
|
||||
- `assets/disk0/AUTOEXEC.BAT`: boots into `tvdos/sbin/vtmgr`, with
|
||||
`command -fancy` as a fallback once vtmgr exits
|
||||
- `assets/disk0/tvdos/bin/taut.js`, `assets/disk0/hopper/include/aa.mjs`:
|
||||
`vaddr` VT-aware direct-VRAM addressing
|
||||
|
||||
### Gotcha: injectIntChk vs. embedded source
|
||||
|
||||
`execApp`/`require` run a program's source through `injectIntChk` (TVDOS.SYS),
|
||||
which sed-rewrites the **first** `while`/`for`/`do` of each kind to call a
|
||||
per-exec `tvdosSIGTERM_<hash>()` SIGTERM check. When vtmgr embeds the pane
|
||||
bootstrap as a string literal, one of those rewrites can land inside the literal
|
||||
— and the pane context has no such symbol. vtmgr strips them from the bootstrap
|
||||
string with `raw.replace(/tvdosSIGTERM_[A-Za-z0-9_]+\(\);?/g, '')`. Any future
|
||||
code that builds executable source as a string literal must do the same.
|
||||
|
||||
@@ -6,7 +6,15 @@ set INCLPATH=\hopper\include;$INCLPATH
|
||||
set HELPPATH=\hopper\help;$HELPPATH
|
||||
set KEYBOARD=us_colemak
|
||||
|
||||
rem this line specifies which shell to be presented after the boot precess:
|
||||
rem load Korean font / IME (font upload is global hardware)
|
||||
tvdos/i18n/korean
|
||||
zfm
|
||||
|
||||
rem Boot into virtual consoles. vtmgr owns the keyboard and screen, and spawns
|
||||
rem a `command -fancy` shell per VT (Alt-1..6 / chvt to switch, Alt-0 to exit).
|
||||
rem It snapshots the environment set above and replays it into every pane.
|
||||
rem NOTE: `fsh` is a graphical shell and must not run inside a VT pane; launch
|
||||
rem it directly (not via vtmgr) if you want it. (Old boot line: fsh)
|
||||
tvdos/sbin/vtmgr
|
||||
|
||||
rem Fallback shell once vtmgr exits (Alt-0), so the console is never left bare.
|
||||
command -fancy
|
||||
|
||||
@@ -1471,9 +1471,17 @@ try {
|
||||
serial.println("Warning: Could not load HSDPA driver: " + e.message)
|
||||
}
|
||||
|
||||
// Boot script
|
||||
serial.println(`TVDOS.SYS initialised on VM ${sys.getVmId()}, running boot script...`);
|
||||
// Boot script. When vtmgr re-evaluates TVDOS.SYS inside a per-VT pane
|
||||
// context, the pane already has a SKIP flag set so we don't recursively
|
||||
// kick off AUTOEXEC.BAT (which would itself invoke command -fancy and
|
||||
// nest a shell underneath vtmgr).
|
||||
if (typeof _TVDOS_SKIP_AUTOEXEC === "undefined" || !_TVDOS_SKIP_AUTOEXEC) {
|
||||
serial.println(`TVDOS.SYS initialised on VM ${sys.getVmId()}, running boot script...`);
|
||||
|
||||
let cmdfile = files.open("A:/tvdos/bin/command.js")
|
||||
eval(`var _AUTOEXEC=function(exec_args){${cmdfile.sread()}\n};` +
|
||||
`_AUTOEXEC`)(["", "-c", "\\AUTOEXEC.BAT"])
|
||||
let cmdfile = files.open("A:/tvdos/bin/command.js")
|
||||
eval(`var _AUTOEXEC=function(exec_args){${cmdfile.sread()}\n};` +
|
||||
`_AUTOEXEC`)(["", "-c", "\\AUTOEXEC.BAT"])
|
||||
}
|
||||
else {
|
||||
serial.println(`TVDOS.SYS re-initialised in VT pane on VM ${sys.getVmId()}`);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,18 @@ function makeHash() {
|
||||
const shellID = makeHash()
|
||||
|
||||
function print_prompt_text() {
|
||||
// VT pane indicator: shown for VT 2..6, not VT 1 (the default) so the
|
||||
// unmodified prompt is what users see when they never touch virtual
|
||||
// consoles. VT_NUM is set by vtmgr's pane bootstrap.
|
||||
let vtPrefix = ""
|
||||
if (typeof VT_NUM !== "undefined" && VT_NUM > 1) vtPrefix = "[" + VT_NUM + "] "
|
||||
if (goFancy) {
|
||||
if (vtPrefix) {
|
||||
con.color_pair(161,253)
|
||||
print(`\u00DD${VT_NUM}`)
|
||||
con.color_pair(253,161)
|
||||
con.addch(16);con.curs_right()
|
||||
}
|
||||
con.color_pair(239,161)
|
||||
print(" "+CURRENT_DRIVE+":")
|
||||
con.color_pair(161,253)
|
||||
@@ -49,9 +60,9 @@ function print_prompt_text() {
|
||||
else {
|
||||
// con.color_pair(253,255)
|
||||
if (errorlevel != 0 && errorlevel != "undefined" && errorlevel != undefined)
|
||||
print(CURRENT_DRIVE + ":\\" + shell_pwd.join("\\") + " [" + errorlevel + "]" + PROMPT_TEXT)
|
||||
print(vtPrefix + CURRENT_DRIVE + ":\\" + shell_pwd.join("\\") + " [" + errorlevel + "]" + PROMPT_TEXT)
|
||||
else
|
||||
print(CURRENT_DRIVE + ":\\" + shell_pwd.join("\\") + PROMPT_TEXT)
|
||||
print(vtPrefix + CURRENT_DRIVE + ":\\" + shell_pwd.join("\\") + PROMPT_TEXT)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,6 +631,21 @@ shell.coreutils = {
|
||||
},
|
||||
panic: function(args) {
|
||||
throw Error("Panicking command.js")
|
||||
},
|
||||
chvt: function(args) {
|
||||
// Request a switch to another virtual console. Only meaningful when
|
||||
// running inside a pane spawned by vtmgr (VT_CTRL_ADDR is set by the
|
||||
// pane bootstrap). Outside that environment this is a no-op error.
|
||||
if (args[1] === undefined) { printerrln("Usage: chvt N (1..6)"); return 1 }
|
||||
let n = parseInt(args[1])
|
||||
if (isNaN(n) || n < 1 || n > 6) { printerrln("chvt: N must be in 1..6"); return 1 }
|
||||
if (typeof VT_CTRL_ADDR === "undefined") {
|
||||
printerrln("chvt: not running under vtmgr (no VT context)"); return 1
|
||||
}
|
||||
// CTRL_SWITCH_REQUEST is byte +1 of the shared CTRL area. Dispatcher
|
||||
// picks this up on its next 30 Hz tick and performs the switch.
|
||||
sys.poke(VT_CTRL_ADDR + 1, n)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
// define command aliases here
|
||||
@@ -636,10 +662,14 @@ shell.coreutils.where = shell.coreutils.which
|
||||
Object.freeze(shell.coreutils)
|
||||
shell.stdio = {
|
||||
out: {
|
||||
print: function(s) { sys.print(s) },
|
||||
println: function(s) { if (s === undefined) sys.print("\n"); else sys.print(s+"\n") },
|
||||
printerr: function(s) { sys.print("\x1B[31m"+s+"\x1B[m") },
|
||||
printerrln: function(s) { if (s === undefined) sys.print("\n"); else sys.print("\x1B[31m"+s+"\x1B[m\n") },
|
||||
// When running inside a vtmgr virtual console, __VT_OUT routes output
|
||||
// to the pane's text-plane buffer instead of the physical GPU (which
|
||||
// the compositor would otherwise overwrite). Outside a VT the hook is
|
||||
// absent and these fall through to sys.print exactly as before.
|
||||
print: function(s) { if (globalThis.__VT_OUT) globalThis.__VT_OUT.print(s); else sys.print(s) },
|
||||
println: function(s) { if (globalThis.__VT_OUT) globalThis.__VT_OUT.println(s); else { if (s === undefined) sys.print("\n"); else sys.print(s+"\n") } },
|
||||
printerr: function(s) { if (globalThis.__VT_OUT) globalThis.__VT_OUT.printerr(s); else sys.print("\x1B[31m"+s+"\x1B[m") },
|
||||
printerrln: function(s) { if (globalThis.__VT_OUT) globalThis.__VT_OUT.printerrln(s); else { if (s === undefined) sys.print("\n"); else sys.print("\x1B[31m"+s+"\x1B[m\n") } },
|
||||
},
|
||||
pipe: {
|
||||
print: function(s) { if (shell.getPipe() === undefined) throw Error("No pipe opened"); shell.appendToCurrentPipe(s); },
|
||||
|
||||
@@ -1231,7 +1231,7 @@ function drawSeparators(style) {
|
||||
for (let x = PTNVIEW_OFFSET_X; x < SCRW - 3; x += COLSIZE_TIMELINE_FULL) {
|
||||
for (let y = 0; y < PTNVIEW_HEIGHT+1; y++) {
|
||||
let memOffset = (y+PTNVIEW_OFFSET_Y-2) * SCRW + (x-1)
|
||||
let bgColOffset = GPU_MEM - TEXT_BACK_OFF - memOffset
|
||||
let bgColOffset = vaddr(TEXT_BACK_OFF + memOffset)
|
||||
let oldBgCol = sys.peek(bgColOffset)
|
||||
if (oldBgCol == 255) {
|
||||
sys.poke(bgColOffset, colColumnSep)
|
||||
@@ -1806,6 +1806,17 @@ const TEXT_BACK_OFF = 2 + 2560
|
||||
const TEXT_CHAR_OFF = 2 + 2560 + 2560
|
||||
const TEXT_PLANES = [TEXT_CHAR_OFF, TEXT_BACK_OFF, TEXT_FORE_OFF]
|
||||
|
||||
// Direct text-VRAM addressing. On real hardware the GPU text area is addressed
|
||||
// backward (byte m at GPU_MEM - m). Under vtmgr's virtual consoles the physical
|
||||
// GPU is owned by the compositor, so direct writes must instead target this
|
||||
// pane's forward text-plane buffer (VT_TEXT_PLANE + m), which the compositor
|
||||
// blits to the screen. vaddr(m) returns the address of text-area byte m for the
|
||||
// current environment; the physical branch is identical to the old arithmetic.
|
||||
const _VT_VRAM = (typeof globalThis.VT_TEXT_PLANE !== 'undefined')
|
||||
const VRAM_BASE = _VT_VRAM ? globalThis.VT_TEXT_PLANE : GPU_MEM
|
||||
const VRAM_SGN = _VT_VRAM ? 1 : -1
|
||||
function vaddr(m) { return VRAM_BASE + VRAM_SGN * m }
|
||||
|
||||
// One scratch strip, reused across shifts
|
||||
const SCRATCH_PTR = sys.malloc(SCRW * PTNVIEW_HEIGHT)
|
||||
|
||||
@@ -1828,8 +1839,8 @@ function shiftPatternArea(dy) {
|
||||
|
||||
for (let p = 0; p < 3; p++) {
|
||||
const chanOff = TEXT_PLANES[p]
|
||||
const srcAddr = GPU_MEM - chanOff - (srcTopY - 1) * SCRW
|
||||
const dstAddr = GPU_MEM - chanOff - (dstTopY - 1) * SCRW
|
||||
const srcAddr = vaddr(chanOff + (srcTopY - 1) * SCRW)
|
||||
const dstAddr = vaddr(chanOff + (dstTopY - 1) * SCRW)
|
||||
sys.memcpy(srcAddr, SCRATCH_PTR, stripBytes)
|
||||
sys.memcpy(SCRATCH_PTR, dstAddr, stripBytes)
|
||||
}
|
||||
@@ -1850,9 +1861,9 @@ function shiftPatternAreaHorizontal(dVoice) {
|
||||
for (let p = 0; p < 3; p++) {
|
||||
const chanOff = TEXT_PLANES[p]
|
||||
for (let vr = 0; vr < PTNVIEW_HEIGHT; vr++) {
|
||||
const rowBase = GPU_MEM - chanOff - (PTNVIEW_OFFSET_Y + vr - 1) * SCRW
|
||||
sys.memcpy(rowBase - srcOff, SCRATCH_PTR, SALVAGE_HORIZ_LEN)
|
||||
sys.memcpy(SCRATCH_PTR, rowBase - dstOff, SALVAGE_HORIZ_LEN)
|
||||
const idxBase = chanOff + (PTNVIEW_OFFSET_Y + vr - 1) * SCRW
|
||||
sys.memcpy(vaddr(idxBase + srcOff), SCRATCH_PTR, SALVAGE_HORIZ_LEN)
|
||||
sys.memcpy(SCRATCH_PTR, vaddr(idxBase + dstOff), SALVAGE_HORIZ_LEN)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2165,9 +2176,9 @@ function shiftOrdersAreaHorizontal(dVoice) {
|
||||
for (let p = 0; p < 3; p++) {
|
||||
const chanOff = TEXT_PLANES[p]
|
||||
for (let vr = 0; vr < PTNVIEW_HEIGHT; vr++) {
|
||||
const rowBase = GPU_MEM - chanOff - (PTNVIEW_OFFSET_Y + vr - 1) * SCRW
|
||||
sys.memcpy(rowBase - srcOff, SCRATCH_PTR, stripWidth)
|
||||
sys.memcpy(SCRATCH_PTR, rowBase - dstOff, stripWidth)
|
||||
const idxBase = chanOff + (PTNVIEW_OFFSET_Y + vr - 1) * SCRW
|
||||
sys.memcpy(vaddr(idxBase + srcOff), SCRATCH_PTR, stripWidth)
|
||||
sys.memcpy(SCRATCH_PTR, vaddr(idxBase + dstOff), stripWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
564
assets/disk0/tvdos/sbin/vtmgr.js
Normal file
564
assets/disk0/tvdos/sbin/vtmgr.js
Normal file
@@ -0,0 +1,564 @@
|
||||
// vtmgr — virtual console manager for TVDOS
|
||||
//
|
||||
// Spawns up to 6 independent shell sessions (virtual consoles), each in its
|
||||
// own parallel GraalVM context with its own thread. Each pane runs a real
|
||||
// `command -fancy` shell. The dispatcher (this file) owns the physical
|
||||
// keyboard, polls Alt-N hotkeys at 30 Hz, blits the active pane's text
|
||||
// plane to the GPU's text area, and routes typed characters into the
|
||||
// active pane's input ring buffer.
|
||||
//
|
||||
// Hotkeys: Alt-1..Alt-6 switch to that VT (lazy-spawn on first use).
|
||||
// Alt-0 cleanly tears down vtmgr.
|
||||
// Builtins: `chvt N` from inside a pane writes to the switch register.
|
||||
|
||||
// ─── shared memory layout ───────────────────────────────────────────────────
|
||||
// CTRL_AREA (64 bytes from base)
|
||||
// +0 active_vt u8 (1..6)
|
||||
// +1 switch_request u8 (0 = none, 1..6 = target; set by chvt, cleared by dispatcher)
|
||||
// +2 debounce_held u8
|
||||
// +3 vt_spawned_bits u8 (bit n-1 set if VT n is alive)
|
||||
// +4..63 reserved
|
||||
// VT block (× MAX_VT) starting at base + 64, each VT_BLOCK_SIZE bytes
|
||||
// +0..7 reserved (cursor & color state lives inside text plane itself)
|
||||
// +8 queue_head u8 (next-read index)
|
||||
// +9 queue_tail u8 (next-write index)
|
||||
// +10..11 reserved
|
||||
// +12..267 queue_data (256-byte ring buffer; one slot lost to full/empty disambiguation)
|
||||
// +268..271 reserved (alignment)
|
||||
// +272..7953 text_plane (7682 bytes; mirrors GPU textArea layout exactly)
|
||||
|
||||
const MAX_VT = 6
|
||||
const CTRL_AREA_SIZE = 64
|
||||
const VT_BLOCK_SIZE = 8000
|
||||
const TEXT_PLANE_OFFSET = 272
|
||||
const TEXT_PLANE_SIZE = 7682
|
||||
const QUEUE_DATA_OFFSET = 12
|
||||
|
||||
const CTRL_ACTIVE_VT = 0
|
||||
const CTRL_SWITCH_REQUEST = 1
|
||||
const CTRL_DEBOUNCE_HELD = 2
|
||||
const CTRL_SPAWNED_BITS = 3
|
||||
|
||||
const GPU_TEXTAREA_OFFSET = 253950
|
||||
const TEXT_COLS = 80
|
||||
const TEXT_ROWS = 32
|
||||
|
||||
const TP_FORE_BASE = 2
|
||||
const TP_BACK_BASE = 2 + 2560
|
||||
const TP_TEXT_BASE = 2 + 2560 + 2560
|
||||
|
||||
const TOTAL_ALLOC_SIZE = CTRL_AREA_SIZE + MAX_VT * VT_BLOCK_SIZE
|
||||
const BASE = sys.malloc(TOTAL_ALLOC_SIZE)
|
||||
if (!BASE || BASE === 0) { printerrln("vtmgr: sys.malloc failed"); return 1 }
|
||||
for (let i = 0; i < TOTAL_ALLOC_SIZE; i++) sys.poke(BASE + i, 0)
|
||||
|
||||
const CTRL = BASE
|
||||
function vtBlockAddr(n) { return BASE + CTRL_AREA_SIZE + (n - 1) * VT_BLOCK_SIZE }
|
||||
function vtTextPlaneAddr(n) { return vtBlockAddr(n) + TEXT_PLANE_OFFSET }
|
||||
|
||||
// ─── pane bootstrap ─────────────────────────────────────────────────────────
|
||||
// Read TVDOS.SYS once at startup. Each pane's bootstrap embeds the source
|
||||
// (via JSON.stringify-escaped string literal) and evaluates it together with
|
||||
// the shell-start code as ONE direct-eval call. This matters because strict-
|
||||
// mode direct eval is scope-isolated; if TVDOS.SYS and the shell launcher
|
||||
// were two separate evals, the shell launcher wouldn't see `_TVDOS`,
|
||||
// `files`, `execApp`, etc. defined by the first eval.
|
||||
|
||||
const TVDOS_SYS_SRC = files.open("A:/tvdos/TVDOS.SYS").sread()
|
||||
|
||||
// _BIOS is set by the real BIOS before TVDOS.SYS runs; TVDOS.SYS reads
|
||||
// _BIOS.FIRST_BOOTABLE_PORT during init. Each pane is a fresh context with no
|
||||
// BIOS, so capture the live value here (vtmgr runs in the main context where
|
||||
// _BIOS is visible) and re-declare it in every pane bootstrap.
|
||||
const BIOS_FIRST_BOOTABLE_PORT = JSON.stringify(_BIOS.FIRST_BOOTABLE_PORT)
|
||||
|
||||
// Snapshot the live environment from the main context. vtmgr runs after
|
||||
// AUTOEXEC.BAT, so _TVDOS.variables already holds the fully expanded PATH,
|
||||
// INCLPATH, HELPPATH, KEYBOARD, etc. Each pane is a fresh context whose
|
||||
// TVDOS.SYS only sets the bare defaults, so we replay this snapshot over the
|
||||
// pane's defaults — giving panes the same path/variable resolution as the
|
||||
// boot shell without re-running AUTOEXEC (which would relaunch the GUI shell).
|
||||
const ENV_JSON = JSON.stringify(_TVDOS.variables)
|
||||
|
||||
function makePaneBootstrap(vtNum) {
|
||||
const TP_BASE = vtTextPlaneAddr(vtNum)
|
||||
const VT_BLK = vtBlockAddr(vtNum)
|
||||
|
||||
// Shell-launcher code runs after TVDOS.SYS in the SAME eval scope, so
|
||||
// `files`, `eval`, `_TVDOS` etc. resolve via lexical closure. Apply the
|
||||
// captured environment before launching the shell.
|
||||
const SHELL_START = ";\n"
|
||||
+ "Object.assign(_TVDOS.variables, " + ENV_JSON + ");\n"
|
||||
+ "var _cmdfileSrc = files.open('A:/tvdos/bin/command.js').sread();\n"
|
||||
+ "eval('var _VTSHELL=function(exec_args){' + _cmdfileSrc + '\\n};_VTSHELL')(['', '-fancy']);\n"
|
||||
|
||||
const combined = TVDOS_SYS_SRC + SHELL_START
|
||||
|
||||
const raw = `
|
||||
globalThis.VT_NUM = ${vtNum}
|
||||
globalThis.VT_TEXT_PLANE = ${TP_BASE}
|
||||
globalThis.VT_BLOCK_ADDR = ${VT_BLK}
|
||||
globalThis.VT_CTRL_ADDR = ${CTRL}
|
||||
const TP = ${TP_BASE}
|
||||
const VT_BLK = ${VT_BLK}
|
||||
const CTRL = ${CTRL}
|
||||
const QUEUE_DATA = VT_BLK + ${QUEUE_DATA_OFFSET}
|
||||
const QUEUE_HEAD_ADDR = VT_BLK + 8
|
||||
const QUEUE_TAIL_ADDR = VT_BLK + 9
|
||||
const ACTIVE_VT_ADDR = CTRL + ${CTRL_ACTIVE_VT}
|
||||
const COLS = ${TEXT_COLS}, ROWS = ${TEXT_ROWS}
|
||||
const FORE_BASE = ${TP_FORE_BASE}, BACK_BASE = ${TP_BACK_BASE}, TEXT_BASE = ${TP_TEXT_BASE}
|
||||
|
||||
// ── output shims (write into the per-VT text-plane buffer in shared mem) ──
|
||||
// This is a faithful JS port of the GPU's TTY interpreter (GlassTty.acceptChar
|
||||
// + GraphicsAdapter handlers). TVDOS apps drive the screen by printing control
|
||||
// bytes and escape sequences through print(), so the shim must interpret them
|
||||
// exactly as the hardware would: the \\x84<decimal>u "emit char by code" escape
|
||||
// (used by con.prnch), CSI cursor moves / erase / SGR colours, and the ?25
|
||||
// cursor-visibility private sequence.
|
||||
let curX = 0, curY = 0
|
||||
let foreCol = 254
|
||||
let backCol = 255
|
||||
|
||||
// Per-pane cursor visibility lives at VT_BLK+2 (1 = blink on, 0 = hidden).
|
||||
// The compositor pushes the active pane's value into the GPU's blink bit.
|
||||
const CURSOR_VIS_ADDR = VT_BLK + 2
|
||||
sys.poke(CURSOR_VIS_ADDR, 1)
|
||||
|
||||
// SGR 30-37 / 40-47 → default 8-colour palette (matches GraphicsAdapter).
|
||||
const SGR_PAL = [240, 211, 61, 230, 49, 219, 114, 254]
|
||||
|
||||
function writeCursor() {
|
||||
let pos = curY * COLS + curX
|
||||
sys.poke(TP + 0, pos & 0xFF)
|
||||
sys.poke(TP + 1, (pos >> 8) & 0xFF)
|
||||
}
|
||||
function scrollBufUp(n) {
|
||||
if (n < 1) n = 1
|
||||
if (n > ROWS) n = ROWS
|
||||
for (let p of [FORE_BASE, BACK_BASE, TEXT_BASE]) {
|
||||
for (let y = 0; y < ROWS - n; y++) {
|
||||
for (let x = 0; x < COLS; x++) {
|
||||
sys.poke(TP + p + y * COLS + x, sys.peek(TP + p + (y + n) * COLS + x))
|
||||
}
|
||||
}
|
||||
let clearVal = (p === TEXT_BASE) ? 0 : (p === FORE_BASE ? foreCol : backCol)
|
||||
for (let y = ROWS - n; y < ROWS; y++)
|
||||
for (let x = 0; x < COLS; x++) sys.poke(TP + p + y * COLS + x, clearVal)
|
||||
}
|
||||
}
|
||||
function putCharRaw(x, y, c) {
|
||||
if (x < 0 || x >= COLS || y < 0 || y >= ROWS) return
|
||||
let off = y * COLS + x
|
||||
sys.poke(TP + TEXT_BASE + off, c & 0xFF)
|
||||
sys.poke(TP + FORE_BASE + off, foreCol)
|
||||
sys.poke(TP + BACK_BASE + off, backCol)
|
||||
}
|
||||
// Mirror of GraphicsAdapter.setCursorPos: wrap on overflow x, scroll on
|
||||
// overflow y, clamp y above the screen.
|
||||
function setCursorPos(x, y) {
|
||||
let nx = x, ny = y
|
||||
if (nx >= COLS) { nx = 0; ny += 1 }
|
||||
else if (nx < 0) nx = 0
|
||||
if (ny < 0) ny = 0
|
||||
else if (ny >= ROWS) { scrollBufUp(ny - ROWS + 1); ny = ROWS - 1 }
|
||||
curX = nx; curY = ny
|
||||
writeCursor()
|
||||
}
|
||||
|
||||
// ── TTY actions (mirror the GraphicsAdapter overrides) ────────────────────
|
||||
function ttyPrintable(c) { putCharRaw(curX, curY, c); setCursorPos(curX + 1, curY) }
|
||||
function ttyCrlf() {
|
||||
let ny = curY + 1
|
||||
setCursorPos(0, (ny >= ROWS) ? ROWS - 1 : ny)
|
||||
if (ny >= ROWS) scrollBufUp(1)
|
||||
}
|
||||
function ttyBackspace() { let x = curX, y = curY; setCursorPos(x - 1, y); putCharRaw(curX, curY, 0x20) }
|
||||
function ttyTab() { setCursorPos(((curX / 8 | 0) + 1) * 8, curY) }
|
||||
function ttyResetStatus() { foreCol = 253; backCol = 255 }
|
||||
function ttyEmitChar(code) { putCharRaw(curX, curY, code); setCursorPos(curX + 1, curY) }
|
||||
function ttyCursorUp(n) { setCursorPos(curX, curY - n) }
|
||||
function ttyCursorDown(n) { let ny = curY + n; setCursorPos(curX, (ny >= ROWS) ? ROWS - 1 : ny) }
|
||||
function ttyCursorFwd(n) { setCursorPos(curX + n, curY) }
|
||||
function ttyCursorBack(n) { setCursorPos(curX - n, curY) }
|
||||
function ttyCursorNextLine(n) { let ny = curY + n; setCursorPos(0, (ny >= ROWS) ? ROWS - 1 : ny); if (ny >= ROWS) scrollBufUp(ny - ROWS + 1) }
|
||||
function ttyCursorPrevLine(n) { setCursorPos(0, curY - n) }
|
||||
function ttyCursorX(n) { setCursorPos(n, curY) }
|
||||
function ttyCursorXY(row, col) { setCursorPos(col - 1, row - 1) }
|
||||
function ttyEraseInDisp(arg) {
|
||||
if (arg === 2) {
|
||||
for (let i = 0; i < COLS * ROWS; i++) {
|
||||
sys.poke(TP + TEXT_BASE + i, 0)
|
||||
sys.poke(TP + FORE_BASE + i, foreCol)
|
||||
sys.poke(TP + BACK_BASE + i, backCol)
|
||||
}
|
||||
curX = 0; curY = 0; writeCursor()
|
||||
}
|
||||
// other args: GraphicsAdapter TODOs (throws); we no-op for safety
|
||||
}
|
||||
function ttySgr1(arg) {
|
||||
if (arg >= 30 && arg <= 37) foreCol = SGR_PAL[arg - 30]
|
||||
else if (arg >= 40 && arg <= 47) backCol = SGR_PAL[arg - 40]
|
||||
else if (arg === 7) { let t = foreCol; foreCol = backCol; backCol = t }
|
||||
else if (arg === 0) { foreCol = 253; backCol = 255; sys.poke(CURSOR_VIS_ADDR, 1) }
|
||||
}
|
||||
function ttySgr3(a1, a2, a3) {
|
||||
if (a1 === 38 && a2 === 5) foreCol = a3
|
||||
else if (a1 === 48 && a2 === 5) backCol = a3
|
||||
}
|
||||
function ttyPrivH(arg) { if (arg === 25) sys.poke(CURSOR_VIS_ADDR, 1) }
|
||||
function ttyPrivL(arg) { if (arg === 25) sys.poke(CURSOR_VIS_ADDR, 0) }
|
||||
|
||||
// ── escape-sequence state machine (mirror of GlassTty.acceptChar) ─────────
|
||||
// States: 0 INITIAL, 1 ESC, 2 CSI, 3 NUM1, 4 SEP1, 5 NUM2, 6 SEP2, 7 NUM3,
|
||||
// 8 PRIVATESEQ, 9 PRIVATENUM, 10 XCSI, 11 XNUM1
|
||||
let escState = 0
|
||||
let escArgs = []
|
||||
function isDig(c) { return c >= 0x30 && c <= 0x39 }
|
||||
function escReset() { escState = 0; escArgs.length = 0 }
|
||||
// reject() in hardware returns the char as printable; replicate by printing it
|
||||
function escRejectPrint(c) { escReset(); ttyPrintable(c) }
|
||||
|
||||
function processByte(c) {
|
||||
switch (escState) {
|
||||
case 0: // INITIAL
|
||||
if (c === 0x1B) escState = 1
|
||||
else if (c === 0x84) escState = 10
|
||||
else if (c === 0x0A) ttyCrlf()
|
||||
else if (c === 0x08) ttyBackspace()
|
||||
else if (c === 0x09) ttyTab()
|
||||
else if (c === 0x07) { /* bell */ }
|
||||
else if (c >= 0x00 && c <= 0x1F) { /* other control: ignored */ }
|
||||
else ttyPrintable(c)
|
||||
break
|
||||
case 1: // ESC
|
||||
if (c === 0x63) { ttyResetStatus(); escReset() } // 'c'
|
||||
else if (c === 0x5B) escState = 2 // '['
|
||||
else escRejectPrint(c)
|
||||
break
|
||||
case 2: // CSI
|
||||
if (c === 0x41) { ttyCursorUp(1); escReset() }
|
||||
else if (c === 0x42) { ttyCursorDown(1); escReset() }
|
||||
else if (c === 0x43) { ttyCursorFwd(1); escReset() }
|
||||
else if (c === 0x44) { ttyCursorBack(1); escReset() }
|
||||
else if (c === 0x45) { ttyCursorNextLine(1); escReset() }
|
||||
else if (c === 0x46) { ttyCursorPrevLine(1); escReset() }
|
||||
else if (c === 0x47) { ttyCursorX(1); escReset() }
|
||||
else if (c === 0x4A) { ttyEraseInDisp(0); escReset() }
|
||||
else if (c === 0x4B) { escReset() } // eraseInLine: no-op
|
||||
else if (c === 0x53) { scrollBufUp(1); escReset() } // S
|
||||
else if (c === 0x54) { escReset() } // T scrollDown: no-op
|
||||
else if (c === 0x6D) { ttySgr1(0); escReset() } // m
|
||||
else if (c === 0x3F) escState = 8 // '?'
|
||||
else if (c === 0x3B) { escArgs.push(0); escState = 4 } // ';'
|
||||
else if (isDig(c)) { escArgs.push(c - 0x30); escState = 3 }
|
||||
else escRejectPrint(c)
|
||||
break
|
||||
case 3: // NUM1
|
||||
if (c === 0x41) { ttyCursorUp(escArgs.pop()); escReset() }
|
||||
else if (c === 0x42) { ttyCursorDown(escArgs.pop()); escReset() }
|
||||
else if (c === 0x43) { ttyCursorFwd(escArgs.pop()); escReset() }
|
||||
else if (c === 0x44) { ttyCursorBack(escArgs.pop()); escReset() }
|
||||
else if (c === 0x45) { ttyCursorNextLine(escArgs.pop()); escReset() }
|
||||
else if (c === 0x46) { ttyCursorPrevLine(escArgs.pop()); escReset() }
|
||||
else if (c === 0x47) { ttyCursorX(escArgs.pop()); escReset() }
|
||||
else if (c === 0x4A) { ttyEraseInDisp(escArgs.pop()); escReset() }
|
||||
else if (c === 0x4B) { escArgs.pop(); escReset() }
|
||||
else if (c === 0x53) { scrollBufUp(escArgs.pop()); escReset() }
|
||||
else if (c === 0x54) { escArgs.pop(); escReset() }
|
||||
else if (c === 0x6D) { ttySgr1(escArgs.pop()); escReset() }
|
||||
else if (c === 0x3B) escState = 4
|
||||
else if (isDig(c)) escArgs.push(escArgs.pop() * 10 + (c - 0x30))
|
||||
else escRejectPrint(c)
|
||||
break
|
||||
case 4: // SEP1 (seen "n;")
|
||||
if (isDig(c)) { escArgs.push(c - 0x30); escState = 5 }
|
||||
else if (c === 0x48) { let a1 = escArgs.pop(); ttyCursorXY(a1, 0); escReset() } // H
|
||||
else if (c === 0x6D) { ttySgr1(escArgs.pop()); escReset() } // m (2-arg unimpl in HW)
|
||||
else if (c === 0x3B) { escArgs.push(0); escState = 6 }
|
||||
else escRejectPrint(c)
|
||||
break
|
||||
case 5: // NUM2 (seen "n;n")
|
||||
if (isDig(c)) escArgs.push(escArgs.pop() * 10 + (c - 0x30))
|
||||
else if (c === 0x48) { let a2 = escArgs.pop(), a1 = escArgs.pop(); ttyCursorXY(a1, a2); escReset() }
|
||||
else if (c === 0x6D) { escArgs.pop(); escArgs.pop(); escReset() } // 2-arg SGR unimpl in HW
|
||||
else if (c === 0x3B) escState = 6
|
||||
else escRejectPrint(c)
|
||||
break
|
||||
case 6: // SEP2 (seen "n;n;")
|
||||
if (c === 0x6D) { let a2 = escArgs.pop(), a1 = escArgs.pop(); ttySgr3(a1, a2, 0); escReset() }
|
||||
else if (isDig(c)) { escArgs.push(c - 0x30); escState = 7 }
|
||||
else escRejectPrint(c)
|
||||
break
|
||||
case 7: // NUM3 (seen "n;n;n")
|
||||
if (isDig(c)) escArgs.push(escArgs.pop() * 10 + (c - 0x30))
|
||||
else if (c === 0x6D) { let a3 = escArgs.pop(), a2 = escArgs.pop(), a1 = escArgs.pop(); ttySgr3(a1, a2, a3); escReset() }
|
||||
else escRejectPrint(c)
|
||||
break
|
||||
case 8: // PRIVATESEQ (seen "?")
|
||||
if (isDig(c)) { escArgs.push(c - 0x30); escState = 9 }
|
||||
else escRejectPrint(c)
|
||||
break
|
||||
case 9: // PRIVATENUM (seen "?n")
|
||||
if (c === 0x68) { ttyPrivH(escArgs.pop()); escReset() } // h
|
||||
else if (c === 0x6C) { ttyPrivL(escArgs.pop()); escReset() } // l
|
||||
else if (isDig(c)) escArgs.push(escArgs.pop() * 10 + (c - 0x30))
|
||||
else escRejectPrint(c)
|
||||
break
|
||||
case 10: // XCSI (seen \\x84)
|
||||
if (c === 0x75) { ttyEmitChar(0); escReset() } // 'u'
|
||||
else if (isDig(c)) { escArgs.push(c - 0x30); escState = 11 }
|
||||
else escRejectPrint(c)
|
||||
break
|
||||
case 11: // XNUM1 (seen \\x84<digits>)
|
||||
if (c === 0x75) { ttyEmitChar(escArgs.pop()); escReset() } // 'u'
|
||||
else if (isDig(c)) escArgs.push(escArgs.pop() * 10 + (c - 0x30))
|
||||
else escRejectPrint(c)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
print = function(s) {
|
||||
if (s === undefined || s === null) return
|
||||
let str = '' + s
|
||||
for (let i = 0; i < str.length; i++) processByte(str.charCodeAt(i))
|
||||
}
|
||||
println = function(s) {
|
||||
if (s === undefined) print("\\n")
|
||||
else print(s + "\\n")
|
||||
}
|
||||
printerr = function(s) { print(s) }
|
||||
printerrln = function(s) { println(s) }
|
||||
|
||||
// command.js's shell.execute reassigns the global print/println/printerr/
|
||||
// printerrln to shell.stdio.out.* (which call sys.print → physical GPU,
|
||||
// bypassing these shims). Expose the buffer writers through a global hook so
|
||||
// shell.stdio.out can delegate to them when running inside a VT pane. The
|
||||
// non-VT path in command.js stays unchanged (hook is undefined there).
|
||||
globalThis.__VT_OUT = { print: print, println: println, printerr: printerr, printerrln: printerrln }
|
||||
|
||||
// con.move / con.getyx are 1-based in TVDOS (graphics.setCursorYX does cx-1,
|
||||
// getCursorYX returns cx+1). Internal curX/curY are 0-based, so convert.
|
||||
con.move = function(y, x) {
|
||||
curY = Math.max(0, Math.min(ROWS - 1, (y | 0) - 1))
|
||||
curX = Math.max(0, Math.min(COLS - 1, (x | 0) - 1))
|
||||
writeCursor()
|
||||
}
|
||||
con.getyx = function() { return [curY + 1, curX + 1] }
|
||||
con.getmaxyx = function() { return [ROWS, COLS] }
|
||||
con.color_pair = function(f, b) { foreCol = f & 0xFF; backCol = b & 0xFF }
|
||||
con.color_fore = function(n) { foreCol = n & 0xFF }
|
||||
con.color_back = function(n) { backCol = n & 0xFF }
|
||||
con.get_color_fore = function() { return foreCol }
|
||||
con.get_color_back = function() { return backCol }
|
||||
// addch writes a glyph at the cursor WITHOUT advancing — matching
|
||||
// graphics.putSymbol(). TVDOS code pairs addch with explicit curs_right();
|
||||
// advancing here would double-step and leave gaps (e.g. the fancy prompt).
|
||||
con.addch = function(c) { putCharRaw(curX, curY, c) }
|
||||
con.mvaddch = function(y, x, c) { con.move(y, x); con.addch(c) }
|
||||
con.curs_up = function(n) { n = n || 1; curY = Math.max(0, curY - n); writeCursor() }
|
||||
con.curs_down = function(n) { n = n || 1; curY = Math.min(ROWS - 1, curY + n); writeCursor() }
|
||||
con.curs_left = function(n) { n = n || 1; curX = Math.max(0, curX - n); writeCursor() }
|
||||
con.curs_right = function(n) { n = n || 1; curX = Math.min(COLS - 1, curX + n); writeCursor() }
|
||||
con.curs_set = function(arg) { sys.poke(CURSOR_VIS_ADDR, ((arg | 0) === 0) ? 0 : 1) }
|
||||
con.video_reverse = function() { /* unsupported; ANSI swallowed */ }
|
||||
con.reset_graphics = function() { foreCol = 254; backCol = 255 }
|
||||
con.clear = function() {
|
||||
for (let i = 0; i < COLS * ROWS; i++) {
|
||||
sys.poke(TP + TEXT_BASE + i, 0)
|
||||
sys.poke(TP + FORE_BASE + i, foreCol)
|
||||
sys.poke(TP + BACK_BASE + i, backCol)
|
||||
}
|
||||
curX = 0; curY = 0; writeCursor()
|
||||
}
|
||||
// prnch prints a glyph and DOES advance (unlike addch) — the real impl emits
|
||||
// it through print() as \\x84<code>u, so route it through the interpreter.
|
||||
con.prnch = function(c) {
|
||||
if (Array.isArray(c)) c.forEach(x => ttyEmitChar(x))
|
||||
else ttyEmitChar(c)
|
||||
}
|
||||
|
||||
// ── input shims ──────────────────────────────────────────────────────────
|
||||
// Pane reads from its own ring buffer in shared mem. NEVER touches physical
|
||||
// keyboard MMIO — that's the dispatcher's exclusive territory. Cooperative
|
||||
// gate on active_vt keeps background panes parked when they call getch.
|
||||
|
||||
function queuePop() {
|
||||
let head = sys.peek(QUEUE_HEAD_ADDR)
|
||||
let tail = sys.peek(QUEUE_TAIL_ADDR)
|
||||
if (head === tail) return -1
|
||||
let b = sys.peek(QUEUE_DATA + head)
|
||||
sys.poke(QUEUE_HEAD_ADDR, (head + 1) & 0xFF)
|
||||
return b
|
||||
}
|
||||
con.getch = function() {
|
||||
while (true) {
|
||||
if (sys.peek(ACTIVE_VT_ADDR) === VT_NUM) {
|
||||
let k = queuePop()
|
||||
if (k >= 0) return k
|
||||
}
|
||||
sys.sleep(20)
|
||||
}
|
||||
}
|
||||
con.hitterminate = function() { return false }
|
||||
con.hiteof = function() { return false }
|
||||
con.resetkeybuf = function() { sys.poke(QUEUE_HEAD_ADDR, sys.peek(QUEUE_TAIL_ADDR)) }
|
||||
con.poll_keys = function() { return [0,0,0,0,0,0,0,0] }
|
||||
|
||||
// ── TVDOS.SYS init flags + BIOS stub ───────────────────────────────────────
|
||||
globalThis._TVDOS_IS_VT_PANE = true
|
||||
globalThis._TVDOS_SKIP_AUTOEXEC = true
|
||||
globalThis._BIOS = { FIRST_BOOTABLE_PORT: ${BIOS_FIRST_BOOTABLE_PORT} }
|
||||
|
||||
// ── load TVDOS.SYS and start command -fancy in one direct-eval call ─────
|
||||
// Strict-mode direct eval is scope-isolated, so TVDOS.SYS's \`const _TVDOS\`
|
||||
// only survives within the eval scope. The shell launcher must run inside
|
||||
// the same eval to access it (via lexical closure into nested evals).
|
||||
eval(${JSON.stringify(combined)})
|
||||
`
|
||||
// The outer execApp's injectIntChk rewrote the first while/for/do (each
|
||||
// kind) in our literal source to call a per-exec SIGTERM check function.
|
||||
// Some of those rewrites landed inside this template literal — the pane
|
||||
// has no such symbol in scope. Strip them; panes don't need SIGTERM
|
||||
// checks (parallel.kill handles teardown).
|
||||
return raw.replace(/tvdosSIGTERM_[A-Za-z0-9_]+\(\);?/g, '')
|
||||
}
|
||||
|
||||
// ─── pane lifecycle ─────────────────────────────────────────────────────────
|
||||
// Lazy spawn: VT 1 at boot; VT 2-6 the first time the user requests them.
|
||||
// Re-spawn if the previous pane's thread has died (e.g. user typed `exit`).
|
||||
|
||||
const panes = {} // n -> { runner, thread }
|
||||
|
||||
function isPaneAlive(n) {
|
||||
return panes[n] && parallel.isRunning(panes[n].thread)
|
||||
}
|
||||
|
||||
function spawnPane(n) {
|
||||
serial.println("[vtmgr] spawning VT " + n)
|
||||
let runner = parallel.spawnNewContext()
|
||||
let thread = parallel.attachProgram("vt" + n, runner, makePaneBootstrap(n))
|
||||
parallel.launch(thread)
|
||||
panes[n] = { runner: runner, thread: thread }
|
||||
sys.poke(CTRL + CTRL_SPAWNED_BITS, sys.peek(CTRL + CTRL_SPAWNED_BITS) | (1 << (n - 1)))
|
||||
}
|
||||
|
||||
function ensurePane(n) {
|
||||
if (!isPaneAlive(n)) {
|
||||
sys.poke(CTRL + CTRL_SPAWNED_BITS, sys.peek(CTRL + CTRL_SPAWNED_BITS) & ~(1 << (n - 1)))
|
||||
spawnPane(n)
|
||||
}
|
||||
}
|
||||
|
||||
ensurePane(1)
|
||||
sys.poke(CTRL + CTRL_ACTIVE_VT, 1)
|
||||
// VT 1's TVDOS.SYS eval is slow; give it room before we start compositing.
|
||||
sys.sleep(800)
|
||||
|
||||
// ─── compositor / dispatcher loop ───────────────────────────────────────────
|
||||
// 30 Hz: blit active pane → GPU text area; honour switch_request; detect
|
||||
// Alt-N with debounce; drain typed chars into active pane's queue.
|
||||
|
||||
const gpuBase = graphics.getGpuMemBase()
|
||||
const TEXTAREA_BASE_ABS = gpuBase - GPU_TEXTAREA_OFFSET
|
||||
function blitVt(srcAddr) {
|
||||
sys.memcpy(srcAddr, TEXTAREA_BASE_ABS, TEXT_PLANE_SIZE - 2)
|
||||
sys.poke(TEXTAREA_BASE_ABS - (TEXT_PLANE_SIZE - 2), sys.peek(srcAddr + TEXT_PLANE_SIZE - 2))
|
||||
sys.poke(TEXTAREA_BASE_ABS - (TEXT_PLANE_SIZE - 1), sys.peek(srcAddr + TEXT_PLANE_SIZE - 1))
|
||||
}
|
||||
|
||||
// GPU textmode-attribute MMIO byte (offset 6): bit 0 = blinkCursor, bit 1 =
|
||||
// rawMode, bits 4-7 = chrrom. We flip only bit 0 to match the active pane's
|
||||
// cursor visibility. getGpuMemBase() = -1 - 1MB*slot; the peripheral's MMIO
|
||||
// window sits at IOSpace offset 128KB*slot, so MMIO byte k = -1 - (128KB*slot + k).
|
||||
const gpuSlot = (((-gpuBase) - 1) / 1048576) | 0
|
||||
const GPU_MMIO_ATTR = -1 - (131072 * gpuSlot + 6)
|
||||
let lastCursorVis = -1
|
||||
function applyCursorVis(active) {
|
||||
let vis = sys.peek(vtBlockAddr(active) + 2)
|
||||
if (vis === lastCursorVis) return
|
||||
let attr = sys.peek(GPU_MMIO_ATTR)
|
||||
sys.poke(GPU_MMIO_ATTR, vis ? (attr | 1) : (attr & 0xFE))
|
||||
lastCursorVis = vis
|
||||
}
|
||||
|
||||
function queuePush(vtN, byte) {
|
||||
let qBase = vtBlockAddr(vtN)
|
||||
let head = sys.peek(qBase + 8)
|
||||
let tail = sys.peek(qBase + 9)
|
||||
let next = (tail + 1) & 0xFF
|
||||
if (next === head) return false
|
||||
sys.poke(qBase + QUEUE_DATA_OFFSET + tail, byte)
|
||||
sys.poke(qBase + 9, next)
|
||||
return true
|
||||
}
|
||||
|
||||
function switchTo(n) {
|
||||
if (n < 1 || n > MAX_VT) return
|
||||
ensurePane(n)
|
||||
sys.poke(CTRL + CTRL_ACTIVE_VT, n)
|
||||
}
|
||||
|
||||
sys.poke(-39, 1) // enable physical keyboard input collection
|
||||
|
||||
let running = true
|
||||
while (running) {
|
||||
let active = sys.peek(CTRL + CTRL_ACTIVE_VT)
|
||||
if (active < 1 || active > MAX_VT) active = 1
|
||||
blitVt(vtTextPlaneAddr(active))
|
||||
applyCursorVis(active)
|
||||
|
||||
// honour chvt's switch request
|
||||
let req = sys.peek(CTRL + CTRL_SWITCH_REQUEST)
|
||||
if (req >= 1 && req <= MAX_VT) {
|
||||
if (req !== active) {
|
||||
serial.println("[vtmgr] chvt switch -> VT " + req)
|
||||
switchTo(req)
|
||||
}
|
||||
sys.poke(CTRL + CTRL_SWITCH_REQUEST, 0)
|
||||
}
|
||||
|
||||
// Alt-N (and Alt-0 = exit) detection
|
||||
sys.poke(-40, 1)
|
||||
let keys = [sys.peek(-41), sys.peek(-42), sys.peek(-43), sys.peek(-44),
|
||||
sys.peek(-45), sys.peek(-46), sys.peek(-47), sys.peek(-48)]
|
||||
let altHeld = keys.indexOf(57) >= 0 || keys.indexOf(58) >= 0
|
||||
let digit = -1
|
||||
for (let n = 0; n <= MAX_VT; n++) {
|
||||
if (keys.indexOf(7 + n) >= 0) { digit = n; break }
|
||||
}
|
||||
let debounce = sys.peek(CTRL + CTRL_DEBOUNCE_HELD) !== 0
|
||||
|
||||
if (debounce) {
|
||||
if (!altHeld && digit < 0) sys.poke(CTRL + CTRL_DEBOUNCE_HELD, 0)
|
||||
}
|
||||
else if (altHeld && digit === 0) {
|
||||
serial.println("[vtmgr] Alt-0 -> exit")
|
||||
running = false
|
||||
sys.poke(CTRL + CTRL_DEBOUNCE_HELD, 1)
|
||||
sys.poke(-39, 1)
|
||||
}
|
||||
else if (altHeld && digit >= 1) {
|
||||
serial.println("[vtmgr] Alt-" + digit + " -> switching to VT " + digit)
|
||||
switchTo(digit)
|
||||
sys.poke(CTRL + CTRL_DEBOUNCE_HELD, 1)
|
||||
sys.poke(-39, 1) // swallow the digit char so it doesn't leak into the queue
|
||||
}
|
||||
|
||||
if (!running) break
|
||||
|
||||
// drain typed chars into the active pane's queue
|
||||
while (sys.peek(-50) !== 0) {
|
||||
let k = sys.peek(-38)
|
||||
if (k < 0) k += 256
|
||||
queuePush(active, k)
|
||||
}
|
||||
|
||||
sys.sleep(33)
|
||||
}
|
||||
|
||||
for (let n = 1; n <= MAX_VT; n++) if (panes[n]) parallel.kill(panes[n].thread)
|
||||
con.color_pair(254, 255)
|
||||
con.clear()
|
||||
println("vtmgr exited.")
|
||||
return 0
|
||||
Reference in New Issue
Block a user