99 lines
5.4 KiB
Python
99 lines
5.4 KiB
Python
#!/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])}')
|