304 lines
9.1 KiB
JavaScript
304 lines
9.1 KiB
JavaScript
import test from 'brittle'
|
|
import {
|
|
bareOsQvacParseVulkaninfo,
|
|
bareOsQvacParseNvidiaSmi,
|
|
bareOsQvacMergeNvidiaHints,
|
|
bareOsQvacRankGpus,
|
|
bareOsQvacMainGpuCandidates,
|
|
bareOsQvacProbeGpus
|
|
} from './lib/services/bare-os-qvac-gpu-probe.mjs'
|
|
|
|
const SAMPLE_VULKAN = `
|
|
GPU0:
|
|
apiVersion = 1.3.0
|
|
driverVersion = 1
|
|
vendorID = 0x8086
|
|
deviceID = 0x9a49
|
|
deviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU
|
|
deviceName = Intel(R) UHD Graphics (TGL GT1)
|
|
memoryHeaps: count = 1
|
|
memoryHeaps[0]:
|
|
size = 536870912 (0x20000000) (512.00 MiB)
|
|
flags: count = 1
|
|
MEMORY_HEAP_DEVICE_LOCAL_BIT
|
|
|
|
GPU1:
|
|
apiVersion = 1.3.0
|
|
driverVersion = 1
|
|
vendorID = 0x10de
|
|
deviceID = 0x25a0
|
|
deviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU
|
|
deviceName = NVIDIA GeForce RTX 3050 Ti Laptop GPU
|
|
memoryHeaps: count = 2
|
|
memoryHeaps[0]:
|
|
size = 4294967296 (0x100000000) (4096.00 MiB)
|
|
flags: count = 1
|
|
MEMORY_HEAP_DEVICE_LOCAL_BIT
|
|
memoryHeaps[1]:
|
|
size = 12582912 (0xc00000) (12.00 MiB)
|
|
flags: count = 0
|
|
`
|
|
|
|
test('parse vulkaninfo ranks NVIDIA above Intel by VRAM', async (t) => {
|
|
const gpus = bareOsQvacRankGpus(bareOsQvacParseVulkaninfo(SAMPLE_VULKAN))
|
|
t.is(gpus.length, 2)
|
|
t.is(gpus[0].index, 1)
|
|
t.ok(gpus[0].name.includes('NVIDIA'))
|
|
t.is(gpus[0].memoryBytes, 4294967296)
|
|
t.is(gpus[1].index, 0)
|
|
t.ok(gpus[1].memoryBytes < gpus[0].memoryBytes)
|
|
|
|
const cands = bareOsQvacMainGpuCandidates(gpus)
|
|
t.is(cands[0], 1)
|
|
t.ok(cands.includes('dedicated'))
|
|
})
|
|
|
|
test('nvidia-smi parse + candidates avoid trusting smi index as vulkan', async (t) => {
|
|
const n = bareOsQvacParseNvidiaSmi('NVIDIA GeForce RTX 3050 Ti Laptop GPU, 4096\n')
|
|
t.is(n.length, 1)
|
|
t.is(n[0].memoryBytes, 4096 * 1024 * 1024)
|
|
const synth = n.map((x, i) => ({
|
|
index: i,
|
|
name: x.name,
|
|
memoryBytes: x.memoryBytes,
|
|
deviceType: 'PHYSICAL_DEVICE_TYPE_DISCRETE_GPU',
|
|
source: 'nvidia-smi'
|
|
}))
|
|
const cands = bareOsQvacMainGpuCandidates(synth)
|
|
t.is(cands[0], 'dedicated')
|
|
t.is(cands[1], 1)
|
|
})
|
|
|
|
test('merge nvidia hints upgrades vulkan heap estimate', async (t) => {
|
|
const vulkan = bareOsQvacParseVulkaninfo(`
|
|
GPU0:
|
|
deviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU
|
|
deviceName = NVIDIA GeForce RTX 3050 Ti Laptop GPU
|
|
`)
|
|
t.is(vulkan[0].memoryBytes, 8 * 1024 * 1024 * 1024) // discrete heuristic
|
|
const merged = bareOsQvacMergeNvidiaHints(vulkan, [
|
|
{ name: 'NVIDIA GeForce RTX 3050 Ti Laptop GPU', memoryBytes: 4096 * 1024 * 1024 }
|
|
])
|
|
// hint is smaller than discrete heuristic — keep larger
|
|
t.is(merged[0].memoryBytes, 8 * 1024 * 1024 * 1024)
|
|
const mergedUp = bareOsQvacMergeNvidiaHints(
|
|
[{ ...vulkan[0], memoryBytes: 512 * 1024 * 1024 }],
|
|
[{ name: 'NVIDIA GeForce RTX 3050 Ti Laptop GPU', memoryBytes: 4096 * 1024 * 1024 }]
|
|
)
|
|
t.is(mergedUp[0].memoryBytes, 4096 * 1024 * 1024)
|
|
})
|
|
|
|
test('probe with injected runCommand', async (t) => {
|
|
const runCommand = async (file, args) => {
|
|
if (file === 'system_profiler') {
|
|
return JSON.stringify({
|
|
SPDisplaysDataType: [{ _name: 'Apple M2 Pro' }]
|
|
})
|
|
}
|
|
if (file === 'vulkaninfo') return SAMPLE_VULKAN
|
|
if (file === 'nvidia-smi') return 'NVIDIA GeForce RTX 3050 Ti Laptop GPU, 4096\n'
|
|
return null
|
|
}
|
|
const ranked = await bareOsQvacProbeGpus({ runCommand })
|
|
if (ranked[0] && ranked[0].source === 'metal') {
|
|
// macOS probe path (system_profiler → ggml-metal).
|
|
t.is(ranked.length, 1)
|
|
t.is(ranked[0].index, 0)
|
|
t.ok(ranked[0].name.includes('Apple'))
|
|
} else {
|
|
t.is(ranked[0].index, 1)
|
|
t.ok(String(ranked[0].source).includes('vulkaninfo'))
|
|
}
|
|
})
|
|
|
|
test('parse system_profiler displays JSON → metal GPU', async (t) => {
|
|
const { bareOsQvacParseSystemProfilerDisplays, bareOsQvacParseMetalMemory } =
|
|
await import('./lib/services/bare-os-qvac-gpu-probe.mjs')
|
|
const gpus = bareOsQvacParseSystemProfilerDisplays(
|
|
JSON.stringify({
|
|
SPDisplaysDataType: [
|
|
{
|
|
_name: 'Apple M2 Pro',
|
|
spdisplays_metal: 'spdisplays_metal4',
|
|
spdisplays_vram_shared: '18 GB'
|
|
}
|
|
]
|
|
})
|
|
)
|
|
t.is(gpus.length, 1)
|
|
t.is(gpus[0].index, 0)
|
|
t.ok(gpus[0].name.includes('Apple'))
|
|
t.is(gpus[0].memoryBytes, 18 * 1024 * 1024 * 1024)
|
|
t.is(gpus[0].source, 'metal')
|
|
|
|
// Same GPU listed once per attached display must dedupe.
|
|
const dupes = bareOsQvacParseSystemProfilerDisplays(
|
|
JSON.stringify({
|
|
SPDisplaysDataType: [{ _name: 'Apple M2 Pro' }, { _name: 'Apple M2 Pro' }]
|
|
})
|
|
)
|
|
t.is(dupes.length, 1)
|
|
|
|
t.is(
|
|
bareOsQvacParseMetalMemory('Shared system memory: 18 GB'),
|
|
18 * 1024 * 1024 * 1024
|
|
)
|
|
t.is(bareOsQvacParseMetalMemory(''), 0)
|
|
})
|
|
|
|
test('software Vulkan devices are detected and ranked last', async (t) => {
|
|
const {
|
|
bareOsQvacIsSoftwareVulkanDevice,
|
|
bareOsQvacHardwareGpus,
|
|
bareOsQvacScoreVulkanIcdFilename,
|
|
bareOsQvacDiscoverHardwareVulkanIcds,
|
|
bareOsQvacApplyHardwareVulkanEnv
|
|
} = await import('./lib/services/bare-os-qvac-gpu-probe.mjs')
|
|
|
|
t.ok(
|
|
bareOsQvacIsSoftwareVulkanDevice({
|
|
name: 'llvmpipe (LLVM 22.1.8, 256 bits)',
|
|
deviceType: 'PHYSICAL_DEVICE_TYPE_CPU'
|
|
})
|
|
)
|
|
t.absent(
|
|
bareOsQvacIsSoftwareVulkanDevice({
|
|
name: 'NVIDIA GeForce RTX 3050 Ti',
|
|
deviceType: 'PHYSICAL_DEVICE_TYPE_DISCRETE_GPU'
|
|
})
|
|
)
|
|
|
|
const mixed = bareOsQvacRankGpus([
|
|
{
|
|
index: 0,
|
|
name: 'llvmpipe',
|
|
memoryBytes: 64 * 1024 * 1024 * 1024,
|
|
deviceType: 'PHYSICAL_DEVICE_TYPE_CPU',
|
|
source: 'vulkaninfo'
|
|
},
|
|
{
|
|
index: 1,
|
|
name: 'NVIDIA GeForce RTX 3050 Ti',
|
|
memoryBytes: 4 * 1024 * 1024 * 1024,
|
|
deviceType: 'PHYSICAL_DEVICE_TYPE_DISCRETE_GPU',
|
|
source: 'vulkaninfo'
|
|
}
|
|
])
|
|
t.is(mixed[0].index, 1)
|
|
t.is(bareOsQvacHardwareGpus(mixed).length, 1)
|
|
|
|
t.ok(bareOsQvacScoreVulkanIcdFilename('nvidia_icd.json') > 0)
|
|
t.ok(bareOsQvacScoreVulkanIcdFilename('lvp_icd.x86_64.json') < 0)
|
|
|
|
const icds = bareOsQvacDiscoverHardwareVulkanIcds({
|
|
dirs: ['/fake/icd'],
|
|
existsSync: () => true,
|
|
readdirSync: () => [
|
|
'lvp_icd.x86_64.json',
|
|
'nvidia_icd.json',
|
|
'intel_icd.x86_64.json'
|
|
]
|
|
})
|
|
t.alike(icds, [
|
|
'/fake/icd/nvidia_icd.json',
|
|
'/fake/icd/intel_icd.x86_64.json'
|
|
])
|
|
|
|
/** @type {Record<string, string>} */
|
|
const procEnv = {}
|
|
const applied = bareOsQvacApplyHardwareVulkanEnv(
|
|
{},
|
|
{
|
|
procEnv,
|
|
icds: ['/usr/share/vulkan/icd.d/nvidia_icd.json'],
|
|
hasNvidiaSmi: true
|
|
}
|
|
)
|
|
t.ok(applied.preferNvidia)
|
|
t.ok(String(procEnv.VK_LOADER_DRIVERS_DISABLE).includes('lvp'))
|
|
t.is(procEnv.VK_LOADER_DRIVERS_SELECT, '*nvidia*')
|
|
t.is(procEnv.__NV_PRIME_RENDER_OFFLOAD, '1')
|
|
t.ok(String(procEnv.VK_DRIVER_FILES).includes('nvidia_icd.json'))
|
|
})
|
|
|
|
test('AMD RADV is preferred over unused Mesa ICDs', async (t) => {
|
|
const {
|
|
bareOsQvacScoreVulkanIcdFilename,
|
|
bareOsQvacVulkanIcdVendor,
|
|
bareOsQvacPreferredVulkanIcds,
|
|
bareOsQvacDiscoverHardwareVulkanIcds,
|
|
bareOsQvacApplyHardwareVulkanEnv
|
|
} = await import('./lib/services/bare-os-qvac-gpu-probe.mjs')
|
|
|
|
t.is(bareOsQvacVulkanIcdVendor('radeon_icd.json'), 'amd')
|
|
t.is(bareOsQvacVulkanIcdVendor('gfxstream_vk_icd.json'), 'virtual')
|
|
t.ok(
|
|
bareOsQvacScoreVulkanIcdFilename('radeon_icd.json') >
|
|
bareOsQvacScoreVulkanIcdFilename('intel_icd.json')
|
|
)
|
|
t.ok(bareOsQvacScoreVulkanIcdFilename('gfxstream_vk_icd.json') < 0)
|
|
|
|
const icds = bareOsQvacDiscoverHardwareVulkanIcds({
|
|
dirs: ['/fake/icd'],
|
|
existsSync: () => true,
|
|
readdirSync: () => [
|
|
'radeon_icd.json',
|
|
'intel_hasvk_icd.json',
|
|
'intel_icd.json',
|
|
'asahi_icd.json',
|
|
'gfxstream_vk_icd.json',
|
|
'nouveau_icd.json',
|
|
'virtio_icd.json',
|
|
'lvp_icd.json'
|
|
]
|
|
})
|
|
t.ok(icds[0].endsWith('radeon_icd.json'))
|
|
t.absent(icds.some((p) => /gfxstream|virtio|lvp/.test(p)))
|
|
|
|
const preferred = bareOsQvacPreferredVulkanIcds(icds)
|
|
t.is(preferred.vendor, 'amd')
|
|
t.alike(preferred.icds, ['/fake/icd/radeon_icd.json'])
|
|
t.is(preferred.selectGlob, '*radeon*,*amd*,*radv*')
|
|
|
|
/** @type {Record<string, string>} */
|
|
const procEnv = {}
|
|
const applied = bareOsQvacApplyHardwareVulkanEnv(
|
|
{},
|
|
{
|
|
procEnv,
|
|
icds
|
|
}
|
|
)
|
|
t.ok(applied.preferAmd)
|
|
t.absent(applied.preferNvidia)
|
|
t.is(applied.vendor, 'amd')
|
|
t.is(procEnv.VK_DRIVER_FILES, '/fake/icd/radeon_icd.json')
|
|
t.is(procEnv.VK_LOADER_DRIVERS_SELECT, '*radeon*,*amd*,*radv*')
|
|
t.ok(applied.note.includes('RADV/AMD'))
|
|
})
|
|
|
|
test('nvidia-smi without NVIDIA ICD keeps hardware list and SELECT', async (t) => {
|
|
const { bareOsQvacApplyHardwareVulkanEnv } = await import(
|
|
'./lib/services/bare-os-qvac-gpu-probe.mjs'
|
|
)
|
|
/** @type {Record<string, string>} */
|
|
const procEnv = {}
|
|
const applied = bareOsQvacApplyHardwareVulkanEnv(
|
|
{},
|
|
{
|
|
procEnv,
|
|
hasNvidiaSmi: true,
|
|
icds: [
|
|
'/fake/icd/intel_icd.json',
|
|
'/fake/icd/intel_hasvk_icd.json',
|
|
'/fake/icd/asahi_icd.json'
|
|
]
|
|
}
|
|
)
|
|
t.ok(applied.preferNvidia)
|
|
t.absent(applied.preferAmd)
|
|
t.is(applied.vendor, 'nvidia')
|
|
t.ok(String(procEnv.VK_DRIVER_FILES).includes('intel_icd.json'))
|
|
t.is(procEnv.VK_LOADER_DRIVERS_SELECT, '*nvidia*')
|
|
})
|