diff --git a/src/cursor_decode.cpp b/src/cursor_decode.cpp index 6eda835..6a9db13 100644 --- a/src/cursor_decode.cpp +++ b/src/cursor_decode.cpp @@ -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& 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(&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 @@ -27,12 +39,13 @@ bool DecodeCursorBGRA(HCURSOR hc, std::vector& 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 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) { @@ -44,6 +57,16 @@ bool DecodeCursorBGRA(HCURSOR hc, std::vector& 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 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; diff --git a/src/cursor_tint_pixels.h b/src/cursor_tint_pixels.h index 50f1c2a..0e20e3c 100644 --- a/src/cursor_tint_pixels.h +++ b/src/cursor_tint_pixels.h @@ -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 diff --git a/src/engine_pick.h b/src/engine_pick.h index 24e1859..7827338 100644 --- a/src/engine_pick.h +++ b/src/engine_pick.h @@ -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. diff --git a/src/render_engine.cpp b/src/render_engine.cpp index a759704..a2aeb10 100644 --- a/src/render_engine.cpp +++ b/src/render_engine.cpp @@ -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; @@ -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; diff --git a/src/version.h b/src/version.h index e2f8673..85a3b6b 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 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" diff --git a/tests/test_cursor_tint.cpp b/tests/test_cursor_tint.cpp index 9b63ed7..d073dba 100644 --- a/tests/test_cursor_tint.cpp +++ b/tests/test_cursor_tint.cpp @@ -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