194 lines
11 KiB
Markdown
194 lines
11 KiB
Markdown
# Graphics and recovery verification
|
||
|
||
The [2026-10-13 preview record](releases/2026-10-13/acceptance.json) identifies
|
||
the exact ISO, firmware and harness used for the passing twelve-check acceptance
|
||
run. The unmodified ISO installed onto a fresh 32 GiB qcow2 disk and booted from
|
||
that disk with no ISO attached. Password login, persistent data, removed live
|
||
overrides, installed services, DNS lookup, routed LAN-to-VM TCP, normal-user
|
||
nested KVM/bridge access and normal shutdown passed. An actual logout followed
|
||
by 15 seconds without an open login and a new login preserved the same VM,
|
||
QMP/SPICE sockets and user audio services. Audio playback was not measured.
|
||
|
||
Live graphical recovery was also observed through a private QEMU screenshot;
|
||
the earlier two-guest compositor and profile-refresh probes cover fullscreen
|
||
switching and hidden workload continuity. These virtual checks leave the physical
|
||
ASUS checks listed below pending. No current host configuration or existing user
|
||
VM disk was changed for this validation.
|
||
|
||
Run from the checkout. Tools are temporary Nix development dependencies; no
|
||
persistent host installation is required. The two-guest smoke test needs access
|
||
to `/dev/kvm` and creates its own headless Wayland display, VM disks and sockets
|
||
under a fresh `/tmp/hydra-smoke-*` directory. It never uses the inherited display,
|
||
active TTY, existing guests or existing disks. Evidence remains in that directory;
|
||
the script stops only its own verified QEMU instances on exit.
|
||
|
||
```sh
|
||
python3 -m unittest discover -s tests -v
|
||
nix-build dev/tools.nix -A viewer --no-out-link
|
||
nix-build dev/tools.nix -A qemu --no-out-link
|
||
nix-shell dev/tools.nix --run 'python3 dev/smoke.py'
|
||
nix-shell dev/tools.nix --run 'python3 dev/session-refresh-smoke.py --keyboard "$(dev/build-input-probe.sh)"'
|
||
nix-shell dev/tools.nix --run 'python3 dev/check-network.py'
|
||
```
|
||
|
||
The smoke test builds a private input helper with an ordinary evdev/pc105 US
|
||
keyboard map. It boots two tiny counter workloads from separate scratch qcow2
|
||
disks, connects two fullscreen viewers, and checks reserved switching/recovery,
|
||
manual USB selection, explicit reconnect and independent QMP lifecycles.
|
||
After each confirmed workspace switch, the hidden guest is sampled across a
|
||
half-second interval with no focus changes, switches or reconnects; the check is
|
||
then repeated with the other guest hidden. `result.json` records both intervals.
|
||
Guest memory counters must advance and QEMU PIDs remain unchanged through
|
||
switching. Viewer protocol logs must show no reconnection during switching.
|
||
A test-only `ToggleKeybinds` action and F12 marker establish native labwc
|
||
keybinding inhibition before checking the production reserved bindings. Those
|
||
extra controls are absent from the production session configuration. See the
|
||
[labwc configuration manual](https://labwc.github.io/labwc-config.5.html) for
|
||
`overrideInhibition` and the [actions manual](https://labwc.github.io/labwc-actions.5.html)
|
||
for native `ToggleKeybinds` behavior.
|
||
|
||
USB acceptance checks the actual selector window title, holds the modifiers
|
||
through opening, releases them inside the modal dialog, then asserts fresh
|
||
keyboard leave/enter transitions for both Right and Left switches.
|
||
QEMU's USB device list must stay unchanged without a selection. Both viewer
|
||
USB filters deny automatic hotplug and connection redirection. This check does
|
||
not attach a physical USB device or prove guest driver behavior.
|
||
|
||
The viewer adaptation handles only the configured manual USB accelerator before
|
||
forwarding an event to the guest. Upstream keyboard grabs remove the GTK
|
||
application accelerator group; without this adaptation, the configured USB
|
||
shortcut reaches the viewer but does not open its selector.
|
||
|
||
The pinned labwc 0.20.2 runtime has no documented runtime XWayland-disable
|
||
option. This native-Wayland probe sets `WLR_XWAYLAND=/bin/false` so no X server
|
||
is launched, and explicitly disables activation-environment updates. It can
|
||
still log an occupied X socket while wlroots reserves a display number. Only
|
||
`[xwayland/sockets.c] Failed to bind socket ... Address already in use` is
|
||
classified as expected noise in `result.json`; any other compositor `[ERROR]`
|
||
fails the probe. Full logs remain available. This classification does not
|
||
suppress viewer errors or compiler/test diagnostics. The viewer build's upstream
|
||
Meson boolean-check and compiler-warning-option guidance are build noise; the four-test
|
||
result, compiler errors and process exit status remain the build acceptance gate.
|
||
|
||
## Fresh-session recovery and profile changes
|
||
|
||
A session with no profiles opens recovery with first-use guidance. Enter `:shell`
|
||
to run `hydra add NAME --iso PATH` and `hydra start NAME`; leave the shell and enter
|
||
`:refresh`. After adding or removing profiles, refresh reloads the profile list,
|
||
rebuilds workspace rules, restarts only the session's own compositor, and attaches
|
||
new isolated viewers to running guests. QEMU processes and workloads survive.
|
||
The recovery prompt can then reconnect the new names; reserved Left/Right chords
|
||
switch between their workspaces. `:quit` closes the recovery prompt.
|
||
|
||
`dev/session-refresh-smoke.py` exercises the actual product launch loop, recovery
|
||
helper, `:shell` and `:refresh`, starting with an empty profile directory. A
|
||
test-only labwc wrapper selects private headless backends; a terminal command
|
||
feeder drives recovery and a scratch shell adds and starts two counter guests.
|
||
Actual labwc and native Wayland viewers then demonstrate fresh keyboard focus
|
||
transitions in both directions, hidden workload continuity, unchanged QEMU
|
||
PIDs/UUIDs through refresh, and guest survival after normal compositor exit.
|
||
This checks the recovery command flow, not physical terminal rendering/usability.
|
||
It leaves snapshots, recovery output, protocol logs and `result.json` in
|
||
`/tmp/hydra-session-refresh-*` and stops only its verified scratch QEMU processes.
|
||
|
||
## Bridge identities
|
||
|
||
New profiles persist a locally administered unicast `mac` in their TOML file.
|
||
It remains stable across stop/start, media changes and runtime socket-directory
|
||
changes. The generated address incorporates the VM disk path and user identity,
|
||
so separate profiles/users avoid QEMU's identical default MAC on a shared bridge.
|
||
Existing profiles without `mac` use the same deterministic calculation without
|
||
rewriting their file. Relocating a legacy disk or changing its owner changes
|
||
that fallback; explicitly persisting its current address retains the identity.
|
||
Explicit TOML addresses must also be locally administered unicast, and users
|
||
copying profiles must assign distinct addresses to concurrently used copies.
|
||
|
||
`dev/check-network.py` requires unprivileged user/network/mount namespaces. It
|
||
asserts all three namespace identities changed before creating a private
|
||
`hydrabr0` and mounting a private QEMU bridge-helper ACL over `/etc` within that
|
||
namespace. Two actual scratch QEMUs must report different configured NIC MACs,
|
||
both bridge TAP ports must exist, and restart must preserve the first guest's
|
||
address. Evidence remains under `/tmp/hydra-network-*`; host bridges, host `/etc`
|
||
and existing guests are unchanged. This does not prove guest network drivers,
|
||
DHCP leases or external connectivity; those need the installed-guest harness.
|
||
|
||
## Installed guest workflow
|
||
|
||
`--iso` is optional. Attach or detach media only while the VM is stopped:
|
||
|
||
```sh
|
||
bin/hydra add guest --iso /absolute/path/installer.iso
|
||
bin/hydra start guest
|
||
bin/hydra view guest
|
||
# Shut down from inside the guest, then check it is stopped.
|
||
bin/hydra status guest
|
||
bin/hydra media guest --detach
|
||
bin/hydra start guest
|
||
```
|
||
|
||
For an existing stopped guest, `bin/hydra media guest --iso PATH` attaches
|
||
installation/rescue media. A missing configured ISO prevents a new start with
|
||
an actionable error; it does not prevent listing profiles, status, stopping a
|
||
running guest, reconnecting a viewer or detaching the missing media. Media
|
||
updates share the VM lifecycle lock and preserve its qcow2 disk. Abrupt stopping
|
||
still requires `bin/hydra stop guest --force` and verified QMP identity.
|
||
|
||
## Display preferences and EDID
|
||
|
||
New developer profiles use user networking, 1280×800 and `refresh_millihz=0`.
|
||
Zero omits the patched QEMU refresh property and remains compatible with stock
|
||
QEMU. Hydra's installed defaults can use this flat `/etc/hydra/defaults.toml`:
|
||
|
||
```toml
|
||
network = "bridge"
|
||
bridge = "hydrabr0"
|
||
width = 1920
|
||
height = 1080
|
||
refresh_millihz = 144000
|
||
```
|
||
|
||
Only `add` reads distribution defaults. Explicit `--network`, `--bridge`,
|
||
`--width`, `--height` and `--refresh-millihz` options win; existing profiles do
|
||
not change. `--defaults PATH` selects another preferences file for development.
|
||
Missing defaults use developer preferences; unknown or invalid fields fail
|
||
before creating a profile. Dimensions are integers from 1 to 16384, refresh is
|
||
zero for automatic refresh or an integer from 50000 to 255000 mHz, matching
|
||
the vertical range descriptor limits. This bound does not establish every
|
||
resolution/refresh combination; pixel-clock and horizontal timing limits are
|
||
mode-dependent and the verified target remains 1920×1080/144000 mHz. Nonzero
|
||
refresh requires the tracked patched QEMU provided by `dev/tools.nix` and distribution packaging.
|
||
|
||
The patch keeps the requested refresh through frontend resizing and extends
|
||
EDID monitor ranges to include the generated preferred mode. To verify the
|
||
actual patched QEMU generator with the pinned source archive:
|
||
|
||
```sh
|
||
qemu_source=$(nix-build dev/tools.nix -A qemu.src --no-out-link)
|
||
nix-shell dev/tools.nix --run "python3 dev/check-edid.py '$qemu_source'"
|
||
```
|
||
|
||
The probe applies the tracked patch to four relevant source files in a temporary
|
||
directory, compiles the actual EDID and virtio GPU generator functions with a
|
||
small dependency harness, and decodes the result. It checks 60/75 Hz fallback,
|
||
144 Hz configuration, a conflicting 60 Hz frontend request, resized dimensions,
|
||
checksums and monitor ranges. It also checks the supported vertical-refresh
|
||
boundaries: 1920×1080/50000 mHz and 1280×800/255000 mHz. Range endpoints
|
||
use floor/ceiling of the encoded timing after pixel-clock rounding, so the
|
||
49.998895 Hz lower-boundary timing is included by a 49 Hz minimum. Normal
|
||
automatic 60/75 Hz cases retain the legacy 50–125 Hz range. At 1920×1080/144000 mHz the preferred timing is
|
||
143.997231 Hz after EDID clock rounding. This is an advertised guest mode,
|
||
not measured frame delivery or proof of a 144 Hz physical output.
|
||
|
||
## Still requiring a dedicated physical session
|
||
|
||
- Physical reserved chords, brightness/backlight permissions and touchpad input.
|
||
- Physical held keys/modifiers across guest switching, grabs and modal dialogs;
|
||
guest key-up delivery, pointer grab release and absence of stuck modifiers.
|
||
- Physical USB selection/redirection and guest drivers; no automatic redirection.
|
||
- Toolbar behavior at physical screen edges and actual recovery shell usability.
|
||
- ASUS output selection at 1920×1080/144 Hz, guest-reported display mode, delivered
|
||
frame rate, hidden guest graphics continuity, tearing and switching latency.
|
||
|
||
Headless checks establish CPU workload continuity, protocol focus transitions
|
||
and the exercised compositor actions. They cannot prove physical input or scanout.
|