Changelog
All notable changes to MVGAL (Multi-Vendor GPU Aggregation Layer) are documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[0.7.31] - 2026-10-30
Field-report-driven correctness release addressing the 0.7.30 test report on a
single-GPU Secure Boot host (Fedora 43, kernel 7.2.13-200.fc44.x86_64, Intel
UHD Graphics 770, plus a blacklisted MTT S30). COPR build #11229933 succeeded
on fedora-43-x86_64.
Fixed
- C1 — Kernel module and userspace disagreed on GPU identity/index. The
kernel module enumerated the MTT S30 via PCI and assigned it GPU index 0, but
userspace (DRM/sysfs) could not see it (driver blacklisted) and assigned index
0 to the Intel GPU instead.
mvgal_enumerate_gpus()now includesPCI_CLASS_DISPLAY_OTHER(0x0380) so Intel integrated GPUs are found alongside discrete cards, and skips display-class PCI devices whose native driver is not bound —mvgal_native_driver()checkspdev->driver, which isNULLwhen no driver owns the function. The userspace device registry (enumeratePciDevices) applies the same class filter and warns when a PCI GPU has no DRM node. Both sides now agree on which GPUs exist and their indices. The Intel PCI ID0x4680comment is corrected from "DG2" to "UHD Graphics 770 (Alder Lake)". - C2 — Contradictory MTT backend messages.
kernel/vendors/mvgal_mtt.cchecked the non-existentCONFIG_MVGal_MTT(a typo forCONFIG_MVGAL_MTTthat never appeared in any Kconfig), soMVGAL_MTT_ENABLEDwas always 0 andmvgal_mtt_init()returned-ENOTSUPP, producing the contradictory messages "Registered Moore Threads GPU backend" (frommtt_main.c) followed by "Moore Threads support not compiled in" (frommtt.c). The guard is nowMVGAL_FULL_STACK, the real compile-time symbol fromKbuildccflags. Thefind_module("mtgpu")/find_module("mtgpu_drv")calls — removed from the kernel symbol table in 7.x and causing amodpostbuild failure — are replaced by a/proc/modulesread viafilp_open+kernel_read. - C3 — Group/permission mismatch locked users out of
/dev/mvgal0. The udev rulesconfig/99-mvgal.rulesandpackaging/99-mvgal.rulessetGROUP="video"andMODE="0666", but the daemon and CLI tools use themvgalgroup. Users invideobut notmvgalwere locked out of/dev/mvgal0despite being members of the video group. Both rules now setGROUP="mvgal"andMODE="0660". The privileged helper (mvgal-pkexec-helper.sh) nowchown root:mvgal(wasroot:video) on device nodes after module load. - C4 —
listen_addralready correct. The[network]section default was confirmed as127.0.0.1from v0.7.30; no code change needed. - H1 — Blacklisted GPUs were silently omitted. The MTT S30 was present on
PCI but invisible to MVGAL because its driver was blacklisted, with no warning.
mvgal-enumandmvgal list-gpusnow scan/sys/bus/pci/devices/for display-class PCI devices (classes 0x0300, 0x0302, 0x0380) with known GPU vendor IDs (AMD 0x1002, NVIDIA 0x10de, Intel 0x8086, MTT 0x1ed5), check whether each has a DRM card node, and print a warning to stderr when a GPU has no DRM node — naming the PCI BDF, vendor, and the likely cause (blacklisted or missing driver). The daemon'sDeviceRegistry::enumeratePciDevices()emits the same warning with the bound driver name (or "no driver bound"). - H2 —
mvgal startdid not report degraded mode. After launching the daemon successfully (both systemd and fork paths),mvgal startnow checks for/sys/module/mvgaland prints a degraded-mode warning to stderr if the kernel module is absent or failed to load, with remediation steps (enroll the MOK, thenpkexec mvgal-load).
Added
- MTT S30 PCI ID in the kernel table.
{ PCI_DEVICE(0x1ED5, 0x0102) }is now in themvgal_gpu_pci_ids[]table (kernel/mvgal_core.c), so the MTT S30 is recognized when its driver is bound.
Packaging
- COPR build #11229933 succeeded on
fedora-43-x86_64.
[0.7.30] - 2026-10-28
Field-report-driven correctness and packaging release from the 0.7.30 test campaign on a single-GPU Secure Boot host (Fedora 44, kernel 7.2.12-200.fc44.x86_64, Intel UHD Graphics 770). Multi-GPU aggregation could not be exercised (single GPU); the kernel module path remains unavailable under Secure Boot until the MOK is enrolled (see SECURE_BOOT.md). COPR build #11217288 succeeded on all 18 enabled chroots.
Fixed
- The Vulkan aggregated ICD opt-in broke every Vulkan app ([2], [13]) —
mvgal-vulkan-env.shsetVK_ICD_FILENAMES/VK_DRIVER_FILESto only/usr/share/vulkan/mvgal/mvgal_icd.json(withMVGAL_ICD_ONLY=1). That ICD deliberately reports zero physical devices when the kernel module is absent, so sourcing the script madevulkaninfoand every Vulkan app see no GPU at all. The script now defaults toMVGAL_ICD_ONLY=0, preserving the loaded vendor ICDs as fallback; the duplicateMVGAL_VULKAN_ENABLEexport and the staleVK_LAYER_PATHare normalised, andMVGAL_PREFIXdefaults to/usr. Sourcing the script no longer disables the vendor renderer. - OpenCL ICD exposed a half-working platform when the kernel module was absent ([5]) —
libmvgal_opencl.sonow reports 0 platforms (stepping aside) when the kernel module is not loaded/degraded, instead of exposing a platform whose context creation fails. - GUI dashboard status cards stayed "Checking…" forever ([6]) —
MainWindownow auto-refreshes the status cards on startup via a single-shotQTimer, so they resolve to real values instead of never firing. - Benchmark caveats were missing from single-suite paths ([8]) —
mvgal-bench compute(synthetic) and other single-suite invocations now print the "What is actually measured" caveat block in the same form asall, so the host-proxy vs. GPU distinction is unambiguous. - Tools disagreed on the usage/error exit code ([9], [17]) — usage/error paths are standardised to exit
2across the CLI tools;mvgal --help/--versionexit0. The contract is documented in the man pages. - Configuration log lines spammed stderr before logging was initialised ([10]) — the two configuration log lines are now
DEBUG-only and gated behind--verbose, so unstripped CLI output no longer falls through to stderr beforemvgal_log_init. - A stale parked OpenCL ICD could be picked up after upgrade ([11]) — the RPM
%postnow removes any leftover/etc/OpenCL/vendors/mvgal.icd.disabledat install time (the park now lives at/usr/lib/mvgal/mvgal.icd.parked). mvgal-dmabuf-matrixleaked build-instruction text into its cdoc header output ([12]) — the stray lines are removed.
Added
- Man pages ([16]) — added
mvgal-gui,mvgal-dashboard,mvgal-load,mvgal-unload,mvgal-workloadandmvgal-probe. mvgal-daemon.preset([19]) — a disabled preset documenting that%postenables the unit on first install.
Packaging
- COPR build
#11217288succeeded on all 18 enabled chroots. The Qt GUI is not built onamazonlinux-2023(the spec skips its Qt build-requires there via%if ! 0%{?amzn}), but that build still succeeds.
[0.7.29] - 2026-10-27
Test-report-driven correctness release addressing 15 issues from the v0.7.28 field validation on a single-GPU Secure Boot host (Fedora 44, kernel 7.2.11-200.fc44.x86_64, Intel UHD 770). COPR build #11212845 succeeded on all enabled chroots.
Fixed
- Vulkan layer activation env var was wrong in
--helpand all Steam/compat scripts. Every user-facing path told users to setVK_LAYER_MVGAL=1orENABLE_MVGAL=1, but the layer manifest only enables onMVGAL_VULKAN_ENABLE=1. Users following the docs got no Vulkan interception. All references corrected acrossmvgal --help,mvgal-config,mvgal-steam-setup,mvgal-vulkan-env.sh,mvgal_steam_compat.sh, andREADME.md. /etc/mvgalwas group-writable (0775 root:mvgal) with no sticky bit. Any user in themvgalgroup could unlink and replace root-owned files there; combined withstrategy = custombeing settable by unprivileged users and reloaded by the root daemon, this was a local privilege-escalation path. The RPM%postnow explicitlychmod 0755+chown root:rootthe directory and forcescustom_strategy.luato0644 root:root;%filesdeclares%dir %attr(755,root,root)for/etc/mvgal.mvgal statusalways exited 1, even when the daemon was running. Bothmvgal statusandmvgal-statusnow use a three-tier exit code:0= healthy (daemon running + module loaded),1= degraded (daemon running, module absent),2= daemon down. On a Secure Boot machine with an unenrolled key, the code is1, not0.- Benchmark subcommands printed "No workload data" and exited 0.
mvgal benchmark,bench-synthetic,bench-realworld, andbench-stressinvokedmvgal-benchwith>/dev/null 2>&1, suppressing all output, and then read zero in-process counters. They now forwardmvgal-bench's stdout/stderr directly and propagate its exit code. mvgal set-strategydid not validate multi-GPU requirements. It acceptedafr,sfr,round_robin,hybrid,task, andcompute_offloadon a single-GPU machine, whilemvgal-config set-strategyalready refused them. The CLI now callsstrategy_requires_multiple_gpus()andmvgal_gpu_get_count()before proceeding;--forceoverrides.mvgal-config show-configrenderedENUM-typed entries as "(unsupported type)".log_levelandscheduler.strategy(bothMVGAL_CONFIG_ENUM) now print their integer value. The default view also returns early after showing the effective configuration —--rawis required to see the literal file.-c/--configpath constraint was not enforced. The CLI accepted any path; only the pkexec helper validated it. The CLI now rejects paths that are not a*.confinside/etc/mvgal(after symlink resolution), matching the helper's check.mvgal startreturned exit 1 when already running (via both systemd andpidof). Now exits 0 with an informational message.mvgal stopreturned exit 1 when not running (via both systemd andpidof). Now exits 0 with an informational message.- OpenCL ICD park file was at
/etc/OpenCL/vendors/mvgal.icd.disabled. Because it sat in the loader's scan directory, some distributions picked it up during rescan. The parked manifest now lives at/usr/lib/mvgal/mvgal.icd.parked, outside the scan path entirely; the force-unregister action creates/usr/lib/mvgal/if missing. mvgal-workloadcounted the MVGAL aggregating ICD platform as a second device. On a single GPU it reported 2 devices and refused to run. It now skips the platform whoseCL_PLATFORM_VENDORis"Multi-Vendor GPU Aggregation Layer", counting each physical GPU once.mvgal-probeprinted a scary "Failed to open /dev/mvgal0" error on systems without the kernel module. The message is demoted to a notice: "kernel module not loaded; falling back to DRM".listen_addrdefaulted to0.0.0.0in[network]. Changed to127.0.0.1so the listener (when enabled) only accepts local connections.- Steam profiles directory (
steam/steam_profiles/) shipped 3 profile files but no directory listing. The RPM%filesnow includes the directory as%{_sysconfdir}/mvgal/steam_profiles/. mvgal-steam-setup --helpexited non-zero with no arguments. Fixed to exit 0.
Packaging
- COPR build #11212845 succeeded.
/etc/mvgalandcustom_strategy.luapermissions hardened as described above.- OpenCL ICD park path updated in
%filesandmvgal-opencl-icd.sh.
[0.7.28] - 2026-10-25
Phase C (documentation gap closure) and Phase D (CUDA shim multi-device correctness) of the 2026-10-25 full campaign, validated on the physical three-vendor bench (AMD Navi 24 + NVIDIA RTX 3050 + Intel Arc A380).
Fixed
- CUDA device counts pass through natively.
cuDeviceGetCount/cudaGetDeviceCountno longer return the MVGAL GPU count; the fabricated override is gone. - CUDA ordinal → MVGAL GPU attribution is BDF-based. A one-time PCI BDF probe replaces the modulo mapping that sent CUDA device 0 (RTX 3050) to MVGAL GPU 0 (Intel Arc).
- No more out-of-bounds GPU indexing. The
-1sentinel is rejected by>= 0checks incuCtxCreate,cuMemAlloc,cuLaunchKernel, and the runtime launch path; the oldmvgal_gpu < gpu_countguard was true for-1. - Kernel launches are restricted to eligible GPUs.
MVGAL_CUDA_DEBUG=1actually raises the log level, so thecuCtxCreate: CUDA device=N -> MVGAL GPU=Mattribution line is visible.
Added
test_cuda_multidevice— LD_PRELOAD integration harness asserting ordinal resolution, BDF set equality, andcuCtxCreateattribution. ctest is now 27/27 (4.73 s) on the bench.- Documentation: Phase D addendum in
HARDWARE_VALIDATION_2026-10-25.md;POWER_MANAGEMENT.md,WINDOWS_COMPAT.md,STATUS.md, andSPEC_COMPLIANCE.mdgap closures (findings 4/5 resolved, 6 partially addressed).
[0.7.27] - 2026-10-24
Fixes three hardware-detection and reporting defects that made valid hardware look missing or report no stats.
Fixed
- AMD ICD detection now matches Fedora's
radeon_icdmanifest. The Vulkan ICD filename check only matchedamd/radv/intel/mttsubstrings, but Fedora ships mesa's manifest asradeon_icd.x86_64.json, which contains none of them, so AMD GPUs were reported as having no Vulkan driver. The matcher now also acceptsradeon/radvfor AMD. - AMD shader-backend library list no longer probes lavapipe. The list
pointed at mesa's software rasterizer (
libvulkan_lvp.so), not an AMD driver. It now probes the real RADV paths (libvulkan_radeon.soin the Fedora/RHEL/Arch and Debian layouts). - WoW64 layer detection accepts the
/usr/lib64install path. Multilib Fedora installslibmvgal-wow64.sounder/usr/lib64, not/usr/lib, so the WoW64 check reported "not built" on a correctly installed package. Both paths are now checked. mvgal-inforeports NVIDIA temperature and utilization. The proprietary NVIDIA driver exposes no hwmontemp1_inputand nogpu_busy_percent, so those fields were left at their-1"unavailable" sentinel.mvgal-infonow queries NVML (already linked in) for temperature and utilization by PCI bus id, filling only the fields sysfs left empty and leaving the sentinel when NVML is unavailable — never fabricating a value.
[0.7.26] - 2026-10-24
Packaging release. No runtime behaviour changes.
Fixed
mvgal-workloadcould be built but not packaged. The tool is built whenever CMake finds OpenCL, while the RPM files list made its inclusion conditional, so a Fedora build that found OpenCL failed in%installwithInstalled (but unpackaged) file(s) found: /usr/bin/mvgal-workload. The files list now always includes the binary that the build produces.- The AppImage dependency self-check failed on a library it had just
staged. The
lddwalk now resolvessoname => not foundentries instead of skipping them, and the final check runs withLD_LIBRARY_PATHpointing at the stagedusr/lib, somvgal-probeand itslibmvgal.so.0dependency are verified inside the image rather than against the host. - Icon dimensions broke the Flatpak build. The application icons were 1056×1024; the exported hicolor icons are normalized to 256×256, which is the size the manifest installs and the size the RPM and Flatpak packages ship.
[0.7.25] - 2026-10-22
Packaging and reporting release. The headline defect made the modules unloadable, and the documented Secure Boot recovery could not have repaired it.
Fixed
- Signed modules could not be decompressed by the kernel. The modules
were compressed with
xz -9e, producing a CRC64-checked xz stream. The kernel's xz decoder rejects any check type above CRC32 (lib/xz/xz_dec_stream.c), and it carries only an internal CRC32 implementation. The module failed at decompression withInvalid parametersanddecompression failed with status 6— before the Secure Boot signature was examined, so enrolling the MOK could not fix it. CRC64 is xz's default check at every compression level, soxz -9was equally broken. v0.7.25 uses the kernel's own settings fromscripts/Makefile.modinst(xz --check=crc32 --lzma2=dict=1MiB -f), which is byte-identical to what DKMS builds. - A module already signed with the MVGAL key was never repaired. The "already signed" check compares the decompressed image, so a module signed by v0.7.21/v0.7.22 was left with its unloadable container intact and the upgrade silently fixed nothing.
- Enrolling the MOK now also makes the modules loadable:
mvgal-enroll-mokre-signs the installed modules before queueing the certificate. systemctl reload mvgaldfailed permanently (status=233): the daemon chowned systemd'sRuntimeDirectory=/run/mvgaltoroot:mvgal, and systemd requiresroot:root. The daemon no longer touches the directory's ownership.- A reload did nothing. SIGHUP was installed but the handler had no case for it, so a reload signalled the daemon and reported success while nothing happened. The daemon now re-reads the configuration on SIGHUP.
- "Vulkan aggregated ICD: ACTIVE" was a false positive — the staging copy in the opt-in directory was treated as a registration. The ICD now reports registered / available / absent.
- The OpenCL ICD registration was decided once, from a stale snapshot,
so on a host where the vendor runtime was installed after MVGAL it
stayed parked and every OpenCL application ran without MVGAL in the
path. The decision moved to
/usr/lib/mvgal/mvgal-opencl-icd.shand the manifest is registered by default. - Module load failures were always blamed on Secure Boot. The loader
used
insmodon a pathmodinfocan legitimately return compressed (.ko.xz), whichinsmodcannot load, and then printed the MOK remedy for a failure enrollment could not fix. It now usesmodprobefirst and classifies the failure. - The D-Bus API refused every non-root call. The methods were
registered without
SD_BUS_VTABLE_UNPRIVILEGED, so sd-bus applied its own default policy and rejected them withAccessDeniedregardless of the bus policy that allows themvgalgroup. - A multi-GPU strategy was accepted on a single-GPU machine. It is now checked against the detected GPU count in three places.
mvgal-config list-gpusreported shared system RAM as dedicated VRAM, without the caveatmvgal-infogave.mvgal-compatexited 0 while reporting "Not operational", andmvgal-dashboard --checkprinted "daemon not running" and then "OK". Both now have meaningful exit codes.mvgal-steam-setup --checkreported two false results — it checked forlibmvgal.soon disk rather than the runtime, and looked for a Steam compatdata directory rather than themvgal-compatbinary.- Every vendor shader backend was skipped, even for a present GPU, because the check only knew the Debian library layout.
- The DKMS build and the packaged modules used two different layouts and
compression conventions, so
rpm -Vanddkms statusboth reported them as mismatched. mvgal-config show-configdumped the raw file rather than the effective configuration the daemon runs on.--rawnow prints the file./etc/OpenCL/layerswas missing, so the loader warned on every run.- The pkexec log was world-readable and recorded configuration values.
/var/log/mvgalwas created and never written to.- The key directory was 0700 root, so the documented
mvgal-signing.derpath was unreadable by the group it exists for. It is now 0750 root:mvgal, with the public certificate at 0640 and the private key still 0600. - Stale DKMS build trees accumulated for every kernel the machine had ever had.
[ai_scheduler] enabled = falsewas never read, so a default install complained on every start about a feature that was switched off.mvgal-config --helpdemonstrated destructive global mutations; the examples are now read-only.journalctl -u mvgaldreturned nothing becausejournalctldoes not resolve unit aliases. The canonical name ismvgal-daemon.service.
Notes
- The kernel's own module compression is
xz --check=crc32 --lzma2=dict=1MiB; matching it exactly is what makes the signed module byte-identical to the DKMS-built one. - The marker terminator in an appended module signature is a NEWLINE, not a NUL, and the 4-byte big-endian DER length sits 32 bytes before the end of the file.
sign-filehas no verify mode and re-signing APPENDS rather than replaces, so the signer strips any existing signature first.- The Mageia 9 COPR chroot was removed from the upstream project: Mageia 9 is end-of-life and its repository content has been purged from the mirrors, so every build failed at bootstrap with a mirrorlist 404 for reasons unrelated to the package. Builds now cover 18 chroots.
[0.7.24] - 2026-10-18
Audit release: eleven defects found by exercising the installed package on a three-GPU AMD/NVIDIA/Intel host, most of which had been silently disabling parts of the system rather than failing visibly.
Fixed
- Daemon: the systemd unit's
DeviceAllow=used glob paths. systemd does not expand them, and their presence flipped the implicitDevicePolicyfromautotoclosed, so mvgald could open neither/dev/mvgal0nor any/dev/dri/*node. It ran permanently in userspace-only fallback while reporting only that the kernel device was unavailable. The allowlist is removed: the GPU node set depends on topology and udev render-node numbering, and GPUs hot-plug at runtime, so no static list can be correct. - Daemon: surface
KernelDevice::lastErrorwhen the kernel device cannot be opened. The errno was discarded, which is why the above went unnoticed -- ENOENT and EPERM call for completely different investigations. - Daemon: request the D-Bus name with
DO_NOT_QUEUEand check the reply code. With flags=0 a taken name was queued and returned a positive value, so a takeover would have looked like success while every method call went to the other instance. Conflicts now name the owning pid instead of advising a dbus reload that could not help. - Kernel:
pcie_gen/pcie_lanesstored rawenum pci_bus_speedvalues, which are bus-speed codes rather than generations (PCIE_SPEED_2_5GTis 0x14). A Gen4 x16 GPU reported as "Gen 23 x16". Both are now decoded from the PCI Express capability register; the legacy wrappers also signal failure withPCI_SPEED_UNKNOWN, so the>= 0guard never fired. - Kernel:
numa_nodeuseddev_to_node()on a PCI device, whosedevis never NUMA-aware, so it always read unknown. Now usespcibus_to_node(), with a single-node fallback for hosts that have no ACPI/SRAT topology. - Kernel: the
numa_node >= 0guard was tautological on an unsigned field, so two GPUs with unknown nodes compared as same-node and P2P was admitted on the strength of a value carrying no information. - Kernel:
class_codewas declared in the UABI and printed by mvgal-info, but no code ever assigned it. Every GPU reported0x0000instead of0x030000. - Kernel:
mvgal_sysfs_init()calledclass_create("mvgal")a second time whenmvgal_core.chad already created it. The -EEXIST was swallowed, so the module reported a clean load while the documented per-GPUcapabilitiesattributes did not exist -- and the stale class made every load after the first fail. sysfs_init now parents its per-GPU kobjects to the device kobject the core already registered instead of creating a competing class. - Kernel:
mvgal_device_free()cancelled the same delayed work twice, via*_stop()and again via*_fini(), trippingWARN_ONin__flush_workon every rmmod -- once per GPU. Both cancels are now guarded, which also makesfini()safe standalone. The DVFS work item had the same defect. - Kernel: fix a use-after-free in PCI enumeration.
pci_get_device()takes a reference on what it returns but not on the cursor it is handed, and the skip path released a reference on a cursor it did not own, so the next iteration walked freed memory. The cursor now owns one reference for the whole scan. - Kernel:
*_init()passed NULL as the delayed-work handler, so anything queuing the work before*_start()would have faulted. - Tools: mvgal-info pooled an integrated GPU's shared system memory into "Aggregate VRAM", claiming a 66 GiB pool on a machine with 10 GiB of real VRAM. Dedicated VRAM and shared system RAM are now accumulated and reported separately.
- Daemon:
writeFrequency()returned success without writing a clock, so the DVFS controller recorded clocks the GPU was never running at. It now reports clock control as unavailable and leaves last-known state intact. - Daemon: the PSU headroom and power curve controllers both wrote
targetClockMhz, which the DVFS controller rewrites from utilisation every tick, making both silently inert. Each now holds its own cap that the DVFS controller clamps against. - Steam: shipped game profiles pinned
primary_gpu_mask = 0x3, freezing them to a two-GPU machine regardless of hardware. Now0, which the scheduler already defines as "all GPUs".
Added
- D1-D5 design/docs: narrow end-to-end userspace workload path (OpenCL ICD loader, host-staged, CPU reference validation)
- Tool: mvgal-workload (end-to-end OpenCL workload gate; validates >=2 distinct vendors)
Changed
- Build: link mvgal-info with nvml_query.c and ${CMAKE_DL_LIBS} (standalone tool needs NVML helper)
-
Kernel: PCIe link attributes are read from the capability register (maximum supported) rather than the status register, so a powered-down GPU reports its real capability instead of a negotiated width of zero.
-
packaging: ship
/usr/bin/mvgal-workload. The tool and its CMake install rule arrived in 0.7.23 but were never added to the RPM%files, so every COPR build since then has failed with "Installed (but unpackaged) file(s) found". It is built only whenfind_package(OpenCL)succeeds, so it is now listed under the existing%bcond_without opencl.
Known issues
- The PCI enumeration use-after-free is source-verified only. On the host where
it was found, earlier load/unload cycles had already driven a device refcount
to zero, and such refcounts are not recoverable without a reboot. It needs a
clean-boot check;
test_uapi_probefails on an affected machine because enumeration reports zero GPUs.
[0.7.23] - 2026-10-18
Fixed
- DKMS: ensure KERNELDIR is correctly resolved in kernel/Makefile and dkms.conf (fixes module build on kernel 7.2.11-200.fc44.x86_64)
- Userspace: fix NVIDIA VRAM reporting (0 GB) via NVML fallback by PCI BDF; aggregate VRAM correctly labels/excludes shared system memory
- Daemon: eliminate AMD frequency-set log spam by initializing C logger at INFO level (mvgal_log_init) so DEBUG messages are filtered
- Tools: mvgal-info shows full driver names (amdgpu/nvidia/i915); mvgal status displays VRAM with "(shared system memory)" for shared RAM
[v0.7.22] - 2026-10-14
Fixes a Secure Boot packaging defect that made the documented remediation impossible to complete, plus five reporting defects found by a black-box audit of 0.7.21 on a Secure Boot host.
Fixed
- Modules were signed with two different keys, so Secure Boot could never be made to work.
mvgal_ntsync.ko— a hard dependency ofmvgal.ko— was signed with DKMS's own key while every other module carried the per-machine MVGAL key, andmvgal-enroll-mokenrolled only the MVGAL key. A user could follow the printed instructions exactly and still getKey was rejected by service. Three faults combined: the signing loops globbed"$dir/"*.ko, which cannot match the.ko.xza compression-enabled distro installs; they looked only in the kernel tree and.../updates/dkmswhen DKMS's real destination is distro-dependent (on Fedora,.../extra); and they discarded every failure, so the install reported success. Both loops are replaced by/usr/lib/mvgal/mvgal-sign-modules.sh, which searches every location a module can land in, handles.ko,.ko.xzand.ko.zst, decompresses to sign and recompresses, and exits non-zero when a module could not be signed. - A scriptlet-ordering bug could leave every module unsigned.
mvgal-dkmsdid not depend onmvgal, so on a first install its%postcould run before the main package's%postgenerated the signing key. It now requiresmvgalat the same version. - Kernel updates re-signed themselves again. DKMS rebuilds automatically on a new kernel, and that runs no RPM scriptlet, so the rebuilt modules returned with DKMS's key.
dkms.confnow setsPOST_INSTALLto re-run the signer, and the signer with no argument covers every installed kernel release rather than only the running one. - The Secure Boot check could pass while the key was unusable. The daemon tested enrollment with
mokutil --list-enrolled | grep -qi MVGAL, which matches on subject name. Every MVGAL keypair shares one subject, so after a key rotation the old certificate is still listed, the check passes, and the daemon stays silent while modules signed by the new key are rejected. It now usesmokutil --test-key. set-power-curveaccepted a GPU index it discarded. The helper persists a single globalpower.power_curve_profile, so every index wrote the same key. Its documented example,set-power-curve 0 gaming, also failed, becausegamingis not a valid profile. The argument is removed and the example corrected; the old two-argument form still works with a warning. The help now states that the profile is persisted but not yet consumed, because nothing reads the key.- Informational log output still reached stdout. v0.7.21 redirected
get_default_output(), butlog_internal()has a separate fallback for whenmvgal_log_init()was never called, and that fallback still sent INFO and above to stdout — which is exactly the path the affected tools take. Both paths now write to stderr. tool | headkilled the producer with SIGPIPE. The CLI tools now ignore it, so a closed downstream pipe yields exit 0 instead of status 141.- "Vulkan registered: yes" implied aggregation was active. The implicit interception layer ships globally and only intercepts calls; the aggregated ICD, which would expose an MVGAL
VkPhysicalDevice, is a separate opt-in.mvgal-infoandmvgal-compatnow report the two separately and name the opt-in script. mvgal-compatadvised a command that cannot succeed.pkexec modprobe mvgalfails on a Secure Boot machine whose key is not enrolled, andmvgal.kodepends onmvgal_ntsync.ko, so modprobe reports only the outermost failure. The hint now points atmvgal-load.mvgal-benchpresented host measurements as GPU figures. Memory bandwidth is anmmap'd host RAM copy and compute is a single-threaded host CPU DAXPY; both were printed under headings that read as GPU performance. Every measurement is now labelled with its hardware path, the run header states when no GPU path is exercised, and--metadatagainshardware_path,mvgal_device_present,measures_gpuandmeasurement_note.
Notes
- Re-signing a module that already carries a signature appends a second one rather than replacing it; the signer strips the existing signature first.
- A signed module ends with the DER blob, an 8-byte
struct module_signature, a 4-byte big-endian DER length, and the marker"~Module signature appended~\n"— the terminator is a newline, not a NUL. - The per-machine key is preserved across upgrades:
%postregenerates it only when missing and no%postunremoves it. - Verified by extracting the built RPMs and executing the shipped binaries, not by reading the source. Upstream COPR build #11118333 is green on all 19 enabled chroots.
[v0.7.21] - 2026-10-12
Correctness and packaging release driven by a black-box test report against 0.7.20 on a single-GPU Intel host. Most of the report concerned reporting and packaging rather than device paths, but two findings were genuine defects and two of the fixes below replaced a first attempt that would not have worked.
Fixed
- The daemon's IPC socket was created
root:root, locking out every user in themvgalgroup. All CLI-to-daemon IPC failed:mvgal show-statsreturned stub zeros,mvgal statuscould not read live state, andnc -U /run/mvgal/mvgal.sockreturnedPermission denied, even though the package adds the installing user tomvgaland the shipped unit comments say the socket must be0660 root:mvgal. The cause was not thechown()call itself. The unit'sCapabilityBoundingSetlistedCAP_SYS_ADMIN CAP_DAC_OVERRIDE CAP_SYS_RAWIO CAP_SYS_MODULEand omittedCAP_CHOWN, and a bounding set caps the permitted set a uid-0 process can ever hold.chown()only avoids needingCAP_CHOWNwhen the target group is one the caller is already a member of, and root's supplementary groups do not includemvgal— so the call failed withEPERM, the return value was discarded, and the daemon reported a listener it had in fact left root-only.CAP_CHOWNis now in bothCapabilityBoundingSetandAmbientCapabilities; it is needed exactly once, at startup. The unit's comment claiming the daemon "drops capabilities itself after init" was also false and has been corrected — there is noprctl/capset/setuidinruntime/. Both daemons now log the failure rather than swallowing it. See Troubleshooting § Daemon socket permissions. - The OpenCL ICD advertised a version it did not implement.
clGetPlatformInforeported"OpenCL 1.2 MVGAL"while the library exported only 2 of the 7 entry points that OpenCL 1.2 added to the core API. Because the library is registered as an ICD, ocl-icd and the CUDA loaderdlsym()them from the shared object, so an application using any 1.2 feature receivedNULLand would fault rather than fail gracefully. AddedclCreateCommandQueueWithProperties,clCreateSubDevices,clCreateSamplerWithProperties,clSetUserEventStatusandclGetEventProfilingInfoas pass-throughs, returningCL_INVALID_OPERATIONwhere no vendor implements them — deliberately not falling back to the deprecatedclCreateCommandQueue, which would silently drop queue properties that 1.2 requires to be honoured. The 1.2 ceiling itself is deliberate and is now documented in the source: the layer enumerates and delegates vendor devices rather than virtualising the device API, and 3.0's pipe/sparse/dma-buf behaviours are not emulated, so claiming 3.0 would overstate the aggregation path. - Configuration was silently reset on RPM upgrade.
mvgal.confis%config(noreplace), so an upgrade that changes the schema activates the new default file and leaves the user's edits in.rpmsave— the only record. A merge now runs in%post(/usr/lib/mvgal/mvgal-merge-config.sh), restoring the user's values per section and key, re-adding keys the new release dropped, and appending sections it dropped entirely. It also preserves the new release's default alongside asmvgal.conf.mvgal-default, so nothing is lost either way. mvgal load-modulehung indefinitely. The CLI spawned the polkit helper throughsystem()with no bound, so a cancelled or ignored authentication dialog left it waiting forever and the user never saw the result. It now runs under a 60-second timeout, distinguishes that case explicitly, and prints the Secure Boot remediation on failure.- Two fixes corrected first attempts that would not have worked. The configuration merge originally keyed on bare key names;
priorityappears in both[gpu_0]and[gpu_1],enabledin five sections, andpsu_capacity_wattsexists in the new file with a different value — so a user'spsu_capacity_watts = 750would still have been dropped, which is the exact case reported. The shared-memory flag originally occupied a bit in the kernel-owned UAPIflagsword; a kernel-set bit 31 would have mislabelled a real GPU's dedicated VRAM as shared system memory. Both were replaced with unambiguous mechanisms. mvgal-compatadvised adding the user to group 0. When the socket was inaccessible it printed that'id -nG'must list the socket's gid, "gid 0 means the daemon did not chown its socket" — self-contradictory, and it framed a privilege escalation as a fix. It now distinguishes the two cases: gid 0 names the daemon-side chown bug, anything else points at group membership.- Exit codes disagreed about the same degraded state.
mvgal statusexited 0 with the module unloaded whilemvgal-status --onceexited 1;mvgal reloadprinted "configuration not reloaded" and exited 0. Degraded/not-applied now returns non-zero consistently, andmvgal reloadreports failure when there was no daemon to reload. - Informational log output polluted stdout.
get_default_output()returnedstdoutfor INFO and above, soConfiguration system initializedand friends interleaved into command output and would break a JSON consumer. All levels now go to stderr. - Placeholder values were presented as measurements.
mvgal-info --jsonemitted"driver": "","class_code": "0x0000","pcie_link_gen": 0andvram_used_bytes: -1. Unavailable values now emitnull, and a newvram_typefield distinguishesdedicated,shared_systemandunavailable. Two sentinels had a genuine ambiguity:utilization_pctandtemperature_cstarted at 0, so a real 0% busy was indistinguishable from "no such node"; these now use -1, and NUMA node usesUINT32_MAXbecause 0 is a real node. - Integrated GPUs reported system RAM as VRAM. An Intel UHD 770 was shown as
VRAM: 7.43 GBandAggregate VRAM: 7.43 GiB, the latter being the headline figure of an aggregation layer. Shared memory is now labelled as such in both the per-GPU listing and the aggregate total. - Cross-tool disagreement about the same GPU.
mvgal-inforeported an empty driver whilemvgal-enumreportedi915;mvgal-enum'stimestamp_nsfield held epoch seconds. The driver is now resolved from the device's bound sysfs symlink, and the field istimestamp_s. -c/--configaccepted only/etc/mvgal/*.conf, undocumented. The restriction exists because the privileged helper edits the path, so an unconstrained value would be a privilege-escalation primitive; it is now stated in the help text. The raw wait-status256that masked the real error is now decoded.--versionwas missing on three shipped tools andmvgal-infosilently ignored unknown options while also ignoring--version. All shipped tools now answer--version, andmvgal-inforejects unknown arguments.mvgal-proberejected long options, printinginvalid option -- '-'. It accepts--help,--rescan,--caps,--stats,--jsonand others.- The AI scheduler logged a per-boot warning. No model ships, so
AI scheduler: model ... not foundappeared at info level on every start. Demoted to debug.
Packaging
- COPR build #11110014 succeeded on all 19 enabled chroots (Fedora 43/44/45/ELN/Rawhide, RHEL 8/9/10 and +epel, AlmaLinux 9/10, AlmaLinux Kitten 10, AlmaLinux +epel, CentOS Stream 9/10 and +epel-next, Amazon Linux 2023, Mageia 9).
- The Secure Boot banner is now printed by the daemon at startup as well as by
%post, so an unenrolled key is visible on first run rather than only at install. - Verified by extracting the published
mvgal-0.7.21-1.fc44.x86_64.rpmand checking the artefacts in place: the OpenCL shared object exports all seven 1.2 entry points, the configuration-merge script is installed mode 755, and the shipped unit carriesCAP_CHOWN.
Notes
- L7 is not an MVGAL defect. The report attributed a Vulkan loader warning about missing display entry points to the MVGAL ICD. Both
vkGetPhysicalDeviceDisplayPropertiesKHRand...PlanePropertiesKHRare implemented and resolve through the shipped ICD's negotiated interface version 5, which is the path the loader uses; the library exports only the five spec-mandatedvk_icd*symbols, as an ICD must. Since 0.7.14 no ICD manifest ships invulkan/icd.dat all (the manifest is opt-in), so the loader cannot be loading MVGAL as an ICD unless a user explicitly opts in. The reporter also noted a second ICD warned in the same run. - L8 required no change. Enumeration selects on PCI vendor ID and resolves the driver from the bound sysfs symlink, never on card index or driver name. Confirmed on a host exposing only
card1with bothi915andxeloaded.
[v0.7.20] - 2026-10-10
Build system fix and version consistency across all components. The 0.7.19 release had a version drift: the Cargo workspace and several source headers still reported 0.7.19 while the CMake project and spec file reported 0.7.20. This release aligns every version source and fixes the daemon compilation.
Fixed
- Daemon compilation failed with
testing/mvgal_mock_internal.h: No such file or directory. The daemon'sgpu_manager.cincludes the mock header, butsrc/userspace/CMakeLists.txtdid not expose thetesting/directory to the daemon target. Added the include path. build.shmanual compilation test used the wrong include path. The test now uses-Isrc/userspaceso it findstesting/mvgal_mock_internal.hthrough the daemon's include path.
Changed
- Version alignment across all sources:
CMakeLists.txt,packaging/rpm/mvgal.spec,Cargo.toml(workspace),bindings/rust/Cargo.toml,Cargo.lock(viacargo update --workspace),include/mvgal/mvgal_version.h,kernel/mvgal_version.h,kernel/dkms/dkms.conf,tools/libmvgal/CMakeLists.txtall report 0.7.20.
Packaging
- COPR build #11104568 succeeded on Fedora 43/44/45, Rawhide, ELN, RHEL 8/9/10, AlmaLinux 9/10, CentOS Stream 9/10, Amazon Linux 2023, Mageia 9.
- Jenkins build #18 produced all 13 artifact formats: RPM (SRPM, binary, DKMS, metapackage), DEB (main, daemon, dev, dkms, vulkan), AppImage, tar.zst, Flatpak.
- All 28 unit tests pass (
ctest --output-on-failure).
[v0.7.19] - 2026-10-04
Makes a release reproducible from its commit: rebuilding the source tarball and the SRPM from the same tree now yields byte-identical files. Two independent causes had to be removed.
Fixed
- The source tarball embedded a second, complete copy of
vendor/.scripts/mksrpm.shassumedvendor/was gitignored and therefore absent fromgit archive, then copied it in — butvendor/is tracked (.gitignoresays so explicitly: the vendored crates have to travel with the source, because.cargo/config.tomlreplaces crates.io with them).git archivealready emitted all 1109vendor/paths, so the copy nested a duplicate atvendor/vendor/and restamped the parent's mtime with the wall clock. That single mtime was the only difference between two builds of an identical tree. The copy is gone; the script now fails ifvendor/is absent from the archive rather than silently duplicating it. The tarball shrank from 11 MB to 8.3 MB and is now reproducible. BUILDTIMEwas the wall clock. rpm only rewritesBUILDTIMEfromSOURCE_DATE_EPOCHwhen the macrouse_source_date_epoch_as_buildtimeis defined, and it ships undefined on purpose — exportingSOURCE_DATE_EPOCHalone changes nothing, which is exactly what the first attempt at this fix demonstrated. Bothscripts/mksrpm.shandpackaging/jenkins/build_rpm.shnow defaultSOURCE_DATE_EPOCHto the commit date (git log -1 --format=%ct) and pass the opt-in macro. Verified: two consecutive runs produce the same tarball digest, the same SRPM digest, and the sameBUILDTIME, equal to the commit date. The Jenkins path was confirmed against a real build — its SRPM is byte-identical to the onemksrpm.shproduced locally from the same commit, so the two independent code paths agree.
Notes
- Binary RPMs are still not bit-reproducible, and this release does not claim they are. Two axes remain. Fedora's toolchain links with a bare
--build-id, so each ELF gets a randomNT_GNU_BUILD_IDnote; and the absolute build path is baked intoDW_AT_comp_dir,DW_AT_nameand the generatedmvgalConfig.cmake, so two different build directories also diverge. Both are addressable —-Wl,--build-id=sha1with-fdebug-prefix-map/-ffile-prefix-mapwas verified to collapse two builds from different directories to a single digest, and unlike stripping the note it keeps the content-derived build-id that debuginfod depends on. It is not enabled because the spec's%builddrivescmakethroughcmake-rpm-macros, whose%cmakemacro has already overridden spec-level flag and cache workarounds in five earlier releases. This is recorded as open work in Project Status § Reproducibility limits rather than papered over.
[v0.7.18] - 2026-10-04
Adds a synthetic GPU topology backend, so the heterogeneous code paths can be exercised and asserted on a single machine instead of only being reachable on a box that happens to have two vendors installed.
Added
mvgal_mock— the 30th public header.include/mvgal/mvgal_mock.hpublishes a backend that advertises a configurable set of fake GPUs: vendor, VRAM size, PCIe generation and lane count, feature bits and driver identity. The GPU manager's rescan substitutes it for the hardware scan, so the scheduler, the memory manager and the P2P cost model all see synthetic devices through the normal discovery path rather than a parallel test-only one. With the backend inactive,mvgal_mock_is_active()returns false and discovery is untouched. See API Reference § Synthetic Topology Backend.- Seven topology presets —
NONE,SINGLE,HETERO_DUAL,HETERO_TRI,HETERO_QUAD,AMD_DUALandINTEGRATED_ONLY— covering the single-GPU optimisation case, dual-vendor P2P, multi-root-complex host staging, homogeneous AMD peer-to-peer and the integrated-only case with no VRAM.append_to_hardwareadds synthetic devices alongside the real scan instead of replacing it; it is off by default so a test gets the same device population on any machine. - Modelled physics rather than canned values. Temperature approaches an asymptote between a 35 C ambient and a TDP-scaled peak with a ~15 s time constant; throttling begins 5 C above TDP and is capped at a 60 % utilisation reduction; VRAM carries a 256 MiB baseline before any test allocation; and per-pair link bandwidth is derived from the pair's negotiated PCIe generation and lane count, so two Gen4 x16 peers model about 15.7 GB/s while a pair with no P2P path reports 0.
mvgal_mock_advance()integrates elapsed time and then refreshes telemetry. Tests assert on transitions — a device heating into throttling and cooling back out — rather than on fixed numbers. - Per-device control from the test:
set_load,allocate_vram(0 releases),kill_deviceandresurrect_device, andset_link_bandwidth.
Fixed
mvgal_p2p_query_caps()fabricated a 16 GB/s peer link. Any pair on the same root complex reportedsupportedwithmax_bandwidth_mb_sdefaulting to 16000 — including synthetic devices whose peer path was disabled. The cost model reads that number, so it would selectREPLICATEfor a small buffer orMIGRATEfor a large one between two devices that cannot transfer at all. Synthetic pairs now report the bandwidth the topology actually models, andsupportedrequires a path to exist.same_root_complexremains pure topology. The real-hardware path is unchanged.mvgal_p2p_cross_vendor_supported()ignored device health. It checked only that both descriptors could be read, so a failed device still advertised itself as shareable and the scheduler would plan work onto it. Both descriptors must now also beavailable.- Host state leaked into the simulation.
finalize_gpu_descriptor()fills an integrated GPU's zerovram_totalfromsysinfo(), which meant a synthetic integrated device reported the build host's RAM — so the same topology produced different descriptors on different machines, quietly defeating the point of a deterministic fixture. Synthetic integrated devices now use the new publicMVGAL_MOCK_SYSTEM_RAM_BYTES(32 GiB). The real-hardware behaviour is deliberately unchanged. mvgal_mock_init()rejected its own documented preset.MVGAL_MOCK_TOPOLOGY_NONE— "no synthetic devices, hardware discovery behaves unchanged" — was refused withMVGAL_ERROR_INVALID_ARGUMENTby apreset <= NONEbounds check, while the header promisedMVGAL_ERROR_NO_GPUSfor an empty preset. The check now rejects only out-of-range values, andNONEfalls through to the documented result.
Tests
test_mock_gpu— 193 assertions across the presets, device selection, failure isolation and resurrection, memory pressure, thermal throttling and cooling, configurable health thresholds, transfer-bottleneck topology, cross-vendor sharing, and determinism (two passes over the same topology must agree). Determinism is asserted by reading descriptors back through the publicmvgal_gpu_get_descriptor()rather than through mock-internal accessors, so the assertion covers the discovery path the rest of the project uses.
[v0.7.17] - 2026-10-04
Repairs the Rust quality gate. The crates had never been linted at all.
Fixed
- The crates were never linted.
clippy.tomlcarried three keys that modern clippy rejects:panic-in-upgrade, which is not a clippy configuration field;large-digit-grouping, a rust-analyzer setting rather than a clippy one; andcyclomatic-complexity-threshold, a deprecated alias that now collides withcognitive-complexity-threshold. Clippy aborts on an unreadable configuration file before it lints anything, so every one of these turned the entire gate off rather than producing a warning. disallowed-macroslisted two methods.unwrapandexpectare methods, not macros, so clippy reported them as unreachable macros on every invocation and-D warningspromoted that reporting quirk to a hard failure. The thresholds that are still meaningful are kept.CrossDeviceFencecould never cross a thread boundary. It held its signal timestamp in aCelland its external handle in a raw pointer, soArc<CrossDeviceFence>was neitherSendnorSync— meaning any attempt to share a fence across threads did not compile. The timestamp moved behind a mutex and the handle is now documented as synchronised by the caller.create_fence()returned a fence that was not the fence being tracked.GpuTimeline::create_fence()andFenceManager::create_fence()each stored a clone in their map and returned a second, independentArc, so signalling the returned handle had no effect on the fence the manager held. Both now hand out the sameArcthey retain.MvgalErrorcould not represent its own error. It relied on the implicit C-like enum representation, which cannot holdUnknown = 0xFFFFFFFF. It is now explicitly#[repr(u32)]; thei32conversion used across the FFI boundary still yields-1forUnknown.align_up()hand-rolleddiv_ceil(). The ceiling division is now the standard library's.
[v0.7.16] - 2026-09-30
This release completes the custom splitter API and makes the command DAG translation and emission paths real instead of returning their arguments unchanged. Every item below was found by reading the source rather than from a field report, and each was reproduced before it was changed.
Fixed
mvgal_unregister_custom_splitter()could never succeed. Splitters were stored by value on registration but the removal loop compared the caller's pointer against the address of the internal slot,&splitters[i], which is never exposed to any caller. Because the register path had already copied the struct, no caller could ever supply a pointer that matched, so the function reported the splitter as absent on every call. Removal now matches by value acrossanalyze,split,mergeanduser_datatogether, which is what distinguishes two registrations that share callbacks but differ in their private state.mvgal_unregister_custom_splitter()was not declared in any public header. The definition was reachable inside the library but not from outside it, so a program could register a splitter and had no way to release it. The declaration is now ininclude/mvgal/mvgal.hbeside its counterpart.- Both custom-splitter entry points discarded their arguments.
mvgal_register_custom_splitter()andmvgal_unregister_custom_splitter()insrc/userspace/api/mvgal_api.ccast their context and splitter tovoid, logged that the feature was not yet implemented, and returnedMVGAL_ERROR_NOT_SUPPORTED. They now validate the context is initialised and the descriptor is non-NULL, then forward to the scheduler. mvgal_translate_dag()returned its argument unchanged. The header promised a translated copy, so callers had every reason to mutate the result; instead they were handed a pointer aliasing the original, and any edit silently corrupted the source DAG. It now deep copies, carrying the queue family, command buffer index and primary flag, and rewires everydeps[]edge onto the copies through an old-to-new pointer map. An edge whose target is not in the map fails the whole translation rather than dropping a synchronization edge.vendor_data[]is reset toNULLon the copy, because that array is owned per-vendor data whose lifetime the copy does not share; the recordedargs.barrier.infostays a borrowed pointer and the caller must keep it alive.mvgal_emit_dag()always failed. It returnedVK_ERROR_LAYER_NOT_PRESENTunconditionally, so a translated DAG could never be replayed. It now resolvesvkCmdDraw,vkCmdDrawIndirect,vkCmdDispatch,vkCmdPipelineBarrier2,vkCmdPipelineBarrier,vkCmdBlitImage,vkCmdClearAttachmentsandvkCmdBindPipelinethrough the device dispatch table and replays the nodes in recording order. Node types with no captured arguments return a specific error instead of being skipped, because a silent skip renders the wrong frame. A newmvgal_cmd_dag_begin_for_device()records which device a command buffer belongs to; the existingmvgal_cmd_dag_begin()still works, and emission on a buffer with no known device reports that rather than pretending to succeed.- The command buffer to DAG map leaked on collision. It was an open-addressed table with no collision handling, so
mvgal_cmd_dag_begin()overwrote whatever DAG occupied the slot whenever two command buffers hashed alike — losing the live DAG and leaking it. Lookups now probe linearly,mvgal_cmd_dag_end()probes instead of hashing once, and beginning a recording for a buffer that already has one destroys the old DAG rather than dropping it.
Notes
- Barrier lowering refuses rather than approximates. A sync2 dependency carrying a queue family transfer or an image layout transition is rejected with
VK_ERROR_FEATURE_NOT_PRESENTand an explanation, instead of being flattened into a plain memory barrier.mvgal_cmd_node_trecords no fields for either, so emitting them would silently under-synchronize. The same applies to mask widening: 64-bit sync2 stage and access masks are widened to their 32-bit equivalents, and any bit above 31 becomesVK_PIPELINE_STAGE_ALL_COMMANDSor a read-and-write access mask rather than being truncated away. MVGAL_STRATEGY_CUSTOMstill returnsMVGAL_ERROR_NOT_SUPPORTED. Registering a splitter works and the descriptor is validated, but applying one to a dequeued workload needs a task tree contract that is not yet defined: how a splitter'ssplit()callback obtains sub-workload handles, who owns them, whenmerge()fires, and how a failed child is reported to the parent. That contract is a design decision, not a defect, so no speculative behaviour was added.
Tests
test_custom_splitter_registration()registers two splitters whose three callbacks are identical and which differ only inuser_data, then unregisters them in the order that exposes a matcher ignoring that field: a matcher that compared only the callbacks would releaseAwhen asked forB, and the following release ofAwould then wrongly fail. It also checks that a third release reportsMVGAL_ERROR_NOT_FOUNDrather than success, that aNULLargument is rejected by both entry points, that a descriptor missing one callback is rejected outright, and that rejecting it left no stale entry behind — otherwise a stored descriptor with aNULLmerge pointer would later be dispatched through it. Finally it asserts the three callbacks were never invoked, since no workload has been submitted.test_translate_deep_copy()asserts the translated DAG is a distinct object, that dependency edges point at the copies rather than the source nodes, and that mutating the copy leaves the original untouched.
[v0.7.15] - 2026-09-30
A memory-corruption fix in the buffer copy path, found by reading the code rather than from a field report. Every case below was reproduced before it was changed.
Fixed
mvgal_memory_copy()applied the region offsets twice. It passedregion->src_offsetandregion->dst_offsettomvgal_memory_map(), whose documented contract is that the returned pointer already points at the requested offset, and then added the same offsets again at thememcpy. A copy from source offset 1024 to destination offset 2048 therefore read and wrote atbase + 2 * 2048, running past the end of the mapping and corrupting whatever followed it in the allocator.mvgal_memory_copy_gpu()builds a single region and delegates, so it inherited the fix. The pre-existing copies all used offset 0, which is exactly the case where a double-applied offset is a no-op, so the suite passed while the path was broken.mvgal_memory_copy()unmapped buffers it did not own. The cleanup was guarded by a test for theMAPPEDstate bit thatmvgal_memory_map()had already set one line earlier, making it unreachable dead code, and the destination-map failure path tore down a source mapping it had never created. Host-valid buffers carry ahost_ptrowned by the allocator, so unmapping would have freed memory the buffer still points at.
Tests
test_memorygained a case that copies 256 bytes from source offset 1024 to destination offset 2048, then checks both that the payload lands at the destination offset and that exactly 256 bytes anywhere in the destination buffer are non-zero, which is what proves nothing outside the destination window was written. It fails against the previousmvgal_memory_copy().
[v0.7.14] - 2026-09-28
This release is the response to a field report from a Fedora 44 machine with a single Intel UHD 770 and Secure Boot enabled. Every item below was reproduced before it was changed.
Fixed
- The MOK enrolment never completed.
mokutil --importprompts for the one-time password twice, butmvgal-enroll-mokfed it one line, so every enrolment ended incould not read confirmation password. It now writes the password twice. The script then verified success withmokutil --list-new | grep -qi "mvgal-signing", a string mokutil never emits — it printsIssuer/Subject: CN=MVGAL Kernel Module Signing Key, O=MVGAL— so even a successful import was reported as failed. The check now matches the certificate's actual common name. Because the check could never pass, Secure Boot machines were told to reboot and still had no trusted key. mvgal-enroll-mok -p <password>silently dropped the password. The script parsed its options and only then re-executed itself throughpkexecwith"$@"— which the parse loop had already emptied. The password reached the elevated process as nothing. The script now elevates before parsing, with a non-consuming pre-scan that keeps--helpworking unprivileged, and handspkexeca pristine argument vector (a password containing a space survives).- Secure Boot detection could not say "off". The state was matched with two nested
caseglobs that only tested for substrings, sodisabledandnot enabledwere not both recognised. The state is now lower-cased once and matched against explicit patterns; an unreadable or empty state warns instead of silently proceeding. mvgald'sExecStartPrecould never load a module. The unit'sCapabilityBoundingSetandAmbientCapabilitiesomittedCAP_SYS_MODULE, so bothmodprobelines were guaranteedEPERM. The-prefix in front of each command discarded the exit status and the message, so the unit reportedactive (running)with no kernel module.CAP_SYS_MODULEis now granted, the failing status is no longer discarded, and the message names the real fix (pkexec mvgal-enroll-mok)./run/mvgaldid not survive a reboot. No unit declaredRuntimeDirectory=, and/runis tmpfs, so the daemon's own directory creation was the only thing keeping the socket's parent alive. The unit now declaresRuntimeDirectory=mvgalwithRuntimeDirectoryPreserve=yes.- The daemon IPC socket was unreachable by ordinary users. The legacy C daemon created it
root:rootmode0660and never changed the group, so no unprivileged process could connect, while the error message (tools/mvgal-compat.c) told users to join themvgalgroup they were already in. Both the legacy C daemon and the C++ runtime daemon now resolve the group (mvgal, falling back tovideo) andchownthe socket beforechmod.mvgal-compatnow reports the socket's actual owner, mode and the caller's uid, and distinguishes "your supplementary groups have not picked up" from "the daemon did not chown". - The pkexec helper's
--configaccepted any path on the filesystem. It filtered metacharacters but never constrained the target, so any user authorised for the helper could have rootsed-edit arbitrary files such as/etc/sudoers.d/x. It now requires the path to resolve (symlinks followed) to a regular file inside/etc/mvgal. - GPU indices were interpolated into
sedaddresses unvalidated.enable_gpu,disable_gpuandset_power_statebuilt an address range from user input, so a crafted index could rewrite lines outside the intended section. A dedicated validator rejects non-digits, over-long input, non-canonical leading zeros and out-of-range values (bounded against the GPUs actually present under sysfs when the module is loaded). - GPUs were listed as
Intel GPU 4680.4680is a device id, not a model, and both GPU managers synthesised that name when no label was read. Both now read the DRMlabelattribute — the pattern the CLI tools already used — and fall back toIntel [8086:4680], which cannot be misread as a model. mvgal-hw-validatelabelled the first GPUGPU[1]and read adevice_nameattribute that does not exist, so itsName:line never printed. The counter is now zero-based and reset per run, thereaddirloop respects theMAX_GPUSbound it declared but never used, and the name comes fromdevice/label. Skipped virtual devices are labelled rather than numbered.- The four
mvgal benchmarksubcommands exited 1 with no data. The counters are per-process and only move when a workload runs inside that same invocation, so a plainmvgal benchmarkalways found zero and always reported failure. It now says so plainly and exits 0, matching the documented contract inmvgal(1); a genuine failure to read statistics still exits 1. Each branch had also left itsmvgal_stats_tuninitialised and discardedmvgal_get_stats()'s return value, so a read failure produced an indeterminate stack read. - The polkit policy was invalid and authorised nothing.
data/com.mvgal.policyused<_description>/<_message>instead of<description>/<message>, so it failed DTD validation and no description ever rendered. It also declared ten action ids that no executable is annotated with, two of which (com.mvgal.config.user,com.mvgal.monitor) grantedallow_any=yes— no authentication at all. It now declares exactly the two actions that exist and are annotated (com.mvgal.privileged.helperfor the helper,com.mvgal.daemon.startfor/usr/sbin/mvgald), both requiringauth_admin. The eight orphans are gone.
Added
- One module inventory, shared by all three loaders.
config/mvgal-modules.sh(/usr/lib/mvgal/modules.sh) is now the single source of truth for load order, andmvgal-load,mvgal-unloadand the pkexec helper all source it; the helper derives its unload order by reversing it. The three had drifted: the helper loadedmvgalbefore its own dependencymvgal_ntsync, and the unload paths were missingmvgal_mttandmvgal_adrenoentirely, so a module could never be unloaded once loaded. Overridable with$MVGAL_MODULE_LIST; a missing file warns rather than falling back silently. - An honest diagnostic for the AI scheduler. The daemon loaded its model from a hardcoded path that no package ever creates, and
mvgal_ai_model_load()returned a valid handle for any non-empty string without touching the filesystem, so the daemon believed it had an AI model for its entire life and never said otherwise. The loader now validates the path and reports what it found; the daemon reads the configured path (or$MVGAL_AI_MODEL) once at startup and logs plainly that inference is a built-in heuristic, not ONNX Runtime.
Changed
- Every version surface now reads one place. Six files carried the version as a literal of their own and every one of them had drifted:
mvgald --version, themodinfoversion ofmvgal.ko,mvgal-gui --version, theversionfield ofmvgal-bench --json, the DKMS registration version, andCargo.lock(still two releases back). They now readinclude/mvgal/mvgal_version.h, so the daemon, the CLI, the module and the package cannot disagree. The generatedmvgal_version.halso now includes<stdint.h>and<stdbool.h>, which it uses and never included — it only ever compiled when something else had included them first, so it could not be included early. - The generated version header is now safe for the kernel to include. The kernel build reaches the same header path (
Kbuildpasses-I$(src)/../include, andkernel/mvgal_core.cincludes<mvgal/mvgal_version.h>for the three numbers), and that header is macro-only in the source tree on purpose. It stayed macro-only only by accident of out-of-source builds: on RHEL 8cmake-rpm-macros'%cmakeis broken, so the spec invokescmakewith no-Band the build is in-source, whereconfigure_filewrites the generated copy straight over the source tree's. There the kernel compiled the user-space half,<stdint.h>resolved to GCC's, whose#include_nexthas no libc headers to find — and the resulting error cascade also brokemodule_param()for an unrelated module parameter. Only RHEL 8 was ever affected, which is exactly why a latent header-layout dependency survived every other distro. The user-space half is now fenced with__KERNEL__, which kbuild always defines and no user-space build does, so which copy of the header wins no longer decides whether the kernel compiles. scripts/mksrpm.shnow checks every one of those. It verified the six files that derive the version and ignored the ones that copied it, which is why the copy drifted while the check kept passing. It now also verifiestools/libmvgal/CMakeLists.txtandkernel/dkms/dkms.conf, checks thatCargo.lock's workspace members are on the built version, and rejects anyMAJOR.MINOR.PATCHliteral appearing in the six source files that report a version — with C comments stripped first, so a version quoted in a comment explaining the check is not itself flagged.- The Vulkan ICD is no longer registered globally.
mvgal_icd.jsonwas installed intoshare/vulkan/icd.d, so every Vulkan application on the system loaded it — and since v0.7.12 it reports zero physical devices by design. With no device handle the loader cannot build a per-device dispatch table, so it printedCannot query ICD interface function vkGetPhysicalDeviceDisplayPropertiesKHR, vkGetPhysicalDeviceDisplayPlanePropertiesKHR in ICD N. The ICD is not conformant.on every startup. The manifest now lives inshare/vulkan/mvgal/and is applied only on request, viamvgal-vulkan-env.sh(which is now installed, unlike before) orMVGAL_ICD_JSON. Itslibrary_pathis absolute rather than relying on the loader'sdlopensearch order. The implicit interception layer stays registered globally, because that is the layer that does the work. Upgrades remove a stale manifest fromicd.donly when it is recognisably ours. - The OpenCL ICD is parked when it has nothing to aggregate.
libmvgal_opencl.sois a real aggregating ICD — it scans/etc/OpenCL/vendors/, skips itself and delegates to every peer — but with no vendor OpenCL runtime installed it produced zero platforms and no explanation. On first install with no peers present, the manifest is moved aside asmvgal.icd.disabledwith a message naming the package that provides a peer and the exact command to re-enable it. Erasing the package re-enables it if a peer has since appeared. packaging/rpm/mvgal.confremoved. It was a dead duplicate ofconfig/mvgal.confwith a completely different section layout, and the pkexec helper'ssededits only ever matched the latter. Leaving two files namedmvgal.confwith different schemas is a trap; the installed file is now the only one.scripts/mksrpm.shrefuses to build from a dirty tree. It builds the source tarball withgit archive HEAD, so the SRPM describesHEADand nothing else — an uncommitted change, and in particular an untracked new file, is simply absent from the package. That is not hypothetical: the first 0.7.14 SRPM shipped the pre-fix source tree, becauseconfig/mvgal-modules.shhad not been added yet, and failed at the far end on four missing files in%files. The script now stops with the offending paths listed.- Documentation corrected where it described the socket as
root:root-only and the helper as living in/usr/bin. - Version bumped 0.7.13 to 0.7.14.
[v0.7.12] - 2026-09-27
This release removes the fabricated-success paths found by a full audit of the kernel and Vulkan subsystems. MVGAL reported hardware capabilities it could not deliver; where a capability is not obtainable on Linux, it now says so and the caller falls back.
Removed
- GPU PCI binding.
mvgal_pci_driverand itsMODULE_DEVICE_TABLEalias table are gone. Registering a PCI driver whose probe callspci_enable_device()andpci_set_master()competes with amdgpu, nvidia, i915/xe and mtgpu for a device MVGAL cannot drive. GPUs are still discovered read-only throughpci_get_device()and the PCI bus notifier, andmvgal.konow publishes noalias:at all, so udev cannot autoload it onto a GPU. This also removed a double registration:mvgal_enumerate_gpus()added every GPU, thenpci_register_driver()re-probed the same devices and added them again. - Fabricated VRAM allocation, all four vendors.
alloc_vramwas a bump allocator returning addresses with no backing buffer object: amd seeded0x100000000, mtt0x40000000, nvidia0x10000000, and intel returned the allocation's sequential id as the address, so the first allocation yielded address 0. All now return-EOPNOTSUPPand write*gpu_addr = 0so no stale address escapes. submit_csthat dispatched nothing, all four vendors. Each validated arguments, logged a debug line and returned 0. Two dispatcher branches inmvgal_scheduler.cwent further and marked the workload COMPLETE with the comment "No vendor ops - mark complete immediately for testing". All now return-EOPNOTSUPP, and the scheduler restores the workload to PENDING instead of stranding it at RUNNING.- A synthetic Vulkan physical device. The ICD reported one device whenever a GPU was present, carrying GPU 0's real
vendorID/deviceID/deviceTypeand the name"MVGAL Aggregated (<gpu0> + N more)". It now reports zero devices. The interception layer already enumerated the real devices truthfully; that is the honest multi-GPU surface. - Dead fabrication helpers in
physical_device.c.mvgal_physical_device_create,mvgal_physical_device_destroy,mvgal_get_physical_device_group_propertiesandmvgal_get_real_gpu_counthad zero callers. The two live functions remain.
Fixed
pci_devreference leak. Every enumerated GPU held a reference that was never released.mvgal_gpu_free()now releases it, and deliberately does not callpci_disable_device()orpci_set_master(), which belong to the native driver.- Double
pci_dev_put(). Both error paths ofmvgal_enumerate_gpus()released a reference that the shared exit path released again. - Two crossed power-state enums.
mvgal_set_power_state()tookenum mvgal_power_state(OFF=0, SUSPEND=1, IDLE=2, ON=3, PERFORMANCE=4) while every vendor op and the ops-table member takeenum mvgal_device_power_state(ACTIVE=0, SUSTAINED=1, IDLE=2, PARK=3, OFF=4). Values were silently reinterpreted: requestingONreached the driver asPARK,PERFORMANCEasOFF. The redundant enum is deleted and the power manager now speaks the vendor-op vocabulary. mvgal_set_power_state()never updated the tracked state. When a vendor op existed it returned immediately, sogpu->power_statediverged from what the driver was doing. With no vendor op it recorded the request and returned success; it now reports that the state was recorded but not applied.wait_idlereturned success without waiting, in all four vendors, with no exported idle-wait entry point on any of them. Now-EOPNOTSUPP.- Unverified capability flags.
can_export_dmabuf,can_import_dmabuf,ttm_availableanddpm_supportedwere set totruefrom the vendor family with no test.dpm_supportednow requires the sysfs node to exist. - The memory KUnit suite asserted nothing.
test_memory_alloc()returned NULL because no fixture had calledtest_memory_manager_init(), so everyKUNIT_EXPECT_NOT_ERR_OR_NULLwas exercising the NULL path rather than the real one. The suite now registers proper init and exit fixtures. - Duplicate module detection in the NVIDIA driver.
mvgal_nvidia_initinlined the same/sys/modulelookup twice; a singlemvgal_nvidia_module_present()helper now serves both call sites. - A dead shader route call.
vk_layer.cran a full SPIR-V optimise and decompile on everyvkCreateShaderModule, against a hardcoded AMD vendor regardless of the actual GPU, then discarded the result.
Added
- Runtime capability probing. Each GPU carries an
MVGAL_CAP_*mask built by an actual test: opening the sysfs node, or checking which driver owns the PCI function. Nothing is inferred from the vendor family.probe_capabilitiesis a new vendor op, and a capability that cannot be verified is simply absent.kernel-submit,kernel-vram,dmabuf-exportandwait-idleare deliberately never set for any vendor, for reasons recorded per vendor.dma-buf-importandbo-queryare set where a driver owns the function, because those paths are real. - Callers consult capabilities, not ops pointers. The scheduler, power manager and memory manager previously tested
ops->foo != NULL, which an op that logs and returns 0 satisfies just as well as a working one. They now test the probed bit. - Per-GPU sysfs attributes
capabilitiesandnative_driver, both printingnonewhen there is nothing to report, so an unprobed device cannot be mistaken for a capable one. - Idle gating, which requires a measurable busy signal and therefore
MVGAL_CAP_BUSY_PERCENT. A device without one reports idle-gating as unavailable and refuses to arm rather than gating on a guess. Enabling it on such a device returns-EOPNOTSUPPto the writer. Exposed asidle_gate,idle_gate_statusandidle_gate_timeout. - Corrected documentation.
docs/SPEC_COMPLIANCE.mdfindings 1, 2 and 3 are marked resolved with the evidence, and a new section records the probing contract. The README status table no longer claims the PCI binding and fake vendor operations are still present.
Changed
- Version bumped 0.7.11 to 0.7.12.
[v0.7.11] — 2026-09-26
Fixed
- CRITICAL-1: The Vulkan shader compiler no longer overflows the heap when
ftell()fails.compile_nvidia_ptx()andcompile_intel_igc()(src/userspace/intercept/vulkan/mvgal_shader_compiler.c) sized their output buffer fromftell()and then read into it without validating the result. On failure the length is-1, so the allocation ismalloc(0)— which glibc satisfies with a non-NULL unique pointer, meaning the existingif (!*out_source)guard did not catch it, and the followingfread(*out_source, 1, (size_t)-1, f)then requestedSIZE_MAXbytes into a zero-byte block. The trailing(*out_source)[-1] = '\0'was an out-of-bounds write in the same way. Both functions now reject a non-positive length and verify the short-read case, mirroring the checks the siblingcompile_amd_llc()already had. - HIGH-1: The daemon no longer writes out of bounds when the config file length cannot be determined. The
GET_CONFIGhandler (runtime/daemon/ipc_server.cpp) sized its response vector fromftell()and then wrote the terminator at that offset, butftell()returns-1on failure — on the resulting zero-length vector,configData[fsize]isconfigData[-1], a heap write one byte before the allocation, reachable by any client that can open the IPC socket. Thefread()result was also unchecked. Bothfseek()calls and thefread()are now verified, a negative length is rejected withIpcErrorCode::GNU_ERROR, and the payload length reported to the client is the number of bytes actually read — which additionally stops a stray NUL terminator from being shipped as payload. - LOW-1: The discovery message header now documents that
DiscoveryMessageHeader::tcpPortis a 32-bit wire slot carrying a 16-bit port, and thehtons()/ntohs()call sites narrow explicitly. The field width is deliberately unchanged, so the on-wire layout of the packed struct is byte-for-byte identical; the previous implicituint32_t→uint16_tnarrowing was self-consistent only because the upper 16 bits happened to stay zero. - MED-1: The version can no longer drift between the number and the string in
include/mvgal/mvgal_version.h. The header carriedMVGAL_VERSION_MAJOR/_MINOR/_PATCHand a hand-writtenMVGAL_VERSION_STRINGindependently, and the string is now built from the three numbers by a stringify macro, so a bump that edits only the numbers cannot leave the string behind. The previous in-tree copy had already drifted:MVGAL_VERSION_STRINGread0.7.10whileMVGAL_VERSION_PATCHread10after a bump that only touched the dotted form, and becausemvgal_get_version_numbers()(src/userspace/api/mvgal_api.c) reports the numbers rather than the string,mvgal --versionprinted a patch number one release behind whatmvgald --versionand the generated header reported. Commit95d0974had generated an authoritative header fromproject()to prevent the two forms drifting apart, but that generated copy only shadows the in-tree one for some targets, so the in-tree copy remained live for the CLI.
Added
scripts/mksrpm.shnow verifies that the spec,CMakeLists.txt, bothCargo.tomlmanifests and both in-tree version headers agree on one version before it cuts the tarball, and reports every mismatch at once. Nothing previously forced those copies to agree, so a bump that missed one shipped a package whose daemon, CLI and library each reported a different version. This is the check that would have caught MED-1.
Changed
- Version bumped 0.7.10 → 0.7.11 (bugfix release)
[v0.7.10] — 2026-09-26
Fixed
- HIGH-1: The daemon no longer invents a GPU that does not exist.
rescan_gpus_locked()(src/userspace/daemon/gpu_manager.c) calledadd_placeholder_gpu_locked()whenever the DRM and NVIDIA scans both came back empty, registering a synthetic descriptor:MVGAL_VENDOR_UNKNOWN,MVGAL_GPU_TYPE_VIRTUAL, 4 GiB of VRAM, named "MVGAL Placeholder GPU", withavailable = false. Every consumer of the descriptor list then operated on that fiction — the scheduler would place work on it, VRAM accounting would report 4 GiB that is not there, andmvgal list-gpuswould show a device no driver backs. On a host with no supported GPU, or with themvgalkernel module not loaded, the daemon now logs the warning and reports zero GPUs. - HIGH-2: The Vulkan ICD no longer advertises a fictitious physical device.
mvgal_vkEnumeratePhysicalDevices()(src/userspace/vulkan_icd/icd_entry.c) returned a count of 1 and ag_virtual_physical_devicehandle named "MVGAL Virtual GPU (no physical GPUs detected)" wheneverg_daemon_gpu_count == 0, andmvgal_vkCreateInstance()populated the properties struct to match. The loader would then select a device that has no backing, and the failure surfaced much later inside the application's own initialisation, far from the real cause. The ICD now reports 0 devices and leaves*pPhysicalDeviceCountat 0 — the contract the loader expects, which lets it fall through to the next real ICD on the system instead of dying here.
Changed
- Version bumped 0.7.9 → 0.7.10 (bugfix release)
sudo→pkexecacross all shipped user-facing text (spec%posthints,config/scripts, docs, wiki, packaging helpers) to match the documented polkit-based privilege model.
[v0.7.9] — 2026-09-26
Fixed
- HIGH-1: Stale kernel header cache no longer breaks the build —
kernel/CMakeLists.txtusesfind_path(KERNEL_HEADERS include/generated/utsrelease.h), and CMake never re-validates a cachedfind_pathhit. A build directory reused across a kernel upgrade kept pointing at the removed header tree, so themvgal-kerneltarget failed withNo such file or directoryeven though the tree was present for the running kernel. The cache entry is now dropped when the generated headers are missing, so the build self-heals on any future kernel upgrade. This was the only failing target in a full build. - HIGH-2:
libmvgal_cuda.socould never be built —src/userspace/CMakeLists.txtgated the CUDA wrapper onWITH_CUDA, a variable that was never declared viaoption()orset()anywhere in the project. Undefined CMake variables are falsy, so the target was unreachable. Now built wheneverMVGAL_BUILD_API=ON. - HIGH-3:
libmvgal_cuda.sois now packaged, and packaging no longer depends on the build host having a CUDA toolkit —cuda_wrapper.cis a puredlopen/dlsyminterposition shim: it includes no CUDA headers and links no CUDA libraries, resolvinglibcudaat load time. Gating the target onCUDAToolkit_FOUNDtherefore only served to make the library unbuildable, and so unshippable, in any environment withoutnvcc. The gate and the dead${CUDAToolkit_INCLUDE_DIRS}/${CUDAToolkit_LIBRARIES}are gone, and%fileslists the library, fixingInstalled (but unpackaged) file(s) found: /usr/lib64/libmvgal_cuda.soin every toolkit-less build root. - MED-1:
memory.cno longer fails a strict-warning build —mvgal_migration_scheduler_dispatch()declared astruct timespec ts;that was never used, left over from an untimed-wait rewrite of the migration condition variable. Removed. - MED-2:
ai_data_collector.cno longer fails a strict-warning build — the file used the POSIX timer API (timer_create,struct sigevent,SIGEV_THREAD,union sigval) without including<signal.h>, producing six cascading-Werrordiagnostics. The siblingai_predictor.calready included it. - LOW-1:
cuda_wrapper.cis clean under-Wall -Wextra -Werror— the two cross-GPU copy stubs left theirCUdeviceptrparameters unused. Annotated with(void)casts; no behaviour change.
Changed
- Version bumped 0.7.8 → 0.7.9 (bugfix release)
src/tests/tests/test_all.shrewritten against the real tree layout. It referenced roughly nineteen paths that do not exist (dist/mvgal-0.2.2.tar.gzfrom the 0.2.x era,pkg/debian/,pkg/rpm/,pkg/arch/,pkg/flatpak/,pkg/snap/, and three phantombenchmarks/{synthetic,real_world,stress}/binaries) and usedset -ewithoutpipefail. Now 10 sections and 72 checks, all resolving to real files.src/tests/tests/test_cuda_wrapper.shrewritten. It was fully broken: a hardcoded stale absolute path (/home/axogm/Documents/Driver/mvgal) instead of derivingBASE_DIRfrom$0, two tests that printed a verdict without touching the exit status (so a real regression was invisible to CI), and annm -Dgrep forcuda_wrapper_init, which isstaticand could never match. Now 7 real assertions including adlopenharness and the eight interposed CUDA symbols, with real pass/fail counters and a real exit code.test_all.sh,test_compile_all.shandtest_cuda_wrapper.share clean undershellcheck --severity=warning.
[v0.7.8] — 2026-09-22
Fixed
- HIGH-1:
mvgal-enroll-mokno longer reports false success —mokutil --importis now fed the password via stdin (the-pflag is rejected by mokutil 0.7.2) and the pending MOK list is verified withmokutil --list-newbefore claiming enrollment. - HIGH-2: Daemon drops capabilities after init —
mvgaldnow clears effective/permitted/inheritable sets and prunes the bounding set toCAP_SYS_ADMIN CAP_DAC_OVERRIDE CAP_SYS_RAWIO; the systemd unit addsCapabilityBoundingSet/AmbientCapabilities/NoNewPrivileges. - HIGH-3:
mvgal-statusreports degraded mode — when themvgalkernel module is not loaded it printsKernel Module: NOT LOADED (degraded userspace-only mode)with a MOK enrollment hint, and the hard-codedLoad Balance: 100.00%is replaced by a real estimate (orN/Awhen fewer than 2 enabled GPUs / no workload data). - HIGH-4: Vulkan ICD reports a real driver version —
apiVersion/driverVersionare set from the MVGAL version macros in the daemon-GPU path and copied inmvgal_vkGetPhysicalDeviceProperties(previously0.0.0). - MED-1:
mvgal-infoDRM fallback reads theenabledstate from/etc/mvgal/mvgal.confinstead of hard-coding every GPU as enabled. - MED-2:
mvgal-benchmulti-GPU scaling figures are labeled as theoretical estimates, not measured results. - MED-3:
load-module.shresolves module paths viamodinfo -ninstead of a hard-coded/lib/modules/.../updates/dkmspath. - MED-4: D-Bus policy for
org.mvgal.MVGALis restricted to root and themvgalgroup (previously open to all users). - MED-5:
mvgal-hw-validateattributes device-node failures to "kernel module not loaded" with a load hint instead of a bare FAIL. - MED-6:
/etc/mvgal/custom_strategy.luais now shipped as a no-op stub so the[custom] script_pathreference inmvgal.confalways resolves.
Changed
- Version bumped 0.7.7 → 0.7.8 (bugfix release)
[v0.7.7] — 2026-09-21
Fixed
- CRITICAL-1: Vulkan ICD device dispatch —
vkCreateImage,vkGetImageMemoryRequirementsandvkDestroyImagewere missing fromvk_icdGetDeviceProcAddr, leaving NULL entries in the loader's device dispatch table. Any app callingvkCreateImage(e.g.vulkaninfo'sFillImageTypeSupport) crashed with SIGSEGV (exit 139). The three image functions are now implemented (tagged allocation records, plausible size/alignment/memoryTypeBits) and registered. - CRITICAL-2: Vulkan ICD
vkGetPhysicalDeviceQueueFamilyProperties2wrote past the caller's array — the 1.0 helper writes all queue families starting at element 0's inner struct, overflowing theVkQueueFamilyProperties2array and corrupting the pNext chain (heap corruption / SIGABRT under multi-ICD setups). Now fills a temp buffer and copies per element. - MED-1: Version macros unified —
mvgal-gui --versionnow reports 0.7.7 (CMakeLists.txt was stale at 0.7.5 while Cargo.toml and the spec said 0.7.6).
Changed
- Version bumped 0.7.6 → 0.7.7 (bugfix release)
[v0.7.6] — 2026-09-17
Fixed
- CRITICAL-1: Vulkan layer no longer returns
VK_ERROR_LAYER_NOT_PRESENTfromvkEnumerateDeviceExtensionPropertiesfor physical devices it did not wrap (e.g. the Mesa dzn ICD). The loader aborted with aVUID-physicalDevice-parametererror (SIGABRT, exit 134) inmvgal_amd_external_mem --scan; the layer now forwards the call through any registered instance dispatch chain. - HIGH-1:
mvgal-statusnow prints an explicit DEGRADED MODE warning and exits non-zero when themvgalkernel module is not loaded (Secure Boot / MOK rejection previously left the daemon silently running on sysfs/stub data). - HIGH-2:
mvgal-config -c/--confignow writes to the requested config file — the pkexec helper parsed--configbutset_config_valueignored it and always wrote/etc/mvgal/mvgal.conf. - HIGH-3: Config helper append check is now section-scoped, so a key that also appears in a later section no longer suppresses appending it to the target section.
- MED-1: Daemon
SET_CONFIGIPC handler validates the payload (printable ASCII + at least one[section]) and writes atomically (temp file + rename) instead of truncating the live config with a raw write. - MED-2:
mvgal-dashboardwith no arguments prints a banner + usage hint instead of exiting silently. - MED-3: Version macros unified —
mvgal --versionnow reports 0.7.6 everywhere (MVGAL_VERSION_PATCHwas stale at 3 while the string said 0.7.5).
Changed
- Version bumped 0.7.5 → 0.7.6 (bugfix release)
[v0.7.5] — 2026-09-15
Fixed
- CRITICAL-1: DKMS install —
BUILT_MODULE_LOCATION=/kernelfor all modules so DKMS finds the.kofiles built into thekernel/subdirectory. The previousMAKE[1]copy approach never ran because DKMS executes only one make command. - CRITICAL-2: Secure Boot —
%post -n mvgal-dkmsnow signs the DKMS-installed modules in/lib/modules/*/updates/dkmswith the per-machine MVGAL key (previously only the kernel-tree modules were signed, so DKMS modules were rejected under Secure Boot).mvgal-enroll-moknow generates a one-time password when-pis omitted and always passes it tomokutil --import, so enrollment works through pkexec without a TTY. - CRITICAL-3: Vulkan ICD implements the WSI entry points the loader requires —
vkGetPhysicalDeviceSurfaceSupportKHR,SurfaceCapabilitiesKHR,SurfaceFormatsKHR,SurfacePresentModesKHR, plusFormatProperties2,ImageFormatProperties2,SparseImageFormatProperties2,ExternalBuffer/Fence/SemaphoreProperties,ToolProperties— so apps no longer abort with "ICD does not export vkGetPhysicalDeviceSurfaceSupportKHR!".
[v0.7.4] — 2026-09-07
Fixed
- CRITICAL-1: DKMS build layout —
dkms.confnow copies the built.kofiles from thekernel/subdirectory up to the DKMS build root sodkms installactually finds and installsmvgal.ko/mvgal_ntsync.ko; spec%postno longer swallowsdkms build/dkms installfailures - CRITICAL-2: Vulkan ICD now exposes a placeholder physical device (instead of 0 devices) when no physical GPU is available or the daemon is not running
- CRITICAL-3: OpenCL CMake propagation fixed —
OpenCL_LIBRARIES/OpenCL_INCLUDE_DIRSare now populated from the pkg-config result so the ICD links the loader correctly - HIGH-1:
mvgal_amd_external_mem --no-mvgalnow implies--scaninstead of printing usage - HIGH-2:
mvgal-dashboardandmvgal-guigained--check/--self-testfor non-hanging headless verification - HIGH-3:
mvgal-enroll-mok -p/--passwordis now passed tomokutil --importso enrollment works without a TTY through pkexec - MED-1:
mvgal-probeexits 0 when the DRM sysfs fallback detects GPUs (was 2) - MED-2:
mvgal-steam-setupwith no arguments prints usage and exits 0 - MED-3:
mvgal-config set-power-profilehelp now marks the option AMD-only and the error suggestsmvgal list-gpus - MED-4:
mvgal-load/mvgal-unloadself-escalate via pkexec using the existingmvgal-pkexec-helper.sh
Changed
- Version bumped 0.7.3 → 0.7.4 (bugfix release)
[v0.7.3] — 2026-09-02
Fixed
- CRITICAL-2:
mvgal/mvgal-configconfig writes now route through the-c-specified config file instead of always writing the default path;mvgal-pkexec-helper.shgained a--configoption - HIGH-1: D-Bus policy now takes effect immediately after install — spec
%postreloads the D-Bus daemon - HIGH-2:
mvgal_amd_external_mem --scanprints progress before each blocking Vulkan call and gained--no-mvgalto enumerate real vendor ICDs only (bypasses the MVGAL ICD) - HIGH-3:
mvgal-dashboardprints a startup banner when launched from a terminal so it no longer looks like it hangs with no output - HIGH-4:
mvgal --versionreported 0.7.1 becauseMVGAL_VERSION_PATCHwas stale; version macros unified - MED-1:
mvgal-benchacceptssynthetic/realworldsuite aliases matchingmvgal --help - MED-2:
mvgal-probeno longer passes a closed fd to UAPI ioctls when the module is absent, and honors-jin the DRM sysfs fallback - MED-4:
mvgal stopreturns non-zero when the daemon is not running - MED-5:
mvgal start/stop/restartdetect systemd via/run/systemd/systeminstead ofsystemctl is-system-running, so degraded systems still use the systemd unit - MED-6:
mvgal-guidisables xdg-desktop-portal registration in the Application constructor (defense in depth) - MED-7:
mvgal-compatno longer shows the "Unknown title" note for applications found in the compatibility database - MED-8:
--versionadded tomvgal-config,mvgal-probe,mvgal-enum,mvgal-hw-validate,mvgal-status,mvgal-compat, andmvgal-bench
Changed
- Version bumped 0.7.2 → 0.7.3 (bugfix release)
[v0.7.2] — 2026-09-01
Fixed
- CRITICAL-2:
mvgal-enroll-mokfalse positive — verify output contains 'is enrolled' - CRITICAL-3: daemon reads
[core] default_strategyfrom config and maps it to the scheduler mode - CRITICAL-4: D-Bus policy adds
receive_senderfor signal reception - HIGH-2/3:
QT_ENABLE_PORTAL=0set beforeQApplicationconstruction (GUI + dashboard) to prevent portal hangs - HIGH-4:
mvgal-statusfalls back topidofwhen systemctl is unavailable - HIGH-5:
mvgal-infofalls back topidofon socket ENOENT - MED-2/3:
mvgal-configusesstrtolwith validation instead ofatoi - MED-5:
modeToStringhandles theAI_DRIVENenum
Changed
- Version bumped 0.7.1 → 0.7.2 (bugfix release)
[v0.7.1] — 2026-09-01
Fixed
- DKMS build failure:
kernel/Makefilewas a CMake-generated file with hardcoded local paths; replaced with a Kbuild wrapper that delegates to the kernel build system mvgal-loadused the wrong module path and was missingmvgal_mttandmvgal_adreno; now points at/updates/dkmsand loads all 7 modules- Custom strategy was accepted without verifying the script exists; both
mvgalandmvgal-confignow require/etc/mvgal/custom_strategy.lua mvgal statusalways reported "single GPU" because the CLI did not load the default config; now loads/etc/mvgal/mvgal.confby defaultconfig/mvgal.confenabledgpu_1on single-GPU systems; now disabled
Changed
- Version bumped 0.7.0 → 0.7.1 (bugfix release)
[v0.7.0] — 2026-08-31
Added
- Multi-vendor OpenCL ICD aggregation: platforms/devices from all vendor ICDs merged into one logical OpenCL platform
-Bsymbolic-functionslink flag to fix re-entrant deadlock in the OpenCL interceptormvgalCLI OpenCL platform/device enumeration reporting
Changed
- Version bumped 0.6.0 → 0.7.0 across spec, CMake, Rust workspace, kernel module, daemon, GUI, dashboard, mvgal-bench, libmvgal, docs, and README
[v0.4.0] — 2026-06-07
Added
- GPU power management: NVML-based frequency/thermal control, sysfs power limit fallback, AMD PPL interception wrappers
- Dynamic GPU scoring scheduler with real-time telemetry integration
- NTSYNC kernel module with cross-device synchronization ioctls
- Multi-vendor Vulkan barrier translation and shader backend dispatching
- PCIe P2P DMA support with connectivity matrix in UAPI
- AI-driven scheduler model inference and prediction
- Unified VRAM heap management with DMA-BUF zero-copy
- Real-time Intel GPU VRAM and utilization queries
- NVML loader for NVIDIA power/temperature monitoring
- Proton bridge for Steam/Proton integration
- Full-stack build support via DKMS
- Tool suite: mvgal-info (heap tracking), mvgal-bench, mvgal-compat documentation
- P2P and DMA-BUF telemetry in kernel UAPI
- Cross-platform multi-GPU abstraction layers
- RPM/COPR: vendored Rust deps for offline builds, multi-distro support (Fedora 40-44, Rawhide, RHEL/AlmaLinux/Rocky 9 & 10, CentOS Stream 9 & 10, openSUSE Tumbleweed, Amazon Linux 2023)
- Author metadata updated to 'AxoGM' project-wide
Changed
- Kernel module: C89 compat fixes, MODULE_IMPORT_NS threshold lowered to 6.12.0
- CMake: improved kernel version detection, LTO gated behind GCC >= 9
- PID files migrated to /run, D-Bus event loop integrated
- Daemon status IPC handling refactored
- Rust crate versions synced to workspace 0.4.0 (fence_manager, memory_safety, capability_model)
- All version references (spec, README, CMake) synced to 0.4.0
Fixed
- Kernel module struct redefinition and duplicated code in mvgal_device.c
- Missing linux/version.h include in mvgal_device.c
- GCC 15+ RHEL_RELEASE_VERSION preprocessor syntax error
- class_create compat for EL9, libelf for EL8, Tumbleweed build scripts
- SPEC: LTO guard for GCC version, EL8 cmake bypass, glob patterns, Source0 URL
- RHEL 8 LTO linker errors for static archives and tools
- Daemon init order, IPC parsing, socket EACCES cleanup
- Vulkan 1.3 API guard for legacy headers
- Mageia kernel header paths and empty kernel-module.list
- Prometheus and SYCL backend static libs in spec
- Build: all -Werror warnings resolved (26/26 compile tests)
Removed
- Stale Module.symvers and modules.order generated kernel module files
- Stale GitHub Actions CI/CD workflows (ci.yml, copr.yml)
v0.2.2 — 2026-05-16
Changed
- Bumped active project, runtime, API wrapper, Vulkan ICD, Steam compatibility, UI, binding, and packaging metadata from 0.2.1/older stale versions to 0.2.2.
- Added Nix and Gentoo package metadata to the supported distribution set.
Fixed
- Runtime daemon scheduler queue consumption and wait predicate behavior.
- Runtime daemon memory replication deadlock when populating additional GPUs.
Added
- Remote GPU network pooling: UDP/TCP peer discovery, heartbeat health tracking, RemoteGpu integration with DeviceRegistry, latency-weighted scheduler scoring
- AI-driven scheduler:
mvgal_ai.hmodel inference C API (load_model,predict),[ai_scheduler]config section, runtime enable/disable toggle - Cross-crate Rust FFI integration test suite (
safe/ffi_tests/) — 9 tests validating fence, memory, and capability lifecycle across C FFI boundaries
Changed
- Rust FFI audit:
panic::catch_unwindwrappers on all 19extern "C"functions acrosscapability_model,fence_manager,memory_safety - Metrics collector expansion with additional GPU telemetry fields
Fixed
- SDL error handling in benchmarks
mvgal_resolvetype mismatch in stress benchmark- Unused variable warnings in stress benchmark
- Missing CMake include paths for
mvgaldtarget
Removed
- Stale GitHub Actions CI/CD workflows (ci.yml, copr.yml)
v0.2.1 — 2026-05-01
Added
- RPM/COPR packaging: spec file fixes for Fedora, conditional OpenCL, debuginfo
- Vulkan ICD with device, memory, command buffer, and sync entry points
- Real GPU querying for Vulkan ICD, stress benchmark cleanup
- Vulkan API interception tests
- Fedora COPR package badge and install instructions
- Configurable GPU selection strategies: AFR, single, all-device modes
- Automatic frame rendering (AFR) support for aggregated Vulkan devices
- Dynamic load balancing with mutex lifecycle fix in Vulkan device group emulator
- GPU driver telemetry integration into load balancer
- Cross-vendor semaphore synchronization and tile assembly logic
- Kernel stub ABI, Vulkan layer updates, pkexec privilege helper
- Steam/Proton compatibility layer
Changed
- CMake: use GNUInstallDirs for proper lib64 detection, compatibility aliases
- RPM spec: remove non-ASCII chars for RPM 6.x compatibility
- Project homepage URL updated
- Documentation rewritten based on full code analysis
Fixed
- Vulkan 1.4 header incompatibilities and duplicate declarations
- CMake nesting error in Vulkan detection logic
- Manual Vulkan header search fallback, path syntax
- GCC 15+ implicit-declaration error in
mvgal_rewrite_update_gpu_utilization - Missing MVGAL_INCLUDE_DIRS on Vulkan layer target
- Dead
mvgal_rewrite_update_gpu_utilizationcall removed - Forward declaration for
mvgal_get_queue_family_propertiesin device_group.c - Function name mismatch (
mvgal_gpu_→mvgal_) in device_group.c - Build fixes for openSUSE, Mageia 8 compat
[0.2.1-5..0.2.1-12] — 2026-04/05
Added
- Moore Threads Driver Installer with Loginwall Support
- Multi-Vendor Vulkan Device Group Emulation
- Dynamic Workload Rebalancing Engine
- Security Policies (Polkit)
- Weighted dynamic load balancing for SFR
- Moore Threads vendor ID (0x1ED5)
Fixed
- RPM debuginfo package conflict
- Spec file to match actual build outputs
[0.2.0-1] — 2026-04-21
Added
- GPU enumeration and capability discovery
- Device group creation and management
- Basic workload distribution across GPUs
- Initial Vulkan device group emulation
- Compute kernel dispatch infrastructure
[0.1.0-1] — 2026-04-20
Added
- Initial project structure and build system
- Multi-vendor GPU abstraction layer design
- Basic memory management for cross-device buffers
- Fence synchronization primitives
- Core API specification