diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/tty/tty.zig | 56 |
1 files changed, 55 insertions, 1 deletions
diff --git a/src/tty/tty.zig b/src/tty/tty.zig index 007abd49..b4bd4175 100644 --- a/src/tty/tty.zig +++ b/src/tty/tty.zig @@ -161,7 +161,47 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { // resize watcher: plain detached thread (not io.concurrent — teardown // joins those, and sigwait never returns); dies with the process (try std.Thread.spawn(.{}, winchWatch, .{ &loop, &vx, &tty })).detach(); - vx.queryTerminal(tty.writer(), std.Io.Duration.fromMilliseconds(2)) catch {}; + // Capability handshake — SEND the probes, do not wait on them. This was + // queryTerminal(2ms), which blocks on a futex until DA1 comes back. The + // number has to beat one terminal round trip: a local terminal answers in + // microseconds, `ssh localhost` in under 1ms, and any real link never. + // Measured over sshd on :22 with the replies delayed to model the wire, + // 2ms already loses at 5ms RTT and everything above. + // + // Losing it is worse than never probing, because vaxis splits detect from + // enable and only detect respects the deadline. queryTerminal sets + // queries_done the moment the futex times out, and the two replies vaxis + // gates on that flag — explicit width and scaled text, both answered as a + // cursor-position report — stop being recognised as probe replies and are + // handed to US as shift-F3/alt-F3 keypresses. The replies it does NOT + // gate (mode 2027, kitty keyboard/graphics, sgr-pixels) keep landing and + // keep mutating vx.caps from the reader thread, long after + // enableDetectedFeatures ran and declined to switch those modes on. So + // over ssh the terminal sat in its default modes while caps claimed + // otherwise — kitty keyboard was never actually pushed, ever. Raising the + // timeout only moves the link speed at which that happens. + // + // Resolve it on the loop instead. DA1 is last in the probe string and + // terminals answer in order, so when vaxis's reader flips queries_done + // every earlier reply is already applied — no window left to miss at any + // latency, and the enable lands before the next frame (see caps_pending + // in the render path). A terminal that never answers keeps the defaults, + // which is what the 2ms timeout produced anyway, and with no caps + // enableDetectedFeatures writes no bytes — the snapshot goldens, where + // nothing ever answers, do not move. Startup gets 2ms faster, not slower. + // + // ponytail: nothing wakes the loop for DA1 alone. In practice the reply + // burst carries the mode-2048 size report too, which posts a winsize and + // turns the loop; a terminal idle from boot that lands DA1 between two + // parks keeps the defaults until the user's first keystroke (decoded + // legacy, which vaxis handles). Ceiling accepted because nothing in the + // render path reads the missing caps: pardes writes one codepoint per + // cell plus an explicit blank spacer under a wide glyph, and vaxis's + // Cell.width defaults to 1, so gwidth — the only consumer of + // caps.unicode — is never called. If that ever changes, wake the loop on + // vx.query_futex from a one-shot thread instead. + try vx.queryTerminalSend(tty.writer()); + var caps_pending = true; // now threads are fine: start a reader task per pty for (&ptys, 0..) |*slot, id| if (slot.*) |*pt| { @@ -287,6 +327,20 @@ pub fn run(init: std.process.Init, opts: pardes.Options) !void { if (look.shellCwd(pt.pid, &lbuf)) |cwd| core.setCwd(id, cwd); }; + // The handshake landed (vaxis's reader flips queries_done on DA1, the + // last probe answered): put the terminal into the modes the caps now + // claim, before anything is drawn under them. Polled here rather than + // done where the replies arrive because that is the reader thread, and + // this is the only thread allowed to touch the tty writer. The repaint + // matters as much as the enable: earlier frames were drawn under the + // pre-handshake caps, and vaxis's shadow grid has to be re-established + // under the new ones or it keeps skipping cells it thinks are current. + if (caps_pending and vx.queries_done.load(.unordered)) { + caps_pending = false; + vx.enableDetectedFeatures(tty.writer()) catch {}; + vx.queueRefresh(); + } + // ---- render: surface -> vaxis, cell for cell ---- _ = frame_arena.reset(.retain_capacity); const tz_core = tracy.zone(@src(), "core.render"); |
