diff --git a/app/windows/BUILD-APP.bat b/app/windows/BUILD-APP.bat index c9f1c9f48..929de8dd3 100644 --- a/app/windows/BUILD-APP.bat +++ b/app/windows/BUILD-APP.bat @@ -45,7 +45,7 @@ echo [build] rc rc.exe /nologo /i "%ART%" /fo build\host.res host.rc || (pause & exit /b 1) echo [build] cl cl.exe /nologo /O2 /MT /EHsc /W3 /std:c++17 /DUNICODE /D_UNICODE /I "build\webview2\build\native\include" /Fo"build\\" host.cpp build\host.res ^ - /link /SUBSYSTEM:WINDOWS /OUT:"dist\Igneum Miner.exe" "build\webview2\build\native\x64\WebView2LoaderStatic.lib" ^ + /link /SUBSYSTEM:WINDOWS /MANIFEST:NO /OUT:"dist\Igneum Miner.exe" "build\webview2\build\native\x64\WebView2LoaderStatic.lib" ^ user32.lib shell32.lib ole32.lib advapi32.lib gdi32.lib version.lib shlwapi.lib || (pause & exit /b 1) echo [build] done: dist\Igneum Miner.exe diff --git a/app/windows/host.cpp b/app/windows/host.cpp index 22d426a9c..496aa8a8d 100644 --- a/app/windows/host.cpp +++ b/app/windows/host.cpp @@ -348,14 +348,35 @@ static void beginQuit() { // (WindowW, WindowH) and used on the next start while it still fits the work area. The Mac window does the same // through its frame autosave name (app/mac/IgneumMiner.swift). static const wchar_t* kSizeKey = L"Software\\Igneum\\Miner"; +// dpi-23 (7 October 2026): the host is per-monitor DPI aware (host.manifest, and the runtime call below for a Windows that +// ignores the manifest), so every rectangle it reads is PHYSICAL pixels; the window's monitor DPI (96 = 100 percent) turns +// them into the logical pixels the opening-size rule and the remembered frame use. GetDpiForMonitor is looked up at run +// time (Shcore, Windows 8.1 and later) so the build links nothing new; the fallback is the desktop's LOGPIXELSX. +typedef HRESULT (WINAPI *GetDpiForMonitorFn)(HMONITOR, int, UINT*, UINT*); +static int monitorDpi(HMONITOR mon) { + HMODULE sh = LoadLibraryW(L"Shcore.dll"); + if (sh) { GetDpiForMonitorFn f = (GetDpiForMonitorFn)GetProcAddress(sh, "GetDpiForMonitor"); UINT x = 96, y = 96; if (f && mon && SUCCEEDED(f(mon, 0 /* MDT_EFFECTIVE_DPI */, &x, &y)) && x > 0) { FreeLibrary(sh); return (int)x; } FreeLibrary(sh); } + HDC dc = GetDC(nullptr); int dpi = dc ? GetDeviceCaps(dc, LOGPIXELSX) : 96; if (dc) ReleaseDC(nullptr, dc); return dpi > 0 ? dpi : 96; +} +static void enableDpiAwareness() { + // the manifest asks for PerMonitorV2; this is the runtime path for a Windows build that ignores it (older than 1703) + HMODULE u = GetModuleHandleW(L"user32.dll"); if (!u) return; + typedef BOOL (WINAPI *SetCtxFn)(HANDLE); + SetCtxFn setCtx = (SetCtxFn)GetProcAddress(u, "SetProcessDpiAwarenessContext"); + if (setCtx && setCtx((HANDLE)-4 /* DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 */)) return; + typedef BOOL (WINAPI *SetAwareFn)(void); + SetAwareFn setAware = (SetAwareFn)GetProcAddress(u, "SetProcessDPIAware"); if (setAware) setAware(); +} static void saveWindowSize(HWND hwnd) { if (IsIconic(hwnd) || IsZoomed(hwnd)) return; RECT r; if (!GetWindowRect(hwnd, &r)) return; - DWORD w = (DWORD)(r.right - r.left), h = (DWORD)(r.bottom - r.top); + int dpi = monitorDpi(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST)); + DWORD w = (DWORD)igneum_scale_to_logical(r.right - r.left, dpi), h = (DWORD)igneum_scale_to_logical(r.bottom - r.top, dpi), units = 1; if (w < 900 || h < 600 || w > (DWORD)(h * 1.6)) return; HKEY k; if (RegCreateKeyExW(HKEY_CURRENT_USER, kSizeKey, 0, nullptr, 0, KEY_SET_VALUE, nullptr, &k, nullptr) != ERROR_SUCCESS) return; RegSetValueExW(k, L"WindowW", 0, REG_DWORD, (const BYTE*)&w, sizeof(w)); RegSetValueExW(k, L"WindowH", 0, REG_DWORD, (const BYTE*)&h, sizeof(h)); + RegSetValueExW(k, L"WindowUnits", 0, REG_DWORD, (const BYTE*)&units, sizeof(units)); // 1 = logical pixels, written by a DPI-aware host RegCloseKey(k); } static void openingSize(int& w, int& h, int& sx, int& sy) { @@ -370,7 +391,8 @@ static void openingSize(int& w, int& h, int& sx, int& sy) { RegQueryValueExW(k, L"WindowH", nullptr, nullptr, (LPBYTE)&sh, &n); RegCloseKey(k); } - igneum_opening_size(ww, wh, (int)sw, (int)sh, &w, &h); + // physical work area and DPI in, physical window out; the rule itself runs in logical pixels (opening_size.h) + igneum_opening_size_scaled(ww, wh, monitorDpi(mon), (int)sw, (int)sh, &w, &h, nullptr, nullptr); sx = work.left + (ww - w) / 2; sy = work.top + (wh - h) / 2; } @@ -382,6 +404,11 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { case WM_EXITSIZEMOVE: saveWindowSize(hwnd); return 0; + case WM_DPICHANGED: { // the window moved to a monitor of another scale: take the rectangle Windows suggests, WebView2 re-renders at the new DPI + const RECT* r = (const RECT*)lp; + if (r) SetWindowPos(hwnd, nullptr, r->left, r->top, r->right - r->left, r->bottom - r->top, SWP_NOZORDER | SWP_NOACTIVATE); + return 0; + } case WM_GETMINMAXINFO: // the dashboard lays out from 900 x 600 up (app/igneum-app/ui); the Mac window says the same ((MINMAXINFO*)lp)->ptMinTrackSize.x = 900; ((MINMAXINFO*)lp)->ptMinTrackSize.y = 600; return 0; @@ -533,6 +560,7 @@ static bool handleCliFlags() { int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) { if (handleCliFlags()) return 0; + enableDpiAwareness(); HANDLE once = CreateMutexW(nullptr, TRUE, L"Local\\IgneumMinerWindow"); if (GetLastError() == ERROR_ALREADY_EXISTS) { HWND other = FindWindowW(L"IgneumMinerWindow", nullptr); diff --git a/app/windows/host.manifest b/app/windows/host.manifest new file mode 100644 index 000000000..7cb2f0b76 --- /dev/null +++ b/app/windows/host.manifest @@ -0,0 +1,19 @@ + + + + + + + true/pm + PerMonitorV2 + + + + + + + + diff --git a/app/windows/host.rc b/app/windows/host.rc index e14e9c163..3fbfffb08 100644 --- a/app/windows/host.rc +++ b/app/windows/host.rc @@ -6,6 +6,9 @@ 1 ICON "igneum.ico" +// dpi-23: the application manifest (per-monitor DPI awareness v2); the linker runs with /MANIFEST:NO +1 24 "host.manifest" + 1 VERSIONINFO FILEVERSION IGNEUM_HOST_VERSION_RC PRODUCTVERSION IGNEUM_HOST_VERSION_RC diff --git a/app/windows/opening_size.h b/app/windows/opening_size.h index 8d3497c93..bd3121666 100644 --- a/app/windows/opening_size.h +++ b/app/windows/opening_size.h @@ -23,4 +23,20 @@ static void igneum_opening_size(int workW, int workH, int savedW, int savedH, in if (savedW >= 900 && savedH >= 600 && savedW <= workW && savedH <= workH && savedW <= (int)(savedH * 1.632)) { w = savedW; h = savedH; } *outW = w; *outH = h; } +/* dpi-23 (7 October 2026): the same rule on a per-monitor-aware host, where the work area and the frame come in PHYSICAL + pixels and the monitor's DPI is known (96 = 100 percent). The rule runs in logical pixels (physical x 96 / dpi), so the + 1440 by 900 and 1920 caps mean CSS pixels as they do on the Mac, and the result goes back to physical for CreateWindow. + The remembered frame is logical on both the old host (a DPI-unaware process reads virtualised, logical coordinates) and + this one (it saves physical / scale), so no stored value is thrown away for its units; the WindowUnits marker records which + host wrote it. PC 2: 5120 by 2112 physical at 192 DPI reads as 2560 by 1056 logical, opens 1625 by 1016 logical, 3250 by + 2032 physical, sharp. */ +static int igneum_scale_to_logical(int px, int dpi) { return dpi > 0 ? (int)((long long)px * 96 / dpi) : px; } +static int igneum_scale_to_physical(int lg, int dpi) { return dpi > 0 ? (int)((long long)lg * dpi / 96) : lg; } +static void igneum_opening_size_scaled(int workWpx, int workHpx, int dpi, int savedWlogical, int savedHlogical, int* outWpx, int* outHpx, int* outWlogical, int* outHlogical) { + int lw = 0, lh = 0; + igneum_opening_size(igneum_scale_to_logical(workWpx, dpi), igneum_scale_to_logical(workHpx, dpi), savedWlogical, savedHlogical, &lw, &lh); + if (outWlogical) *outWlogical = lw; + if (outHlogical) *outHlogical = lh; + *outWpx = igneum_scale_to_physical(lw, dpi); *outHpx = igneum_scale_to_physical(lh, dpi); +} #endif diff --git a/app/windows/opening_size_test.c b/app/windows/opening_size_test.c index 49c86fe39..2615e5253 100644 --- a/app/windows/opening_size_test.c +++ b/app/windows/opening_size_test.c @@ -6,6 +6,11 @@ #include #include "opening_size.h" static int fails = 0; +static void checkS(const char* name, int ww, int wh, int dpi, int sw, int sh, int ew, int eh, int elw, int elh) { + int w = 0, h = 0, lw = 0, lh = 0; igneum_opening_size_scaled(ww, wh, dpi, sw, sh, &w, &h, &lw, &lh); + if (w != ew || h != eh || lw != elw || lh != elh) { printf("FAIL %s: work %dx%d @%d dpi saved %dx%d -> %dx%d px (%dx%d logical), wanted %dx%d px (%dx%d)\n", name, ww, wh, dpi, sw, sh, w, h, lw, lh, ew, eh, elw, elh); fails++; } + else printf("ok %s: %dx%d px, %dx%d logical\n", name, w, h, lw, lh); +} static void check(const char* name, int ww, int wh, int sw, int sh, int ew, int eh) { int w = 0, h = 0; igneum_opening_size(ww, wh, sw, sh, &w, &h); if (w != ew || h != eh) { printf("FAIL %s: work %dx%d saved %dx%d -> %dx%d, wanted %dx%d\n", name, ww, wh, sw, sh, w, h, ew, eh); fails++; } @@ -26,5 +31,12 @@ int main(void) { check("a remembered frame under the minimum is discarded", 1920, 1040, 800, 500, 1440, 900); check("a small laptop: 80 percent wide, 16:10", 1280, 760, 0, 0, 1024, 640); check("a tiny work area: the work area itself", 800, 560, 0, 0, 800, 560); + /* dpi-23: the per-monitor-aware host reads physical pixels and the monitor's DPI */ + checkS("1080p at 100 percent: unchanged", 1920, 1040, 96, 0, 0, 1440, 900, 1440, 900); + checkS("PC 2: 5120 by 2112 physical at 200 percent (2560 by 1056 logical)", 5120, 2112, 192, 0, 0, 3250, 2032, 1625, 1016); + checkS("4K at 150 percent: 1920 by 1200 logical, 2880 by 1800 physical", 3840, 2120, 144, 0, 0, 2880, 1800, 1920, 1200); + checkS("a 1440p panel at 125 percent (2048 by 1120 logical)", 2560, 1400, 120, 0, 0, 2045, 1278, 1636, 1023); + checkS("PC 2's remembered 1329 by 825 (logical, from the unaware host) is kept and placed at 200 percent", 5120, 2112, 192, 1329, 825, 2658, 1650, 1329, 825); + checkS("a 250 percent laptop panel (1536 by 832 logical): 80 percent wide at 16:10", 3840, 2080, 240, 0, 0, 3067, 1917, 1227, 767); printf(fails ? "%d FAILED\n" : "all ok\n", fails); return fails ? 1 : 0; } diff --git a/tools/windows/dpi-check.ps1 b/tools/windows/dpi-check.ps1 new file mode 100644 index 000000000..54d31d73a --- /dev/null +++ b/tools/windows/dpi-check.ps1 @@ -0,0 +1,36 @@ +# dpi-check.ps1 (dpi-23, 7 October 2026): READ-ONLY. Reports the installed Igneum Miner window's DPI awareness, its rectangle in +# physical pixels, its monitor's DPI and scale, and the logical size that follows; the PC job runs it at 100, 150, 200 and 250 +# percent after the user sets the scale (the script sets nothing). With -Expect PerMonitorV2 it exits 1 when the awareness +# differs (known-failed on the 0.3.22 host, which is Unaware, so Windows stretched its render). Touches nothing: no click, no +# message to the window, no setting; a job never quits or restarts the installed app. +param([string]$Expect = "") +Add-Type @" +using System; using System.Runtime.InteropServices; +public static class Dpi { + [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; } + [DllImport("user32.dll")] public static extern IntPtr FindWindowW([MarshalAs(UnmanagedType.LPWStr)] string cls, [MarshalAs(UnmanagedType.LPWStr)] string title); + [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r); + [DllImport("user32.dll")] public static extern IntPtr GetWindowDpiAwarenessContext(IntPtr h); + [DllImport("user32.dll")] public static extern int GetAwarenessFromDpiAwarenessContext(IntPtr ctx); + [DllImport("user32.dll")] public static extern bool AreDpiAwarenessContextsEqual(IntPtr a, IntPtr b); + [DllImport("user32.dll")] public static extern IntPtr MonitorFromWindow(IntPtr h, uint flags); + [DllImport("shcore.dll")] public static extern int GetDpiForMonitor(IntPtr mon, int type, out uint x, out uint y); + [DllImport("user32.dll")] public static extern bool SetProcessDpiAwarenessContext(IntPtr ctx); +} +"@ +[void][Dpi]::SetProcessDpiAwarenessContext([IntPtr](-4)) # this script reads physical pixels itself +$h = [Dpi]::FindWindowW("IgneumMinerWindow", $null) +if ($h -eq [IntPtr]::Zero) { Write-Output "dpi-check: no Igneum Miner window (class IgneumMinerWindow) on this desktop"; exit 2 } +$ctx = [Dpi]::GetWindowDpiAwarenessContext($h) +$aw = [Dpi]::GetAwarenessFromDpiAwarenessContext($ctx) # 0 Unaware, 1 System, 2 PerMonitor +$v2 = [Dpi]::AreDpiAwarenessContextsEqual($ctx, [IntPtr](-4)) +$word = if ($v2) { "PerMonitorV2" } elseif ($aw -eq 2) { "PerMonitor" } elseif ($aw -eq 1) { "System" } else { "Unaware" } +$r = New-Object Dpi+RECT; [void][Dpi]::GetWindowRect($h, [ref]$r) +$mon = [Dpi]::MonitorFromWindow($h, 2); $x = [uint32]96; $y = [uint32]96; [void][Dpi]::GetDpiForMonitor($mon, 0, [ref]$x, [ref]$y) +$w = $r.Right - $r.Left; $hh = $r.Bottom - $r.Top; $scale = [math]::Round($x / 96.0, 2) +$lw = [math]::Round($w * 96 / $x); $lh = [math]::Round($hh * 96 / $x) +Write-Output ("dpi-check: awareness {0}; window {1}x{2} physical at {3},{4}; monitor {5} dpi ({6} percent); logical {7}x{8}" -f $word, $w, $hh, $r.Left, $r.Top, $x, [int]($scale * 100), $lw, $lh) +$reg = Get-ItemProperty -Path "HKCU:\Software\Igneum\Miner" -ErrorAction SilentlyContinue +if ($reg) { Write-Output ("dpi-check: remembered WindowW {0} WindowH {1} WindowUnits {2}" -f $reg.WindowW, $reg.WindowH, $reg.WindowUnits) } +if ($Expect -ne "" -and $word -ne $Expect) { Write-Output ("dpi-check: FAIL, expected {0}" -f $Expect); exit 1 } +exit 0