test: verify independent guests and recovery on private displays
This commit is contained in:
@@ -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])}')
|
||||
Reference in New Issue
Block a user