@ohmykreee/dsh-remote-control 0.0.0-stage → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +556 -0
- package/INSTALLATION.md +310 -0
- package/LICENSE +21 -0
- package/README.md +195 -2
- package/client.js +3 -0
- package/components.json +20 -0
- package/cordis.patch.yml +3 -0
- package/docs/README.en.md +196 -0
- package/docs/screenshots/en/overview.png +0 -0
- package/docs/screenshots/en/question.png +0 -0
- package/docs/screenshots/zh/overview.png +0 -0
- package/docs/screenshots/zh/question.png +0 -0
- package/icon.svg +24 -0
- package/index.js +2 -0
- package/lib/account.js +1 -0
- package/lib/app-mapping.js +1 -0
- package/lib/i18n.js +1 -0
- package/lib/media-enrich.js +1 -0
- package/lib/notifications.js +1 -0
- package/lib/performance.js +1 -0
- package/lib/platform/index.js +1 -0
- package/lib/probe-adapter.js +1 -0
- package/lib/probe-daemon.js +1 -0
- package/lib/probe.js +1 -0
- package/lib/qr.js +1 -0
- package/lib/runtime.js +1 -0
- package/lib/service.js +1 -0
- package/lib/sessions.js +1 -0
- package/lib/settings.js +1 -0
- package/lib/types.js +1 -0
- package/lib/util.js +1 -0
- package/lib/weather.js +1 -0
- package/lib/ws.js +1 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +118 -4
- package/scripts/browser-shot.mjs +217 -0
- package/scripts/build-dsh.mjs +228 -0
- package/scripts/build-rust.mjs +319 -0
- package/scripts/build-web.mjs +17 -0
- package/scripts/check-npm-package.mjs +322 -0
- package/scripts/mock-preview.py +574 -0
- package/scripts/screenshot-compose.py +89 -0
- package/scripts/screenshot-device.sh +75 -0
- package/src/dsh/client/api.ts +68 -0
- package/src/dsh/client/i18n.ts +573 -0
- package/src/dsh/client/icons.ts +56 -0
- package/src/dsh/client/index.ts +95 -0
- package/src/dsh/client/logic.ts +310 -0
- package/src/dsh/client/panel.ts +483 -0
- package/src/dsh/client/sections/apps.ts +40 -0
- package/src/dsh/client/sections/balance.ts +38 -0
- package/src/dsh/client/sections/connection.ts +97 -0
- package/src/dsh/client/sections/notifications.ts +43 -0
- package/src/dsh/client/sections/pomodoro.ts +184 -0
- package/src/dsh/client/sections/preferences.ts +138 -0
- package/src/dsh/client/sections/system.ts +120 -0
- package/src/dsh/client/sections/weather.ts +276 -0
- package/src/dsh/client/styles.ts +1119 -0
- package/src/dsh/client/types.ts +366 -0
- package/src/dsh/client/ui.ts +142 -0
- package/src/dsh/index.ts +108 -0
- package/src/dsh/lib/account.ts +520 -0
- package/src/dsh/lib/app-mapping.ts +366 -0
- package/src/dsh/lib/i18n.ts +82 -0
- package/src/dsh/lib/media-enrich.ts +202 -0
- package/src/dsh/lib/notifications.ts +322 -0
- package/src/dsh/lib/performance.ts +279 -0
- package/src/dsh/lib/platform/index.ts +43 -0
- package/src/dsh/lib/probe-adapter.ts +275 -0
- package/src/dsh/lib/probe-daemon.ts +359 -0
- package/src/dsh/lib/probe.ts +395 -0
- package/src/dsh/lib/qr.ts +42 -0
- package/src/dsh/lib/runtime.ts +126 -0
- package/src/dsh/lib/service.ts +1645 -0
- package/src/dsh/lib/sessions.ts +907 -0
- package/src/dsh/lib/settings.ts +468 -0
- package/src/dsh/lib/types.ts +231 -0
- package/src/dsh/lib/util.ts +115 -0
- package/src/dsh/lib/weather.ts +532 -0
- package/src/dsh/lib/ws.ts +328 -0
- package/src/dsh/types/qrcode-generator.d.ts +16 -0
- package/src/rust/Cargo.lock +1431 -0
- package/src/rust/Cargo.toml +68 -0
- package/src/rust/src/cli.rs +233 -0
- package/src/rust/src/daemon.rs +140 -0
- package/src/rust/src/location/linux.rs +118 -0
- package/src/rust/src/location/macos.rs +26 -0
- package/src/rust/src/location/mod.rs +170 -0
- package/src/rust/src/location/windows.rs +205 -0
- package/src/rust/src/main.rs +42 -0
- package/src/rust/src/media/catalog.rs +454 -0
- package/src/rust/src/media/linux.rs +350 -0
- package/src/rust/src/media/macos/accessibility.rs +59 -0
- package/src/rust/src/media/macos.rs +366 -0
- package/src/rust/src/media/mod.rs +298 -0
- package/src/rust/src/media/vendor/apple_music.rs +90 -0
- package/src/rust/src/media/vendor/common.rs +186 -0
- package/src/rust/src/media/vendor/kugou.rs +52 -0
- package/src/rust/src/media/vendor/kuwo.rs +49 -0
- package/src/rust/src/media/vendor/mod.rs +75 -0
- package/src/rust/src/media/vendor/netease.rs +1715 -0
- package/src/rust/src/media/vendor/qishui.rs +36 -0
- package/src/rust/src/media/vendor/qqmusic.rs +33 -0
- package/src/rust/src/media/vendor/spotify.rs +46 -0
- package/src/rust/src/media/vendor/wesing.rs +39 -0
- package/src/rust/src/media/vendor/youtube_music.rs +49 -0
- package/src/rust/src/media/windows/accessibility.rs +85 -0
- package/src/rust/src/media/windows/audio.rs +116 -0
- package/src/rust/src/media/windows/private.rs +291 -0
- package/src/rust/src/media/windows.rs +775 -0
- package/src/rust/src/notifications/linux.rs +276 -0
- package/src/rust/src/notifications/macos.rs +382 -0
- package/src/rust/src/notifications/mod.rs +244 -0
- package/src/rust/src/notifications/windows.rs +565 -0
- package/src/rust/src/perf/common.rs +109 -0
- package/src/rust/src/perf/linux.rs +282 -0
- package/src/rust/src/perf/macos.rs +28 -0
- package/src/rust/src/perf/mod.rs +157 -0
- package/src/rust/src/perf/windows.rs +981 -0
- package/src/rust/src/util.rs +360 -0
- package/src/rust/src/volume/linux.rs +195 -0
- package/src/rust/src/volume/macos.rs +54 -0
- package/src/rust/src/volume/mod.rs +116 -0
- package/src/rust/src/volume/windows.rs +75 -0
- package/src/rust/tests/cli.rs +79 -0
- package/src/rust/tests/daemon.rs +107 -0
- package/src/svelte/App.svelte +385 -0
- package/src/svelte/index.html +18 -0
- package/src/svelte/lib/billing.svelte.ts +228 -0
- package/src/svelte/lib/components/BalanceCard.svelte +173 -0
- package/src/svelte/lib/components/ClockCard.svelte +183 -0
- package/src/svelte/lib/components/CountdownCard.svelte +261 -0
- package/src/svelte/lib/components/CpuMemoryCard.svelte +238 -0
- package/src/svelte/lib/components/EmptyState.svelte +34 -0
- package/src/svelte/lib/components/GpuCard.svelte +245 -0
- package/src/svelte/lib/components/HomePage.svelte +34 -0
- package/src/svelte/lib/components/MusicControls.svelte +256 -0
- package/src/svelte/lib/components/MusicCover.svelte +88 -0
- package/src/svelte/lib/components/MusicPage.svelte +116 -0
- package/src/svelte/lib/components/NotificationsCard.svelte +109 -0
- package/src/svelte/lib/components/Panel.svelte +22 -0
- package/src/svelte/lib/components/PerformancePage.svelte +55 -0
- package/src/svelte/lib/components/PomodoroCard.svelte +250 -0
- package/src/svelte/lib/components/QuestionSheet.svelte +354 -0
- package/src/svelte/lib/components/RadialGauge.svelte +99 -0
- package/src/svelte/lib/components/Rail.svelte +217 -0
- package/src/svelte/lib/components/SectionHeader.svelte +40 -0
- package/src/svelte/lib/components/SeekBar.svelte +421 -0
- package/src/svelte/lib/components/SessionList.svelte +364 -0
- package/src/svelte/lib/components/SessionStats.svelte +98 -0
- package/src/svelte/lib/components/SessionsPage.svelte +61 -0
- package/src/svelte/lib/components/SpendCard.svelte +105 -0
- package/src/svelte/lib/components/ThermalCard.svelte +249 -0
- package/src/svelte/lib/components/Toast.svelte +32 -0
- package/src/svelte/lib/components/WeatherIcon.svelte +40 -0
- package/src/svelte/lib/components/WeatherPanel.svelte +420 -0
- package/src/svelte/lib/components/ui/alert/alert-action.svelte +20 -0
- package/src/svelte/lib/components/ui/alert/alert-description.svelte +23 -0
- package/src/svelte/lib/components/ui/alert/alert-title.svelte +23 -0
- package/src/svelte/lib/components/ui/alert/alert.svelte +43 -0
- package/src/svelte/lib/components/ui/alert/index.ts +17 -0
- package/src/svelte/lib/components/ui/avatar/avatar-badge.svelte +26 -0
- package/src/svelte/lib/components/ui/avatar/avatar-fallback.svelte +20 -0
- package/src/svelte/lib/components/ui/avatar/avatar-group-count.svelte +23 -0
- package/src/svelte/lib/components/ui/avatar/avatar-group.svelte +23 -0
- package/src/svelte/lib/components/ui/avatar/avatar-image.svelte +17 -0
- package/src/svelte/lib/components/ui/avatar/avatar.svelte +26 -0
- package/src/svelte/lib/components/ui/avatar/index.ts +22 -0
- package/src/svelte/lib/components/ui/badge/badge.svelte +49 -0
- package/src/svelte/lib/components/ui/badge/index.ts +2 -0
- package/src/svelte/lib/components/ui/button/button.svelte +82 -0
- package/src/svelte/lib/components/ui/button/index.ts +17 -0
- package/src/svelte/lib/components/ui/card/card-action.svelte +23 -0
- package/src/svelte/lib/components/ui/card/card-content.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card-description.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card-footer.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card-header.svelte +23 -0
- package/src/svelte/lib/components/ui/card/card-title.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card.svelte +22 -0
- package/src/svelte/lib/components/ui/card/index.ts +25 -0
- package/src/svelte/lib/components/ui/dialog/dialog-close.svelte +11 -0
- package/src/svelte/lib/components/ui/dialog/dialog-content.svelte +48 -0
- package/src/svelte/lib/components/ui/dialog/dialog-description.svelte +17 -0
- package/src/svelte/lib/components/ui/dialog/dialog-footer.svelte +32 -0
- package/src/svelte/lib/components/ui/dialog/dialog-header.svelte +20 -0
- package/src/svelte/lib/components/ui/dialog/dialog-overlay.svelte +17 -0
- package/src/svelte/lib/components/ui/dialog/dialog-portal.svelte +7 -0
- package/src/svelte/lib/components/ui/dialog/dialog-title.svelte +17 -0
- package/src/svelte/lib/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/src/svelte/lib/components/ui/dialog/dialog.svelte +7 -0
- package/src/svelte/lib/components/ui/dialog/index.ts +34 -0
- package/src/svelte/lib/components/ui/empty/empty-content.svelte +23 -0
- package/src/svelte/lib/components/ui/empty/empty-description.svelte +23 -0
- package/src/svelte/lib/components/ui/empty/empty-header.svelte +20 -0
- package/src/svelte/lib/components/ui/empty/empty-media.svelte +41 -0
- package/src/svelte/lib/components/ui/empty/empty-title.svelte +20 -0
- package/src/svelte/lib/components/ui/empty/empty.svelte +23 -0
- package/src/svelte/lib/components/ui/empty/index.ts +22 -0
- package/src/svelte/lib/components/ui/input/index.ts +7 -0
- package/src/svelte/lib/components/ui/input/input.svelte +48 -0
- package/src/svelte/lib/components/ui/progress/index.ts +7 -0
- package/src/svelte/lib/components/ui/progress/progress.svelte +27 -0
- package/src/svelte/lib/components/ui/scroll-area/index.ts +10 -0
- package/src/svelte/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte +30 -0
- package/src/svelte/lib/components/ui/scroll-area/scroll-area.svelte +43 -0
- package/src/svelte/lib/components/ui/separator/index.ts +7 -0
- package/src/svelte/lib/components/ui/separator/separator.svelte +21 -0
- package/src/svelte/lib/components/ui/sheet/index.ts +34 -0
- package/src/svelte/lib/components/ui/sheet/sheet-close.svelte +7 -0
- package/src/svelte/lib/components/ui/sheet/sheet-content.svelte +55 -0
- package/src/svelte/lib/components/ui/sheet/sheet-description.svelte +17 -0
- package/src/svelte/lib/components/ui/sheet/sheet-footer.svelte +20 -0
- package/src/svelte/lib/components/ui/sheet/sheet-header.svelte +20 -0
- package/src/svelte/lib/components/ui/sheet/sheet-overlay.svelte +20 -0
- package/src/svelte/lib/components/ui/sheet/sheet-portal.svelte +7 -0
- package/src/svelte/lib/components/ui/sheet/sheet-title.svelte +17 -0
- package/src/svelte/lib/components/ui/sheet/sheet-trigger.svelte +7 -0
- package/src/svelte/lib/components/ui/sheet/sheet.svelte +7 -0
- package/src/svelte/lib/components/ui/skeleton/index.ts +7 -0
- package/src/svelte/lib/components/ui/skeleton/skeleton.svelte +17 -0
- package/src/svelte/lib/components/ui/slider/index.ts +7 -0
- package/src/svelte/lib/components/ui/slider/slider.svelte +52 -0
- package/src/svelte/lib/components/ui/tooltip/index.ts +19 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-content.svelte +51 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-portal.svelte +7 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-provider.svelte +7 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-trigger.svelte +7 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip.svelte +7 -0
- package/src/svelte/lib/countdown.svelte.ts +252 -0
- package/src/svelte/lib/format.ts +199 -0
- package/src/svelte/lib/i18n.svelte.ts +583 -0
- package/src/svelte/lib/media.ts +32 -0
- package/src/svelte/lib/pages.ts +18 -0
- package/src/svelte/lib/pomodoro.svelte.ts +223 -0
- package/src/svelte/lib/session-tree.ts +80 -0
- package/src/svelte/lib/state.svelte.ts +359 -0
- package/src/svelte/lib/timer-alarm.svelte.ts +319 -0
- package/src/svelte/lib/types.ts +306 -0
- package/src/svelte/lib/utils.ts +8 -0
- package/src/svelte/lib/weather.ts +84 -0
- package/src/svelte/lib/ws.ts +104 -0
- package/src/svelte/main.ts +13 -0
- package/src/svelte/styles/global.css +315 -0
- package/src/svelte/vite-env.d.ts +12 -0
- package/static/fonts/LICENSE.txt +287 -0
- package/static/icon.svg +24 -0
- package/static/manifest.webmanifest +18 -0
- package/svelte.config.js +5 -0
- package/tsconfig.dsh-client.json +18 -0
- package/tsconfig.dsh.json +21 -0
- package/tsconfig.json +20 -0
- package/vite.config.js +31 -0
- package/web/assets/index-DLdSfnJ3.css +1 -0
- package/web/assets/index-YNE8X0Uy.js +6 -0
- package/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/web/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
- package/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/web/assets/noto-sans-sc-100-wght-normal-DluiXYzi.woff2 +0 -0
- package/web/assets/noto-sans-sc-101-wght-normal-CPkZHnw0.woff2 +0 -0
- package/web/assets/noto-sans-sc-102-wght-normal-gzIulUH6.woff2 +0 -0
- package/web/assets/noto-sans-sc-103-wght-normal-CJfSBxFo.woff2 +0 -0
- package/web/assets/noto-sans-sc-104-wght-normal-B3QMglgE.woff2 +0 -0
- package/web/assets/noto-sans-sc-105-wght-normal-xXh_OMfk.woff2 +0 -0
- package/web/assets/noto-sans-sc-106-wght-normal-B9n3jMjP.woff2 +0 -0
- package/web/assets/noto-sans-sc-107-wght-normal-D7fvVprN.woff2 +0 -0
- package/web/assets/noto-sans-sc-108-wght-normal-J0_LhxVo.woff2 +0 -0
- package/web/assets/noto-sans-sc-109-wght-normal-Dsp-De_0.woff2 +0 -0
- package/web/assets/noto-sans-sc-110-wght-normal-DE_6NUrb.woff2 +0 -0
- package/web/assets/noto-sans-sc-111-wght-normal-C4S-TlTn.woff2 +0 -0
- package/web/assets/noto-sans-sc-112-wght-normal-BcFuP3w6.woff2 +0 -0
- package/web/assets/noto-sans-sc-113-wght-normal-Dk932kmj.woff2 +0 -0
- package/web/assets/noto-sans-sc-114-wght-normal-CO_5Xo-i.woff2 +0 -0
- package/web/assets/noto-sans-sc-115-wght-normal-BWy07H14.woff2 +0 -0
- package/web/assets/noto-sans-sc-116-wght-normal-B4HVAfHo.woff2 +0 -0
- package/web/assets/noto-sans-sc-117-wght-normal-siZzFqgK.woff2 +0 -0
- package/web/assets/noto-sans-sc-118-wght-normal-xhBB7f6V.woff2 +0 -0
- package/web/assets/noto-sans-sc-119-wght-normal-Ck_g2FGz.woff2 +0 -0
- package/web/assets/noto-sans-sc-21-wght-normal-D0x9-yBt.woff2 +0 -0
- package/web/assets/noto-sans-sc-22-wght-normal-BBUVTShg.woff2 +0 -0
- package/web/assets/noto-sans-sc-23-wght-normal-Du1oYWkE.woff2 +0 -0
- package/web/assets/noto-sans-sc-24-wght-normal-CmWjJ0gt.woff2 +0 -0
- package/web/assets/noto-sans-sc-25-wght-normal-BFHiqilg.woff2 +0 -0
- package/web/assets/noto-sans-sc-26-wght-normal-C0sF2Z2f.woff2 +0 -0
- package/web/assets/noto-sans-sc-27-wght-normal-COFdrquJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-28-wght-normal-DMgdNu1o.woff2 +0 -0
- package/web/assets/noto-sans-sc-29-wght-normal-Cmq8em30.woff2 +0 -0
- package/web/assets/noto-sans-sc-30-wght-normal-CZWqZ3yU.woff2 +0 -0
- package/web/assets/noto-sans-sc-31-wght-normal-Wfi_lutW.woff2 +0 -0
- package/web/assets/noto-sans-sc-32-wght-normal-BRJQ4PY5.woff2 +0 -0
- package/web/assets/noto-sans-sc-33-wght-normal-CUVQXNAJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-34-wght-normal-Bspba0wC.woff2 +0 -0
- package/web/assets/noto-sans-sc-35-wght-normal-K8fX_YPG.woff2 +0 -0
- package/web/assets/noto-sans-sc-36-wght-normal-CkITyjoT.woff2 +0 -0
- package/web/assets/noto-sans-sc-37-wght-normal-WCZ2zfEe.woff2 +0 -0
- package/web/assets/noto-sans-sc-38-wght-normal-DeM7CUce.woff2 +0 -0
- package/web/assets/noto-sans-sc-39-wght-normal-BMZNLWQU.woff2 +0 -0
- package/web/assets/noto-sans-sc-4-wght-normal-AR5bdDjz.woff2 +0 -0
- package/web/assets/noto-sans-sc-40-wght-normal-BQ5xpS5j.woff2 +0 -0
- package/web/assets/noto-sans-sc-41-wght-normal-Hthm7FFi.woff2 +0 -0
- package/web/assets/noto-sans-sc-42-wght-normal-eaS2gKds.woff2 +0 -0
- package/web/assets/noto-sans-sc-43-wght-normal-o-T53XXV.woff2 +0 -0
- package/web/assets/noto-sans-sc-44-wght-normal-BJH5J4Ez.woff2 +0 -0
- package/web/assets/noto-sans-sc-45-wght-normal-BKuxSCOJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-46-wght-normal-BR_5Mu6e.woff2 +0 -0
- package/web/assets/noto-sans-sc-47-wght-normal-CyY7gKNX.woff2 +0 -0
- package/web/assets/noto-sans-sc-48-wght-normal-BNMfTR6k.woff2 +0 -0
- package/web/assets/noto-sans-sc-49-wght-normal-DX-0y7wg.woff2 +0 -0
- package/web/assets/noto-sans-sc-5-wght-normal-CfWugllB.woff2 +0 -0
- package/web/assets/noto-sans-sc-50-wght-normal-DAOGW-Dr.woff2 +0 -0
- package/web/assets/noto-sans-sc-51-wght-normal-BlFLOjcP.woff2 +0 -0
- package/web/assets/noto-sans-sc-52-wght-normal-OZjrybnw.woff2 +0 -0
- package/web/assets/noto-sans-sc-53-wght-normal-CwyWNT6x.woff2 +0 -0
- package/web/assets/noto-sans-sc-54-wght-normal-DM0nRe18.woff2 +0 -0
- package/web/assets/noto-sans-sc-55-wght-normal-DBK2wUOR.woff2 +0 -0
- package/web/assets/noto-sans-sc-56-wght-normal-C-VKK1LS.woff2 +0 -0
- package/web/assets/noto-sans-sc-57-wght-normal-Cdbfh5kq.woff2 +0 -0
- package/web/assets/noto-sans-sc-58-wght-normal-CflMKARZ.woff2 +0 -0
- package/web/assets/noto-sans-sc-59-wght-normal-BS4Kwzxx.woff2 +0 -0
- package/web/assets/noto-sans-sc-6-wght-normal-ChnZIQfQ.woff2 +0 -0
- package/web/assets/noto-sans-sc-60-wght-normal-B7QOYsXJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-61-wght-normal-C8JjQS7w.woff2 +0 -0
- package/web/assets/noto-sans-sc-62-wght-normal-bS4wq1Gg.woff2 +0 -0
- package/web/assets/noto-sans-sc-63-wght-normal-BJr8P-OB.woff2 +0 -0
- package/web/assets/noto-sans-sc-64-wght-normal-DVB11t7j.woff2 +0 -0
- package/web/assets/noto-sans-sc-65-wght-normal-DMPCXBvK.woff2 +0 -0
- package/web/assets/noto-sans-sc-66-wght-normal-BobaG10V.woff2 +0 -0
- package/web/assets/noto-sans-sc-67-wght-normal-Dzk7WhCR.woff2 +0 -0
- package/web/assets/noto-sans-sc-68-wght-normal-DGbCD9xu.woff2 +0 -0
- package/web/assets/noto-sans-sc-69-wght-normal-Ctrv1EqA.woff2 +0 -0
- package/web/assets/noto-sans-sc-70-wght-normal-RgTO941D.woff2 +0 -0
- package/web/assets/noto-sans-sc-71-wght-normal-D9sG-CWv.woff2 +0 -0
- package/web/assets/noto-sans-sc-72-wght-normal-C4oWy3ul.woff2 +0 -0
- package/web/assets/noto-sans-sc-73-wght-normal-B1ZoOTgM.woff2 +0 -0
- package/web/assets/noto-sans-sc-74-wght-normal-F7dH_rZJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-75-wght-normal-Cp2wp4si.woff2 +0 -0
- package/web/assets/noto-sans-sc-76-wght-normal-B494D7xS.woff2 +0 -0
- package/web/assets/noto-sans-sc-77-wght-normal-B3MVY9Ax.woff2 +0 -0
- package/web/assets/noto-sans-sc-78-wght-normal-5jX7iNMC.woff2 +0 -0
- package/web/assets/noto-sans-sc-79-wght-normal-BIoZy8fr.woff2 +0 -0
- package/web/assets/noto-sans-sc-80-wght-normal-Bje1Fxy1.woff2 +0 -0
- package/web/assets/noto-sans-sc-81-wght-normal-B2NN7Qi3.woff2 +0 -0
- package/web/assets/noto-sans-sc-82-wght-normal-CUggNZi4.woff2 +0 -0
- package/web/assets/noto-sans-sc-83-wght-normal-99rD8YO-.woff2 +0 -0
- package/web/assets/noto-sans-sc-84-wght-normal-zMBmUocm.woff2 +0 -0
- package/web/assets/noto-sans-sc-85-wght-normal-CjI3qdxG.woff2 +0 -0
- package/web/assets/noto-sans-sc-86-wght-normal-BHY8TVEH.woff2 +0 -0
- package/web/assets/noto-sans-sc-87-wght-normal-DioZSHnU.woff2 +0 -0
- package/web/assets/noto-sans-sc-88-wght-normal-BXYxwf2t.woff2 +0 -0
- package/web/assets/noto-sans-sc-89-wght-normal-ClMFrFaL.woff2 +0 -0
- package/web/assets/noto-sans-sc-90-wght-normal-BnOlPreP.woff2 +0 -0
- package/web/assets/noto-sans-sc-91-wght-normal-DTr9RQcS.woff2 +0 -0
- package/web/assets/noto-sans-sc-97-wght-normal-9u6Aiav-.woff2 +0 -0
- package/web/assets/noto-sans-sc-98-wght-normal-BR-H2C0V.woff2 +0 -0
- package/web/assets/noto-sans-sc-99-wght-normal-BI6XPoxp.woff2 +0 -0
- package/web/assets/noto-sans-sc-cyrillic-wght-normal-BGmsQlw-.woff2 +0 -0
- package/web/assets/noto-sans-sc-latin-ext-wght-normal-DuJTpTg_.woff2 +0 -0
- package/web/assets/noto-sans-sc-latin-wght-normal-DDW8mKAR.woff2 +0 -0
- package/web/assets/noto-sans-sc-vietnamese-wght-normal-Bi3ATJ76.woff2 +0 -0
- package/web/fonts/LICENSE.txt +287 -0
- package/web/icon.svg +24 -0
- package/web/index.html +19 -0
- package/web/manifest.webmanifest +18 -0
|
@@ -0,0 +1,981 @@
|
|
|
1
|
+
//! Windows performance probe.
|
|
2
|
+
//!
|
|
3
|
+
//! CPU/memory come from sysinfo. The current core clock uses the PDH
|
|
4
|
+
//! `% Processor Performance` counter with a `CallNtPowerInformation` fallback.
|
|
5
|
+
//! GPU name/VRAM come from DXGI (registry as a fallback), GPU utilization and
|
|
6
|
+
//! dedicated memory from PDH GPU performance counters, and temperatures from the
|
|
7
|
+
//! PDH thermal-zone counters. GPU clocks, die temperature, power and fan speed
|
|
8
|
+
//! come from the WDDM performance data (`D3DKMTQueryAdapterInfo`), which is the
|
|
9
|
+
//! same source Task Manager uses. None of this goes through WMI or PowerShell.
|
|
10
|
+
|
|
11
|
+
use std::cell::RefCell;
|
|
12
|
+
use std::collections::{HashMap, HashSet};
|
|
13
|
+
use std::mem::size_of;
|
|
14
|
+
use std::time::{Duration, Instant};
|
|
15
|
+
|
|
16
|
+
use windows::core::{HSTRING, PCWSTR};
|
|
17
|
+
use windows::Wdk::Graphics::Direct3D::{
|
|
18
|
+
D3DKMTCloseAdapter, D3DKMTOpenAdapterFromLuid, D3DKMTQueryAdapterInfo, D3DKMT_ADAPTER_PERFDATA,
|
|
19
|
+
D3DKMT_CLOSEADAPTER, D3DKMT_NODEMETADATA, D3DKMT_NODE_PERFDATA, D3DKMT_OPENADAPTERFROMLUID,
|
|
20
|
+
D3DKMT_QUERYADAPTERINFO, DXGK_ENGINE_TYPE_3D, KMTQAITYPE_ADAPTERPERFDATA,
|
|
21
|
+
KMTQAITYPE_NODEMETADATA, KMTQAITYPE_NODEPERFDATA, KMTQUERYADAPTERINFOTYPE,
|
|
22
|
+
};
|
|
23
|
+
use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
|
|
24
|
+
use windows::Win32::Graphics::Dxgi::{CreateDXGIFactory1, IDXGIFactory1};
|
|
25
|
+
use windows::Win32::Storage::FileSystem::{
|
|
26
|
+
CreateFileW, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
|
|
27
|
+
};
|
|
28
|
+
use windows::Win32::System::Performance::{
|
|
29
|
+
PdhAddEnglishCounterW, PdhCloseQuery, PdhCollectQueryData, PdhGetFormattedCounterArrayW,
|
|
30
|
+
PdhGetFormattedCounterValue, PdhOpenQueryW, PDH_FMT_COUNTERVALUE, PDH_FMT_COUNTERVALUE_ITEM_W,
|
|
31
|
+
PDH_FMT_DOUBLE, PDH_HCOUNTER, PDH_HQUERY,
|
|
32
|
+
};
|
|
33
|
+
use windows::Win32::System::Power::{
|
|
34
|
+
CallNtPowerInformation, ProcessorInformation, PROCESSOR_POWER_INFORMATION,
|
|
35
|
+
};
|
|
36
|
+
use windows::Win32::System::IO::DeviceIoControl;
|
|
37
|
+
use winreg::enums::{HKEY_LOCAL_MACHINE, REG_BINARY, REG_QWORD};
|
|
38
|
+
use winreg::RegKey;
|
|
39
|
+
|
|
40
|
+
use super::{common, Gpu, Performance, Temperature};
|
|
41
|
+
use crate::util;
|
|
42
|
+
|
|
43
|
+
pub const BACKEND: &str = "win32-dxgi-pdh-d3dkmt+sysinfo";
|
|
44
|
+
|
|
45
|
+
/// `\\Processor Information(_Total)\\% Processor Performance`, the same counter
|
|
46
|
+
/// the old PowerShell probe used (`PercentProcessorPerformance`).
|
|
47
|
+
const PROCESSOR_PERFORMANCE_COUNTER: &str =
|
|
48
|
+
"\\Processor Information(_Total)\\% Processor Performance";
|
|
49
|
+
const GPU_ENGINE_COUNTER: &str = "\\GPU Engine(*)\\Utilization Percentage";
|
|
50
|
+
const GPU_MEMORY_COUNTER: &str = "\\GPU Adapter Memory(*)\\Dedicated Usage";
|
|
51
|
+
const THERMAL_PRECISION_COUNTER: &str = "\\Thermal Zone Information(*)\\High Precision Temperature";
|
|
52
|
+
const THERMAL_COUNTER: &str = "\\Thermal Zone Information(*)\\Temperature";
|
|
53
|
+
const PDH_MORE_DATA: u32 = 0x8000_07D2;
|
|
54
|
+
/// Rate counters need two collects; skip a re-collect when requests arrive in a
|
|
55
|
+
/// burst so the second read reuses the last valid value instead of sleeping.
|
|
56
|
+
const PDH_MIN_INTERVAL: Duration = Duration::from_millis(200);
|
|
57
|
+
/// After a long idle gap the previous rate baseline is too old; take a fresh
|
|
58
|
+
/// two-sample window instead of averaging across the whole idle period.
|
|
59
|
+
const PDH_RESAMPLE_AFTER: Duration = Duration::from_secs(5);
|
|
60
|
+
const PDH_WARMUP: Duration = Duration::from_millis(150);
|
|
61
|
+
const GPU_CLASS_KEY: &str =
|
|
62
|
+
"SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}";
|
|
63
|
+
|
|
64
|
+
fn registry_cpu() -> (Option<String>, Option<f64>) {
|
|
65
|
+
let Ok(hklm) = RegKey::predef(HKEY_LOCAL_MACHINE)
|
|
66
|
+
.open_subkey("HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0")
|
|
67
|
+
else {
|
|
68
|
+
return (None, None);
|
|
69
|
+
};
|
|
70
|
+
let name = hklm
|
|
71
|
+
.get_value::<String, _>("ProcessorNameString")
|
|
72
|
+
.ok()
|
|
73
|
+
.map(|name| name.trim().to_string())
|
|
74
|
+
.filter(|name| !name.is_empty());
|
|
75
|
+
let max_clock = hklm
|
|
76
|
+
.get_value::<u32, _>("MaxClockSpeed")
|
|
77
|
+
.ok()
|
|
78
|
+
.map(|value| value as f64)
|
|
79
|
+
.filter(|value| *value > 0.0);
|
|
80
|
+
(name, max_clock)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fn power_frequency() -> Option<(f64, f64)> {
|
|
84
|
+
let count = std::thread::available_parallelism()
|
|
85
|
+
.map(|count| count.get())
|
|
86
|
+
.unwrap_or(1)
|
|
87
|
+
.max(1);
|
|
88
|
+
let mut info: Vec<PROCESSOR_POWER_INFORMATION> =
|
|
89
|
+
(0..count).map(|_| unsafe { std::mem::zeroed() }).collect();
|
|
90
|
+
let bytes = (info.len() * size_of::<PROCESSOR_POWER_INFORMATION>()) as u32;
|
|
91
|
+
let status = unsafe {
|
|
92
|
+
CallNtPowerInformation(
|
|
93
|
+
ProcessorInformation,
|
|
94
|
+
None,
|
|
95
|
+
0,
|
|
96
|
+
Some(info.as_mut_ptr() as *mut core::ffi::c_void),
|
|
97
|
+
bytes,
|
|
98
|
+
)
|
|
99
|
+
};
|
|
100
|
+
if status.0 < 0 {
|
|
101
|
+
return None;
|
|
102
|
+
}
|
|
103
|
+
let mut current = 0.0;
|
|
104
|
+
let mut max = 0.0;
|
|
105
|
+
let mut seen = 0_u32;
|
|
106
|
+
for entry in &info {
|
|
107
|
+
if entry.MaxMhz > 0 {
|
|
108
|
+
max += entry.MaxMhz as f64;
|
|
109
|
+
}
|
|
110
|
+
if entry.CurrentMhz > 0 {
|
|
111
|
+
current += entry.CurrentMhz as f64;
|
|
112
|
+
seen += 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
let current = if seen > 0 { current / seen as f64 } else { 0.0 };
|
|
116
|
+
let max = if count > 0 { max / count as f64 } else { 0.0 };
|
|
117
|
+
if current <= 0.0 && max <= 0.0 {
|
|
118
|
+
return None;
|
|
119
|
+
}
|
|
120
|
+
Some((util::round1(current), util::round1(max)))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// A PDH query kept open across samples. Opening the query and resolving the
|
|
124
|
+
/// counter path is expensive for wildcard GPU counters, and the rate counters
|
|
125
|
+
/// need a previous sample anyway; both costs disappear after the first request.
|
|
126
|
+
struct PdhCounter {
|
|
127
|
+
query: PDH_HQUERY,
|
|
128
|
+
counter: PDH_HCOUNTER,
|
|
129
|
+
last_collect: Option<Instant>,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
impl PdhCounter {
|
|
133
|
+
fn open(path: &str) -> Result<Self, String> {
|
|
134
|
+
unsafe {
|
|
135
|
+
let mut query = PDH_HQUERY(std::ptr::null_mut());
|
|
136
|
+
if PdhOpenQueryW(PCWSTR::null(), 0, &mut query) != 0 {
|
|
137
|
+
return Err("PdhOpenQueryW failed".to_string());
|
|
138
|
+
}
|
|
139
|
+
let wide = HSTRING::from(path);
|
|
140
|
+
let mut counter = PDH_HCOUNTER(std::ptr::null_mut());
|
|
141
|
+
let status = PdhAddEnglishCounterW(query, &wide, 0, &mut counter);
|
|
142
|
+
if status != 0 {
|
|
143
|
+
let _ = PdhCloseQuery(query);
|
|
144
|
+
return Err(format!("counter unavailable (status {status})"));
|
|
145
|
+
}
|
|
146
|
+
Ok(Self {
|
|
147
|
+
query,
|
|
148
|
+
counter,
|
|
149
|
+
last_collect: None,
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fn collect(&mut self) -> Result<(), String> {
|
|
155
|
+
let now = Instant::now();
|
|
156
|
+
let elapsed = self.last_collect.map(|last| now.duration_since(last));
|
|
157
|
+
if let Some(elapsed) = elapsed {
|
|
158
|
+
if elapsed < PDH_MIN_INTERVAL {
|
|
159
|
+
// A burst of requests (overview + page refresh) must not pay the
|
|
160
|
+
// warm-up window again; PDH still holds the last valid sample.
|
|
161
|
+
return Ok(());
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
let stale = elapsed.map_or(true, |elapsed| elapsed >= PDH_RESAMPLE_AFTER);
|
|
165
|
+
unsafe {
|
|
166
|
+
if PdhCollectQueryData(self.query) != 0 {
|
|
167
|
+
return Err("PdhCollectQueryData failed".to_string());
|
|
168
|
+
}
|
|
169
|
+
if stale {
|
|
170
|
+
std::thread::sleep(PDH_WARMUP);
|
|
171
|
+
if PdhCollectQueryData(self.query) != 0 {
|
|
172
|
+
return Err("PdhCollectQueryData failed".to_string());
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
self.last_collect = Some(Instant::now());
|
|
177
|
+
Ok(())
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fn formatted_array(&self) -> Result<Vec<(String, f64)>, String> {
|
|
181
|
+
unsafe {
|
|
182
|
+
let mut size = 0_u32;
|
|
183
|
+
let mut count = 0_u32;
|
|
184
|
+
let status = PdhGetFormattedCounterArrayW(
|
|
185
|
+
self.counter,
|
|
186
|
+
PDH_FMT_DOUBLE,
|
|
187
|
+
&mut size,
|
|
188
|
+
&mut count,
|
|
189
|
+
None,
|
|
190
|
+
);
|
|
191
|
+
if status != 0 && status != PDH_MORE_DATA {
|
|
192
|
+
return Err(format!("counter query failed (status {status})"));
|
|
193
|
+
}
|
|
194
|
+
if size == 0 || count == 0 {
|
|
195
|
+
return Ok(Vec::new());
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// `size` is a byte count; allocate word-aligned storage for the array.
|
|
199
|
+
let words = (size as usize).div_ceil(size_of::<usize>());
|
|
200
|
+
let mut buffer = vec![0_usize; words];
|
|
201
|
+
let items = buffer.as_mut_ptr() as *mut PDH_FMT_COUNTERVALUE_ITEM_W;
|
|
202
|
+
let status = PdhGetFormattedCounterArrayW(
|
|
203
|
+
self.counter,
|
|
204
|
+
PDH_FMT_DOUBLE,
|
|
205
|
+
&mut size,
|
|
206
|
+
&mut count,
|
|
207
|
+
Some(items),
|
|
208
|
+
);
|
|
209
|
+
if status != 0 {
|
|
210
|
+
return Err(format!("counter array failed (status {status})"));
|
|
211
|
+
}
|
|
212
|
+
let slice = std::slice::from_raw_parts(items, count as usize);
|
|
213
|
+
let mut values = Vec::with_capacity(slice.len());
|
|
214
|
+
for item in slice {
|
|
215
|
+
let name = if item.szName.is_null() {
|
|
216
|
+
String::new()
|
|
217
|
+
} else {
|
|
218
|
+
item.szName.to_string().unwrap_or_default()
|
|
219
|
+
};
|
|
220
|
+
values.push((name, item.FmtValue.Anonymous.doubleValue));
|
|
221
|
+
}
|
|
222
|
+
Ok(values)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
impl Drop for PdhCounter {
|
|
228
|
+
fn drop(&mut self) {
|
|
229
|
+
unsafe {
|
|
230
|
+
let _ = PdhCloseQuery(self.query);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
enum PdhEntry {
|
|
236
|
+
Ready(PdhCounter),
|
|
237
|
+
/// Cache a missing/failed counter for a while so an unsupported thermal or
|
|
238
|
+
/// GPU counter does not retry its setup on every sample.
|
|
239
|
+
Failed {
|
|
240
|
+
until: Instant,
|
|
241
|
+
error: String,
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
thread_local! {
|
|
246
|
+
static PDH_COUNTERS: RefCell<HashMap<String, PdhEntry>> = RefCell::new(HashMap::new());
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
fn with_pdh_counter<T>(
|
|
250
|
+
path: &str,
|
|
251
|
+
read: impl FnOnce(&PdhCounter) -> Result<T, String>,
|
|
252
|
+
) -> Result<T, String> {
|
|
253
|
+
PDH_COUNTERS.with(|cell| {
|
|
254
|
+
let mut counters = cell.borrow_mut();
|
|
255
|
+
let now = Instant::now();
|
|
256
|
+
let failure = counters.get(path).and_then(|entry| match entry {
|
|
257
|
+
PdhEntry::Failed { until, error } => Some((*until, error.clone())),
|
|
258
|
+
PdhEntry::Ready(_) => None,
|
|
259
|
+
});
|
|
260
|
+
if let Some((until, error)) = failure {
|
|
261
|
+
if now < until {
|
|
262
|
+
return Err(error);
|
|
263
|
+
}
|
|
264
|
+
counters.remove(path);
|
|
265
|
+
}
|
|
266
|
+
if !counters.contains_key(path) {
|
|
267
|
+
match PdhCounter::open(path) {
|
|
268
|
+
Ok(counter) => {
|
|
269
|
+
counters.insert(path.to_string(), PdhEntry::Ready(counter));
|
|
270
|
+
}
|
|
271
|
+
Err(error) => {
|
|
272
|
+
counters.insert(
|
|
273
|
+
path.to_string(),
|
|
274
|
+
PdhEntry::Failed {
|
|
275
|
+
until: now + Duration::from_secs(60),
|
|
276
|
+
error: error.clone(),
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
return Err(error);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let collected = match counters.get_mut(path) {
|
|
285
|
+
Some(PdhEntry::Ready(counter)) => counter.collect(),
|
|
286
|
+
_ => unreachable!("PDH counter was inserted above"),
|
|
287
|
+
};
|
|
288
|
+
if let Err(error) = collected {
|
|
289
|
+
counters.insert(
|
|
290
|
+
path.to_string(),
|
|
291
|
+
PdhEntry::Failed {
|
|
292
|
+
until: now + Duration::from_secs(30),
|
|
293
|
+
error: error.clone(),
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
return Err(error);
|
|
297
|
+
}
|
|
298
|
+
match counters.get(path) {
|
|
299
|
+
Some(PdhEntry::Ready(counter)) => read(counter),
|
|
300
|
+
_ => unreachable!("PDH counter was collected above"),
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/// Returns the counter value as a percentage of nominal frequency (100 = base).
|
|
306
|
+
fn pdh_performance_percent() -> Option<f64> {
|
|
307
|
+
with_pdh_counter(PROCESSOR_PERFORMANCE_COUNTER, |counter| {
|
|
308
|
+
let mut value = PDH_FMT_COUNTERVALUE::default();
|
|
309
|
+
let status = unsafe {
|
|
310
|
+
PdhGetFormattedCounterValue(counter.counter, PDH_FMT_DOUBLE, None, &mut value)
|
|
311
|
+
};
|
|
312
|
+
if status != 0 {
|
|
313
|
+
return Err(format!("counter value failed (status {status})"));
|
|
314
|
+
}
|
|
315
|
+
let percent = unsafe { value.Anonymous.doubleValue };
|
|
316
|
+
if percent.is_finite() && percent > 0.0 {
|
|
317
|
+
Ok(Some(percent))
|
|
318
|
+
} else {
|
|
319
|
+
Ok(None)
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
.ok()
|
|
323
|
+
.flatten()
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/// Read every instance of a wildcard PDH counter. The query is reused between
|
|
327
|
+
/// samples; the first read (and any read after an idle gap) takes two samples so
|
|
328
|
+
/// rate counters have a value.
|
|
329
|
+
fn pdh_array(counter_path: &str) -> Result<Vec<(String, f64)>, String> {
|
|
330
|
+
with_pdh_counter(counter_path, PdhCounter::formatted_array)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fn is_virtual_adapter(name: &str) -> bool {
|
|
334
|
+
let name = name.to_lowercase();
|
|
335
|
+
[
|
|
336
|
+
"virtual",
|
|
337
|
+
"gameviewer",
|
|
338
|
+
"remote",
|
|
339
|
+
"basic display",
|
|
340
|
+
"microsoft basic",
|
|
341
|
+
]
|
|
342
|
+
.iter()
|
|
343
|
+
.any(|marker| name.contains(marker))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
fn wide_string(value: &[u16]) -> String {
|
|
347
|
+
let end = value
|
|
348
|
+
.iter()
|
|
349
|
+
.position(|character| *character == 0)
|
|
350
|
+
.unwrap_or(value.len());
|
|
351
|
+
String::from_utf16_lossy(&value[..end]).trim().to_string()
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#[derive(Debug, Clone, Default)]
|
|
355
|
+
struct RegistryAdapter {
|
|
356
|
+
name: String,
|
|
357
|
+
driver: String,
|
|
358
|
+
memory_total: Option<u64>,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/// Per-adapter performance data from the WDDM kernel-mode interface
|
|
362
|
+
/// (`D3DKMTQueryAdapterInfo`). This is the vendor-neutral source Task Manager
|
|
363
|
+
/// uses for the GPU clock, temperature, power and fan speed.
|
|
364
|
+
#[derive(Debug, Clone, Copy, Default)]
|
|
365
|
+
struct AdapterPerf {
|
|
366
|
+
core_clock_mhz: Option<f64>,
|
|
367
|
+
core_clock_max_mhz: Option<f64>,
|
|
368
|
+
memory_clock_mhz: Option<f64>,
|
|
369
|
+
temperature_celsius: Option<f64>,
|
|
370
|
+
power_percent: Option<f64>,
|
|
371
|
+
fan_rpm: Option<u32>,
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
fn query_adapter_info<T>(h_adapter: u32, kind: KMTQUERYADAPTERINFOTYPE, value: &mut T) -> bool {
|
|
375
|
+
let mut query = D3DKMT_QUERYADAPTERINFO {
|
|
376
|
+
hAdapter: h_adapter,
|
|
377
|
+
Type: kind,
|
|
378
|
+
pPrivateDriverData: value as *mut T as *mut core::ffi::c_void,
|
|
379
|
+
PrivateDriverDataSize: size_of::<T>() as u32,
|
|
380
|
+
};
|
|
381
|
+
unsafe { D3DKMTQueryAdapterInfo(&mut query).0 >= 0 }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/// Read WDDM performance data for an adapter identified by its DXGI LUID.
|
|
385
|
+
///
|
|
386
|
+
/// * `D3DKMT_ADAPTER_PERFDATA` reports memory clock, die temperature, power
|
|
387
|
+
/// (tenths of a percent) and fan RPM.
|
|
388
|
+
/// * `D3DKMT_NODE_PERFDATA` reports per-engine clocks in hertz; the 3D engine is
|
|
389
|
+
/// the graphics clock shown in Task Manager.
|
|
390
|
+
fn adapter_perf(luid: LUID) -> Option<AdapterPerf> {
|
|
391
|
+
unsafe {
|
|
392
|
+
let mut open = D3DKMT_OPENADAPTERFROMLUID {
|
|
393
|
+
AdapterLuid: luid,
|
|
394
|
+
hAdapter: 0,
|
|
395
|
+
};
|
|
396
|
+
if D3DKMTOpenAdapterFromLuid(&mut open).0 < 0 || open.hAdapter == 0 {
|
|
397
|
+
return None;
|
|
398
|
+
}
|
|
399
|
+
let mut perf = AdapterPerf::default();
|
|
400
|
+
|
|
401
|
+
let mut adapter = D3DKMT_ADAPTER_PERFDATA::default();
|
|
402
|
+
if query_adapter_info(open.hAdapter, KMTQAITYPE_ADAPTERPERFDATA, &mut adapter) {
|
|
403
|
+
if adapter.Temperature > 0 {
|
|
404
|
+
let celsius = adapter.Temperature as f64 / 10.0;
|
|
405
|
+
if (5.0..=125.0).contains(&celsius) {
|
|
406
|
+
perf.temperature_celsius = Some(util::round1(celsius));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if adapter.MemoryFrequency > 0 {
|
|
410
|
+
perf.memory_clock_mhz =
|
|
411
|
+
Some(util::round1(adapter.MemoryFrequency as f64 / 1_000_000.0));
|
|
412
|
+
}
|
|
413
|
+
// The driver reports tenths of a percent; 0 means "unsupported".
|
|
414
|
+
if adapter.Power > 0 && adapter.Power <= 1000 {
|
|
415
|
+
perf.power_percent = Some(util::round1(adapter.Power as f64 / 10.0));
|
|
416
|
+
}
|
|
417
|
+
if adapter.FanRPM > 0 {
|
|
418
|
+
perf.fan_rpm = Some(adapter.FanRPM);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Engine nodes are 1-based in the WDDM metadata (node 0 is a placeholder
|
|
423
|
+
// on some drivers), so probe a generous range and pick the 3D engine.
|
|
424
|
+
let mut fallback_clock: Option<(f64, f64)> = None;
|
|
425
|
+
for node in 0..16u32 {
|
|
426
|
+
let mut metadata = D3DKMT_NODEMETADATA {
|
|
427
|
+
NodeOrdinalAndAdapterIndex: node,
|
|
428
|
+
..Default::default()
|
|
429
|
+
};
|
|
430
|
+
if !query_adapter_info(open.hAdapter, KMTQAITYPE_NODEMETADATA, &mut metadata) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
let engine = metadata.NodeData.EngineType;
|
|
434
|
+
let mut data = D3DKMT_NODE_PERFDATA {
|
|
435
|
+
NodeOrdinal: node,
|
|
436
|
+
PhysicalAdapterIndex: 0,
|
|
437
|
+
..Default::default()
|
|
438
|
+
};
|
|
439
|
+
if !query_adapter_info(open.hAdapter, KMTQAITYPE_NODEPERFDATA, &mut data) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if data.Frequency == 0 && data.MaxFrequency == 0 {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
let frequency = data.Frequency as f64 / 1_000_000.0;
|
|
446
|
+
let maximum = data.MaxFrequency as f64 / 1_000_000.0;
|
|
447
|
+
if engine == DXGK_ENGINE_TYPE_3D {
|
|
448
|
+
// An idle GPU may legitimately report 0 MHz on the 3D node;
|
|
449
|
+
// forward it instead of pretending the clock is unavailable.
|
|
450
|
+
perf.core_clock_mhz = Some(util::round1(frequency));
|
|
451
|
+
if maximum > 0.0 {
|
|
452
|
+
perf.core_clock_max_mhz = Some(util::round1(maximum));
|
|
453
|
+
}
|
|
454
|
+
} else if frequency > 0.0 && fallback_clock.map_or(true, |(value, _)| frequency > value)
|
|
455
|
+
{
|
|
456
|
+
fallback_clock = Some((frequency, maximum));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if perf.core_clock_mhz.is_none() {
|
|
460
|
+
if let Some((frequency, maximum)) = fallback_clock {
|
|
461
|
+
perf.core_clock_mhz = Some(util::round1(frequency));
|
|
462
|
+
if maximum > 0.0 {
|
|
463
|
+
perf.core_clock_max_mhz = Some(util::round1(maximum));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let close = D3DKMT_CLOSEADAPTER {
|
|
469
|
+
hAdapter: open.hAdapter,
|
|
470
|
+
};
|
|
471
|
+
let _ = D3DKMTCloseAdapter(&close);
|
|
472
|
+
Some(perf)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
fn registry_adapters() -> Vec<RegistryAdapter> {
|
|
477
|
+
let Ok(root) = RegKey::predef(HKEY_LOCAL_MACHINE).open_subkey(GPU_CLASS_KEY) else {
|
|
478
|
+
return Vec::new();
|
|
479
|
+
};
|
|
480
|
+
let mut adapters = Vec::new();
|
|
481
|
+
for subkey in root.enum_keys().flatten() {
|
|
482
|
+
let Ok(key) = root.open_subkey(&subkey) else {
|
|
483
|
+
continue;
|
|
484
|
+
};
|
|
485
|
+
let Ok(name) = key.get_value::<String, _>("DriverDesc") else {
|
|
486
|
+
continue;
|
|
487
|
+
};
|
|
488
|
+
let name = name.trim().to_string();
|
|
489
|
+
if name.is_empty() || is_virtual_adapter(&name) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
let driver = key
|
|
493
|
+
.get_value::<String, _>("DriverVersion")
|
|
494
|
+
.ok()
|
|
495
|
+
.map(|value| value.trim().to_string())
|
|
496
|
+
.unwrap_or_default();
|
|
497
|
+
let memory_total = key
|
|
498
|
+
.get_value::<u64, _>("HardwareInformation.qwMemorySize")
|
|
499
|
+
.ok()
|
|
500
|
+
.or_else(|| {
|
|
501
|
+
key.get_raw_value("HardwareInformation.qwMemorySize")
|
|
502
|
+
.ok()
|
|
503
|
+
.and_then(|raw| {
|
|
504
|
+
if raw.bytes.len() < 8 {
|
|
505
|
+
return None;
|
|
506
|
+
}
|
|
507
|
+
let bytes: [u8; 8] = raw.bytes[..8].try_into().ok()?;
|
|
508
|
+
match raw.vtype {
|
|
509
|
+
REG_QWORD | REG_BINARY => Some(u64::from_le_bytes(bytes)),
|
|
510
|
+
_ => None,
|
|
511
|
+
}
|
|
512
|
+
})
|
|
513
|
+
})
|
|
514
|
+
.filter(|value| *value > 0);
|
|
515
|
+
adapters.push(RegistryAdapter {
|
|
516
|
+
name,
|
|
517
|
+
driver,
|
|
518
|
+
memory_total,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
adapters
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
#[derive(Debug, Clone, Default)]
|
|
525
|
+
struct DxgiAdapter {
|
|
526
|
+
name: String,
|
|
527
|
+
memory_total: u64,
|
|
528
|
+
driver: String,
|
|
529
|
+
/// DXGI LUID, used to open the adapter for WDDM performance data.
|
|
530
|
+
luid: Option<LUID>,
|
|
531
|
+
/// Whether the adapter drives at least one display output.
|
|
532
|
+
has_output: bool,
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
fn dxgi_adapters() -> Result<Vec<DxgiAdapter>, String> {
|
|
536
|
+
let registry = registry_adapters();
|
|
537
|
+
unsafe {
|
|
538
|
+
let factory: IDXGIFactory1 =
|
|
539
|
+
CreateDXGIFactory1().map_err(|error| format!("DXGI unavailable: {error}"))?;
|
|
540
|
+
let mut adapters = Vec::new();
|
|
541
|
+
let mut index = 0_u32;
|
|
542
|
+
loop {
|
|
543
|
+
let adapter = match factory.EnumAdapters1(index) {
|
|
544
|
+
Ok(adapter) => adapter,
|
|
545
|
+
Err(_) => break,
|
|
546
|
+
};
|
|
547
|
+
index += 1;
|
|
548
|
+
let Ok(desc) = adapter.GetDesc1() else {
|
|
549
|
+
continue;
|
|
550
|
+
};
|
|
551
|
+
let name = wide_string(&desc.Description);
|
|
552
|
+
if name.is_empty() || is_virtual_adapter(&name) {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
let driver = registry
|
|
556
|
+
.iter()
|
|
557
|
+
.find(|candidate| candidate.name.eq_ignore_ascii_case(&name))
|
|
558
|
+
.map(|candidate| candidate.driver.clone())
|
|
559
|
+
.unwrap_or_default();
|
|
560
|
+
adapters.push(DxgiAdapter {
|
|
561
|
+
name,
|
|
562
|
+
memory_total: desc.DedicatedVideoMemory as u64,
|
|
563
|
+
driver,
|
|
564
|
+
luid: Some(desc.AdapterLuid),
|
|
565
|
+
has_output: adapter.EnumOutputs(0).is_ok(),
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
Ok(adapters)
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
fn gpu_from_adapters(adapters: &[DxgiAdapter], source: &str) -> Option<Gpu> {
|
|
573
|
+
// Some drivers expose the same physical adapter through more than one DXGI
|
|
574
|
+
// entry; collapse identical descriptions before reporting/aggregating.
|
|
575
|
+
let mut seen = HashSet::new();
|
|
576
|
+
let adapters: Vec<DxgiAdapter> = adapters
|
|
577
|
+
.iter()
|
|
578
|
+
.filter(|adapter| seen.insert(adapter.name.to_lowercase()))
|
|
579
|
+
.cloned()
|
|
580
|
+
.collect();
|
|
581
|
+
if adapters.is_empty() {
|
|
582
|
+
return None;
|
|
583
|
+
}
|
|
584
|
+
// The adapter that drives a display output is the "primary" one and the one
|
|
585
|
+
// Task Manager reports; fall back to the largest VRAM.
|
|
586
|
+
let primary = adapters
|
|
587
|
+
.iter()
|
|
588
|
+
.find(|adapter| adapter.has_output)
|
|
589
|
+
.or_else(|| adapters.iter().max_by_key(|adapter| adapter.memory_total))?;
|
|
590
|
+
let registry = registry_adapters();
|
|
591
|
+
let registry_total = registry
|
|
592
|
+
.iter()
|
|
593
|
+
.filter_map(|adapter| adapter.memory_total)
|
|
594
|
+
.max();
|
|
595
|
+
let memory_total = registry_total.or_else(|| {
|
|
596
|
+
if primary.memory_total > 0 {
|
|
597
|
+
Some(primary.memory_total)
|
|
598
|
+
} else {
|
|
599
|
+
None
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
let memory_used = pdh_array(GPU_MEMORY_COUNTER).ok().and_then(|values| {
|
|
603
|
+
values
|
|
604
|
+
.into_iter()
|
|
605
|
+
.map(|(_, value)| value)
|
|
606
|
+
.filter(|value| value.is_finite() && *value > 0.0)
|
|
607
|
+
.reduce(f64::max)
|
|
608
|
+
.map(|value| value.round() as u64)
|
|
609
|
+
});
|
|
610
|
+
let utilization = pdh_array(GPU_ENGINE_COUNTER)
|
|
611
|
+
.ok()
|
|
612
|
+
.map(|values| {
|
|
613
|
+
values
|
|
614
|
+
.into_iter()
|
|
615
|
+
.map(|(_, value)| value)
|
|
616
|
+
.filter(|value| value.is_finite() && *value > 0.0)
|
|
617
|
+
.sum::<f64>()
|
|
618
|
+
})
|
|
619
|
+
.filter(|value| *value > 0.0)
|
|
620
|
+
.map(|value| util::round1(util::clamp(value, 0.0, 100.0)));
|
|
621
|
+
let memory_percent = match (memory_used, memory_total) {
|
|
622
|
+
(Some(used), Some(total)) if total > 0 => {
|
|
623
|
+
Some(util::round1(used as f64 / total as f64 * 100.0))
|
|
624
|
+
}
|
|
625
|
+
_ => None,
|
|
626
|
+
};
|
|
627
|
+
let perf = primary.luid.and_then(adapter_perf);
|
|
628
|
+
Some(Gpu {
|
|
629
|
+
name: primary.name.clone(),
|
|
630
|
+
driver: primary.driver.clone(),
|
|
631
|
+
adapters: adapters
|
|
632
|
+
.iter()
|
|
633
|
+
.map(|adapter| adapter.name.clone())
|
|
634
|
+
.collect(),
|
|
635
|
+
utilization,
|
|
636
|
+
memory_used,
|
|
637
|
+
memory_total,
|
|
638
|
+
memory_percent,
|
|
639
|
+
core_clock_mhz: perf.and_then(|value| value.core_clock_mhz),
|
|
640
|
+
core_clock_max_mhz: perf.and_then(|value| value.core_clock_max_mhz),
|
|
641
|
+
memory_clock_mhz: perf.and_then(|value| value.memory_clock_mhz),
|
|
642
|
+
temperature_celsius: perf.and_then(|value| value.temperature_celsius),
|
|
643
|
+
power_percent: perf.and_then(|value| value.power_percent),
|
|
644
|
+
fan_rpm: perf.and_then(|value| value.fan_rpm),
|
|
645
|
+
ok: true,
|
|
646
|
+
source: source.to_string(),
|
|
647
|
+
error: None,
|
|
648
|
+
})
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
fn read_gpu() -> Option<Gpu> {
|
|
652
|
+
if let Ok(adapters) = dxgi_adapters() {
|
|
653
|
+
if let Some(gpu) = gpu_from_adapters(&adapters, "dxgi-pdh") {
|
|
654
|
+
return Some(gpu);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
let registry = registry_adapters();
|
|
658
|
+
let adapters: Vec<DxgiAdapter> = registry
|
|
659
|
+
.iter()
|
|
660
|
+
.map(|adapter| DxgiAdapter {
|
|
661
|
+
name: adapter.name.clone(),
|
|
662
|
+
memory_total: adapter.memory_total.unwrap_or(0),
|
|
663
|
+
driver: adapter.driver.clone(),
|
|
664
|
+
..Default::default()
|
|
665
|
+
})
|
|
666
|
+
.collect();
|
|
667
|
+
gpu_from_adapters(&adapters, "registry-pdh")
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
fn thermal_value(raw: f64, high_precision: bool) -> Option<f64> {
|
|
671
|
+
let candidates: [f64; 2] = if high_precision {
|
|
672
|
+
[raw / 10.0 - 273.15, raw - 273.15]
|
|
673
|
+
} else {
|
|
674
|
+
[raw - 273.15, raw / 10.0 - 273.15]
|
|
675
|
+
};
|
|
676
|
+
candidates
|
|
677
|
+
.into_iter()
|
|
678
|
+
.find(|celsius| (5.0..=125.0).contains(celsius))
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/// `IOCTL_STORAGE_QUERY_PROPERTY` with `StorageDeviceTemperatureProperty`.
|
|
682
|
+
/// Works for NVMe/SATA drives as a standard user (the device is opened with no
|
|
683
|
+
/// access rights: the query itself only needs a handle).
|
|
684
|
+
const IOCTL_STORAGE_QUERY_PROPERTY: u32 = 0x002D_1400;
|
|
685
|
+
const STORAGE_DEVICE_TEMPERATURE_PROPERTY: u32 = 51;
|
|
686
|
+
/// `STORAGE_TEMPERATURE_VALUE_NOT_REPORTED` as a signed 16-bit value.
|
|
687
|
+
const TEMPERATURE_NOT_REPORTED: i16 = i16::MIN;
|
|
688
|
+
const MAX_PHYSICAL_DRIVES: u32 = 8;
|
|
689
|
+
|
|
690
|
+
#[repr(C)]
|
|
691
|
+
#[derive(Default)]
|
|
692
|
+
struct StoragePropertyQuery {
|
|
693
|
+
property_id: u32,
|
|
694
|
+
query_type: u32,
|
|
695
|
+
additional: [u8; 1],
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
#[repr(C)]
|
|
699
|
+
#[derive(Debug, Clone, Copy, Default)]
|
|
700
|
+
struct StorageTemperatureInfo {
|
|
701
|
+
index: u16,
|
|
702
|
+
temperature: i16,
|
|
703
|
+
over_threshold: i16,
|
|
704
|
+
under_threshold: i16,
|
|
705
|
+
over_threshold_changeable: u8,
|
|
706
|
+
under_threshold_changeable: u8,
|
|
707
|
+
event_generated: u8,
|
|
708
|
+
reserved0: u8,
|
|
709
|
+
reserved1: u32,
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
#[repr(C)]
|
|
713
|
+
#[derive(Default)]
|
|
714
|
+
struct StorageTemperatureDescriptor {
|
|
715
|
+
version: u32,
|
|
716
|
+
size: u32,
|
|
717
|
+
critical_temperature: i16,
|
|
718
|
+
warning_temperature: i16,
|
|
719
|
+
info_count: u16,
|
|
720
|
+
reserved0: [u8; 2],
|
|
721
|
+
reserved1: [u32; 2],
|
|
722
|
+
temperature_info: [StorageTemperatureInfo; 16],
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/// The device temperature (sensor index 0 is the composite reading) of one
|
|
726
|
+
/// physical drive, or `None` when the drive does not report it.
|
|
727
|
+
fn disk_temperature(index: u32) -> Option<f64> {
|
|
728
|
+
let path = HSTRING::from(format!(r"\\.\PhysicalDrive{index}"));
|
|
729
|
+
let handle: HANDLE = unsafe {
|
|
730
|
+
CreateFileW(
|
|
731
|
+
&path,
|
|
732
|
+
0,
|
|
733
|
+
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
734
|
+
None,
|
|
735
|
+
OPEN_EXISTING,
|
|
736
|
+
FILE_ATTRIBUTE_NORMAL,
|
|
737
|
+
None,
|
|
738
|
+
)
|
|
739
|
+
}
|
|
740
|
+
.ok()?;
|
|
741
|
+
if handle.is_invalid() {
|
|
742
|
+
return None;
|
|
743
|
+
}
|
|
744
|
+
let mut query = StoragePropertyQuery {
|
|
745
|
+
property_id: STORAGE_DEVICE_TEMPERATURE_PROPERTY,
|
|
746
|
+
query_type: 0,
|
|
747
|
+
additional: [0],
|
|
748
|
+
};
|
|
749
|
+
let mut output = StorageTemperatureDescriptor::default();
|
|
750
|
+
let mut returned = 0_u32;
|
|
751
|
+
let ok = unsafe {
|
|
752
|
+
DeviceIoControl(
|
|
753
|
+
handle,
|
|
754
|
+
IOCTL_STORAGE_QUERY_PROPERTY,
|
|
755
|
+
Some(&mut query as *mut _ as *mut core::ffi::c_void),
|
|
756
|
+
size_of::<StoragePropertyQuery>() as u32,
|
|
757
|
+
Some(&mut output as *mut _ as *mut core::ffi::c_void),
|
|
758
|
+
size_of::<StorageTemperatureDescriptor>() as u32,
|
|
759
|
+
Some(&mut returned),
|
|
760
|
+
None,
|
|
761
|
+
)
|
|
762
|
+
};
|
|
763
|
+
unsafe {
|
|
764
|
+
let _ = CloseHandle(handle);
|
|
765
|
+
}
|
|
766
|
+
if ok.is_err() {
|
|
767
|
+
return None;
|
|
768
|
+
}
|
|
769
|
+
let count = (output.info_count as usize).min(output.temperature_info.len());
|
|
770
|
+
let mut fallback = None;
|
|
771
|
+
for info in output.temperature_info.iter().take(count) {
|
|
772
|
+
let value = info.temperature;
|
|
773
|
+
if value == TEMPERATURE_NOT_REPORTED || value <= 0 || value > 125 {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
// Sensor 0 is the composite (drive-level) reading; prefer it.
|
|
777
|
+
if info.index == 0 {
|
|
778
|
+
return Some(util::round1(value as f64));
|
|
779
|
+
}
|
|
780
|
+
fallback = Some(fallback.map_or(value, |current: i16| current.max(value)));
|
|
781
|
+
}
|
|
782
|
+
fallback.map(|value| util::round1(value as f64))
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
fn disk_temperatures() -> Vec<(String, f64)> {
|
|
786
|
+
(0..MAX_PHYSICAL_DRIVES)
|
|
787
|
+
.filter_map(|index| {
|
|
788
|
+
disk_temperature(index).map(|celsius| (format!("磁盘 {index}"), celsius))
|
|
789
|
+
})
|
|
790
|
+
.collect()
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
fn is_cpu_sensor_name(name: &str) -> bool {
|
|
794
|
+
let lower = name.to_lowercase();
|
|
795
|
+
["cpu", "core", "package", "processor", "tdie", "tctl"]
|
|
796
|
+
.iter()
|
|
797
|
+
.any(|marker| lower.contains(marker))
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
fn read_temperatures(gpu: Option<&Gpu>) -> Vec<Temperature> {
|
|
801
|
+
let mut temperatures = Vec::new();
|
|
802
|
+
let mut seen = HashSet::new();
|
|
803
|
+
// The WDDM die temperature is the reading Task Manager shows under GPU.
|
|
804
|
+
if let Some(gpu) = gpu {
|
|
805
|
+
if let Some(celsius) = gpu.temperature_celsius {
|
|
806
|
+
seen.insert(gpu.name.to_lowercase());
|
|
807
|
+
temperatures.push(Temperature {
|
|
808
|
+
name: gpu.name.clone(),
|
|
809
|
+
celsius,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
for (name, celsius) in disk_temperatures() {
|
|
814
|
+
if !seen.insert(name.to_lowercase()) {
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
temperatures.push(Temperature { name, celsius });
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
let mut zones: Vec<Temperature> = Vec::new();
|
|
821
|
+
for (counter, high_precision) in [(THERMAL_PRECISION_COUNTER, true), (THERMAL_COUNTER, false)] {
|
|
822
|
+
let Ok(values) = pdh_array(counter) else {
|
|
823
|
+
continue;
|
|
824
|
+
};
|
|
825
|
+
let mut zone_seen = HashSet::new();
|
|
826
|
+
for (name, raw) in values {
|
|
827
|
+
if !raw.is_finite() {
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
let Some(celsius) = thermal_value(raw, high_precision) else {
|
|
831
|
+
continue;
|
|
832
|
+
};
|
|
833
|
+
let name = if name.is_empty() {
|
|
834
|
+
"Thermal Zone".to_string()
|
|
835
|
+
} else {
|
|
836
|
+
name
|
|
837
|
+
};
|
|
838
|
+
if !zone_seen.insert(name.to_lowercase()) {
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
zones.push(Temperature {
|
|
842
|
+
name,
|
|
843
|
+
celsius: util::round1(celsius),
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
if !zones.is_empty() {
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
// ACPI thermal zones rarely carry a friendly name. The hottest zone is the
|
|
851
|
+
// CPU package on virtually every laptop/board that exposes zones, so label
|
|
852
|
+
// it explicitly instead of showing an opaque `\_TZ.TZ00`.
|
|
853
|
+
if !zones.is_empty() && !zones.iter().any(|zone| is_cpu_sensor_name(&zone.name)) {
|
|
854
|
+
if let Some(hottest) = zones
|
|
855
|
+
.iter_mut()
|
|
856
|
+
.max_by(|left, right| left.celsius.total_cmp(&right.celsius))
|
|
857
|
+
{
|
|
858
|
+
hottest.name = "CPU".to_string();
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
for zone in zones {
|
|
862
|
+
if seen.insert(zone.name.to_lowercase()) {
|
|
863
|
+
temperatures.push(zone);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
temperatures
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
pub fn read() -> Result<Performance, String> {
|
|
870
|
+
let (mut cpu, memory) = common::sample_system();
|
|
871
|
+
let (model, registry_base) = registry_cpu();
|
|
872
|
+
if let Some(name) = model {
|
|
873
|
+
cpu.model = name;
|
|
874
|
+
}
|
|
875
|
+
if cpu.base_mhz.is_none() {
|
|
876
|
+
cpu.base_mhz = registry_base;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Prefer the PDH percentage (matches the old probe and reacts to boost);
|
|
880
|
+
// fall back to CallNtPowerInformation on machines without the counter.
|
|
881
|
+
let mut speed = None;
|
|
882
|
+
if let (Some(base), Some(percent)) = (cpu.base_mhz, pdh_performance_percent()) {
|
|
883
|
+
let mut current = base * percent / 100.0;
|
|
884
|
+
if current > base * 2.0 {
|
|
885
|
+
current = base * 2.0;
|
|
886
|
+
}
|
|
887
|
+
if current > 0.0 {
|
|
888
|
+
speed = Some(util::round1(current));
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if speed.is_none() {
|
|
892
|
+
if let Some((current, max)) = power_frequency() {
|
|
893
|
+
if current > 0.0 {
|
|
894
|
+
speed = Some(current);
|
|
895
|
+
}
|
|
896
|
+
if cpu.base_mhz.is_none() && max > 0.0 {
|
|
897
|
+
cpu.base_mhz = Some(max);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
cpu.speed_mhz = speed;
|
|
902
|
+
|
|
903
|
+
let gpu = read_gpu();
|
|
904
|
+
let temperatures = read_temperatures(gpu.as_ref());
|
|
905
|
+
let mut performance = Performance::success(cpu, memory);
|
|
906
|
+
performance.gpu = gpu;
|
|
907
|
+
performance.temperatures = temperatures;
|
|
908
|
+
Ok(performance)
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
#[cfg(test)]
|
|
912
|
+
mod tests {
|
|
913
|
+
use super::*;
|
|
914
|
+
|
|
915
|
+
#[test]
|
|
916
|
+
fn registry_name_is_readable() {
|
|
917
|
+
if let (Some(name), _) = registry_cpu() {
|
|
918
|
+
assert!(!name.trim().is_empty());
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
#[test]
|
|
923
|
+
fn pdh_counter_is_available() {
|
|
924
|
+
if let Some(percent) = pdh_performance_percent() {
|
|
925
|
+
assert!(percent > 0.0);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
#[test]
|
|
930
|
+
fn filters_virtual_adapters() {
|
|
931
|
+
assert!(is_virtual_adapter("Microsoft Basic Display Adapter"));
|
|
932
|
+
assert!(is_virtual_adapter("GameViewer Virtual Display"));
|
|
933
|
+
assert!(!is_virtual_adapter("NVIDIA GeForce RTX 4070"));
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
#[test]
|
|
937
|
+
fn converts_thermal_kelvin_values() {
|
|
938
|
+
let high = thermal_value(3001.5, true).unwrap();
|
|
939
|
+
assert!((high - 27.0).abs() < 0.2);
|
|
940
|
+
let normal_tenths = thermal_value(300.15, false).unwrap();
|
|
941
|
+
assert!((normal_tenths - 27.0).abs() < 0.2);
|
|
942
|
+
let normal_kelvin = thermal_value(300.15, true).unwrap();
|
|
943
|
+
assert!((normal_kelvin - 27.0).abs() < 0.2);
|
|
944
|
+
assert_eq!(thermal_value(10.0, false), None);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
#[test]
|
|
948
|
+
fn gpu_reading_has_a_plausible_shape() {
|
|
949
|
+
if let Some(gpu) = read_gpu() {
|
|
950
|
+
assert!(gpu.ok);
|
|
951
|
+
assert!(!gpu.name.is_empty());
|
|
952
|
+
if let Some(value) = gpu.utilization {
|
|
953
|
+
assert!((0.0..=100.0).contains(&value));
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
#[test]
|
|
959
|
+
fn gpu_perf_values_are_plausible() {
|
|
960
|
+
if let Some(gpu) = read_gpu() {
|
|
961
|
+
if let Some(value) = gpu.core_clock_mhz {
|
|
962
|
+
assert!((0.0..10_000.0).contains(&value), "core clock {value}");
|
|
963
|
+
}
|
|
964
|
+
if let Some(value) = gpu.core_clock_max_mhz {
|
|
965
|
+
assert!(value > 0.0 && value < 10_000.0, "max clock {value}");
|
|
966
|
+
}
|
|
967
|
+
if let Some(value) = gpu.memory_clock_mhz {
|
|
968
|
+
assert!(value > 0.0 && value < 30_000.0, "memory clock {value}");
|
|
969
|
+
}
|
|
970
|
+
if let Some(value) = gpu.temperature_celsius {
|
|
971
|
+
assert!((5.0..=125.0).contains(&value), "temperature {value}");
|
|
972
|
+
}
|
|
973
|
+
if let Some(value) = gpu.power_percent {
|
|
974
|
+
assert!((0.0..=100.0).contains(&value), "power {value}");
|
|
975
|
+
}
|
|
976
|
+
if let Some(value) = gpu.fan_rpm {
|
|
977
|
+
assert!(value < 20_000, "fan {value}");
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|