test: verify independent guests and recovery on private displays

This commit is contained in:
Codex
2026-10-13 04:44:46 +03:00
parent 30ac68ed60
commit 664c995f84
11 changed files with 608 additions and 47 deletions
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Compile actual QEMU EDID code and decode the guest-visible response.
Catches loss of the configured refresh rate, a frontend overriding it, and
monitor ranges excluding the generated preferred mode. No VM is touched.
"""
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import tarfile
source_root = Path(sys.argv[1])
# Accept the pinned QEMU source archive directly. Read only the four files
# needed here, avoiding unrelated upstream absolute symlinks in rom sources.
if source_root.is_file():
extracted = tempfile.TemporaryDirectory(prefix='hydra-edid-source-')
with tarfile.open(source_root) as archive:
members = {name: next(member for member in archive.getmembers()
if member.name.endswith('/' + name)) for name in (
'hw/display/edid-generate.c', 'include/hw/display/edid.h',
'include/hw/virtio/virtio-gpu.h', 'hw/display/virtio-gpu-base.c')}
source_root = Path(extracted.name)
for name, member in members.items():
destination = source_root / name
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(archive.extractfile(member).read())
subprocess.run(['patch', '--batch', '-p1', '-i',
str(Path(__file__).resolve().parents[1] / 'patches/qemu-virtio-refresh-rate.patch')],
cwd=source_root, check=True)
edid = (source_root / 'hw/display/edid-generate.c').read_text()
edid = '\n'.join(line for line in edid.splitlines() if not line.startswith('#include'))
header = (source_root / 'include/hw/display/edid.h').read_text()
header = header[header.index('typedef struct qemu_edid_info'):header.index('void qemu_edid_region_io')]
gpu = (source_root / 'hw/display/virtio-gpu-base.c').read_text()
start = gpu.index('void\nvirtio_gpu_base_generate_edid')
gpu = gpu[start:gpu.index('static void virtio_gpu_invalidate_display', start)]
stubs = r'''
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#define ARRAY_SIZE(x) (sizeof(x)/sizeof((x)[0]))
#define MAX(a,b) ((a) > (b) ? (a) : (b))
#define MIN(a,b) ((a) < (b) ? (a) : (b))
#define DIV_ROUND_UP(n,d) (((n) + (d) - 1) / (d))
#define cpu_to_le32(x) (x)
static void stw_le_p(void *p,uint16_t v) { uint8_t *b=p; b[0]=v; b[1]=v>>8; }
static void stw_be_p(void *p,uint16_t v) { uint8_t *b=p; b[1]=v; b[0]=v>>8; }
static void stl_le_p(void *p,uint32_t v) { uint8_t *b=p; for(int i=0;i<4;i++) b[i]=v>>(8*i); }
typedef struct VirtIOGPUOutput { const char *name; } VirtIOGPUOutput;
typedef struct VirtIOGPUOutputList { struct VirtIOGPUOutputList *next; VirtIOGPUOutput *value; } VirtIOGPUOutputList;
struct virtio_gpu_requested_state { uint16_t width_mm,height_mm; uint32_t width,height,refresh_rate; int x,y; };
typedef struct VirtIOGPUBase { struct { uint32_t refresh_rate; VirtIOGPUOutputList *outputs; } conf; struct virtio_gpu_requested_state req_state[1]; } VirtIOGPUBase;
struct virtio_gpu_resp_edid { uint32_t size; uint8_t edid[1024]; };
'''
main = r'''
int main(int argc,char **argv) {
VirtIOGPUBase gpu = {0}; struct virtio_gpu_resp_edid out = {0};
gpu.conf.refresh_rate=atoi(argv[1]); gpu.req_state[0].refresh_rate=atoi(argv[2]);
gpu.req_state[0].width=atoi(argv[3]); gpu.req_state[0].height=atoi(argv[4]);
virtio_gpu_base_generate_edid(&gpu,0,&out);
fwrite(out.edid,1,qemu_edid_size(out.edid),stdout); return 0;
}
'''
compiler = os.environ.get('CC', 'cc')
with tempfile.TemporaryDirectory(prefix='qemu-edid-probe-') as temporary:
directory = Path(temporary)
(directory / 'probe.c').write_text(stubs + header + edid + gpu + main)
subprocess.run([compiler, '-O2', str(directory / 'probe.c'), '-o', str(directory / 'probe')], check=True)
for configured, frontend, width, height, expected in [
(0, 0, 1920, 1080, 75),
(0, 60000, 1920, 1080, 60),
(144000, 0, 1920, 1080, 144),
(144000, 60000, 1920, 1080, 144),
(144000, 0, 1600, 900, 144),
(0, 144000, 1920, 1080, 144),
]:
data = subprocess.check_output([str(directory / 'probe'), *map(str, (configured, frontend, width, height))])
assert all(sum(data[offset:offset + 128]) % 256 == 0 for offset in range(0, len(data), 128)), 'invalid EDID checksum'
timing = data[54:72]
clock = int.from_bytes(timing[:2], 'little') * 10000
horizontal = timing[2] + ((timing[4] >> 4) << 8)
hblank = timing[3] + ((timing[4] & 15) << 8)
vertical = timing[5] + ((timing[7] >> 4) << 8)
vblank = timing[6] + ((timing[7] & 15) << 8)
actual = clock / (horizontal + hblank) / (vertical + vblank)
assert (horizontal, vertical) == (width, height)
assert abs(actual - expected) < 0.01, f'configured={configured}, frontend={frontend}: wanted {expected} Hz, got {actual}'
ranges = next(data[offset:offset + 18] for offset in (54, 72, 90, 108) if data[offset:offset + 5] == b'\0\0\0\xfd\0')
assert ranges[5] <= actual <= ranges[6], f'vertical range {ranges[5:7]} excludes {actual}'
horizontal_khz = clock / (horizontal + hblank) / 1000
assert ranges[7] <= horizontal_khz <= ranges[8], f'horizontal range {ranges[7:9]} excludes {horizontal_khz}'
if expected <= 75:
assert ranges[5:9] == bytes((50, 125, 30, 160)), 'legacy monitor range changed'
print(f'PASS configured={configured}, frontend={frontend}: {width}x{height}@{actual:.6f}, ranges={list(ranges[5:9])}')