2026-08-04 17:33:15 +02:00
|
|
|
package playback
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"os"
|
|
|
|
|
"sync"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// fakeDeviceStat installs a stat function that reports only the given paths as
|
|
|
|
|
// present, restoring the real one on cleanup.
|
|
|
|
|
func fakeDeviceStat(t *testing.T, present ...string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
set := make(map[string]bool, len(present))
|
|
|
|
|
for _, p := range present {
|
|
|
|
|
set[p] = true
|
|
|
|
|
}
|
|
|
|
|
orig := hwDeviceStat
|
|
|
|
|
hwDeviceStat = func(path string) error {
|
|
|
|
|
if set[path] {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return os.ErrNotExist
|
|
|
|
|
}
|
|
|
|
|
t.Cleanup(func() { hwDeviceStat = orig })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func resetDeviceLoad(t *testing.T) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
hwDeviceLoad.mu.Lock()
|
|
|
|
|
hwDeviceLoad.counts = map[string]int{}
|
|
|
|
|
hwDeviceLoad.mu.Unlock()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestParseHWDeviceSet(t *testing.T) {
|
|
|
|
|
cases := []struct {
|
|
|
|
|
in string
|
|
|
|
|
want []string
|
|
|
|
|
}{
|
|
|
|
|
{"", nil},
|
|
|
|
|
{"/dev/dri/renderD128", []string{"/dev/dri/renderD128"}},
|
|
|
|
|
{"/dev/dri/renderD128,/dev/dri/renderD129", []string{"/dev/dri/renderD128", "/dev/dri/renderD129"}},
|
|
|
|
|
{" /dev/dri/renderD128 , /dev/dri/renderD129 ,", []string{"/dev/dri/renderD128", "/dev/dri/renderD129"}},
|
|
|
|
|
}
|
|
|
|
|
for _, tc := range cases {
|
|
|
|
|
got := ParseHWDeviceSet(tc.in)
|
|
|
|
|
if got.Empty() != (len(tc.want) == 0) || got.Multi() != (len(tc.want) > 1) {
|
|
|
|
|
t.Fatalf("ParseHWDeviceSet(%q) Empty/Multi mismatch for %v", tc.in, tc.want)
|
|
|
|
|
}
|
|
|
|
|
list := got.List()
|
|
|
|
|
if len(list) != len(tc.want) {
|
|
|
|
|
t.Fatalf("ParseHWDeviceSet(%q).List() = %v, want %v", tc.in, list, tc.want)
|
|
|
|
|
}
|
|
|
|
|
for i := range list {
|
|
|
|
|
if list[i] != tc.want[i] {
|
|
|
|
|
t.Fatalf("ParseHWDeviceSet(%q).List() = %v, want %v", tc.in, list, tc.want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAcquireHWDeviceEmptyValueStaysEmpty(t *testing.T) {
|
|
|
|
|
resetDeviceLoad(t)
|
|
|
|
|
device, release := AcquireHWDevice("", "qsv")
|
|
|
|
|
defer release()
|
|
|
|
|
if device != "" {
|
|
|
|
|
t.Fatalf("device = %q, want empty so auto-detection applies", device)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAcquireHWDeviceSingleValuePassesThrough(t *testing.T) {
|
|
|
|
|
resetDeviceLoad(t)
|
|
|
|
|
fakeDeviceStat(t) // nothing exists; single value must still pass through
|
|
|
|
|
for _, accel := range []string{"qsv", "vaapi", "nvenc", "none"} {
|
|
|
|
|
device, release := AcquireHWDevice("/dev/dri/renderD128", accel)
|
|
|
|
|
if device != "/dev/dri/renderD128" {
|
|
|
|
|
t.Fatalf("accel %s: device = %q, want explicit single value unchanged", accel, device)
|
|
|
|
|
}
|
|
|
|
|
release()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAcquireHWDeviceBalancesAcrossList(t *testing.T) {
|
|
|
|
|
resetDeviceLoad(t)
|
|
|
|
|
fakeDeviceStat(t, "/dev/dri/renderD128", "/dev/dri/renderD129")
|
|
|
|
|
configured := "/dev/dri/renderD128,/dev/dri/renderD129"
|
|
|
|
|
|
|
|
|
|
dev1, release1 := AcquireHWDevice(configured, "qsv")
|
|
|
|
|
if dev1 != "/dev/dri/renderD128" {
|
|
|
|
|
t.Fatalf("first workload device = %q, want first listed on tie", dev1)
|
|
|
|
|
}
|
|
|
|
|
dev2, release2 := AcquireHWDevice(configured, "vaapi")
|
|
|
|
|
if dev2 != "/dev/dri/renderD129" {
|
|
|
|
|
t.Fatalf("second workload device = %q, want least-loaded second device", dev2)
|
|
|
|
|
}
|
|
|
|
|
dev3, release3 := AcquireHWDevice(configured, "qsv")
|
|
|
|
|
if dev3 != "/dev/dri/renderD128" {
|
|
|
|
|
t.Fatalf("third workload device = %q, want round-back to first on tie", dev3)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Releasing the first workload makes renderD128 least-loaded again.
|
|
|
|
|
release1()
|
|
|
|
|
release3()
|
|
|
|
|
dev4, release4 := AcquireHWDevice(configured, "qsv")
|
|
|
|
|
if dev4 != "/dev/dri/renderD128" {
|
|
|
|
|
t.Fatalf("post-release device = %q, want freed first device", dev4)
|
|
|
|
|
}
|
|
|
|
|
release2()
|
|
|
|
|
release4()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAcquireHWDeviceSkipsMissingDevices(t *testing.T) {
|
|
|
|
|
resetDeviceLoad(t)
|
|
|
|
|
fakeDeviceStat(t, "/dev/dri/renderD129")
|
|
|
|
|
device, release := AcquireHWDevice("/dev/dri/renderD128,/dev/dri/renderD129", "qsv")
|
|
|
|
|
defer release()
|
|
|
|
|
if device != "/dev/dri/renderD129" {
|
|
|
|
|
t.Fatalf("device = %q, want the only present device", device)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAcquireHWDeviceAllMissingFallsBackToFirst(t *testing.T) {
|
|
|
|
|
resetDeviceLoad(t)
|
|
|
|
|
fakeDeviceStat(t) // none exist
|
|
|
|
|
device, release := AcquireHWDevice("/dev/dri/renderD128,/dev/dri/renderD129", "qsv")
|
|
|
|
|
defer release()
|
|
|
|
|
if device != "/dev/dri/renderD128" {
|
|
|
|
|
t.Fatalf("device = %q, want deterministic first entry when none exist", device)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAcquireHWDeviceNVENCMultiListUsesFirstWithoutReserving(t *testing.T) {
|
|
|
|
|
resetDeviceLoad(t)
|
|
|
|
|
fakeDeviceStat(t) // NVENC entries are CUDA indexes/UUIDs, never present as paths
|
|
|
|
|
device, release := AcquireHWDevice("0,1", "nvenc")
|
|
|
|
|
defer release()
|
|
|
|
|
if device != "0" {
|
|
|
|
|
t.Fatalf("device = %q, want first NVENC entry", device)
|
|
|
|
|
}
|
|
|
|
|
if got := hwDeviceActiveCount("0"); got != 0 {
|
|
|
|
|
t.Fatalf("active count = %d, want no reservation for NVENC", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAcquireHWDeviceSoftwareAccelDoesNotReserve(t *testing.T) {
|
|
|
|
|
resetDeviceLoad(t)
|
|
|
|
|
fakeDeviceStat(t, "/dev/dri/renderD128", "/dev/dri/renderD129")
|
|
|
|
|
configured := "/dev/dri/renderD128,/dev/dri/renderD129"
|
|
|
|
|
|
|
|
|
|
_, releaseNone := AcquireHWDevice(configured, "none")
|
|
|
|
|
defer releaseNone()
|
|
|
|
|
|
|
|
|
|
// A software workload must not shift the balance: the next GPU workload
|
|
|
|
|
// still lands on the first device.
|
|
|
|
|
device, release := AcquireHWDevice(configured, "qsv")
|
|
|
|
|
defer release()
|
|
|
|
|
if device != "/dev/dri/renderD128" {
|
|
|
|
|
t.Fatalf("device = %q, want first device unaffected by software workload", device)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAcquireHWDeviceReleaseIsIdempotent(t *testing.T) {
|
|
|
|
|
resetDeviceLoad(t)
|
|
|
|
|
fakeDeviceStat(t, "/dev/dri/renderD128", "/dev/dri/renderD129")
|
|
|
|
|
configured := "/dev/dri/renderD128,/dev/dri/renderD129"
|
|
|
|
|
|
|
|
|
|
_, release1 := AcquireHWDevice(configured, "qsv")
|
|
|
|
|
release1()
|
|
|
|
|
release1() // double release must not underflow the count
|
|
|
|
|
|
|
|
|
|
dev, release2 := AcquireHWDevice(configured, "qsv")
|
|
|
|
|
defer release2()
|
|
|
|
|
if dev != "/dev/dri/renderD128" {
|
|
|
|
|
t.Fatalf("device = %q, want first device after idempotent release", dev)
|
|
|
|
|
}
|
|
|
|
|
hwDeviceLoad.mu.Lock()
|
|
|
|
|
defer hwDeviceLoad.mu.Unlock()
|
|
|
|
|
for device, count := range hwDeviceLoad.counts {
|
|
|
|
|
if count < 0 {
|
|
|
|
|
t.Fatalf("device %s count = %d, want never negative", device, count)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-10 18:14:49 -04:00
|
|
|
func TestAcquireHWDeviceAvoidsFailedRenderDeviceAndReservesAlternate(t *testing.T) {
|
|
|
|
|
resetDeviceLoad(t)
|
|
|
|
|
fakeDeviceStat(t, "/dev/dri/renderD128", "/dev/dri/renderD129")
|
|
|
|
|
configured := "/dev/dri/renderD128,/dev/dri/renderD129"
|
|
|
|
|
got, release := acquireHWDevice(configured, "qsv", "/dev/dri/renderD128")
|
|
|
|
|
defer release()
|
|
|
|
|
if got != "/dev/dri/renderD129" {
|
|
|
|
|
t.Fatalf("alternate device = %q, want renderD129", got)
|
|
|
|
|
}
|
|
|
|
|
if active := hwDeviceActiveCount(got); active != 1 {
|
|
|
|
|
t.Fatalf("alternate device active count = %d, want 1", active)
|
|
|
|
|
}
|
|
|
|
|
if got, releaseNVENC := acquireHWDevice(configured, "nvenc", "/dev/dri/renderD128"); got != "/dev/dri/renderD128" {
|
|
|
|
|
releaseNVENC()
|
|
|
|
|
t.Fatalf("NVENC retry device = %q, want first configured device", got)
|
|
|
|
|
} else {
|
|
|
|
|
releaseNVENC()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 17:33:15 +02:00
|
|
|
func TestPickRenderDeviceExplicitValuePassesThrough(t *testing.T) {
|
|
|
|
|
// PickRenderDevice is auto-detection only; list resolution happens in
|
|
|
|
|
// AcquireHWDevice before args are built, so an explicit value — even a
|
|
|
|
|
// stale CSV — passes through untouched.
|
|
|
|
|
if got := PickRenderDevice("/dev/dri/renderD42"); got != "/dev/dri/renderD42" {
|
|
|
|
|
t.Fatalf("PickRenderDevice(single) = %q, want unchanged explicit value", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDetectHWAccelRenderDeviceDetails(t *testing.T) {
|
|
|
|
|
driDir := t.TempDir()
|
|
|
|
|
sysDir := t.TempDir()
|
|
|
|
|
for name, ids := range map[string][2]string{
|
|
|
|
|
"renderD128": {"0x8086", "0x56a6"},
|
|
|
|
|
"renderD129": {"0x10de", "0x2489"},
|
|
|
|
|
} {
|
|
|
|
|
if err := os.WriteFile(driDir+"/"+name, nil, 0o644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
devDir := sysDir + "/" + name + "/device"
|
|
|
|
|
if err := os.MkdirAll(devDir, 0o755); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(devDir+"/vendor", []byte(ids[0]+"\n"), 0o644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(devDir+"/device", []byte(ids[1]+"\n"), 0o644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
origDRI, origSys := defaultDRIDir, sysClassDRMDir
|
|
|
|
|
defaultDRIDir, sysClassDRMDir = driDir, sysDir
|
|
|
|
|
t.Cleanup(func() { defaultDRIDir, sysClassDRMDir = origDRI, origSys })
|
|
|
|
|
|
|
|
|
|
info := DetectHWAccel()
|
|
|
|
|
if len(info.RenderDeviceDetails) != 2 {
|
|
|
|
|
t.Fatalf("RenderDeviceDetails len = %d, want 2: %+v", len(info.RenderDeviceDetails), info.RenderDeviceDetails)
|
|
|
|
|
}
|
|
|
|
|
first, second := info.RenderDeviceDetails[0], info.RenderDeviceDetails[1]
|
|
|
|
|
if first.Path != driDir+"/renderD128" || first.Description != "Intel GPU (0x56a6)" {
|
|
|
|
|
t.Fatalf("first device = %+v, want Intel description", first)
|
|
|
|
|
}
|
|
|
|
|
if second.Path != driDir+"/renderD129" || second.Description != "NVIDIA GPU (0x2489)" {
|
|
|
|
|
t.Fatalf("second device = %+v, want NVIDIA description", second)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDescribeRenderDeviceUnknownVendor(t *testing.T) {
|
|
|
|
|
sysDir := t.TempDir()
|
|
|
|
|
devDir := sysDir + "/renderD130/device"
|
|
|
|
|
if err := os.MkdirAll(devDir, 0o755); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.WriteFile(devDir+"/vendor", []byte("0x1002\n"), 0o644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
origSys := sysClassDRMDir
|
|
|
|
|
sysClassDRMDir = sysDir
|
|
|
|
|
t.Cleanup(func() { sysClassDRMDir = origSys })
|
|
|
|
|
|
|
|
|
|
if got := describeRenderDevice("/dev/dri/renderD130"); got != "AMD GPU" {
|
|
|
|
|
t.Fatalf("describeRenderDevice() = %q, want AMD GPU without device id", got)
|
|
|
|
|
}
|
|
|
|
|
if got := describeRenderDevice("/dev/dri/renderD999"); got != "GPU" {
|
|
|
|
|
t.Fatalf("describeRenderDevice() = %q, want bare GPU for unreadable sysfs", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAcquireHWDeviceConcurrentStartsBalanceExactly(t *testing.T) {
|
|
|
|
|
resetDeviceLoad(t)
|
|
|
|
|
fakeDeviceStat(t, "/dev/dri/renderD128", "/dev/dri/renderD129")
|
|
|
|
|
configured := "/dev/dri/renderD128,/dev/dri/renderD129"
|
|
|
|
|
|
|
|
|
|
const workloads = 8
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
devices := make([]string, workloads)
|
|
|
|
|
releases := make([]func(), workloads)
|
|
|
|
|
for i := range workloads {
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
devices[i], releases[i] = AcquireHWDevice(configured, "qsv")
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
wg.Wait()
|
|
|
|
|
defer func() {
|
|
|
|
|
for _, release := range releases {
|
|
|
|
|
release()
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
counts := map[string]int{}
|
|
|
|
|
for _, device := range devices {
|
|
|
|
|
counts[device]++
|
|
|
|
|
}
|
|
|
|
|
// Atomic select+reserve guarantees an exact split; a two-step selection
|
|
|
|
|
// could pile concurrent starts onto one device.
|
|
|
|
|
if counts["/dev/dri/renderD128"] != workloads/2 || counts["/dev/dri/renderD129"] != workloads/2 {
|
|
|
|
|
t.Fatalf("concurrent workload split = %v, want exact %d/%d", counts, workloads/2, workloads/2)
|
|
|
|
|
}
|
|
|
|
|
}
|