238 lines
10 KiB
Python
238 lines
10 KiB
Python
"""GPU capability profiles (v4.8.0) — combined card generations.
|
||
|
||
Cards within the same hardware-encoder generation are functionally
|
||
identical for transcoding, so the dropdown lists CAPABILITY CLASSES,
|
||
not individual SKUs: one Pascal entry covers the GTX 10-series, Tesla
|
||
P40/P4/P100 and mobile chips; one Turing entry covers RTX 20-series,
|
||
GTX 16-series, the Tesla T4 and the crypto-era CMP 30/40/50HX cards.
|
||
|
||
Oddballs are included with their real capabilities:
|
||
- CMP 90HX is GA102-based (Ampere NVENC), but CMP 170HX is GA100-based
|
||
and has NO NVENC at all (like A100/V100/H100 compute boards).
|
||
- Intel Arc (QSV) and AMD RDNA (VAAPI) cover the rest of the trending
|
||
list; RDNA 3 added AV1 encode, RDNA 1/2 and GCN can only encode
|
||
H.264/HEVC.
|
||
|
||
Pure data + pure functions: no I/O, safe to import anywhere.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from dataclasses import dataclass
|
||
|
||
# ──────────────────────────────────────────────
|
||
# GPU PROFILES
|
||
# ──────────────────────────────────────────────
|
||
|
||
@dataclass(frozen=True)
|
||
class GpuProfile:
|
||
key: str # stable id (CLI/UI)
|
||
label: str # dropdown entry
|
||
vendor: str # nvidia | amd | intel | none
|
||
api: str # nvenc | vaapi | qsv | none
|
||
# codec family → ffmpeg encoder name
|
||
encoders: dict[str, str]
|
||
# case-insensitive substrings matched against the detected GPU name
|
||
# (nvidia-smi / lspci) for auto-detection. First match wins; lists
|
||
# are ordered most-specific first.
|
||
match: tuple[str, ...] = ()
|
||
notes: str = ""
|
||
# extra args that must come BEFORE -i (hardware device init)
|
||
hw_device_args: tuple[str, ...] = ()
|
||
# filter-chain fragment required before the encoder (vaapi hwupload)
|
||
filter_tail: tuple[str, ...] = ()
|
||
|
||
|
||
# NVENC encoders by generation class. Quality control: -rc vbr -cq N
|
||
# (-b:v 0). 8-bit yuv420p everywhere — Pascal 10-bit HEVC runs at half
|
||
# speed and the archival targets here are 8-bit sources.
|
||
_NV = {"h264": "h264_nvenc", "hevc": "hevc_nvenc"}
|
||
|
||
GPU_PROFILES: list[GpuProfile] = [
|
||
GpuProfile(
|
||
key="nv-kepler-maxwell",
|
||
label="NVIDIA Kepler / Maxwell 1.0 (GTX 600/700/800M) — H.264 only",
|
||
vendor="nvidia", api="nvenc",
|
||
encoders={"h264": "h264_nvenc"},
|
||
match=("GTX 6", "GTX 7", "GT 7", "GTX 8", "GT 8", "840M", "860M", "750"),
|
||
notes="First NVENC generations: H.264 only, no HEVC.",
|
||
),
|
||
GpuProfile(
|
||
key="nv-pascal",
|
||
label="NVIDIA Pascal (GTX 10-series, TITAN Xp, Tesla P40/P4/P100) — H.264 + HEVC 8/10-bit",
|
||
vendor="nvidia", api="nvenc",
|
||
encoders=dict(_NV),
|
||
match=("GTX 10", "1070", "1080", "1060", "1050", "TITAN Xp",
|
||
"Tesla P40", "Tesla P4", "P100", "Quadro P"),
|
||
notes="Pascal NVENC: HEVC Main/Main10. 10-bit runs at ~half speed.",
|
||
),
|
||
GpuProfile(
|
||
key="nv-turing",
|
||
label="NVIDIA Turing (RTX 20-series, GTX 16-series, Tesla T4, CMP 30/40/50HX) — H.264 + HEVC + B-frames",
|
||
vendor="nvidia", api="nvenc",
|
||
encoders=dict(_NV),
|
||
match=("RTX 20", "GTX 16", "2060", "2070", "2080", "1660", "1650",
|
||
"Tesla T4", "CMP 30", "CMP 40", "CMP 50"),
|
||
notes="Turing NVENC: first gen with HEVC B-frames; big quality jump.",
|
||
),
|
||
GpuProfile(
|
||
key="nv-compute",
|
||
label="NVIDIA data-center compute (V100/A100/H100, CMP 170HX) — no NVENC (CPU path)",
|
||
vendor="nvidia", api="none",
|
||
encoders={},
|
||
match=("V100", "A100", "H100", "B200", "GB200", "CMP 170"),
|
||
notes="Compute boards ship without NVENC silicon. CMP 170HX is "
|
||
"GA100-based — the fastest mining card that cannot hardware-encode.",
|
||
),
|
||
GpuProfile(
|
||
key="nv-ampere",
|
||
label="NVIDIA Ampere (RTX 30-series, A10/A40/A2, CMP 90HX) — H.264 + HEVC (no AV1 encode)",
|
||
vendor="nvidia", api="nvenc",
|
||
encoders=dict(_NV),
|
||
match=("RTX 30", "3090", "3080", "3070", "3060", "3050",
|
||
"A10", "A40", "CMP 90"),
|
||
notes="Ampere added AV1 DECODE but not encode — AV1 stays on CPU.",
|
||
),
|
||
GpuProfile(
|
||
key="nv-ada",
|
||
label="NVIDIA Ada / Blackwell (RTX 40/50-series, L4/L40) — H.264 + HEVC + AV1 10-bit",
|
||
vendor="nvidia", api="nvenc",
|
||
encoders={"h264": "h264_nvenc", "hevc": "hevc_nvenc", "av1": "av1_nvenc"},
|
||
match=("RTX 40", "RTX 50", "4090", "4080", "4070", "4060",
|
||
"5090", "5080", "5070", "5060", "L4", "L40"),
|
||
notes="Ada introduced AV1 NVENC; Blackwell doubles AV1 throughput.",
|
||
),
|
||
GpuProfile(
|
||
key="intel-arc",
|
||
label="Intel Arc (Alchemist A-series, Battlemage B-series) — QSV: H.264 + HEVC + AV1",
|
||
vendor="intel", api="qsv",
|
||
encoders={"h264": "h264_qsv", "hevc": "hevc_qsv", "av1": "av1_qsv"},
|
||
match=("Arc A", "Arc B", "A380", "A750", "A770", "B570", "B580"),
|
||
notes="Arc media engines encode AV1 8/10-bit — best value encode card.",
|
||
),
|
||
GpuProfile(
|
||
key="intel-xe",
|
||
label="Intel Iris / UHD integrated (Gen9–Xe) — QSV: H.264 + HEVC",
|
||
vendor="intel", api="qsv",
|
||
encoders={"h264": "h264_qsv", "hevc": "hevc_qsv"},
|
||
match=("Iris", "UHD", "HD Graphics"),
|
||
notes="Integrated media engines; HEVC 8/10-bit, no AV1 encode.",
|
||
),
|
||
GpuProfile(
|
||
key="amd-rdna3",
|
||
label="AMD RDNA 3 (RX 7000-series) — VAAPI: H.264 + HEVC + AV1",
|
||
vendor="amd", api="vaapi",
|
||
encoders={"h264": "h264_vaapi", "hevc": "hevc_vaapi", "av1": "av1_vaapi"},
|
||
match=("RX 7", "7900", "7800", "7700", "7600"),
|
||
notes="RDNA 3 VCN: first AMD generation with AV1 encode.",
|
||
),
|
||
GpuProfile(
|
||
key="amd-rdna12",
|
||
label="AMD RDNA 1/2 (RX 5000/6000-series) — VAAPI: H.264 + HEVC (AV1 decode only)",
|
||
vendor="amd", api="vaapi",
|
||
encoders={"h264": "h264_vaapi", "hevc": "hevc_vaapi"},
|
||
match=("RX 5", "RX 6", "5700", "5600", "6800", "6700", "6600", "6500"),
|
||
notes="RDNA 2 has AV1 decode only — AV1 encode stays on CPU.",
|
||
),
|
||
GpuProfile(
|
||
key="amd-gcn",
|
||
label="AMD GCN 4/5 / Vega (RX 400/500, Vega 56/64) — VAAPI: H.264 + HEVC",
|
||
vendor="amd", api="vaapi",
|
||
encoders={"h264": "h264_vaapi", "hevc": "hevc_vaapi"},
|
||
match=("RX 4", "RX 5", "Vega", "580", "570", "480", "470", "64", "56"),
|
||
notes="The classic crypto-era mining cards (Polaris/Vega).",
|
||
),
|
||
GpuProfile(
|
||
key="cpu",
|
||
label="None (CPU-only encode)",
|
||
vendor="none", api="none",
|
||
encoders={},
|
||
),
|
||
]
|
||
|
||
_GPU_PROFILES_BY_KEY: dict[str, GpuProfile] = {p.key: p for p in GPU_PROFILES}
|
||
|
||
|
||
def gpu_profile_by_key(key: str | None) -> GpuProfile | None:
|
||
if not key:
|
||
return None
|
||
return _GPU_PROFILES_BY_KEY.get(key)
|
||
|
||
|
||
def match_gpu_profile(gpu_name: str) -> GpuProfile | None:
|
||
"""Best-effort auto-detection from a GPU name string (nvidia-smi or
|
||
lspci output). Case-insensitive; first matching profile wins (the
|
||
match lists are ordered most-specific first, and the compute boards
|
||
are matched before the consumer generations they share names with —
|
||
e.g. 'CMP 170HX' must not hit the Ampere 'A10' style entries)."""
|
||
if not gpu_name:
|
||
return None
|
||
name = gpu_name.lower()
|
||
for profile in GPU_PROFILES:
|
||
for frag in profile.match:
|
||
if frag.lower() in name:
|
||
return profile
|
||
return None
|
||
|
||
|
||
# ──────────────────────────────────────────────
|
||
# FFMPEG ARG HELPERS (per hardware API)
|
||
# ──────────────────────────────────────────────
|
||
|
||
def encoder_for_family(profile: GpuProfile | None, family: str) -> str | None:
|
||
"""Hardware encoder name for a codec family on this profile, or None."""
|
||
if not profile or profile.api == "none":
|
||
return None
|
||
return profile.encoders.get(family)
|
||
|
||
|
||
def resolve_vaapi_device() -> str:
|
||
"""First render node, or the classic fallback path. (Best-effort I/O —
|
||
callers that need purity pass the result into encoder_pre_args.)"""
|
||
import glob
|
||
nodes = sorted(glob.glob("/dev/dri/renderD*"))
|
||
return nodes[0] if nodes else "/dev/dri/renderD128"
|
||
|
||
|
||
def encoder_pre_args(profile: GpuProfile, vaapi_device: str | None = None) -> list[str]:
|
||
"""Args that must precede -i (hardware device initialisation)."""
|
||
if profile.api == "vaapi":
|
||
dev = vaapi_device or resolve_vaapi_device()
|
||
return ["-vaapi_device", dev]
|
||
if profile.api == "qsv":
|
||
return ["-init_hw_device", "qsv=hw"]
|
||
return []
|
||
|
||
|
||
def encoder_filter_chain(profile: GpuProfile) -> list[str]:
|
||
"""Filter args that upload software frames to the hardware surface
|
||
format (VAAPI encoders only accept hw frames; -vaapi_device makes
|
||
its device the default for hwupload)."""
|
||
if profile.api == "vaapi":
|
||
return ["-vf", "format=nv12,hwupload"]
|
||
return []
|
||
|
||
|
||
def encoder_quality_args(api: str, encoder: str, crf: int, preset: int) -> list[str]:
|
||
"""Constant-quality args for a hardware encoder. NVENC maps the CPU
|
||
preset tiers to p-presets; QSV uses very_fast/medium; VAAPI uses CQP
|
||
rate mode which has no preset knob."""
|
||
if api == "nvenc":
|
||
if preset <= 6:
|
||
p = "p7"
|
||
elif preset <= 8:
|
||
p = "p5"
|
||
else:
|
||
p = "p4"
|
||
return ["-preset", p, "-tune", "hq", "-rc", "vbr",
|
||
"-cq", str(crf), "-b:v", "0", "-pix_fmt", "yuv420p",
|
||
"-g", "240"]
|
||
if api == "qsv":
|
||
p = "veryslow" if preset <= 6 else ("medium" if preset <= 8 else "very_fast")
|
||
return ["-preset", p, "-global_quality", str(crf),
|
||
"-pix_fmt", "yuv420p", "-g", "240"]
|
||
if api == "vaapi":
|
||
return ["-rc_mode", "CQP", "-qp", str(crf), "-g", "240"]
|
||
return []
|