fix(caps,ui): Encoder werden gemessen statt behauptet

Der Modulkopf von caps.py verspricht "ehrlich erkannt, nicht behauptet" -
erkenne_encoder() tat aber drei Mal das Gegenteil:

1. cpu-x264 und cpu-x265 standen fest verdrahtet in der Liste ("immer
   dabei"). Ein Rip-Worker OHNE HandBrake behauptete damit, komprimieren zu
   koennen; jeder Transcode dort endete sofort mit "nicht installiert".
2. 'vaapi' wurde allein wegen /dev/dri gemeldet, ohne zu pruefen, ob
   HandBrake das ueberhaupt kann. Auf der VM gemessen: das Worker-Image
   kennt svt_av1/x264/x265/mpeg4/mpeg2/VP8/VP9/theora und KEINEN einzigen
   Hardware-Encoder. Rippy haette VAAPI versprochen und dann versagt.
3. Ueber die Rechenleistung sagte es gar nichts. Genau deshalb war nicht zu
   sehen, dass ein 4K-Encode auf dieser Maschine Tage braucht: CPU-Modell
   ist das generische "QEMU Virtual CPU version 2.5+", grep -c avx2
   /proc/cpuinfo = 0, nur bis sse4_2. x265 lebt von AVX2. Gemessen an der
   Leseposition des Quellstroms: 28-55 h fuer einen Film, 3,84 von 4 Kernen
   gesaettigt. Abhilfe waere der CPU-Typ 'host' in Proxmox.

Jetzt wird die Encoder-Liste aus `HandBrakeCLI --help` geparst (Abschnitt
-e/--encoder, Formatstrings im Worker-Image gemessen - AGENTS Regel D), und
Hardware zaehlt nur, wenn Geraet UND HandBrake-Unterstuetzung da sind. Neu
gemeldet: cpu-av1 (svt_av1 ist wirklich verfuegbar) sowie CPU-Modell,
Kernzahl und Vektorbefehlsstufe je Worker.

UI: Kerne/Vektorbefehle stehen bei jedem Worker (Worker-Tab und
Einstellungen -> System), dazu die ungefilterte HandBrake-Auskunft und eine
sichtbare Warnung, wenn AVX2 fehlt - inklusive des Hinweises auf den
VM-CPU-Typ. Der Warntext liegt in lib/encoder.ts, damit er nicht doppelt
im Code steht.

Mit im gleichen Zug (dieselbe Datei): der Schalter "Alle Tracks rippen" ist
entfallen. Er war ein Placebo - abcde bekommt keine Track-Auswahl und es gab
auch keine Oberflaeche, um eine Teilmenge zu waehlen. Eine Audio-CD wurde
also immer vollstaendig gerippt, egal wie der Schalter stand. An seiner
Stelle steht jetzt die Wahrheit.

7 Tests, Testdaten sind echte HandBrake-Ausgaben von der VM. Der Parser ist
zusaetzlich gegen die vollen 697 Zeilen der echten --help gegengeprueft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-25 21:04:42 +02:00
parent e0cb7b3ddc
commit 883c1c290b
8 changed files with 328 additions and 21 deletions
@@ -14,6 +14,7 @@ interface WorkerInfo {
const ENCODER_LABELS: Record<string, string> = {
'cpu-x264': 'CPU · H.264 (x264)',
'cpu-x265': 'CPU · H.265 (x265)',
'cpu-av1': 'CPU · AV1 (SVT-AV1)',
'vaapi': 'Hardware · VAAPI (AMD/Intel)',
'nvenc': 'Hardware · NVENC (NVIDIA)',
}
+2 -1
View File
@@ -38,7 +38,8 @@ interface WorkerWahl {
}
const ENCODER_KURZ: Record<string, string> = {
'cpu-x264': 'H.264', 'cpu-x265': 'H.265', 'vaapi': 'VAAPI⚡', 'nvenc': 'NVENC⚡',
'cpu-x264': 'H.264', 'cpu-x265': 'H.265', 'cpu-av1': 'AV1',
'vaapi': 'VAAPI⚡', 'nvenc': 'NVENC⚡',
}
interface TitelInfo {
@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'
import { Server, RefreshCw, Copy, CheckCircle, Trash2, Download } from 'lucide-react'
import { api } from '../lib/api'
import { simdWarnung } from '../lib/encoder'
import { useToast } from '../context/ToastContext'
import { Card, CardHeader, CardTitle, CardContent } from './ui/Card'
import { Button } from './ui/Button'
@@ -16,16 +17,22 @@ interface WorkerInfo {
ip?: string
makemkv?: string
handbrake?: string
cpu_modell?: string
cpu_kerne?: string
cpu_simd?: string
handbrake_encoder?: string
}
}
const ENCODER_LABELS: Record<string, string> = {
'cpu-x264': 'CPU · H.264',
'cpu-x265': 'CPU · H.265',
'cpu-av1': 'CPU · AV1 (SVT)',
'vaapi': 'GPU · VAAPI (AMD/Intel)',
'nvenc': 'GPU · NVENC (NVIDIA)',
}
function relativeZeit(iso?: string): string {
if (!iso) return 'nie'
const sekunden = Math.floor((Date.now() - new Date(iso).getTime()) / 1000)
@@ -134,6 +141,18 @@ export default function WorkerVerwaltung() {
{w.info?.ip ? ` · IP ${w.info.ip}` : ''}
{w.info?.hostname && w.info.hostname !== w.name ? ` · ID ${w.info.hostname}` : ''}
</p>
{(w.info?.cpu_kerne || w.info?.cpu_simd) && (
<p className="text-xs text-slate-500 dark:text-slate-400" title={w.info?.cpu_modell || ''}>
{w.info?.cpu_kerne ? `${w.info.cpu_kerne} Kerne` : ''}
{w.info?.cpu_kerne && w.info?.cpu_simd ? ' · ' : ''}
{w.info?.cpu_simd ? `Vektorbefehle ${w.info.cpu_simd}` : ''}
</p>
)}
{simdWarnung(w.info?.cpu_simd) && (
<p className="mt-1 text-xs text-amber-600 dark:text-amber-400 max-w-xl">
{simdWarnung(w.info?.cpu_simd)}
</p>
)}
</div>
</div>
<div className="flex items-center gap-2">
+1
View File
@@ -80,6 +80,7 @@ export const LOG_LEVEL_STYLES = {
export const ENCODER_BADGES: Record<string, { label: string; highlight: boolean }> = {
'cpu-x264': { label: 'H.264 (CPU)', highlight: false },
'cpu-x265': { label: 'H.265 (CPU)', highlight: false },
'cpu-av1': { label: 'AV1 (CPU)', highlight: false },
'vaapi': { label: 'VAAPI ⚡', highlight: true },
'nvenc': { label: 'NVENC ⚡', highlight: true },
}
+24
View File
@@ -0,0 +1,24 @@
// Encoder-Fähigkeiten eines Workers in Klartext übersetzen.
//
// Befund 25.07.2026: Die Rippy-VM lief auf dem generischen QEMU-CPU-Modell
// („QEMU Virtual CPU version 2.5+") und hatte deshalb kein AVX2, nur sse4_2.
// x265 lebt von diesen Vektorbefehlen — ein 4K-Encode brauchte dort gemessene
// 28-55 Stunden, und nirgends im UI war das zu sehen. Der Worker meldet die
// Angaben seit dieser Runde selbst (worker/caps.py), hier werden sie gedeutet.
// Stufen, mit denen Software-Encoding brauchbar schnell ist.
export const SIMD_SCHNELL = ['avx512f', 'avx2']
/**
* Warnt, wenn die CPU dieses Workers zu schwach für Software-Encoding ist.
* Gibt null zurück, wenn alles in Ordnung ist ODER die Stufe unbekannt ist —
* lieber nichts sagen als etwas Falsches behaupten.
*/
export function simdWarnung(simd?: string): string | null {
if (!simd || simd === 'unbekannt') return null
if (SIMD_SCHNELL.includes(simd)) return null
return `Diese CPU kann nur ${simd} (kein AVX2) — Software-Encoding ist hier `
+ 'sehr langsam. Bei einer virtuellen Maschine hilft es meist, den '
+ 'CPU-Typ auf "host" zu stellen; sonst besser einen Worker mit '
+ 'Hardware-Encoder wählen.'
}
+38 -11
View File
@@ -10,6 +10,7 @@ import { Button } from '../components/ui/Button'
import { Input, Select, Toggle } from '../components/ui/Input'
import { MEDIA_SERVER_OPTIONEN } from '../lib/mediaServer'
import { ENCODER_BADGES } from '../lib/design'
import { simdWarnung } from '../lib/encoder'
interface SettingsState {
tmdbApiKey: string
@@ -19,7 +20,6 @@ interface SettingsState {
movieDir: string
seriesDir: string
musicDir: string
ripAllTracks: boolean
mainFeatureOnly: boolean
autoEject: boolean
autoRipStart: boolean
@@ -48,7 +48,6 @@ const defaultSettings: SettingsState = {
movieDir: 'movies',
seriesDir: 'series',
musicDir: 'music',
ripAllTracks: true,
mainFeatureOnly: false,
autoEject: true,
autoRipStart: false,
@@ -74,7 +73,15 @@ interface WorkerInfo {
// keydb: "ja" | "nein" | "unbekannt" — sagt, ob DIESER Worker eine KEYDB.cfg
// in seinem MakeMKV-Datenverzeichnis sieht. Nur der Worker, der wirklich
// rippt, zaehlt, deshalb steht die Angabe pro Worker und nicht global.
info?: { makemkv?: string, handbrake?: string, makemkv_key?: string, keydb?: string }
// cpu_kerne/cpu_simd: Rechenleistung fuers Software-Encodieren. Ohne diese
// Angaben war am 25.07.2026 nicht zu sehen, dass ein 4K-Encode auf der
// Rippy-VM Tage statt Stunden braucht (kein AVX2, 4 Kerne).
// handbrake_encoder: die ungefilterte Encoder-Auskunft von HandBrake selbst.
info?: {
makemkv?: string, handbrake?: string, makemkv_key?: string, keydb?: string,
cpu_modell?: string, cpu_kerne?: string, cpu_simd?: string,
handbrake_encoder?: string,
}
}
// Antwort von GET/POST/DELETE /system/keydb (Feldnamen exakt wie die API sie liefert).
@@ -427,14 +434,16 @@ export default function SettingsPage() {
🎵 Audio-CD
</p>
<div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80">
<Toggle
label="Alle Tracks rippen"
description="Rippe alle Tracks einer CD (FLAC + MusicBrainz-Tags)."
checked={settings.ripAllTracks}
onChange={(v) => handleChange('ripAllTracks', v)}
/>
</div>
{/* Der Schalter „Alle Tracks rippen" ist am 25.07.2026 entfallen.
Er war ein Placebo: abcde bekommt keine Track-Auswahl (siehe
build_abcde_cmd) und es gibt auch keine Oberflaeche, um eine
Teilmenge zu waehlen — eine Audio-CD wurde also immer
vollstaendig gerippt, egal wie der Schalter stand. Statt einer
Wahl, die nichts bewirkt, steht hier jetzt die Wahrheit. */}
<p className="text-xs text-slate-500 dark:text-slate-400 px-1">
💿 Eine Audio-CD wird immer vollständig gerippt alle Tracks als
FLAC, mit MusicBrainz-Tags.
</p>
<p className="text-xs font-bold uppercase tracking-wider pt-2 text-slate-400 dark:text-slate-500">
Allgemein
@@ -555,6 +564,24 @@ export default function SettingsPage() {
{ENCODER_BADGES[e]?.label || e}
</span>
))}
{(w.info?.cpu_kerne || w.info?.cpu_simd) && (
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400" title={w.info?.cpu_modell || ''}>
{w.info?.cpu_modell ? `${w.info.cpu_modell} · ` : ''}
{w.info?.cpu_kerne ? `${w.info.cpu_kerne} Kerne` : ''}
{w.info?.cpu_kerne && w.info?.cpu_simd ? ' · ' : ''}
{w.info?.cpu_simd ? `Vektorbefehle ${w.info.cpu_simd}` : ''}
</p>
)}
{simdWarnung(w.info?.cpu_simd) && (
<p className="mt-1 text-xs text-amber-600 dark:text-amber-400">
{simdWarnung(w.info?.cpu_simd)}
</p>
)}
{w.info?.handbrake_encoder && (
<p className="mt-1 text-xs text-slate-400 dark:text-slate-500 font-mono break-all">
HandBrake meldet: {w.info.handbrake_encoder}
</p>
)}
</div>
))
)}
+128 -9
View File
@@ -6,26 +6,133 @@ Remote-GPU-Worker meldet sich hier genauso wie der eingebaute CPU-Worker.
"""
import os
import platform
import re
import shutil
import subprocess
def erkenne_encoder() -> list:
"""Liste der verfügbaren Encoder-Backends auf dieser Maschine."""
gefunden = ["cpu-x264", "cpu-x265"] # HandBrake-Software-Encoder, immer dabei
HB_ENCODER_KOPF = re.compile(r"^-e,\s*--encoder\b")
# VAAPI: AMD (VCN) und Intel (QuickSync) melden sich über /dev/dri
if os.path.exists("/dev/dri/renderD128"):
# Reihenfolge = Anzeige-Reihenfolge im UI.
SIMD_STUFEN = ("avx512f", "avx2", "avx", "sse4_2")
def parse_encoder_liste(hilfe_text: str) -> list:
"""Video-Encoder, die DIESES HandBrake wirklich anbietet.
Quelle: `HandBrakeCLI --help`, Abschnitt `-e, --encoder <string>`
(AGENTS Regel D — im Worker-Image am 25.07.2026 gemessen, nicht geraten):
-e, --encoder <string> Select video encoder:
svt_av1
x264
x265
...
--encoder-preset <string>
Der Block endet an der nächsten Option — also an der ersten Zeile, deren
erstes nicht-leeres Zeichen ein Bindestrich ist.
"""
encoder = []
im_block = False
for zeile in (hilfe_text or "").splitlines():
inhalt = zeile.strip()
if not im_block:
if HB_ENCODER_KOPF.match(inhalt):
im_block = True
continue
if not inhalt:
continue
if inhalt.startswith("-"):
break
encoder.append(inhalt.split()[0])
return encoder
def leite_backends_ab(hb_encoder, dri_da: bool, nvidia_da: bool) -> list:
"""Übersetzt die HandBrake-Encoder-Namen in die Backends, die das UI zeigt.
Zwei Ehrlichkeits-Regeln, die vorher fehlten (Befund 25.07.2026):
* Ohne HandBrake gibt es KEINE Encoder. Vorher standen `cpu-x264` und
`cpu-x265` fest verdrahtet in der Liste („immer dabei") — ein reiner
Rip-Worker ohne HandBrake behauptete damit, komprimieren zu können.
* Hardware zählt nur, wenn das Gerät da ist UND HandBrake den Encoder
überhaupt kennt. Vorher genügte `/dev/dri` — das Worker-Image kann
laut eigener Auskunft aber gar keinen Hardware-Encoder.
"""
namen = {e.lower() for e in (hb_encoder or [])}
gefunden = []
if any(n.startswith("x264") for n in namen):
gefunden.append("cpu-x264")
if any(n.startswith("x265") for n in namen):
gefunden.append("cpu-x265")
if any(n.startswith("svt_av1") for n in namen):
gefunden.append("cpu-av1")
if dri_da and any(("qsv" in n) or ("vce" in n) or ("vaapi" in n) for n in namen):
gefunden.append("vaapi")
# NVENC: NVIDIA-Treiber im Container sichtbar
if shutil.which("nvidia-smi") or os.path.exists("/usr/lib/x86_64-linux-gnu/libnvidia-encode.so.1"):
if nvidia_da and any("nvenc" in n for n in namen):
gefunden.append("nvenc")
return gefunden
def beste_simd(flags_text: str) -> str:
"""Höchste Vektor-Befehlsstufe der CPU — entscheidend fürs Software-Encodieren.
Befund 25.07.2026: Die Rippy-VM läuft auf dem generischen QEMU-CPU-Modell
(„QEMU Virtual CPU version 2.5+") und hat deshalb KEIN AVX2, nur sse4_2.
x265 lebt von diesen Befehlen; ein 4K-Encode brauchte dort gemessene
28-55 Stunden. Sichtbar war das nirgends — genau diese Lücke schließt die
Anzeige. Abhilfe bei Proxmox/KVM: CPU-Typ der VM auf `host` stellen.
"""
vorhanden = set((flags_text or "").split())
for stufe in SIMD_STUFEN:
if stufe in vorhanden:
return stufe
return "unbekannt"
def _lies_cpuinfo() -> tuple:
"""(Modellname, Flags-Text, Kernzahl) aus /proc/cpuinfo — leer auf Windows."""
modell, flags, kerne = "", "", 0
try:
with open("/proc/cpuinfo", encoding="utf-8", errors="replace") as f:
for zeile in f:
if zeile.startswith("processor"):
kerne += 1
elif not modell and zeile.startswith("model name"):
modell = zeile.split(":", 1)[-1].strip()
elif not flags and zeile.startswith("flags"):
flags = zeile.split(":", 1)[-1].strip()
except OSError:
pass
return modell, flags, kerne
def hole_handbrake_hilfe() -> str:
"""`HandBrakeCLI --help` einmal abrufen (leer, wenn nicht installiert)."""
if not shutil.which("HandBrakeCLI"):
return ""
try:
aus = subprocess.run(
["HandBrakeCLI", "--help"], capture_output=True, text=True, timeout=30
)
return (aus.stdout or "") + (aus.stderr or "")
except (OSError, subprocess.TimeoutExpired):
return ""
def erkenne_encoder() -> list:
"""Encoder-Backends, die auf DIESER Maschine wirklich nutzbar sind."""
hb_encoder = parse_encoder_liste(hole_handbrake_hilfe())
dri_da = os.path.exists("/dev/dri/renderD128")
nvidia_da = bool(
shutil.which("nvidia-smi")
or os.path.exists("/usr/lib/x86_64-linux-gnu/libnvidia-encode.so.1")
)
return leite_backends_ab(hb_encoder, dri_da, nvidia_da)
def erkenne_ip() -> str:
"""Beste erratbare eigene IP (UDP-Route-Trick, KEIN echter Traffic).
@@ -56,11 +163,23 @@ def werkzeug_versionen() -> dict:
"""
import socket
modell, flags, kerne = _lies_cpuinfo()
info = {
# Zuordnung im UI: hostname (für den Celery-Online-Abgleich) + IP
"hostname": socket.gethostname(),
"ip": erkenne_ip(),
# Rechenleistung fürs Software-Encodieren. Ohne diese drei Angaben war
# am 25.07.2026 nicht zu sehen, dass ein 4K-Encode auf dieser Maschine
# Tage statt Stunden braucht (kein AVX2, 4 Kerne).
"cpu_modell": modell or (platform.processor() or "unbekannt"),
"cpu_kerne": str(kerne or os.cpu_count() or 0),
"cpu_simd": beste_simd(flags),
}
hb_encoder = parse_encoder_liste(hole_handbrake_hilfe())
if hb_encoder:
# Die ungefilterte Auskunft von HandBrake — damit im UI nachprüfbar ist,
# woher die Backend-Liste kommt.
info["handbrake_encoder"] = ",".join(hb_encoder)
if shutil.which("makemkvcon"):
info["makemkv"] = os.getenv("MAKEMKV_VERSION") or "installiert"
if shutil.which("HandBrakeCLI"):
+115
View File
@@ -0,0 +1,115 @@
"""Tests der Fähigkeiten-Erkennung — reine Funktionen, keine Infrastruktur.
Die Testdaten sind ECHTE Ausgaben, am 25.07.2026 im Worker-Image der Rippy-VM
abgenommen (AGENTS Regel D). Erfundene Formate hätten hier keinen Wert: genau
daran ist die Erkennung vorher gescheitert.
"""
import caps
# Wörtlich aus `docker exec rippy-worker-1 HandBrakeCLI --help` (HandBrake im
# Worker-Image, 25.07.2026). Bemerkenswert: KEIN Hardware-Encoder dabei.
HB_HILFE_ECHT = """
-Z, --preset <string> Select preset by name
-e, --encoder <string> Select video encoder:
svt_av1
svt_av1_10bit
x264
x264_10bit
x265
x265_10bit
x265_12bit
mpeg4
mpeg2
VP8
VP9
VP9_10bit
theora
--encoder-preset <string>
Adjust video encoding settings
"""
# Wie dieselbe Stelle auf einer Maschine MIT Hardware-Encodern aussieht
# (HandBrake-Windows-Build; Struktur identisch, nur mehr Namen).
HB_HILFE_MIT_HARDWARE = """
-e, --encoder <string> Select video encoder:
x264
x265
nvenc_h264
nvenc_h265
qsv_h264
qsv_h265
--encoder-preset <string>
"""
def test_encoder_liste_wird_aus_echter_hilfe_gelesen():
encoder = caps.parse_encoder_liste(HB_HILFE_ECHT)
assert encoder == [
"svt_av1", "svt_av1_10bit",
"x264", "x264_10bit",
"x265", "x265_10bit", "x265_12bit",
"mpeg4", "mpeg2", "VP8", "VP9", "VP9_10bit", "theora",
]
# Der Block endet an der nächsten Option — sie darf nicht mitgelesen werden
assert not any("encoder-preset" in e for e in encoder)
assert not any(e.startswith("-") for e in encoder)
def test_encoder_liste_ohne_handbrake_ist_leer():
assert caps.parse_encoder_liste("") == []
assert caps.parse_encoder_liste(None) == []
assert caps.parse_encoder_liste("irgendein Text ohne Encoder-Abschnitt") == []
def test_ohne_handbrake_keine_encoder():
"""Der wichtigste Fall: vorher standen cpu-x264/cpu-x265 fest verdrahtet in
der Liste. Ein Rip-Worker ohne HandBrake behauptete damit, komprimieren zu
können — und jeder Transcode dort endete sofort mit 'nicht installiert'."""
assert caps.leite_backends_ab([], dri_da=True, nvidia_da=True) == []
def test_backends_aus_echter_liste():
encoder = caps.parse_encoder_liste(HB_HILFE_ECHT)
# Kein /dev/dri, kein NVIDIA — und HandBrake kann hier ohnehin keine Hardware
assert caps.leite_backends_ab(encoder, dri_da=False, nvidia_da=False) == [
"cpu-x264", "cpu-x265", "cpu-av1",
]
def test_hardware_nur_wenn_geraet_UND_handbrake_es_kann():
"""Befund 25.07.2026: Vorher genügte /dev/dri, um 'vaapi' zu melden. Das
Worker-Image kann laut eigener Auskunft aber gar keinen Hardware-Encoder —
Rippy hätte VAAPI versprochen und wäre beim Encodieren gescheitert."""
ohne_hw = caps.parse_encoder_liste(HB_HILFE_ECHT)
assert "vaapi" not in caps.leite_backends_ab(ohne_hw, dri_da=True, nvidia_da=True)
assert "nvenc" not in caps.leite_backends_ab(ohne_hw, dri_da=True, nvidia_da=True)
mit_hw = caps.parse_encoder_liste(HB_HILFE_MIT_HARDWARE)
# Gerät fehlt → trotz Können kein Hardware-Backend
assert caps.leite_backends_ab(mit_hw, dri_da=False, nvidia_da=False) == [
"cpu-x264", "cpu-x265",
]
# Beides da → beides gemeldet
beides = caps.leite_backends_ab(mit_hw, dri_da=True, nvidia_da=True)
assert "vaapi" in beides # qsv_* vorhanden
assert "nvenc" in beides
def test_simd_stufe_erkennt_die_hoechste():
assert caps.beste_simd("fpu vme de sse4_2 avx avx2") == "avx2"
assert caps.beste_simd("sse4_2 avx avx2 avx512f") == "avx512f"
assert caps.beste_simd("fpu vme de pse tsc sse4_2") == "sse4_2"
assert caps.beste_simd("") == "unbekannt"
assert caps.beste_simd(None) == "unbekannt"
def test_simd_stufe_der_rippy_vm_ist_sse4_2():
"""Echt gemessen auf der Rippy-VM (25.07.2026): generisches QEMU-CPU-Modell,
`grep -c avx2 /proc/cpuinfo` = 0. Genau das macht Software-4K dort so lahm."""
flags_vm = (
"fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 "
"clflush mmx fxsr sse sse2 syscall nx lm rep_good nopl cpuid "
"pni cx16 sse4_1 sse4_2 x2apic popcnt aes hypervisor lahf_lm"
)
assert caps.beste_simd(flags_vm) == "sse4_2"