# Domain Pitfalls: Native DWM Window Capture Addon

**Domain:** C++ NAPI addon for Windows DWM capture, integrated into existing MCP screenshot server
**Researched:** 2026-04-12
**Scope:** Pitfalls specific to adding a native C++ Node.js addon using Windows DWM/WGC APIs
**Supersedes:** v1.0 pitfalls (stdout corruption, sharp memory, timer drift, DPI scaling, window staleness) are already addressed in the existing codebase and are not repeated here.

## Critical Pitfalls

Mistakes that cause rewrites, crashes, or ship-blocking issues.

### Pitfall 1: Native Addon Crash Takes Down the MCP Server

**What goes wrong:** A segfault, access violation, or unhandled C++ exception in the native DWM capture addon crashes the entire Node.js process. Because the MCP server runs in-process, a single bad capture kills the server and the agent loses its connection with no recovery path.

**Why it happens:** Native addons share the Node.js process address space. There is no memory isolation. Common crash sources: dereferencing a freed HWND, accessing a released COM object, buffer overrun in pixel copy, stack overflow in recursive error handling. Unlike JavaScript exceptions, native crashes cannot be caught by try/catch.

**Consequences:** The MCP server dies silently. The agent's capture session is lost. The agent must restart the server and has no diagnostic information about what happened. Repeated crashes make the tool unusable.

**Prevention:**
- Wrap every Win32/COM API call in the addon with error checking. Never assume an HRESULT is S_OK.
- Use structured exception handling (`__try/__except`) around the capture hot path to catch access violations and convert them to NAPI error returns instead of process crashes.
- Validate all HWNDs with `IsWindow()` before use. Validate all COM pointers are non-null.
- Consider a child-process isolation architecture: spawn a small capture helper process that communicates via IPC (shared memory or pipe). If the helper crashes, the MCP server survives and can restart it. This adds complexity but is the only way to get true crash isolation for native code.
- At minimum, use `segfault-handler` npm package during development to get stack traces from crashes.
- Set `napi_fatal_exception` handler to log crash details to stderr before the process dies.

**Detection:** Process exits with no JavaScript error. Exit code is non-zero but no unhandledRejection or uncaughtException fired. Windows Event Viewer shows application crash for node.exe.

**Phase:** Must be the foundational architectural decision. Choose between in-process (faster, riskier) vs child-process isolation (safer, more complex) before writing any C++ code. Recommend starting with in-process + extensive defensive coding, with child-process as a fallback plan if stability proves insufficient.

---

### Pitfall 2: COM/WinRT Initialization Per-Thread Requirement

**What goes wrong:** DWM APIs (especially Windows.Graphics.Capture / WGC) require COM to be initialized on the calling thread. If capture runs on a NAPI AsyncWorker thread, COM is not initialized. Calls fail with `CO_E_NOTINITIALIZED` (0x800401F0) or silently return null.

**Why it happens:** Node.js NAPI AsyncWorker executes `Execute()` on a libuv worker thread from the thread pool. These threads have no COM apartment initialization. WinRT APIs (used by the modern Graphics Capture API) require `RoInitialize(RO_INIT_MULTITHREADED)` or `CoInitializeEx(NULL, COINIT_MULTITHREADED)`. Older DWM APIs like `DwmRegisterThumbnail` must be called from the thread that owns the destination window, which is a further constraint.

**Consequences:** Capture silently fails or returns error codes that are misdiagnosed as "DWM not available." Developers waste hours debugging what looks like an API availability issue when it is actually a thread initialization issue.

**Prevention:**
- Call `CoInitializeEx(NULL, COINIT_MULTITHREADED)` at the start of every `AsyncWorker::Execute()` and `CoUninitialize()` at the end. This is safe to call on worker threads.
- For WinRT/WGC APIs, call `RoInitialize(RO_INIT_MULTITHREADED)` similarly.
- If using `DwmRegisterThumbnail`, be aware it requires the calling thread to own the destination HWND -- this means main thread only. Use `napi_call_threadsafe_function` to bounce back to the main thread for these calls.
- Create a dedicated COM-initialized capture thread rather than relying on the libuv thread pool. This gives you full control over thread lifetime and COM apartment state.
- Always check HRESULT return values and translate COM errors to meaningful NAPI error messages.

**Detection:** HRESULT 0x800401F0 in debug output. Capture works when called synchronously from main thread but fails from AsyncWorker.

**Phase:** First phase of C++ implementation. The thread/COM architecture must be decided and validated before any capture logic is written.

---

### Pitfall 3: GDI/DirectX Resource Leaks Cause System-Wide Degradation

**What goes wrong:** Each DWM capture cycle allocates GDI objects (HDC, HBITMAP, HBRUSH) or DirectX resources (ID3D11Texture2D, IDXGISurface). If any code path -- especially error paths -- fails to release these resources, the process leaks system-global GDI handles. Windows has a per-process limit of ~10,000 GDI objects and a system-wide limit. At high capture rates (e.g., 2 captures/second for 60 seconds = 120 cycles), even a single leaked object per cycle exhausts GDI handles within minutes.

**Why it happens:** C++ error handling is easy to get wrong, especially with multiple resources acquired in sequence. If resource B's acquisition fails after resource A was acquired, resource A must still be freed. Early returns, exceptions, and error branches all create leak opportunities. NAPI's callback-based error model makes RAII patterns harder to apply consistently.

**Consequences:** GDI handle exhaustion causes all GDI-based drawing on the system to fail -- including the Windows desktop itself. The user's entire desktop becomes unresponsive or visually corrupted. This is a catastrophic failure that requires killing the node process or rebooting.

**Prevention:**
- Use RAII wrapper classes for every GDI/DirectX resource. Never use raw `CreateCompatibleDC` / `DeleteDC` pairs -- wrap them in a class whose destructor calls `DeleteDC`.
- Implement a `ScopedHDC`, `ScopedBitmap`, `ScopedTexture` pattern. Every resource acquisition must have exactly one matching release in a destructor.
- Use `GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS)` to monitor GDI handle count during development. Log this value periodically. Assert it stays bounded.
- Test with 100+ rapid capture cycles and verify GDI handle count returns to baseline after captures complete.
- In DirectX paths, use `ComPtr<T>` (from `wrl/client.h`) for all COM interface pointers. Never call `Release()` manually.

**Detection:** Task Manager > Details > node.exe > add column "GDI Objects" -- watch during capture sessions. If it climbs monotonically, there is a leak. System-wide symptoms: windows stop painting, desktop goes black, apps fail to render.

**Phase:** Must be enforced from the very first line of C++ code. Establish RAII wrappers before writing any capture logic. Code review every resource acquisition for matching release.

---

### Pitfall 4: Pixel Format Mismatch (BGRA vs RGBA) Produces Color-Swapped Images

**What goes wrong:** Windows DWM, GDI, and DirectX all produce pixel data in BGRA byte order (Blue-Green-Red-Alpha). Node.js image libraries (sharp, and the existing node-screenshots pipeline) expect RGBA byte order. If the addon returns raw pixel data without swapping, all red and blue channels are reversed. Screenshots appear with an alien blue/orange color cast.

**Why it happens:** This is a fundamental platform mismatch. Windows natively uses BGRA (inherited from GDI's COLORREF which is 0x00BBGGRR). Sharp and most web-oriented image libraries use RGBA. The format names look similar enough that developers miss the swap requirement, especially because grayscale and desaturated content looks "almost right."

**Consequences:** All captures have wrong colors. If the addon outputs PNG internally, the PNG encoder must know the correct channel order or the resulting file is corrupt. The bug is obvious in testing but could slip through if only tested with grayscale or desaturated content.

**Prevention:**
- Perform the BGRA-to-RGBA swap in the C++ addon before returning data to JavaScript. This is a trivial `std::swap(pixel[0], pixel[2])` loop but must be done.
- Alternatively, return raw BGRA data plus a format flag, and let the JavaScript layer use `sharp.raw({ width, height, channels: 4 })` with explicit channel reordering. Sharp can handle this: `sharp(buffer, { raw: { width, height, channels: 4 } }).toColourspace('srgb')` -- but verify the byte order it expects.
- Better yet: use DirectX texture copy with a format conversion to `DXGI_FORMAT_R8G8B8A8_UNORM` during the GPU-side copy, avoiding any CPU-side pixel shuffling.
- Write a simple color test: capture a window with a known pure-red element and verify the output has red in the R channel.

**Detection:** Capture a window with a red button or element. If it appears blue in the output, channels are swapped.

**Phase:** Must be validated immediately when the first pixel data comes out of the addon. Add a color-correctness smoke test.

---

## Moderate Pitfalls

### Pitfall 5: DWM Unavailable Under Remote Desktop / Windows Server

**What goes wrong:** DWM composition may be degraded or behave differently under Remote Desktop Protocol (RDP) sessions and on Windows Server. The `DwmIsCompositionEnabled` API returns TRUE on modern Windows even under RDP, but the actual surface sharing and texture capture may not work as expected. Windows Graphics Capture API may not be available on Server SKUs without the Desktop Experience feature.

**Why it happens:** Under RDP, Windows uses a remoting display driver (WDDM or XDDM depending on policy). DWM runs but may not provide the same shared surface access as on a local desktop. Windows Server 2019+ has DWM always-on, but the Graphics Capture API requires additional Windows features. Administrators sometimes disable WDDM via group policy.

**Consequences:** The addon works perfectly on a developer's local desktop but fails in CI/CD environments, cloud VMs, or remote sessions -- exactly the environments where automated agents often run.

**Prevention:**
- Implement robust capability detection at runtime. Do not rely solely on `DwmIsCompositionEnabled`. Attempt a test capture and verify it returns valid pixel data.
- Design a clean fallback path: if DWM capture fails, fall back to the existing monitor-crop approach (already working in v1.0).
- Make the fallback automatic and transparent -- the `CaptureTarget.capture()` interface should not change based on the capture backend.
- Log which capture backend is active (DWM native vs monitor-crop fallback) so agents and developers can diagnose performance differences.
- Test under RDP during development. Windows Sandbox and Hyper-V local VMs are good test environments.

**Detection:** Capture returns black/empty image or errors when running under RDP or in a VM. Works locally but fails in CI.

**Phase:** Implement fallback path in the same phase as the DWM capture. The two backends should be developed together, not sequentially.

---

### Pitfall 6: Node-gyp Build Failures on User Machines

**What goes wrong:** If the addon requires compilation at install time (no prebuilt binaries for the user's platform), `node-gyp` fails because the user lacks Visual C++ Build Tools, Python, or the Windows SDK. This is the same category as v1.0 Pitfall 7 (native dependency installation) but now the project owns the native code directly.

**Why it happens:** node-gyp requires a full C++ toolchain. Most Windows users (especially those installing an MCP plugin via cplugs marketplace) do not have Visual Studio or Build Tools installed. The error messages from node-gyp are notoriously unhelpful.

**Consequences:** Users cannot install the MCP server. The cplugs marketplace experience is broken. Users file issues saying "npm install fails" with 500-line error logs.

**Prevention:**
- Use `prebuildify` + `node-gyp-build` to ship prebuilt binaries inside the npm package. This eliminates the download step entirely and is the current best practice for native addon distribution.
- Target Node-API (NAPI) version 6+ to ensure ABI stability across Node.js versions. One prebuilt binary per platform covers Node 16-22+ without recompilation.
- Build prebuilt binaries in CI (GitHub Actions with `windows-latest`). Ship binaries for `win32-x64` at minimum; consider `win32-arm64` if the user base warrants it.
- Make native DWM capture optional: if the binary fails to load, fall back to the existing monitor-crop capture. The server must still start and function without the native addon.
- Test installation on a clean Windows machine (no build tools) as part of the release checklist.

**Detection:** `npm install` fails with `gyp ERR!` messages. `require('./build/Release/dwm_capture.node')` throws MODULE_NOT_FOUND.

**Phase:** Must be set up in the first phase alongside the C++ build system. Do not defer binary distribution to later -- it is much harder to retrofit.

---

### Pitfall 7: NAPI Thread Safety Violations

**What goes wrong:** Calling NAPI functions (creating JavaScript objects, resolving promises, calling callbacks) from the AsyncWorker's `Execute()` method or from a spawned thread causes undefined behavior -- typically a segfault or data corruption.

**Why it happens:** V8 (Node.js's JavaScript engine) is single-threaded. NAPI functions that interact with V8 must run on the main thread. `AsyncWorker::Execute()` runs on a libuv worker thread where V8 access is forbidden. The `Napi::Env` handle is available on the AsyncWorker but must NOT be used inside `Execute()` -- it is only safe in `OnOK()` and `OnError()`. This is documented but is a common mistake because the Env is accessible as a public member.

**Consequences:** Intermittent segfaults that are extremely difficult to reproduce and debug. Memory corruption that manifests as unrelated errors later. Process crashes with no useful stack trace.

**Prevention:**
- Never touch `Napi::Env`, `Napi::Value`, `Napi::Object`, or any NAPI type inside `AsyncWorker::Execute()`. Only use plain C++ types and Win32 APIs.
- Return results from `Execute()` by storing them in plain C++ member variables (e.g., `std::vector<uint8_t> pixelData_`, `uint32_t width_`, `uint32_t height_`). Convert to NAPI types in `OnOK()`.
- If you need to stream progress or partial results, use `Napi::ThreadSafeFunction` -- this is the only safe way to call back into JavaScript from a non-main thread.
- Enable Thread Sanitizer (TSan) during development builds if your toolchain supports it.
- Code review checklist: search for any `Napi::` usage inside `Execute()` methods -- any instance is a bug.

**Detection:** Intermittent segfaults during capture. Crashes that only happen under load or on certain machines. Use `segfault-handler` for stack traces.

**Phase:** Establish this pattern in the first C++ file written. Template the AsyncWorker correctly once and copy the pattern.

---

### Pitfall 8: DPI Mismatch Between DWM Coordinates and Screen Coordinates

**What goes wrong:** The existing v1.0 code already handles DPI scaling for monitor-crop capture (see `window-utils.ts` line 49: `const scale = monitor.scaleFactor()`). But DWM APIs may return window thumbnails at a different DPI than the logical coordinates suggest. Windows.Graphics.Capture provides frames at the actual pixel resolution of the captured window's rendering surface, which may differ from both logical coordinates and per-monitor DPI.

**Why it happens:** Windows has three coordinate systems in play: logical (96 DPI baseline), per-monitor physical (varies by display), and the application's rendering DPI (which depends on the app's DPI awareness mode). A DPI-unaware app rendered at 150% scaling is actually composited by DWM at a different size than its HWND rect suggests. The DWM capture surface size may not match `GetWindowRect()` dimensions.

**Consequences:** Captured image dimensions do not match expected dimensions. The image may be larger or smaller than the window appears on screen. Grid compilation assumes consistent dimensions and produces misaligned cells.

**Prevention:**
- After capturing via DWM, always use the actual pixel dimensions returned by the capture, not the HWND rect dimensions.
- Return the actual capture dimensions alongside the pixel data from the addon. Let the JavaScript layer resize to the expected dimensions if needed.
- For the DWM addon's return interface: `{ buffer: Buffer, width: number, height: number, dpiScale: number }` -- always include actual dimensions.
- Do not assume the capture resolution matches `win.width() * scale` from the existing code. Validate with actual captures.

**Detection:** Grid cells have mismatched sizes when mixing DWM-captured and monitor-crop-captured frames. Capture dimensions differ from `GetWindowRect` by non-integer factors.

**Phase:** Must be handled when integrating DWM output into the existing CaptureTarget interface. The adapter layer between native output and the existing pipeline is where this gets resolved.

---

### Pitfall 9: Prebuilt Binary ABI Breaks Across Node.js Major Versions

**What goes wrong:** Even with Node-API's ABI stability guarantee, prebuilt binaries can break if they target a NAPI version higher than what the user's Node.js supports, or if they link against a C++ runtime version (MSVC CRT) not present on the user's machine.

**Why it happens:** NAPI version 6 requires Node.js 14.12+. NAPI version 8 requires Node.js 18.17+. If the addon uses NAPI 9 features but the user runs Node.js 18.0, the binary loads but crashes on missing symbols. Additionally, MSVC runtime versions (ucrtbase.dll, vcruntime140.dll) vary across Windows installations. A binary built with VS2022 may need a newer CRT than what ships with Windows 10.

**Consequences:** Binary loads but crashes at runtime with cryptic symbol errors. Or binary fails to load entirely with a DLL-not-found error for the MSVC runtime.

**Prevention:**
- Target NAPI version 6 (widely supported, Node 14.12+). Do not use features from newer NAPI versions unless necessary.
- Build with `/MT` (static CRT linking) to bundle the MSVC runtime into the binary. This eliminates CRT version dependencies. The binary is slightly larger (~200KB) but installs everywhere.
- Specify the NAPI version in `binding.gyp`: `"defines": ["NAPI_VERSION=6"]`.
- Test the prebuilt binary on a clean Windows 10 install (no Visual Studio, no extra runtimes) to verify it loads.
- In `package.json`, set `engines.node` to the minimum Node.js version that supports your NAPI version.

**Detection:** `Error: The specified module could not be found` (DLL dependency missing) or `Error: Module did not self-register` (NAPI version mismatch). Use `dumpbin /dependents dwm_capture.node` to check DLL dependencies.

**Phase:** Configure in the build system setup phase. The `binding.gyp` and CI build matrix must be correct from the start.

---

## Minor Pitfalls

### Pitfall 10: DwmRegisterThumbnail Destination Window Ownership

**What goes wrong:** `DwmRegisterThumbnail` requires the destination window (where the thumbnail renders) to be owned by the calling process. Creating a hidden window for this purpose seems straightforward, but the window must have a message pump running. If the message pump stalls (because Node.js's event loop is busy), DWM stops updating the thumbnail.

**Prevention:**
- If using `DwmRegisterThumbnail`, create the hidden window and message pump on a dedicated thread, not the main thread or a libuv worker.
- Prefer `Windows.Graphics.Capture` (WGC) over `DwmRegisterThumbnail` if targeting Windows 10 1903+. WGC does not require a destination window.
- If WGC is not available, consider DXGI Desktop Duplication as a simpler alternative to `DwmRegisterThumbnail`.

**Phase:** Architecture/API selection phase. This pitfall is avoided entirely by choosing WGC over DwmRegisterThumbnail.

---

### Pitfall 11: Capturing Minimized or Cloaked Windows via DWM

**What goes wrong:** DWM may not maintain an up-to-date surface for minimized windows. The capture returns the last-rendered frame before minimization, or a black/empty surface. UWP apps can be "cloaked" (hidden by the shell) without being minimized, and their DWM surfaces may not be accessible.

**Prevention:**
- Check `DwmGetWindowAttribute` with `DWMWA_CLOAKED` before attempting capture. If cloaked, report the window state rather than returning a stale/black image.
- For minimized windows, either refuse capture with a clear error (consistent with v1.0 behavior in `window-target.ts`) or document that the capture may be stale.
- Test with UWP apps (Calculator, Settings) which have different compositing behavior than Win32 apps.

**Phase:** Window state validation, implemented alongside the capture logic.

---

### Pitfall 12: stdout Pollution from Native Addon

**What goes wrong:** `printf()`, `std::cout`, or `OutputDebugString` (if redirected) in the C++ addon writes to stdout, corrupting the MCP JSON-RPC stream. This is the v1.0 stdout corruption pitfall (#1) re-emerging from the native layer where the JavaScript stdout guard cannot intercept it.

**Prevention:**
- Ban `printf` and `std::cout` in the addon codebase. Use `fprintf(stderr, ...)` for all debug output.
- Define a debug logging macro that writes to stderr: `#define DWM_LOG(fmt, ...) fprintf(stderr, "[dwm-capture] " fmt "\n", ##__VA_ARGS__)`.
- In code review, grep for `printf` and `cout` in all `.cc` / `.cpp` files.
- The C++ code should never write to file descriptor 1 (stdout) under any circumstances.

**Phase:** Enforce from the first line of C++ code. Include in the project's coding standards.

---

## Phase-Specific Warnings

| Phase Topic | Likely Pitfall | Mitigation |
|-------------|---------------|------------|
| Architecture decision | Crash isolation (#1) | Decide in-process vs child-process before coding |
| Build system setup | node-gyp failures (#6), ABI compat (#9) | prebuildify + NAPI 6 + static CRT from day one |
| COM/thread architecture | COM init (#2), thread safety (#7) | Dedicated capture thread with COM init, RAII wrappers |
| First pixel output | Pixel format (#4), DPI mismatch (#8) | Color test + dimension validation smoke tests |
| DWM API selection | DwmRegisterThumbnail complexity (#10) | Prefer WGC over DwmRegisterThumbnail |
| Resource management | GDI/DirectX leaks (#3) | RAII wrappers, GDI handle count monitoring |
| Fallback integration | DWM unavailable (#5) | Auto-fallback to monitor-crop, transparent to caller |
| Window state handling | Minimized/cloaked (#11) | DwmGetWindowAttribute checks before capture |
| All C++ code | stdout pollution (#12) | Ban printf/cout, stderr-only macro |

## Integration Pitfalls with Existing Capture System

The v1.0 codebase uses a clean `CaptureTarget` interface (returns `Promise<Buffer>` of PNG data). The DWM addon must integrate through this interface. Specific integration risks:

1. **Return format mismatch:** The existing `WindowTarget.capture()` returns a PNG-encoded buffer via `image.toPngSync()`. The DWM addon will return raw pixel data that must be PNG-encoded (via sharp) before returning. This encoding step adds latency -- measure it and ensure it does not regress capture interval timing.

2. **Window lookup divergence:** The existing `findWindow()` uses `node-screenshots`'s `Window.all()` which returns library-specific window objects. The DWM addon needs raw HWNDs. These must be kept in sync -- the same window title search must resolve to the same window in both paths.

3. **Fallback decision point:** The auto-detection logic (use DWM when available, fall back to monitor-crop) must be deterministic and fast. Do not attempt DWM capture and fall back on failure for every frame -- probe once at session start and cache the result.

4. **Mixed capture in a session:** If the DWM backend becomes unavailable mid-session (e.g., RDP connects), switching backends mid-capture-sequence may produce frames with different characteristics (size, color profile, timing). Consider completing the session with the original backend rather than switching.

## Sources

- [Node-API Documentation](https://nodejs.org/api/n-api.html) - ABI stability guarantees, version requirements
- [AsyncWorker structural defects - Issue #231](https://github.com/nodejs/node-addon-api/issues/231) - Thread safety issues with AsyncWorker
- [DwmRegisterThumbnail - Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/nf-dwmapi-dwmregisterthumbnail) - Process ownership requirement
- [Windows.Graphics.Capture API - Microsoft Learn](https://learn.microsoft.com/en-us/uwp/api/windows.graphics.capture) - Modern capture API
- [Win32CaptureSample - GitHub](https://github.com/robmikh/Win32CaptureSample) - Reference WGC implementation
- [prebuildify - GitHub](https://github.com/prebuild/prebuildify) - Prebuilt binary distribution
- [prebuild - Node-API Resource](https://nodejs.github.io/node-addon-examples/build-tools/prebuild/) - Build tool documentation
- [Thread-safe functions - Node-API Resource](https://nodejs.github.io/node-addon-examples/special-topics/thread-safe-functions/) - Cross-thread NAPI calls
- [segfault-handler - npm](https://www.npmjs.com/package/segfault-handler) - Native crash diagnostics
- [How not to access V8 from worker threads](https://nodeaddons.com/how-not-to-access-node-js-from-c-worker-threads/) - Thread safety patterns
- [DwmGetDxSharedSurface - Undocumented API](https://undoc.airesoft.co.uk/user32.dll/DwmGetDxSharedSurface.php) - Shared surface access
- [New Ways to do Screen Capture - Windows Developer Blog](https://blogs.windows.com/windowsdeveloper/2019/09/16/new-ways-to-do-screen-capture/) - Microsoft's recommended capture approaches
- [BitBlt function - Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-bitblt) - Color format conversion behavior
