125 lines
6.4 KiB
Python
125 lines
6.4 KiB
Python
"""Fail-closed guard for the current virtio GPU's cursor-free KMS scanout.
|
|
|
|
Only primary planes visible with Sunshine's exact DRM client capabilities are
|
|
accepted. Virtio cursor hotspot planes are hidden from atomic clients that do
|
|
not request DRM_CLIENT_CAP_CURSOR_PLANE_HOTSPOT. This is not a cursor filter.
|
|
"""
|
|
import ctypes as C
|
|
import os
|
|
import re
|
|
|
|
|
|
class PlaneResources(C.Structure):
|
|
_fields_ = [('count', C.c_uint32), ('planes', C.POINTER(C.c_uint32))]
|
|
|
|
|
|
class Plane(C.Structure):
|
|
_fields_ = [('count_formats', C.c_uint32), ('formats', C.POINTER(C.c_uint32)),
|
|
('plane_id', C.c_uint32), ('crtc_id', C.c_uint32),
|
|
('fb_id', C.c_uint32), ('crtc_x', C.c_uint32),
|
|
('crtc_y', C.c_uint32), ('x', C.c_uint32), ('y', C.c_uint32),
|
|
('possible_crtcs', C.c_uint32), ('gamma_size', C.c_uint32)]
|
|
|
|
|
|
class Properties(C.Structure):
|
|
_fields_ = [('count', C.c_uint32), ('props', C.POINTER(C.c_uint32)),
|
|
('values', C.POINTER(C.c_uint64))]
|
|
|
|
|
|
class PropertyPrefix(C.Structure):
|
|
_fields_ = [('id', C.c_uint32), ('flags', C.c_uint32), ('name', C.c_char * 32)]
|
|
|
|
|
|
class Framebuffer(C.Structure):
|
|
_fields_ = [('id', C.c_uint32), ('width', C.c_uint32), ('height', C.c_uint32),
|
|
('format', C.c_uint32), ('modifier', C.c_uint64),
|
|
('flags', C.c_uint32), ('handles', C.c_uint32 * 4),
|
|
('pitches', C.c_uint32 * 4), ('offsets', C.c_uint32 * 4)]
|
|
|
|
|
|
class VersionPrefix(C.Structure):
|
|
_fields_ = [('major', C.c_int), ('minor', C.c_int), ('patch', C.c_int),
|
|
('name_len', C.c_int), ('name', C.c_char_p)]
|
|
|
|
|
|
class VirtioScanout:
|
|
def __init__(self, config):
|
|
if set(config) != {'backend', 'card', 'size'} or config['backend'] != 'kms-virtio-no-cursor-plane':
|
|
raise ValueError('unsupported KMS cursor configuration')
|
|
self.card = config['card']
|
|
if not isinstance(self.card, str) or not re.fullmatch('/dev/dri/card[0-9]+', self.card):
|
|
raise ValueError('invalid DRM card')
|
|
self.size = config['size']
|
|
if self.size != [2560, 1440]:
|
|
raise ValueError('unverified KMS geometry')
|
|
self.drm = C.CDLL('libdrm.so.2')
|
|
self.drm.drmSetClientCap.argtypes = [C.c_int, C.c_uint64, C.c_uint64]
|
|
for name, args, result in [
|
|
('drmModeGetPlaneResources', [C.c_int], C.POINTER(PlaneResources)),
|
|
('drmModeGetPlane', [C.c_int,C.c_uint32], C.POINTER(Plane)),
|
|
('drmModeObjectGetProperties', [C.c_int,C.c_uint32,C.c_uint32], C.POINTER(Properties)),
|
|
('drmModeGetProperty', [C.c_int,C.c_uint32], C.POINTER(PropertyPrefix)),
|
|
('drmModeGetFB2', [C.c_int,C.c_uint32], C.POINTER(Framebuffer)),
|
|
('drmGetVersion', [C.c_int], C.POINTER(VersionPrefix)),
|
|
]:
|
|
fn = getattr(self.drm, name); fn.argtypes = args; fn.restype = result
|
|
for name in ['drmModeFreePlaneResources','drmModeFreePlane',
|
|
'drmModeFreeObjectProperties','drmModeFreeProperty','drmModeFreeFB2',
|
|
'drmFreeVersion']:
|
|
fn = getattr(self.drm, name); fn.argtypes = [C.c_void_p]; fn.restype = None
|
|
|
|
def check(self):
|
|
fd = os.open(self.card, os.O_RDONLY | os.O_CLOEXEC | os.O_NOFOLLOW)
|
|
try:
|
|
version = self.drm.drmGetVersion(fd)
|
|
if not version: raise RuntimeError('DRM driver unavailable')
|
|
try:
|
|
if version.contents.name != b'virtio_gpu':
|
|
raise RuntimeError('KMS driver changed')
|
|
finally: self.drm.drmFreeVersion(version)
|
|
if self.drm.drmSetClientCap(fd, 2, 1):
|
|
raise RuntimeError('cannot enumerate universal planes')
|
|
# Match the pinned Sunshine KMS backend. Do not request capability
|
|
# 6 (cursor-plane hotspots), which would expose the virtio cursor.
|
|
if self.drm.drmSetClientCap(fd, 3, 1):
|
|
raise RuntimeError('atomic KMS is required')
|
|
resources = self.drm.drmModeGetPlaneResources(fd)
|
|
if not resources: raise RuntimeError('DRM plane list unavailable')
|
|
try:
|
|
# This tested virtio device exposes exactly one primary plane.
|
|
if resources.contents.count != 1:
|
|
raise RuntimeError('unverified DRM plane topology')
|
|
plane = self.drm.drmModeGetPlane(fd, resources.contents.planes[0])
|
|
if not plane: raise RuntimeError('DRM plane unavailable')
|
|
try:
|
|
p = plane.contents
|
|
props = self.drm.drmModeObjectGetProperties(fd, p.plane_id, 0xeeeeeeee)
|
|
if not props: raise RuntimeError('DRM plane properties unavailable')
|
|
kind = None
|
|
crtc_w = None
|
|
crtc_h = None
|
|
try:
|
|
if props.contents.count > 128: raise RuntimeError('invalid property count')
|
|
for i in range(props.contents.count):
|
|
prop = self.drm.drmModeGetProperty(fd, props.contents.props[i])
|
|
if not prop: raise RuntimeError('DRM property unavailable')
|
|
try:
|
|
name = prop.contents.name
|
|
value = props.contents.values[i]
|
|
if name == b'type': kind = value
|
|
elif name == b'CRTC_W': crtc_w = value
|
|
elif name == b'CRTC_H': crtc_h = value
|
|
finally: self.drm.drmModeFreeProperty(prop)
|
|
finally: self.drm.drmModeFreeObjectProperties(props)
|
|
# Atomic virtio clients expose CRTC geometry while the
|
|
# legacy plane fields remain zero. Sunshine uses the same
|
|
# atomic client capability pair and validates the actual
|
|
# framebuffer when it captures it.
|
|
if kind != 1 or [crtc_w, crtc_h] != self.size:
|
|
raise RuntimeError('scanout is not an active primary-only plane')
|
|
return {'driver': 'virtio_gpu', 'plane': p.plane_id, 'type': kind,
|
|
'crtc': int(p.crtc_id), 'size': self.size, 'cursor_planes': 0}
|
|
finally: self.drm.drmModeFreePlane(plane)
|
|
finally: self.drm.drmModeFreePlaneResources(resources)
|
|
finally: os.close(fd)
|