diff --git a/src/servers/sidecar/vnc/vnc-manager.ts b/src/servers/sidecar/vnc/vnc-manager.ts index 0c3ce84f..3d5c690c 100644 --- a/src/servers/sidecar/vnc/vnc-manager.ts +++ b/src/servers/sidecar/vnc/vnc-manager.ts @@ -10,6 +10,8 @@ const MIRROR_DISPLAY = ':0'; const MIRROR_DISPLAY_NUM = 0; const MIRROR_PORT = 5900; const READY_TIMEOUT_MS = 5000; +// Above this framebuffer width the stream is halved; at or below it, pixels are served 1:1. +const SCALE_ABOVE_WIDTH = 2560; // x11vnc reads :0's cookie from the logged-in user's X authority, so no root is needed. Where that // cookie lives depends on the display manager: GDM (Ubuntu GNOME on Xorg) keeps it in the per-session @@ -88,6 +90,20 @@ export async function ensureVncPassword(homeDir: string): Promise<{ password: st return { password, passwdFile }; } +// Width of :0's framebuffer, or null when it cannot be read (xrandr missing, X not up). Callers +// treat null as "don't scale" — serving too many pixels beats serving an unreadable thumbnail. +function getFramebufferWidth(xauthority: string): number | null { + const proc = Bun.spawnSync({ + cmd: ['xrandr', '--current'], + env: { ...process.env, DISPLAY: MIRROR_DISPLAY, XAUTHORITY: xauthority }, + stdout: 'pipe', + stderr: 'ignore', + }); + if (proc.exitCode !== 0) return null; + const match = proc.stdout.toString().match(/current\s+(\d+)\s*x\s*(\d+)/); + return match ? Number(match[1]) : null; +} + async function isPortOpen(port: number): Promise { try { const socket = await Bun.connect({ @@ -126,6 +142,16 @@ export async function startSession(params: VncStartParams): Promise<{ port: numb throw new Error(`No X authority found (GDM or ~/.Xauthority) — nobody is logged in on ${MIRROR_DISPLAY}`); } + // Halving a 4K framebuffer keeps the stream sane over the tailnet, but the same 0.5 applied to a + // small screen is just lost detail — and with no monitor plugged in, X falls back to something + // tiny (800x480 here), which halves to an unreadable 400x240. Scale only when there is genuinely + // too much to send. + const width = getFramebufferWidth(xauthority); + const scale = width !== null && width > SCALE_ABOVE_WIDTH ? ['-scale', '0.5'] : []; + if (width !== null) { + console.log(`[vnc] :0 framebuffer is ${width}px wide — ${scale.length ? 'scaling to 50%' : 'serving 1:1'}`); + } + const proc = Bun.spawn({ cmd: [ 'x11vnc', @@ -140,9 +166,7 @@ export async function startSession(params: VncStartParams): Promise<{ port: numb '-localhost', '-forever', '-shared', - // :0 is 4K; halving the framebuffer keeps the stream sane over the tailnet - '-scale', - '0.5', + ...scale, '-noxdamage', '-quiet', ],