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
35 changes: 29 additions & 6 deletions src/cursor_decode.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
#include "cursor_decode.h"
#include "cursor_tint_pixels.h"
namespace wind {

// 1bpp mask rows, top-down, DWORD-aligned. `rows` is the number of rows to read.
static bool ReadMask1bpp(HDC hdc, HBITMAP bm, int w, int rows, std::vector<uint8_t>& out, int& stride) {
struct { BITMAPINFOHEADER h; RGBQUAD pal[2]; } bi{};
bi.h.biSize = sizeof(BITMAPINFOHEADER);
bi.h.biWidth = w; bi.h.biHeight = -rows; bi.h.biPlanes = 1; bi.h.biBitCount = 1;
bi.h.biCompression = BI_RGB;
stride = ((w + 31) / 32) * 4;
out.assign((size_t)stride * rows, 0);
return GetDIBits(hdc, bm, 0, rows, out.data(), reinterpret_cast<BITMAPINFO*>(&bi), DIB_RGB_COLORS) == rows;
}

// Decode an HCURSOR into top-down 32bpp BGRA (matches B8G8R8A8_UNORM memory order).
// Handles color cursors with per-pixel alpha (arrow, hand) and invert-style cursors with no
// alpha (e.g. the I-beam, which inverts the pixels beneath it). For invert cursors, isInvert
Expand All @@ -27,12 +39,13 @@ bool DecodeCursorBGRA(HCURSOR hc, std::vector<uint32_t>& out,
bool anyAlpha = false;
for (uint32_t px : out) if (px & 0xFF000000u) { anyAlpha = true; break; }
if (!anyAlpha) {
// No alpha channel -> an invert/XOR cursor (the I-beam). Mark the glyph (any
// non-black color) white and the rest black; the invert blend turns white into
// "invert the background" and black into "leave it", so it shows on any color.
isInvert = true;
for (size_t i = 0; i < out.size(); ++i)
out[i] = (out[i] & 0x00FFFFFFu) ? 0xFFFFFFFFu : 0x00000000u;
// An old-style colour cursor: no alpha channel, its AND mask says which pixels are
// opaque (the same rule CursorTint uses). Drawn as plain colour, not inverted.
std::vector<uint8_t> andBits; int mStride = 0;
if (ii.hbmMask && ReadMask1bpp(hdc, ii.hbmMask, w, h, andBits, mStride))
AlphaFromMask(out.data(), w, h, andBits.data(), mStride);
else
for (uint32_t& px : out) px |= 0xFF000000u; // no mask: opaque beats invisible
}
ok = true;
} else if (ii.hbmMask) {
Expand All @@ -44,6 +57,16 @@ bool DecodeCursorBGRA(HCURSOR hc, std::vector<uint32_t>& out,
bi.bmiHeader.biPlanes = 1; bi.bmiHeader.biBitCount = 32; bi.bmiHeader.biCompression = BI_RGB;
GetDIBits(hdc, ii.hbmMask, 0, bm.bmHeight, both.data(), &bi, DIB_RGB_COLORS);
out.assign((size_t)w * h, 0);
// A pure-invert monochrome pointer (the classic I-beam) goes through the invert blend.
std::vector<uint8_t> bits; int mStride = 0;
if (ReadMask1bpp(hdc, ii.hbmMask, w, bm.bmHeight, bits, mStride) &&
MonoIsPureInvert(bits.data(), bits.data() + (size_t)mStride * h, mStride, w, h)) {
isInvert = true;
for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x)
out[(size_t)y * w + x] = MaskBit(bits.data() + (size_t)mStride * h, mStride, x, y)
? 0xFFFFFFFFu : 0x00000000u;
}
if (!isInvert)
for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) {
uint32_t andPx = both[(size_t)y * w + x] & 0xFFFFFFu;
uint32_t xorPx = both[(size_t)(y + h) * w + x] & 0xFFFFFFu;
Expand Down
13 changes: 13 additions & 0 deletions src/cursor_tint_pixels.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ inline void AlphaFromMask(uint32_t* px, int w, int h, const uint8_t* andBits, in
}
}

// True when a monochrome pointer (AND + XOR masks) has at least one "invert the screen" pixel
// (AND 1, XOR 1) and no opaque black/white pixel (AND 0). Such a pointer (the classic text beam)
// is drawn with the invert blend instead of as opaque pixels.
inline bool MonoIsPureInvert(const uint8_t* andBits, const uint8_t* xorBits, int stride, int w, int h) {
bool anyInvert = false;
for (int y = 0; y < h; ++y)
for (int x = 0; x < w; ++x) {
if (!MaskBit(andBits, stride, x, y)) return false;
if (MaskBit(xorBits, stride, x, y)) anyInvert = true;
}
return anyInvert;
}

// A monochrome pointer (AND + XOR masks) as a colour one:
// AND 0 XOR 0 -> black, AND 0 XOR 1 -> white, AND 1 XOR 0 -> transparent,
// AND 1 XOR 1 -> "invert the screen": drawn white, and every transparent pixel touching one
Expand Down
5 changes: 4 additions & 1 deletion src/engine_pick.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ inline bool ShouldPickTransform(const EnginePickInputs& in) {
if (in.renderLost || in.captureProtected || in.renderExcluded || in.rotatedOutput) return true;
// 2. An explicit user preference for this window category. Transform is still refused off the
// primary monitor (no cross-adapter transform chase) and on an excluded exe, because those
// are correctness limits rather than taste.
// are correctness limits rather than taste. The churny list and the input-transform check
// are deliberately NOT applied here: they only keep the AUTO pick conservative, and an
// explicit per-category Transform choice is the user accepting that trade (review
// 2026-10-09 #44).
if (in.pref == EnginePref::Render) return false;
if (in.pref == EnginePref::Transform) return in.primaryMonitor && !in.excluded;
// 3. Auto: the historical behaviour, unchanged.
Expand Down
16 changes: 10 additions & 6 deletions src/render_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,12 @@ void RenderEngine::State::refreshSdrWhite() {
// Recreate the duplication interface (after ACCESS_LOST or first use).
bool RenderEngine::State::recreateDupl() {
dupl.Reset();
// Capture the target monitor's output (matched by device name), falling back to the first
// output for the legacy single-monitor path (empty targetDevice) or any name mismatch.
IDXGIOutput* output = selectOutput(targetDevice, /*fallbackToFirst=*/true);
if (!output) return false;
// Capture the target monitor's output (matched by device name). Only the legacy
// single-monitor path (empty targetDevice) falls back to the first output: a NAMED monitor
// that is not on our adapter (multi-GPU) fails, as retarget() does, rather than silently
// capturing another monitor's pixels.
IDXGIOutput* output = selectOutput(targetDevice, /*fallbackToFirst=*/targetDevice[0] == 0);
if (!output) { RLog("recreateDupl: no output for targetDevice=%ls on our adapter", targetDevice); return false; }
RLog("recreateDupl: targetDevice=%ls", targetDevice[0] ? targetDevice : L"(first)");
// Diagnostics: the output's color space + bit depth (HDR detection).
IDXGIOutput6* output6 = nullptr;
Expand Down Expand Up @@ -1200,8 +1202,10 @@ void RenderEngine::State::render(const RenderFrameParams& p) {
// Inspect mode while zoomed: draw the 48x48 thin full-length crosshair sprite (centered) in
// place of the captured cursor. Otherwise draw the captured cursor.
bool useCross = p.cursorLocked && crosshairSRV;
double cw = useCross ? 46.0 : curW, ch = useCross ? 46.0 : curH;
double chx = useCross ? 23.0 : hotX, chy = useCross ? 23.0 : hotY;
// The crosshair texture is 48x48 with its centre between texels 23 and 24: draw all 48
// texels (a 46 px quad squeezed them, ~0.48*scale px off) and put the hotspot at 23.5.
double cw = useCross ? 48.0 : curW, ch = useCross ? 48.0 : curH;
double chx = useCross ? 23.5 : hotX, chy = useCross ? 23.5 : hotY;
double drawW = cw * scale, drawH = ch * scale;
double tlX = p.cursorScreenX - chx * scale; // top-left so the hotspot lands at cursorScreen
double tlY = p.cursorScreenY - chy * scale;
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 24
#define WIND_VER_PATCH 10
#define WIND_VER_PATCH 11

// String form for logs/snapshot/UI. Keep in sync with the numeric parts above.
#define WIND_VERSION_STR "0.24.10"
#define WIND_VERSION_STR "0.24.11"
9 changes: 9 additions & 0 deletions tests/test_cursor_tint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ TEST_CASE("a monochrome pointer becomes colour: black, white, transparent, inver
CHECK(out[2] == 0xFF000000u); // transparent, but next to the inverting pixel: outline
CHECK(out[3] == 0xFFFFFFFFu); // inverting pixel drawn white
}
TEST_CASE("only a pointer of invert and transparent pixels uses the invert blend") {
const uint8_t andAll[4] = { 0xF0, 0, 0, 0 }; // 4x1, every AND bit 1
const uint8_t xorOne[4] = { 0x40, 0, 0, 0 }; // one XOR bit set
const uint8_t xorNone[4] = { 0, 0, 0, 0 };
CHECK(MonoIsPureInvert(andAll, xorOne, 4, 4, 1));
CHECK_FALSE(MonoIsPureInvert(andAll, xorNone, 4, 4, 1)); // nothing to invert at all
const uint8_t andOpaque[4] = { 0x70, 0, 0, 0 }; // x=3 is opaque
CHECK_FALSE(MonoIsPureInvert(andOpaque, xorOne, 4, 4, 1));
}
TEST_CASE("the outline only hugs inverting pixels; plain transparency stays clear") {
// 5x1: [invert][transparent][transparent][transparent][transparent]
const uint8_t andBits[4] = { 0xF8, 0, 0, 0 }; // all 1
Expand Down
Loading