Releases: TekuSP/SubZeroFramework
Release list
v0.1.5 - NPU/GPU work for Windows and Linux
Added
-
Windows: live GPU and NPU utilization. Each graphics adapter's detail now carries its own live
utilization — the current percentage over a rolling 30-second graph, the same card the CPU uses per core —
and a new Neural processor category presents the NPU the way the CPU category presents packages: a
count, a processor list, and the picked device's detail. On a Framework 16 that is the integrated Radeon,
the discrete GPU module and the Ryzen AI NPU, each with its own reading, never blended into one number.
The service reads the Windows GPU Engine performance counters through one persistent PDH query (~1.5 ms per
second-tick, measured) and derives busy time per adapter the same way Task Manager does; devices are named
via SetupAPI and matched by adapter LUID. Software adapters with no physical device behind them (WARP) are
excluded. The Windows readers and their interop are compiled only into Windows builds — Linux ships none of
it, needs nothing new at install time, and shows an honest empty state until its per-vendor readers land. -
Linux: Device Capabilities finally shows graphics and displays. The page was empty on Linux because
Hardware.Info enumerates both lists by shelling out toxrandr, which needs a display server the background
service does not have. They now come from the kernel instead: adapters from/sys/class/drmwith their
names resolved through the systempci.idsdatabase, and displays from each DRM connector's EDID — model,
manufacturer, product code, serial, manufacture date, physical size and density. Works headless, and
identically under X11, Wayland or no session at all. The current resolution and refresh rate come from the
kernel's mode-setting state, so a 165 Hz panel reads 165 Hz rather than the 60 Hz its EDID advertises as
"preferred". Because a connector belongs to its card by kernel topology, each display is attributed to the
right GPU exactly, instead of by matching names as on Windows. -
Linux: GPU utilization for AMD, NVIDIA and Intel. Same cards as on Windows, one per GPU. AMD reads the
amdgpu driver's owngpu_busy_percent. NVIDIA goes through NVML, loaded at runtime only if the proprietary
driver is already installed — there is no new package dependency, and on an AMD-only machine the attempt is
silently skipped. Intel reads the i915 or xe performance counters, discovering the available engines from
what the driver advertises rather than assuming a layout.Sleeping GPUs are not woken to be measured. Reading AMD's busy attribute reaches the SMU, and an NVML query
takes a power-management reference on the card; either can hold a discrete GPU awake, which is how monitoring
tools have historically ruined laptop battery life. A GPU whose power state already reads "suspended" is
reported as 0% — which is what it is — without being touched.Each source is independent and optional: one vendor's driver misbehaving cannot blank out the others. Where
a reading genuinely cannot be taken the device simply reports nothing. In particular Intel's counters need
kernel 6.15 or newer on the newest GPUs (the xe driver), and the alternatives — inferring load from power
residency, or summing per-process figures — were rejected as misleading rather than shipped as an
approximation. -
Linux: NPU utilization. Intel NPUs (Core Ultra) report through the driver's busy-time counter, which
costs nothing to read and leaves an idle NPU asleep. AMD Ryzen AI NPUs report per-column utilization, on
kernels new enough to expose it and on the Ryzen AI 300 parts where it is wired up; where it is not
available the device is still listed, just without a reading. As with the GPUs, a sleeping NPU is reported
as 0% rather than being woken to be asked — for AMD that gate is the difference between a monitoring
feature and a battery-life regression. -
The Neural processor page now describes the hardware, not just its load. Vendor, driver and driver
version, firmware version, location and description sit alongside the utilization graph, the way the CPU
and graphics pages have always shown their devices. Windows reads them from the device properties; Linux
from the kernel's accelerator class, including the NPU firmware version where the driver publishes one. -
Device Capabilities categories with nothing in them are disabled. A category whose body could only show
an empty state no longer opens: the rail entry dims and says why on hover ("No graphics detected"). Onboard
devices and System profile always stay open. This is what a Linux machine sees for Graphics today, and what
any machine sees for a category its hardware does not have. -
Settings → Service logs. Shows what the background service has logged since it started, with a level
filter and a one-click "Copy all" for pasting into a bug report — no more hunting through Event Viewer or
journalctl. The service mirrors its log into a bounded in-memory buffer (the last 2,000 entries) that sits
alongside the platform sinks rather than replacing them; when older entries have been dropped the page says
so instead of implying a complete history.
Changed
-
Settings ▸ Service logs is now Settings ▸ Logs, and shows the app's logs too. The page only ever showed
the background service. The app's own records had nowhere to go on Windows at all: the desktop head is a
GUI-subsystem binary, so its console sink writes to a console that does not exist and its debug sink only
exists under a debugger. A released build could warn about a broken service connection every second and none
of it would reach you. The app now keeps the same bounded buffer the service does, and the page interleaves
both by timestamp with a source column and a Service / App / Both filter — so a client reconnect warning
reads directly next to the service restart that caused it. Copy-all tags each line with its source, because
"which process said this" is the first question asked of a pasted log. If the service cannot be reached, its
half fails alone and the app's own entries still appear — those are exactly the ones explaining why it looks
dead. -
Telemetry no longer reaches the UI faster than the UI can use it. Thirty-five subscriptions across the
view models and telemetry clients inherited the service's poll cadence directly, so lowering the poll
interval multiplied UI work with it. Each now states its own ceiling — 250 ms for live readouts, 500 ms for
chart history, 1 s for inventory — applied before marshalling to the UI thread, so coalescing happens off
it. These are ceilings, not floors: the operators only emit when something actually arrived, so a
one-second poll still updates once a second.
Fixed
-
Two code-analysis rules had been silently inert. SZF0004 (require
ObserveOn) and SZF0005 (require
DisposeWith) skipped every subscription whose receiver was typed asIObservable<T>— which is nearly all
of them, since everyWatch*/Connect*method and every Rx operator returns the interface. The type check
compared a display string (System.IObservable<T>) against a metadata name (System.IObservable`1),
which never matches for a generic, and the interface check looked at implemented interfaces, which do not
include the type itself. Both rules now apply where they always should have.Fixing that exposed ~53 violations, and working through them showed the rules had been over-broad as well as
under-applied. They now recognise the legitimate patterns they used to flag:Observable.Createfactories
(whose disposal is the returned disposable, and whose scheduler is the consumer's choice), ownership by
storage in a member,SerialDisposableslots and keyed registries,using var, returning the subscription
to a caller, and marshalling inside the handler via the dispatcher rather than upstream.ObserveOnis also
no longer required in the service and Core, which have no UI thread for it to marshal to. -
New rule: SZF0013, telemetry subscriptions must rate-limit. Flags a subscription that starts at a
per-poll telemetry source without bounding its rate. The operator it asks for depends on the stream: change
sets must useBatch, which coalesces, becauseSampleandThrottledrop items and a dropped change set
loses an add or a remove permanently — DynamicData ships noSample/Throttlefor change sets for exactly
this reason. Snapshot streams carry the whole value each time and may use either. -
Uninstalling from inside the app now uninstalls the app. The Uninstall button used to delete the
background service's registration — the same registration the Windows installer owns — so removing SubZero
afterwards through Add/Remove Programs left Windows Installer deleting a service that was already gone. On
an installed build the button now hands over to the real uninstaller and closes the app so its files can be
removed; the installer stops and removes the service itself. It asks first, and says plainly that saved fan
profiles are kept in case you reinstall. On a development build, where there is no installer, it still
removes just the service and the button says so.
Fixed
- Error status text was unreadable. "Unavailable", "Stalled", "Not Present", a critical battery and a
≥85 °C reading were drawn in the palette's dark red fill tone (#442726) as foreground — near-invisible on
the dark card. They now use the error text tone, like the rest of the app. Same swap for the storage/memory
usage bar past 90%, whose fill was the odd one out next to its bright green and amber siblings. - A newer service could kill the telemetry stream on an older client. The app's copy of the wire enum
mapper parsed an unknown value int...
v0.1.4 - Fix sensor availability no longer rewrites fan profiles or the UI
Added
- "Treat missing readings as 0°" per curve profile. A selected driving sensor that loses power counts as
cold instead of dropping out — for sensors that go quiet because whatever they measure is switched off, like
a sleeping GPU. With Maximum aggregation a dark sensor then simply stops mattering; with the other modes it
pulls the driving temperature down, so the editor warns about that combination. - The curve chart shows where fan control actually is. A red ✕ rides the curve at the live driving
temperature and the duty the service last applied. Because that duty already includes the CPU boost, the
marker can sit above the drawn curve — the gap is the boost. It disappears whenever no driving temperature
can be read, rather than freezing at a position the fan is no longer holding. - Windows installer offers a clean install. A new wizard page (before the install location) asks whether
to keep existing fan settings; ticking it deletes the service's saved configuration instead of upgrading
onto it. Off by default, and available unattended asmsiexec /i … CLEANINSTALL=1. Per-user preferences
(display units, startup, alerts) are not touched — the installer only owns machine-wide service data.
Fixed
- A sleeping GPU rewrote the fan's saved profile. When a driving sensor became unavailable the editor
deselected it and adopted whichever sensor happened to be first (Mainboard), and the next Apply persisted
that — the curve silently stopped following the GPU for good. Availability is runtime status, not user data:
a selection now survives its sensors going dark, comes back when they return, and is only auto-seeded for a
slot that has no selection at all. - A powered-down sensor could under-cool the machine. The service read every selected sensor's raw value
without checking whether it was reporting, so a dark sensor's 0 °C was folded into the aggregate — halving
an Average against a hot CPU. Sensor state is now honored, and the client and service share one
implementation of the reduction (FanDrivingTemperature), so the predicted duty matches what the fan does. - "Falls back to its firmware-safe curve" is now true. When no driving sensor of a curve-driven fan could
be read, the service simply skipped the fan — leaving the embedded controller holding the last duty the
curve asked for, with nothing observing the heat. It now hands that fan back to firmware fan control (once
per episode, retried if the handover fails) and resumes the curve automatically when a sensor reports again.
The stored profile is never altered by the fallback. - Opening Fan Control showed the Auto panel for a fan that was not on Auto. The mode selector correctly
read "Custom curve" while the body below it described automatic control. The mode body is a navigation
sub-region whose navigator does not exist yet on first entry; the sync gave up in that case and nothing ever
retried, because a fan already running a curve never raises a mode change to retrigger it. - The sensor list changed length when the GPU powered down. Its four sensors do not report the same way —
three go "not powered" while the die sensor goes "not present" — and a not-present sensor was deleted from
the selector outright, so eight chips became seven and a selected sensor disappeared from view. A sensor
that has never reported is still omitted, but one already on screen now stays put and dims. - An unpowered sensor could not be picked as a driving sensor. Its chip was disabled, so a GPU sensor was
unselectable exactly when you wanted to choose it (with the GPU idle) — and a selected sensor that went dark
could not be deselected either. Chips now stay interactive and simply dim while they have no reading. - Windows: the taskbar showed a generic window icon. An unpackaged WinUI 3 window does not inherit the
icon embedded in the executable, so the app icon appeared in Explorer but not on the taskbar or in Alt+Tab.
Full Changelog: v0.1.3...v0.1.4
v0.1.3 - Fixed out of bounds custom curves issues and added reset to factory settings
What's Changed
- Bump Uno.Sdk from 6.7.0-dev.137 to 6.7.0-dev.139 by @dependabot[bot] in #53
- Bump Grpc.Tools from 2.82.0 to 2.83.0 by @dependabot[bot] in #52
Added
- Settings → Service: "Reset fan settings to factory defaults". Returns every fan to the controller's
automatic mode and deletes all saved fan settings in one confirmed action — curve profiles in every slot,
the active profile per fan, "Applies to" links, CPU boost, and manual/max overrides — including entries for
fan indices the hardware no longer reports, which no per-fan action could reach. Display units, startup and
alert settings are untouched.
Fixed
- Curve points could be dragged off the chart and stranded there. The editor let a point be parked
outside the plotted temperature window (dragging past the plot edge keeps producing coordinates out in the
axis margin), where it was invisible and could never be grabbed again — the curve became uneditable. Points
now snap into a band inside the visible window, and curves already holding stranded points are pulled back
when they load. Pressing empty chart space also picks the new point straight up, so press-drag places a
point in one motion. - A curve no longer flatlines above its last point. The hidden anchor closing every curve at the top of
the temperature range inherited the last point's duty, so a curve ending at 0 % (or any low duty) held that
speed all the way to 130 °C instead of climbing. It is now pinned to full speed, matching the idle anchor at
the cold end: every curve ramps from idle to maximum across the range, however early the user's own points
stop. The client's drawn curve, its predicted-duty readout, and the service's actuation now share ONE
implementation of that rule (FanCurveDomain), so a preview cannot promise a speed the fan does not deliver.
Full Changelog: v0.1.2-alpha...v0.1.3
v0.1.2-Alpha Fixed CPU usage spikes from lshw
- Fixed CPU usage spikes from lshw on Linux
Full Changelog: v0.1.1-alpha...v0.1.2-alpha
v0.1.1-Alpha Fixed empty bay detection on Framework 16
What's Changed
- Bump Uno.Sdk from 6.7.0-dev.127 to 6.7.0-dev.137 by @dependabot[bot] in #49
- Fixed If expansion bay is empty, application incorrectly assumes error state
- Added INSTALL.MD with guide how to install the app
Full Changelog: v0.1.0-alpha...v0.1.1-alpha
Installation
- Debian
-
sudo apt install ./subzeroframework-service_0.1.1_amd64.deb ./subzeroframework_0.1.1_amd64.deb
- Windows
-
- Run SubZeroFramework-Setup-0.1.1-x64.msi
- Fedora
-
sudo dnf install ./subzeroframework-service-0.1.1-1.x86_64.rpm ./subzeroframework-0.1.1-1.x86_64.rpm
- Arch
sudo pacman -U subzeroframework-bin-0.1.1-1-x86_64.pkg.tar.zst
sudo systemctl enable --now subzeroframework.service
- Other Linux distros:
tar -xzf subzeroframework-0.1.0-linux-x64.tar.gz
cd subzeroframework-0.1.0
sudo mkdir -p /usr/lib/subzeroframework
sudo cp -r ui service /usr/lib/subzeroframework/
sudo ln -sf /usr/lib/subzeroframework/ui/SubZeroFramework /usr/bin/subzeroframework
sudo cp subzeroframework.service /usr/lib/systemd/system/
sudo cp subzeroframework.desktop /usr/share/applications/
sudo systemctl daemon-reload
sudo systemctl enable --now subzeroframework.service
v0.1.0-alpha Initial Release of SubZero Framework Edition
This is it, first release of SubZero Framework Edition fan control.
There can be bugs, please report them via Issues.
Whole project is Open-Source and I am glad Framework gave permission for their logo and brand.
Download via Assets below for your platform. You need both service + app for most Linux distros.
Arch and Windows have it combined.
Full Changelog: https://github.com/TekuSP/SubZeroFramework/commits/v0.1.0-alpha