Repository navigation
Draw hot-reloaded code the moment the delta lands - #21
Merged
Merged
Conversation
An idle CuiSharp window sleeps inside the OS message loop, so a delta applied by dotnet watch was live in the process and invisible: the window kept presenting the frame it drew before the edit until the user moved the mouse. The runtime does report the reload - a type named by [assembly: MetadataUpdateHandler] is called ClearCache before the delta and UpdateApplication once it is live - so Hosting/HotReloadWake registers for it and wakes the windows the loop already recovered with a posted WM_NULL, which the pinned backend's MsgWaitForMultipleObjectsEx is already waiting for. The callback runs on the delta applier's thread, so it touches no cui_ctx and never blocks. No polling: a window that is neither animating nor reloaded still draws nothing. A backend that cannot be woken from another thread keeps the cheap fallback of a 500 ms idle cap under dotnet watch, and docs/PLAN.md records the upstream wake API that would retire it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The symptom
An idle CuiSharp window did not show a hot reload. Under
dotnet watch, an edit applied and thedelta went live in the process —
🔥 C# and Razor changes applied in ~2s— but the window keptpresenting the frame it had drawn before the edit until the user clicked or moved the mouse over
it. It looked like hot reload was broken; only the redraw was missing.
Root cause
cui_app_framesleeps incui__plat_waitwhen nothing is animating (cui_needs_redrawfalse, noWantsFrames, no work in flight), and on Windows that isMsgWaitForMultipleObjectsEx(…, INFINITE, QS_KEY | QS_MOUSE | QS_POSTMESSAGE | QS_TIMER | …). Adelta rewrites method bodies in memory; nothing about it is a message, a timer or input, so a
sleeping loop never learns it should draw again.
The runtime does report the reload. A type named by
[assembly: MetadataUpdateHandler]is calledClearCachebefore the delta is applied andUpdateApplicationonce it is live — that is thenotification surface desktop .NET has, and the
MetadataUpdatertype itself only applies deltas(
ApplyUpdate,IsSupported) and raises no event.The fix
src/CuiSharp/Hosting/HotReloadWake.csregisters for that notification and wakes the loop the waythe rest of the framework already does — a posted message:
ClearCachedoes nothing on purpose: an immediate-mode UI has nothing to invalidate, the nextframe rebuilds the tree in call order from the app's own fields.
UpdateApplicationruns on the thread that applied the delta, so it may touch nocui_ctxandmust not block. It posts
WM_NULLto every window the loop recovered, which is exactly what thebackend's wait is listening for: the wait returns, the queue drains, the next frame draws the
reloaded code.
CuiAppattaches the window on creation (RunandCreate) and detaches it on teardown, so alate reload cannot post to a destroyed handle.
Nothing polls, so an idle window still draws nothing and costs no CPU. The previous 500 ms idle cap
survives only as a fallback for a backend that cannot be woken from another thread, armed by
DOTNET_WATCHthere and nowhere else on Windows; PLAN.md records the upstream wakeAPI that would retire even that.
Evidence
dotnet build -warnaserror,dotnet format --verify-no-changesdotnet testdotnet watch, real edit, no input at all--fps 900tools/regen.ps1 -NoNative -NoDiff,tools/api-surface.ps1tools/verify-template.ps1dotnet cui, file-based app)The new test drives a real
CuiApp.Runwindow on its own thread, hidden, and asserts on its framecounter with no input ever sent: it must not drift while idle, and must produce a frame after
UpdateApplication. Three separate mutations confirm it can fail — droppingAttach, makingUpdateApplicationa no-op, and making the end-of-frame cap unconditional each failed itindependently.
What this does not prove
(
HeartbeatArmedFor(watch, windows)) is covered as a table; the cap itself was last exercised onWindows, which now ignores it.
Detachhas no direct test. Asserting "no post to a dead handle" needs process-global statethat the suite's other window tests share, which is how the first version of this change failed
intermittently in the full suite — it posted to whichever window attached last. It now posts to
every window the process owns.
rebuilt on the same tree). The cause is not attributed yet. The size profile already sets
MetadataUpdaterSupport=falsefor that build, so the handler should be unreachable there; whetherthe delta is the
MetadataUpdateHandlerattribute's metadata or unrelated jitter needs a map-filediff to say, and I would rather leave it unexplained than guess in a commit message.
Files
src/CuiSharp/Hosting/HotReloadWake.cssrc/CuiSharp/CuiApp.cssrc/CuiSharp/Interop/Win32.csWmNullsrc/CuiSharp.Tests/HotReloadWakeTests.csdocs/ARCHITECTURE.mdcui_ctxdocs/PLAN.mdCHANGELOG.md,skills/cui-dotnet/SKILL.md