diff --git a/docs/architecture/04-render-engine.md b/docs/architecture/04-render-engine.md index 78c8777b..c4e55c97 100644 --- a/docs/architecture/04-render-engine.md +++ b/docs/architecture/04-render-engine.md @@ -230,7 +230,7 @@ engage by default on the desktop. `GetCursorPos`/`SetCursorPos`. - `DXGI_ERROR_DEVICE_REMOVED/RESET` latches `deviceLost()`. `recoverDeviceLost()` rebuilds through the same `buildDeviceResources` as `initialize`, so the paths cannot drift. -- **Always restore the OS cursor.** `shutdown` and the crash filter (`CursorRestoreFilter`) restore +- **Always restore the OS cursor.** `shutdown` and the shared crash filter (`wind::CursorCrashFilter`, `mag_host.cpp`: report first, then restore) restore the pointer and release any `ClipCursor`. Cursor hiding goes through `MagApiAcquire`/`MagApiRelease`; see [05](05-transform-engine.md) for why the pairing must be shared. diff --git a/installer/README.md b/installer/README.md index 449e4aaa..cde9170a 100644 --- a/installer/README.md +++ b/installer/README.md @@ -49,7 +49,8 @@ which makes `wind.nsi` define `LOCAL_SIGN`. Setup then runs `local-sign.ps1`: signs the executables, and has its private key deleted at once, so nothing can sign with that root again. Older Wind roots are retired on every install; the uninstaller removes them (`local-sign.ps1 -Remove`). -- Setup installs the ordinary build first and signs the UIAccess build in `$PLUGINSDIR`, copying it +- Setup installs the ordinary build first and signs the UIAccess build in the admin-only + `$INSTDIR\.stage` (removed afterwards; never the user-writable `$PLUGINSDIR`), copying it over only after its signature verifies. An unsigned UIAccess `Wind.exe` does not start at all, so any failure leaves the ordinary build. - A release signed with a real certificate has no `WindUA.exe` and skips this. diff --git a/installer/wind.nsi b/installer/wind.nsi index b1a1c954..939025af 100644 --- a/installer/wind.nsi +++ b/installer/wind.nsi @@ -97,7 +97,7 @@ Section "Wind" SEC_WIND ; LocalMachine certificate stores are the machine's either way, but the PKI cmdlets are only ; guaranteed in the native one. ; - ; The standard build goes in FIRST, and the uiAccess build is signed in $PLUGINSDIR and only + ; The standard build goes in FIRST, and the uiAccess build is signed in $INSTDIR\.stage and only ; copied over it once its signature verifies. An unsigned uiAccess build does not start at ; all ("A referral was returned from the server"), so setup must never leave one in place: ; killed mid-signing, it did exactly that (reproduced 2026-09-28). This way an interruption @@ -105,16 +105,19 @@ Section "Wind" SEC_WIND File "..\Wind.exe" File "..\WindConfig.exe" File "..\WindTray.exe" - InitPluginsDir - CreateDirectory "$PLUGINSDIR\ua" - File "/oname=$PLUGINSDIR\ua\Wind.exe" "..\WindUA.exe" - File "/oname=$PLUGINSDIR\local-sign.ps1" "local-sign.ps1" + ; Staged under $INSTDIR, not in $PLUGINSDIR: that is the user's %TEMP%, writable by a + ; non-elevated process, and this step runs a script and signs a binary as admin. Program + ; Files is admin-only, so nothing unprivileged can swap either between copy and use. + RMDir /r "$INSTDIR\.stage" + CreateDirectory "$INSTDIR\.stage\ua" + File "/oname=$INSTDIR\.stage\ua\Wind.exe" "..\WindUA.exe" + File "/oname=$INSTDIR\.stage\local-sign.ps1" "local-sign.ps1" DetailPrint "Signing Wind on this PC..." - nsExec::ExecToLog '"$WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$PLUGINSDIR\local-sign.ps1" -Stage "$PLUGINSDIR\ua\Wind.exe" -Dir "$INSTDIR"' + nsExec::ExecToLog '"$WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$INSTDIR\.stage\local-sign.ps1" -Stage "$INSTDIR\.stage\ua\Wind.exe" -Dir "$INSTDIR"' Pop $0 ${If} $0 == 0 ClearErrors - CopyFiles /SILENT "$PLUGINSDIR\ua\Wind.exe" "$INSTDIR\Wind.exe" + CopyFiles /SILENT "$INSTDIR\.stage\ua\Wind.exe" "$INSTDIR\Wind.exe" ${If} ${Errors} DetailPrint "Could not place the signed build; keeping the standard one." File "..\Wind.exe" @@ -124,6 +127,7 @@ Section "Wind" SEC_WIND ; menus and the Snipping Tool is lost. DetailPrint "Local signing failed ($0); keeping the standard build." ${EndIf} + RMDir /r "$INSTDIR\.stage" !else File "..\Wind.exe" File "..\WindConfig.exe" @@ -181,10 +185,11 @@ Section "Uninstall" ; Retire the trust setup added for the local signature. Always, not only when this build ; signed: an older install may have, and a root left behind for an uninstalled app is litter. - InitPluginsDir - File "/oname=$PLUGINSDIR\local-sign.ps1" "local-sign.ps1" - nsExec::ExecToLog '"$WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$PLUGINSDIR\local-sign.ps1" -Remove' + CreateDirectory "$INSTDIR\.stage" + File "/oname=$INSTDIR\.stage\local-sign.ps1" "local-sign.ps1" + nsExec::ExecToLog '"$WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$INSTDIR\.stage\local-sign.ps1" -Remove' Pop $0 + RMDir /r "$INSTDIR\.stage" Delete "$INSTDIR\Wind.exe" Delete "$INSTDIR\WindConfig.exe" diff --git a/src/cursor_blanker.cpp b/src/cursor_blanker.cpp index 3a04b06e..7e8d2185 100644 --- a/src/cursor_blanker.cpp +++ b/src/cursor_blanker.cpp @@ -1,5 +1,6 @@ #include "cursor_blanker.h" #include "tick_span.h" // #361: per-tick spans +#include #include namespace wind { @@ -30,7 +31,7 @@ static void RestoreAll() { SystemParametersInfoW(SPI_SETCURSORS, 0, nullptr, 0); } -CursorBlanker::CursorBlanker() { +CursorBlanker::CursorBlanker() : st_(std::make_shared()) { // If a previous Wind was hard-killed while cursors were blanked, the desktop still has blank // shared cursors; reload the user's scheme FIRST or the blanks get captured as "originals". RestoreAll(); @@ -40,17 +41,22 @@ CursorBlanker::CursorBlanker() { HCURSOR copy = CopyCursor(shared); if (copy) originals_[shared] = copy; } - worker_ = std::thread([this] { run(); }); + worker_ = std::thread([st = st_] { run(st); }); } CursorBlanker::~CursorBlanker() { if (blanked_) restoreSync(); - { std::lock_guard lk(mx_); stop_ = true; } - cv_.notify_one(); - if (worker_.joinable()) worker_.join(); + std::unique_lock lk(st_->mx); + st_->stop = true; + st_->cv.notify_one(); + // A worker wedged inside a system call must not hang exit: give it 2 s, then let it go (the + // shared state outlives this object). restoreSync above already restored from this thread. + const bool exited = st_->exitCv.wait_for(lk, std::chrono::seconds(2), [this] { return st_->exited; }); + lk.unlock(); + if (worker_.joinable()) { if (exited) worker_.join(); else worker_.detach(); } } -void CursorBlanker::run() { +void CursorBlanker::run(std::shared_ptr st) { SetThreadDescription(GetCurrentThread(), L"Wind cursor swaps"); // Above normal: under a build the swap must still land promptly, or the pointer comes back // late. It never competes with the tick for long: a swap is a few ms of work. @@ -58,19 +64,23 @@ void CursorBlanker::run() { for (;;) { std::function op; { - std::unique_lock lk(mx_); - cv_.wait(lk, [this] { return stop_ || !q_.empty(); }); - if (q_.empty()) return; // stop_ with nothing left to do - op = std::move(q_.front()); - q_.pop_front(); + std::unique_lock lk(st->mx); + st->cv.wait(lk, [&] { return st->stop || !st->q.empty(); }); + if (st->q.empty()) { // stop with nothing left to do + st->exited = true; + st->exitCv.notify_all(); + return; + } + op = std::move(st->q.front()); + st->q.pop_front(); } op(); } } void CursorBlanker::post(std::function op) { - { std::lock_guard lk(mx_); q_.push_back(std::move(op)); } - cv_.notify_one(); + { std::lock_guard lk(st_->mx); st_->q.push_back(std::move(op)); } + st_->cv.notify_one(); } bool CursorBlanker::runSync(std::function op) { diff --git a/src/cursor_blanker.h b/src/cursor_blanker.h index da57f7a9..dffaa44c 100644 --- a/src/cursor_blanker.h +++ b/src/cursor_blanker.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -28,15 +29,21 @@ class CursorBlanker { // Restore and wait for it (up to 2 s). For shutdown, which must not exit with blank cursors. void restoreSync(); private: + // The queue is shared with the worker, so a worker wedged in a system call at exit can be + // detached without leaving it a dangling `this` (the same shape as FocusLookup). + struct State { + std::mutex mx; + std::condition_variable cv; // wakes the worker + std::condition_variable exitCv; // wakes the destructor when the worker has returned + std::deque> q; + bool stop = false, exited = false; + }; void post(std::function op); bool runSync(std::function op); - void run(); + static void run(std::shared_ptr st); std::unordered_map originals_; bool blanked_ = false; - std::mutex mx_; - std::condition_variable cv_; - std::deque> q_; - bool stop_ = false; + std::shared_ptr st_; std::thread worker_; }; } diff --git a/src/mag_host.cpp b/src/mag_host.cpp index bf379f03..9dbca879 100644 --- a/src/mag_host.cpp +++ b/src/mag_host.cpp @@ -40,6 +40,16 @@ void MagApiRelease() { bool MagApiAlive() { return g_magRefs > 0; } +LONG WINAPI CursorCrashFilter(EXCEPTION_POINTERS* ep) { + static LONG s_inHandler = 0; + if (InterlockedExchange(&s_inHandler, 1)) return EXCEPTION_CONTINUE_SEARCH; + wind::WriteCrashReport(ep); // minidump + text summary into the log dir + MagShowSystemCursor(TRUE); // no-op if the Magnification API was never initialized this run + ClipCursor(nullptr); // never leave the cursor clipped if we crash while Inspect-locked + SystemParametersInfoW(SPI_SETCURSORS, 0, nullptr, 0); // heals a blanked cursor scheme, no broadcast + return EXCEPTION_CONTINUE_SEARCH; +} + bool MagHost::initialize() { initialized_ = MagApiAcquire(); privateBroken_ = false; // re-probe the private channel on every (re-)init, not once ever diff --git a/src/mag_host.h b/src/mag_host.h index 3f4b40c4..9ed82fc2 100644 --- a/src/mag_host.h +++ b/src/mag_host.h @@ -12,6 +12,12 @@ namespace wind { bool MagApiAcquire(); void MagApiRelease(); bool MagApiAlive(); +// Crash safety net shared by main.cpp (installed at startup) and RenderEngine (on its first cursor +// hide): if we go down while the cursor is hidden or confined, make it visible and free again. +// Writes the crash report FIRST (the restore can otherwise delay the dump), restores without the +// SPI broadcast (SENDCHANGE would run other windows' handlers, our own tint capture included, +// inside a faulted process), and returns EXCEPTION_CONTINUE_SEARCH so the default handler still runs. +LONG WINAPI CursorCrashFilter(EXCEPTION_POINTERS* ep); class MagHost { public: bool initialize(); diff --git a/src/main.cpp b/src/main.cpp index b1531118..40270470 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2832,7 +2832,7 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { // leave the machine with a hidden/locked cursor - the next launch (and our own exit) heals it. static void RestoreInputState() { ClipCursor(nullptr); // release any cursor confinement - if (MagInitialize()) { MagShowSystemCursor(TRUE); MagUninitialize(); } // un-hide the OS cursor + if (wind::MagApiAcquire()) { MagShowSystemCursor(TRUE); wind::MagApiRelease(); } // un-hide the OS cursor SystemParametersInfoW(SPI_SETCURSORS, 0, nullptr, SPIF_SENDCHANGE); // reload system cursors for (int i = 0; i < 8 && ShowCursor(TRUE) < 0; ++i) {} // bump our show-count back to visible } @@ -2840,26 +2840,15 @@ static void RestoreInputState() { // Minimal restore for abnormal CRT exit (atexit / ExitProcess). Non-blocking: must not touch the // hook thread (its stop() waits, which could hang during teardown); the hook dies with the process // and only swallows side-buttons anyway. The damaging state (hidden/confined cursor) is undone here. -static void AtExitRestore() { RestoreInputState(); } - -// Crash safety net installed BEFORE the magnifier model is constructed. RenderModel hides the OS -// cursor via the process-scoped Magnification API (auto-reverts on process death), but the -// SPI_SETCURSORS reload is kept as a general heal for any -// stale cursor scheme a crashed predecessor left behind. Body mirrors render_engine.cpp's -// CursorRestoreFilter (minimal, allocation-light, one-shot via InterlockedExchange, returns -// EXCEPTION_CONTINUE_SEARCH so the default handler still reports the crash). RenderEngine:: -// hideSystemCursor installs its own filter on the render path's first cursor hide, which REPLACES -// this one (SetUnhandledExceptionFilter keeps only the latest); that is safe because CursorRestoreFilter -// does the identical restore + crash report, so nothing is lost by the replacement. -static LONG WINAPI EarlyCursorRestoreFilter(EXCEPTION_POINTERS* ep) { - static LONG s_inHandler = 0; - if (InterlockedExchange(&s_inHandler, 1)) return EXCEPTION_CONTINUE_SEARCH; - MagShowSystemCursor(TRUE); // no-op if the Magnification API was never initialized this run - ClipCursor(nullptr); // never leave the cursor clipped if we crash while Inspect-locked - SystemParametersInfoW(SPI_SETCURSORS, 0, nullptr, SPIF_SENDCHANGE); // heals a blanked cursor scheme - wind::WriteCrashReport(ep); // minidump + text summary into the log dir - return EXCEPTION_CONTINUE_SEARCH; // let the default handler still report the crash -} +// Only while we own the single-instance mutex: a refused or lingering second process must not heal +// global state under the live owner, and a normal exit restores BEFORE releasing the mutex (see +// ReleaseInstance), so a successor's tint capture never sees a half-restored scheme. +static std::atomic g_ownsInstance{false}; +static void AtExitRestore() { if (g_ownsInstance.load()) RestoreInputState(); } + +// Crash safety net installed BEFORE the magnifier model is constructed, so a crash under +// model=transform (which never touches RenderEngine) still heals the cursor. RenderEngine installs +// the same shared filter (wind::CursorCrashFilter, mag_host.cpp) on its first cursor hide. // Single-instance startup events route through the unified logger (category "startup"). static void SiLog(const char* msg, unsigned long val) { @@ -2894,27 +2883,44 @@ static void TerminateOtherWind() { // two cursor-warp loops = a system-wide input lock. (The previous kill-only version failed here: // TerminateProcess is blocked across the signed UIAccess build's integrity, and it had removed the // refuse-to-start backstop, so a second instance proceeded anyway and locked input.) -static bool AcquireSingleInstance(HANDLE& mtx) { +// +// quitEvent is created here, as soon as the mutex is ours, so a successor that finds us busy can +// always signal it (it used to exist only after the whole startup). A null mutex fails closed. +static bool AcquireSingleInstance(HANDLE& mtx, HANDLE& quitEvent) { mtx = CreateMutexW(nullptr, FALSE, L"Local\\Wind_Magnifier_SingleInstance"); SiLog("createmutex err", GetLastError()); - if (!mtx) { SiLog("mutex null - proceeding unprotected", 0); return true; } // rare; don't block + if (!mtx) { SiLog("mutex null - REFUSING TO START (cannot guard against a second instance)", 0); return false; } + // Quit-request channel for WindConfig.exe (onboarding close) and for a successor Wind. A window + // message can't be used: the deployed Wind.exe is UIAccess, and UIPI silently blocks PostMessage + // from the non-UIAccess WindConfig. A named event is a kernel object (not gated by UIPI) and + // both run as the same user in the same session. Auto-reset, initially unsignaled. + auto makeQuitEvent = [&] { quitEvent = CreateEventW(nullptr, FALSE, FALSE, L"Local\\Wind_QuitRequest"); }; DWORD w = WaitForSingleObject(mtx, 0); // WAIT_ABANDONED = prior owner died holding it -> ours now - if (w == WAIT_OBJECT_0 || w == WAIT_ABANDONED) { SiLog("acquired immediately w", w); return true; } + if (w == WAIT_OBJECT_0 || w == WAIT_ABANDONED) { makeQuitEvent(); SiLog("acquired immediately w", w); return true; } SiLog("busy - signaling quit, w", w); HANDLE ev = OpenEventW(EVENT_MODIFY_STATE, FALSE, L"Local\\Wind_QuitRequest"); if (ev) { SetEvent(ev); CloseHandle(ev); SiLog("quit event set", 0); } else SiLog("quit event open err", GetLastError()); w = WaitForSingleObject(mtx, 3000); // wait for it to release on clean exit - if (w == WAIT_OBJECT_0 || w == WAIT_ABANDONED) { SiLog("acquired after quit w", w); return true; } + if (w == WAIT_OBJECT_0 || w == WAIT_ABANDONED) { makeQuitEvent(); SiLog("acquired after quit w", w); return true; } SiLog("still busy after quit - terminating, w", w); TerminateOtherWind(); // fallback: kill the straggler w = WaitForSingleObject(mtx, 2000); - if (w == WAIT_OBJECT_0 || w == WAIT_ABANDONED) { SiLog("acquired after terminate w", w); return true; } + if (w == WAIT_OBJECT_0 || w == WAIT_ABANDONED) { makeQuitEvent(); SiLog("acquired after terminate w", w); return true; } SiLog("REFUSING TO START - another instance alive, w", w); CloseHandle(mtx); mtx = nullptr; return false; // never stack a second hook/cursor loop } +// Normal-exit hand-over: heal the input state while the mutex is still ours, then release it. +static void ReleaseInstance(HANDLE mtx) { + if (!mtx) return; + RestoreInputState(); + g_ownsInstance.store(false); + ReleaseMutex(mtx); + CloseHandle(mtx); +} + int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) { // Heal any input state a previous (possibly killed) Wind left dirty, then claim SOLE ownership. // If another live instance refuses to yield, exit WITHOUT installing hooks (two instances would @@ -2925,15 +2931,20 @@ int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) { SiLog("=== launch ===", 0); LoadChurnyApps(); // issue #148: learned cursor-churning apps (transform -> render for them) DetectMpoDisabled(); // issue #148: MPO boot state decides whether the pan wall is needed - RestoreInputState(); + // Heal only once we OWN the single-instance mutex (a refused start must not touch the live + // owner's state), and AFTER any hung predecessor was killed, so the scheme a tint capture sees + // is clean. HANDLE mtx = nullptr; - if (!AcquireSingleInstance(mtx)) { RestoreInputState(); return 0; } + HANDLE quitEvent = nullptr; + if (!AcquireSingleInstance(mtx, quitEvent)) return 0; + g_ownsInstance.store(true); + RestoreInputState(); wind::LogClaimBase(); // a restart's per-PID fallback log moves onto wind-core.log (M6) atexit(AtExitRestore); // Installed before either magnifier model is constructed so a crash under model=transform (which // never touches RenderEngine, so RenderEngine's own filter is never installed) still heals a - // blanked system cursor. See EarlyCursorRestoreFilter for why the render path safely replaces this. - SetUnhandledExceptionFilter(EarlyCursorRestoreFilter); + // blanked system cursor. The render path installs the same filter again, which is harmless. + SetUnhandledExceptionFilter(wind::CursorCrashFilter); // Resolve magnifier.ini next to the exe (not the launch cwd). wchar_t exePath[MAX_PATH]; @@ -3073,7 +3084,15 @@ int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) { cfg.zorderBand, cfg.cursorBandAuto != 0); } } - if (!model->initialize(startupMon)) { + bool primaryOk = model->initialize(startupMon); + if (!primaryOk && model2 && model2->initialize(PrimaryMonitor())) { + // Hybrid whose render half cannot start (no D3D / Desktop Duplication): the transform half + // needs neither, so run as model=transform instead of exiting. + wind::Log(wind::LogLevel::Warn, "startup", "hybrid: render half failed to init; transform-only"); + model = std::move(model2); + primaryOk = true; + } + if (!primaryOk) { MessageBoxW(nullptr, L"Could not start the renderer (Direct3D 11 / Desktop Duplication " L"unavailable on this system).", L"Wind", MB_ICONERROR); g_input.stop(); @@ -3140,7 +3159,7 @@ int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) { g_input.stop(); g_track.stop(); wind::TrayHost::Stop(); - ReleaseMutex(mtx); + ReleaseInstance(mtx); return 0; } @@ -3187,7 +3206,7 @@ int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) { g_input.stop(); g_track.stop(); wind::TrayHost::Stop(); - ReleaseMutex(mtx); + ReleaseInstance(mtx); return 0; } @@ -3196,14 +3215,18 @@ int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) { // to the tray. Resolve by full path (exePath is our own dir) so it works regardless of the cwd. if (cfg.onboarded == 0) { std::wstring configExe = std::wstring(exePath) + L"\\WindConfig.exe"; - wchar_t cmd[] = L"WindConfig.exe --onboard"; - STARTUPINFOW si{}; si.cb = sizeof(si); - PROCESS_INFORMATION pi{}; - if (CreateProcessW(configExe.c_str(), cmd, nullptr, nullptr, FALSE, - 0, nullptr, nullptr, &si, &pi)) { - CloseHandle(pi.hThread); - CloseHandle(pi.hProcess); - } + // ShellExecuteEx, never CreateProcess with our token: Wind.exe is UIAccess and a child made + // with CreateProcess inherits it. A shell launch gives WindConfig an ordinary token (the + // same way LaunchTray starts WindTray). + SHELLEXECUTEINFOW sei{}; + sei.cbSize = sizeof(sei); + sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC; + sei.lpVerb = L"open"; + sei.lpFile = configExe.c_str(); + sei.lpParameters = L"--onboard"; + sei.lpDirectory = exePath; + sei.nShow = SW_SHOWNORMAL; + if (ShellExecuteExW(&sei) && sei.hProcess) CloseHandle(sei.hProcess); } QueryPerformanceFrequency(&ts.freq); @@ -3223,12 +3246,7 @@ int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) { ts.configWatch = FindFirstChangeNotificationW(iniDir.c_str(), FALSE, FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_FILE_NAME); - // Quit-request channel for WindConfig.exe (onboarding close). A window message can't be used: - // the deployed Wind.exe is UIAccess, and UIPI silently blocks PostMessage from the non-UIAccess - // WindConfig. A named event is a kernel object (not gated by UIPI) and both run as the same user - // in the same session, so it works in dev and deployed. Auto-reset, initially unsignaled. - HANDLE quitEvent = CreateEventW(nullptr, FALSE, FALSE, L"Local\\Wind_QuitRequest"); - + // quitEvent: created in AcquireSingleInstance. HANDLE timer = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); // Test telemetry opt-in (issue #225). Two channels, either enables it: @@ -3468,7 +3486,7 @@ int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) { wind::WriteTextFileAtomic(wind::ResolveLogDir() + L"/learned_gain.txt", buf); // Win32 accepts '/' } wind::TrayHost::Stop(); - if (mtx) { ReleaseMutex(mtx); CloseHandle(mtx); } + ReleaseInstance(mtx); wind::LogShutdown(); return 0; } diff --git a/src/render_engine.cpp b/src/render_engine.cpp index a2aeb106..4f8ac866 100644 --- a/src/render_engine.cpp +++ b/src/render_engine.cpp @@ -1366,19 +1366,6 @@ bool RenderEngine::dumpFrame(const RenderFrameParams& p, const wchar_t* path) { return dumpBackbufferPng(path); } -// Crash safety net: if we go down while the cursor is hidden, force it visible again so the -// user is never left without a pointer. The magnification runtime is process-scoped (so exit -// usually restores it), but a hard crash mid-hide is exactly when this matters. -static LONG WINAPI CursorRestoreFilter(EXCEPTION_POINTERS* ep) { - static LONG s_inHandler = 0; - if (InterlockedExchange(&s_inHandler, 1)) return EXCEPTION_CONTINUE_SEARCH; - MagShowSystemCursor(TRUE); - ClipCursor(nullptr); // never leave the cursor clipped if we crash while Inspect-locked - SystemParametersInfoW(SPI_SETCURSORS, 0, nullptr, SPIF_SENDCHANGE); - wind::WriteCrashReport(ep); // minidump + text summary into the log dir - return EXCEPTION_CONTINUE_SEARCH; // let the default handler still report the crash -} - bool RenderEngine::waitVBlank() { if (!s_ || !s_->waitOutput) return false; return SUCCEEDED(s_->waitOutput->WaitForVBlank()); @@ -1393,7 +1380,7 @@ void RenderEngine::hideSystemCursor(bool hide) { if (hide) { if (!s_->magInited) { s_->magInited = wind::MagApiAcquire(); - SetUnhandledExceptionFilter(CursorRestoreFilter); // installed once, before first hide + SetUnhandledExceptionFilter(wind::CursorCrashFilter); // installed once, before first hide } if (s_->magInited && MagShowSystemCursor(FALSE) == FALSE) wind::Log(wind::LogLevel::Warn, "render", "MagShowSystemCursor(FALSE) failed"); diff --git a/src/version.h b/src/version.h index 85a3b6bd..f84bfdc8 100644 --- a/src/version.h +++ b/src/version.h @@ -4,7 +4,7 @@ #define WIND_VER_MAJOR 0 #define WIND_VER_MINOR 24 -#define WIND_VER_PATCH 11 +#define WIND_VER_PATCH 12 // String form for logs/snapshot/UI. Keep in sync with the numeric parts above. -#define WIND_VERSION_STR "0.24.11" +#define WIND_VERSION_STR "0.24.12"