Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/architecture/07-cursor.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,11 @@ is hidden and a stand-in drawn.
keeps the originals. It first reloads the user's scheme, so a previously killed Wind's blanks are
never captured as originals. `MagShowSystemCursor(FALSE)` covers app-custom cursors. The blank
runs before the magnification context exists, because each swap under a live context costs a
re-composite.
re-composite. The swaps run in order on the `Wind cursor swaps` worker thread (#363): the restore
is a full `SPI_SETCURSORS` scheme reload (8 ms median, up to 90 ms under load) and the blank 14
`SetSystemCursor` calls, which froze the zoom-in and the 1x landing frame on the tick thread.
The zoom-out repaint nudge rides on the worker after the restore. `restoreSync()` waits (the
input-panel clip nudge, shutdown); the crash filter restores directly.
- **`CursorSprite`** (`src/cursor_sprite.*`) is a small layered window painting the current shape,
or the Inspect crosshair. It sits at the lens point in desktop coordinates, so DWM shows it at the
view's centre, magnified. `keepOnTop()` re-asserts topmost only when displaced.
Expand Down
71 changes: 64 additions & 7 deletions src/cursor_blanker.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "cursor_blanker.h"
#include "tick_span.h" // #361: per-tick spans
#include <future>
namespace wind {

static const UINT kStandardIds[] = {
Expand All @@ -16,32 +17,88 @@ static HCURSOR CreateBlankCursor() {
return CreateCursor(nullptr, 0, 0, 32, 32, andMask, xorMask);
}

static void BlankAll() {
for (UINT id : kStandardIds) {
HCURSOR blank = CreateBlankCursor();
if (blank) SetSystemCursor(blank, id); // SetSystemCursor takes ownership of 'blank'
}
}

static void RestoreAll() {
// Reloads the user's scheme as it is NOW (animated cursors, size and scheme changes made
// while Wind runs all survive), which is why this is not a copy-back of cached handles.
SystemParametersInfoW(SPI_SETCURSORS, 0, nullptr, 0);
}

CursorBlanker::CursorBlanker() {
// 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".
SystemParametersInfoW(SPI_SETCURSORS, 0, nullptr, 0);
RestoreAll();
for (UINT id : kStandardIds) {
HCURSOR shared = LoadCursorW(nullptr, MAKEINTRESOURCEW(id));
if (!shared) continue;
HCURSOR copy = CopyCursor(shared);
if (copy) originals_[shared] = copy;
}
worker_ = std::thread([this] { run(); });
}

CursorBlanker::~CursorBlanker() {
if (blanked_) restoreSync();
{ std::lock_guard<std::mutex> lk(mx_); stop_ = true; }
cv_.notify_one();
if (worker_.joinable()) worker_.join();
}

void CursorBlanker::run() {
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.
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL);
for (;;) {
std::function<void()> op;
{
std::unique_lock<std::mutex> 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();
}
op();
}
}

void CursorBlanker::post(std::function<void()> op) {
{ std::lock_guard<std::mutex> lk(mx_); q_.push_back(std::move(op)); }
cv_.notify_one();
}

bool CursorBlanker::runSync(std::function<void()> op) {
auto done = std::make_shared<std::promise<void>>();
auto fut = done->get_future();
post([op = std::move(op), done] { op(); done->set_value(); });
return fut.wait_for(std::chrono::seconds(2)) == std::future_status::ready;
}

void CursorBlanker::blank() {
SpanScope span(kSpanCursor);
if (blanked_) return;
blanked_ = true;
for (UINT id : kStandardIds) {
HCURSOR blank = CreateBlankCursor();
if (blank) SetSystemCursor(blank, id); // SetSystemCursor takes ownership of 'blank'
}
post(BlankAll);
}

void CursorBlanker::restore() {
void CursorBlanker::restore(std::function<void()> after) {
SpanScope span(kSpanCursor);
if (!blanked_) return;
blanked_ = false;
SystemParametersInfoW(SPI_SETCURSORS, 0, nullptr, 0);
post([after = std::move(after)] { RestoreAll(); if (after) after(); });
}

void CursorBlanker::restoreSync() {
SpanScope span(kSpanCursor);
blanked_ = false;
// Queued behind any pending blank, so the final state is restored. A worker that does not
// answer in 2 s (wedged) must not leave the user without a pointer: restore from here.
if (!runSync(RestoreAll)) RestoreAll();
}
}
29 changes: 28 additions & 1 deletion src/cursor_blanker.h
Original file line number Diff line number Diff line change
@@ -1,16 +1,43 @@
#pragma once
#include <windows.h>
#include <condition_variable>
#include <deque>
#include <functional>
#include <mutex>
#include <thread>
#include <unordered_map>
namespace wind {
// Swaps the system cursor set for blanks while Wind draws its own pointer, and back.
//
// The swaps run on a worker thread, in order (#363). Each one is 14 SetSystemCursor calls or a
// full SPI_SETCURSORS scheme reload from disk: measured 8 ms median and up to 90 ms per zoom-out
// under load, all of it a frozen frame when it ran on the tick thread. blanked() is the caller's
// view (what was last asked for); the worker catches up within a few ms.
class CursorBlanker {
public:
CursorBlanker();
~CursorBlanker();
CursorBlanker(const CursorBlanker&) = delete;
CursorBlanker& operator=(const CursorBlanker&) = delete;
const std::unordered_map<HCURSOR, HCURSOR>& originals() const { return originals_; }
bool blanked() const { return blanked_; }
void blank();
void restore();
// after (optional) runs on the worker once the cursors are back: the repaint nudge, which
// must follow the restore to show the restored shape.
void restore(std::function<void()> after = nullptr);
// Restore and wait for it (up to 2 s). For paths that act on the restored pointer in the same
// breath (the input-panel clip nudge) and for shutdown.
void restoreSync();
private:
void post(std::function<void()> op);
bool runSync(std::function<void()> op);
void run();
std::unordered_map<HCURSOR, HCURSOR> originals_;
bool blanked_ = false;
std::mutex mx_;
std::condition_variable cv_;
std::deque<std::function<void()>> q_;
bool stop_ = false;
std::thread worker_;
};
}
3 changes: 3 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ static void EnsureCompositePulse() {
if (!g_compEvt) return;
HANDLE th = CreateThread(nullptr, 0, [](LPVOID) -> DWORD {
SetThreadDescription(GetCurrentThread(), L"Wind composite pulse");
// HIGHEST like the tick it paces (#363): at normal priority a build starved it and the
// tick waited on a late pulse (pulse-thread-late hitches). It only blocks in DwmFlush.
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
for (;;) {
if (DwmFlush() != S_OK) Sleep(50); // DWM restarting: back off, keep trying
DWM_TIMING_INFO ti{}; ti.cbSize = sizeof(ti);
Expand Down
17 changes: 10 additions & 7 deletions src/transform_model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -400,13 +400,16 @@ void TransformModel::setActive(bool active) {
// a session that never entered the draw branch (cursorVisibility=never, hide-hotkey) still
// has blanked system cursors to give back even though cursorHidden_ never went true.
if (blanker_) {
blanker_->restore();
step(1); // system cursor shapes restored
// Windows repaints the pointer plane only on the next cursor EVENT, so a restored-but-
// still pointer stays invisible until the hand moves (field-verified). A 1px nudge and
// back generates that event invisibly.
POINT np;
if (GetCursorPos(&np)) { SetCursorPos(np.x + 1, np.y); SetCursorPos(np.x, np.y); }
// back generates that event invisibly. It must FOLLOW the restore, which runs on the
// blanker's worker (#363: the scheme reload froze the 1x landing frame for 8-90 ms), so
// the nudge rides along on the worker too.
blanker_->restore([] {
POINT np;
if (GetCursorPos(&np)) { SetCursorPos(np.x + 1, np.y); SetCursorPos(np.x, np.y); }
});
step(1); // system cursor restore queued (it was the whole teardown cost)
}
step(2); // pointer nudge
edgeClipManage(false); // give the clip back before the session winds down
Expand Down Expand Up @@ -945,7 +948,7 @@ void TransformModel::present(const MapResult& r, double level, const Config& cfg
// at zoom-in without cursorHidden_, so a panel open at zoom-in stayed invisible (field).
if (cursorHidden_) { ShowSystemCursorMarshalled(TRUE); cursorHidden_ = false; }
if (blanker_ && blanker_->blanked()) {
blanker_->restore();
blanker_->restoreSync(); // the clip nudge below needs the restored shape in place
// The plane repaints only on the next cursor EVENT, and a same-position SetCursorPos is
// not one; the pointer is pinned by its 1px clip, so nudge the clip a pixel and back
// (review #284). The weld above has already placed the pin this frame.
Expand Down Expand Up @@ -1130,7 +1133,7 @@ bool TransformModel::retarget(const MonitorTarget& m) {
void TransformModel::shutdown() {
teardownMag();
if (sprite_) sprite_->destroy();
if (blanker_) blanker_->restore();
if (blanker_ && blanker_->blanked()) blanker_->restoreSync(); // never exit with blank cursors
pin_.destroy();
mpoGhost_.destroy();
ready_ = false;
Expand Down
4 changes: 2 additions & 2 deletions src/version.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

#define WIND_VER_MAJOR 0
#define WIND_VER_MINOR 23
#define WIND_VER_PATCH 0
#define WIND_VER_PATCH 1

// String form for logs/snapshot/UI. Keep in sync with the numeric parts above.
#define WIND_VERSION_STR "0.23.0"
#define WIND_VERSION_STR "0.23.1"
Loading