Skip to content

Fix Windows canvas lockup: remove SetWindowPos from blit()#628

Draft
Copilot wants to merge 4 commits intomasterfrom
copilot/fix-canvas-lockup-issue
Draft

Fix Windows canvas lockup: remove SetWindowPos from blit()#628
Copilot wants to merge 4 commits intomasterfrom
copilot/fix-canvas-lockup-issue

Conversation

Copy link
Contributor

Copilot AI commented Mar 17, 2026

Random canvas lockup during editing on Windows, caused by SetWindowPos(HWND_TOP,…) being called inside blit() on every WM_PAINT.

Root cause — SetWindowPos(HWND_TOP,…) on every blit()

blit() called SetWindowPos(HWND_TOP,…) at the end of every WM_PAINT. This fires WM_WINDOWPOSCHANGED which can re-invalidate the window, producing a WM_PAINTblit()SetWindowPosWM_PAINT cascade. With screen-sharing hooks (e.g. Zoom) each SetWindowPos call can be arbitrarily slow, keeping the JS thread trapped in the Windows message pump indefinitely — preventing it from returning to the libuv event loop to service pending work.

This is also why ~WindowInformation clears the lockup: zeroing GWLP_USERDATA and sending a synthetic WM_PAINT breaks the cascade by routing subsequent paint messages to ValidateRect instead of blit().

Removed SetWindowPos from blit(). The child HWND is already correctly positioned and z-ordered at creation time in WindowInformation().

Also

  • ValidateRect else-branch: added ValidateRect call with a comment clarifying that GWLP_USERDATA == 0 means WindowInformation has already been torn down, preventing an infinite WM_PAINT loop after destruction.

📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.


This change is Reviewable

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced window rendering to suppress unnecessary paint operations and prevent cascading window events, improving compatibility with screen-sharing functionality.

…_PAINT handler

- RESTService/addon.cc: Make destroyFrame synchronous on Windows/macOS like renderFrame.
  The child window is created on the JS thread (via synchronous renderFrame), so
  destroyFrame must also run on the JS thread to avoid cross-thread SendMessageA
  calls in ~WindowInformation() that block the minsky thread while holding
  minskyCmdMutex, causing a deadlock when the JS thread tries to acquire minskyCmdMutex.

- model/windowInformation.cc: Add ValidateRect in windowProc WM_PAINT handler when
  GWLP_USERDATA is 0 to prevent infinite WM_PAINT loop when window is being destroyed.

- model/windowInformation.cc: Add SWP_NOACTIVATE flag to SetWindowPos in blit() to
  prevent unnecessary window activation messages on every paint, reducing message
  cascade issues with screen sharing tools like Zoom.

Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix random lockup of canvas during editing on Windows Fix Windows canvas deadlock caused by cross-thread SendMessageA in destroyFrame Mar 17, 2026
…SetWindowPos from blit

- RESTService/addon.cc: drawNativeWindows() now swaps nativeWindowsToRedraw under
  minskyCmdMutex then releases the lock before calling draw(), matching the existing
  macOSXDrawNativeWindows() pattern. Previously holding minskyCmdMutex across the
  entire draw() call could deadlock: GDI/Cairo operations inside draw() can need the
  JS thread's message pump while the JS thread is blocked trying to acquire
  minskyCmdMutex for a synchronous command.

- model/windowInformation.cc: Removed SetWindowPos(HWND_TOP,...) from blit(). Calling
  it on every WM_PAINT fired WM_WINDOWPOSCHANGED messages that could cascade into
  further WM_PAINT messages and triggered screen-sharing hooks (e.g. Zoom) on every
  paint, keeping the JS thread's message pump busy and preventing it from returning
  to the libuv event loop to service minsky thread BlockingCall resolutions.

- RESTService/addon.cc: Corrected the comment on synchronous destroyFrame dispatch:
  the real reason is that Windows requires a window to be destroyed on the same thread
  that created it, not (as previously stated) that minskyCmdMutex blocks windowProc.

- model/windowInformation.cc: Expanded the ValidateRect else-branch comment to
  clarify when GWLP_USERDATA is zero (WindowInformation already torn down).

Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com>
Copilot AI changed the title Fix Windows canvas deadlock caused by cross-thread SendMessageA in destroyFrame Fix Windows canvas deadlock: release minskyCmdMutex before draw(), remove SetWindowPos from blit() Mar 17, 2026
decltype(nativeWindowsToRedraw) windowsToRedraw;
{
const lock_guard<recursive_mutex> lock(minskyCmdMutex);
windowsToRedraw.swap(nativeWindowsToRedraw);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole purpose of this mutex is to prevent the model from changing whilst a view is being redrawn. This change invalidates that whole promise.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted in 718825d. The mutex must be held during draw() to protect the model — releasing it before the draw was incorrect.

// ~WindowInformation() (and its SendMessageA/SendMessageW) from a thread
// that does not own the child HWND, which is undefined behaviour and can
// cause hangs or crashes.
if (command.ends_with(".renderFrame") || command.ends_with(".destroyFrame"))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

destroyFrame does not actually destroy the window, but rather posts a WM_CLOSE on the window. I don;t think this fixes anything.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot respond to comments

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted in 718825d. Since destroyFrame only posts WM_CLOSE (via SendMessageA in ~WindowInformation), making it synchronous on the JS thread doesn't address the deadlock. Only renderFrame remains in the synchronous dispatch list as it was originally.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both issues were addressed in 718825d: the destroyFrame addition was reverted (only renderFrame remains in the synchronous dispatch list), and drawNativeWindows() was restored to hold minskyCmdMutex across draw(). The only net change in this PR is the removal of SetWindowPos from blit() in windowInformation.cc.

…ispatch per review feedback

- Revert drawNativeWindows() back to holding minskyCmdMutex during draw() as
  intended: the mutex protects the model from being modified while a view is
  being redrawn, and releasing it before draw() would break that invariant.

- Revert destroyFrame from the synchronous dispatch list: destroyFrame only
  posts WM_CLOSE to the window and does not fix the underlying deadlock.
  Only renderFrame needs to be dispatched synchronously on the JS thread.

The only change retained in this PR is the removal of SetWindowPos from
blit() in windowInformation.cc, which prevents cascaded WM_WINDOWPOSCHANGED/
WM_PAINT messages that kept the JS thread's message pump busy indefinitely.

Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com>
Copilot AI changed the title Fix Windows canvas deadlock: release minskyCmdMutex before draw(), remove SetWindowPos from blit() Fix Windows canvas lockup: remove SetWindowPos from blit() Mar 17, 2026
@highperformancecoder
Copy link
Owner

@CodeRabbit review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 17, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 17, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9ba9d56e-057e-4736-a587-4dcaeeaedb44

📥 Commits

Reviewing files that changed from the base of the PR and between 1315904 and 718825d.

📒 Files selected for processing (1)
  • model/windowInformation.cc

📝 Walkthrough

Walkthrough

Modified model/windowInformation.cc to remove a SetWindowPos call from the Windows blit path and add a fallback in the WM_PAINT handler that calls ValidateRect when WindowInformation is torn down, preventing paint message cascades and suppressing messages for windows without backing buffers.

Changes

Cohort / File(s) Summary
Windows Message Handling
model/windowInformation.cc
Removed SetWindowPos from blit path (documented rationale for avoiding WM_WINDOWPOSCHANGED cascades and screen-sharing conflicts); added fallback in WM_PAINT handler to suppress further paint messages when WindowInformation lacks valid backing data via ValidateRect.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A window's dance, once wild and free,
Now paints with grace, no cascade spree,
Old SetWindowPos we bid adieu,
ValidateRect keeps buffers true—
Hopping forward, clean and lean!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: removing SetWindowPos from the blit() function to fix a Windows canvas lockup issue.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch copilot/fix-canvas-lockup-issue
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

Migrating from UI to YAML configuration.

Use the @coderabbitai configuration command in a PR comment to get a dump of all your UI settings in YAML format. You can then edit this YAML file and upload it to the root of your repository to configure CodeRabbit programmatically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants