Skip to content

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

Merged
highperformancecoder merged 6 commits intomasterfrom
copilot/fix-canvas-lockup-issue
Mar 19, 2026
Merged

Fix Windows canvas lockup: remove SetWindowPos from blit()#628
highperformancecoder merged 6 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

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 65f6ef34-d3ad-4f41-ac9e-c8d5dd3b2da3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 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 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

You can get early access to new features in CodeRabbit.

Enable the early_access setting to enable early access features such as new models, tools, and more.

@highperformancecoder highperformancecoder marked this pull request as ready for review March 19, 2026 06:59
Copilot AI review requested due to automatic review settings March 19, 2026 06:59
@highperformancecoder highperformancecoder merged commit a2dac78 into master Mar 19, 2026
8 checks passed
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a Windows-specific canvas lockup by preventing a paint → window-position-change → paint cascade, and adds safety handling for post-destruction paint messages.

Changes:

  • Removed SetWindowPos(HWND_TOP, ...) from blit() to stop WM_WINDOWPOSCHANGED being triggered on every WM_PAINT.
  • Added a ValidateRect fallback when GWLP_USERDATA indicates WindowInformation has been torn down.
  • Minor cleanups in tests and value-initialization for schema generation.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/testPubTab.cc Removes unused dynamic_cast result variable in a test loop.
test/testMinsky.cc Removes unused variableCast() result variable in a test.
schema/optional.h Value-initializes a temporary to avoid indeterminate state during XSD generation.
model/windowInformation.cc Removes SetWindowPos from blit() and validates paint rects after teardown to prevent infinite WM_PAINT.
RavelCAPI Updates submodule pointer.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +166 to +171
else
// GWLP_USERDATA is zeroed in ~WindowInformation before the window is
// closed, so reaching here means the WindowInformation has already been
// torn down. Validate the rect so Windows stops generating WM_PAINT
// messages for a window that no longer has a backing buffer.
ValidateRect(hwnd, nullptr);
Comment on lines +85 to +89
// Note: SetWindowPos was previously called here on every blit to keep the
// child window at HWND_TOP, but that fired WM_WINDOWPOSCHANGED on every
// paint which could cascade into further WM_PAINT messages and interact
// badly with screen-sharing hooks (e.g. Zoom). The window is already
// positioned correctly at creation time in WindowInformation().
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.

3 participants