Threading Model
Threading Model
Section titled “Threading Model”Engine thread
Section titled “Engine thread”winpane spawns a dedicated thread when Context::new() is called. This thread:
- Creates a message-only control window (not visible, used only for
PostMessageWwakeups) - Enters a
GetMessageWloop that processes Win32 messages and drains pending commands - Owns all HWNDs, GPU devices, swap chains, and renderers
- Runs until
Contextis dropped, which sends aShutdowncommand and joins the thread
No Win32 window or GPU resource is ever accessed from the consumer thread.
Command channel
Section titled “Command channel”The consumer communicates with the engine via an mpsc::Sender<Command>. Commands are fire-and-forget for most operations (set element, show, hide, reposition). The exception is surface creation, which uses a oneshot reply channel so the consumer can block until the engine returns a SurfaceId.
After sending a command, the consumer calls PostMessageW on the control window to wake the engine’s GetMessageW loop. Without this wake, commands would only be processed when the next Win32 message arrives (mouse move, timer, etc.).
Event delivery
Section titled “Event delivery”Events flow from the engine to the consumer through a separate mpsc::Receiver<Event>. The consumer polls with Context::poll_event(), which calls try_recv(). There are no callbacks, no async streams, and no blocking waits. The consumer drives their own loop and checks for events at whatever frequency they want (typically every 16ms for 60fps responsiveness).
Thread-local queues
Section titled “Thread-local queues”Win32 window procedures run on the engine thread but execute synchronously inside DispatchMessageW. They cannot directly modify engine state mid-dispatch. Instead, they write to thread-local queues:
PENDING_DPI_CHANGES-WM_DPICHANGEDevents queued asDpiChangeEventPENDING_TRAY_EVENTS- Tray icon notifications (clicks, menu selections)PENDING_FADE_COMPLETIONS- Timer-based fade animation completions
After each GetMessageW / DispatchMessageW cycle, the engine drains all three queues and processes the events (resize swap chains, emit user events, finalize animations).
Shutdown sequence
Section titled “Shutdown sequence”Context::dropsendsCommand::Shutdown- Engine receives the command, exits the message loop
- All surfaces are destroyed (windows closed, GPU resources freed)
- The engine thread returns;
Context::dropjoins it
If the consumer thread panics or exits without dropping Context, the engine thread detects the broken channel and shuts down on its own.
Thread safety
Section titled “Thread safety”Context is Send but the surface handles (Hud, Panel, Pip, Tray) are also Send because they only hold an mpsc::Sender clone and a control HWND (wrapped in a Send-safe newtype). You can move surface handles to other threads. All operations are serialized through the command channel.