This page describes the rendering architecture shared across Windows Terminal and conhost: the IRenderEngine interface, the Renderer orchestrator, and the set of concrete engine implementations. It covers how a frame is constructed and driven to the screen.
For the DirectX-based Atlas engine used in Windows Terminal, see Atlas Engine. For the VT sequence output renderers used with ConPTY, see VT Output Renderer. For the Renderer's role as driven by the terminal control layer, see Terminal Control Architecture.
The rendering system is built around a single interface (IRenderEngine) and a central coordinator (Renderer). The Renderer runs a dedicated background thread, acquires the console data lock on each frame, walks the TextBuffer, and dispatches paint calls to one or more registered IRenderEngine implementations. After the frame is composed, Present() is called outside the lock to avoid holding it during GPU work.
Rendering Architecture
Sources: src/renderer/inc/IRenderEngine.hpp src/renderer/base/renderer.hpp src/renderer/atlas/AtlasEngine.h src/renderer/gdi/gdirenderer.hpp src/renderer/uia/UiaRenderer.hpp src/renderer/wddmcon/WddmConRenderer.hpp src/interactivity/onecore/BgfxEngine.hpp
IRenderEngine is a pure virtual interface defined in src/renderer/inc/IRenderEngine.hpp Every rendering backend implements it. Methods are grouped into the following categories:
| Category | Methods |
|---|---|
| Frame lifecycle | StartPaint, EndPaint, Present, ScrollFrame, WaitUntilCanRender, RequiresContinuousRedraw |
| Invalidation | Invalidate, InvalidateCursor, InvalidateSystem, InvalidateSelection, InvalidateHighlight, InvalidateScroll, InvalidateAll, InvalidateTitle |
| Paint | PaintBackground, PaintBufferLine, PaintBufferGridLines, PaintImageSlice, PaintSelection, PaintCursor |
| Settings | UpdateDrawingBrushes, UpdateFont, UpdateSoftFont, UpdateDpi, UpdateViewport, UpdateTitle, UpdateHyperlinkHoveredId |
| Preparation | PrepareRenderInfo, PrepareLineTransform, ResetLineTransform |
| Query | GetProposedFont, GetDirtyArea, GetFontSize, IsGlyphWideByFont |
| Notification | NotifyNewText |
HRESULT. S_OK means success; S_FALSE from StartPaint means nothing needs painting and the engine should be skipped for this frame.Present() is explicitly separated from EndPaint() so that GPU presentation can happen outside the console data lock.WaitUntilCanRender() is called before each frame to honor the DXGI frame-latency waitable object, preventing the CPU from running too far ahead of the GPU.RequiresContinuousRedraw() returning true causes the render thread to immediately queue another frame (used by custom HLSL shaders that read a time variable).Sources: src/renderer/inc/IRenderEngine.hpp56-120
Renderer in src/renderer/base/renderer.hpp and src/renderer/base/renderer.cpp is the sole orchestrator. It holds a list of IRenderEngine* via _engines (til::small_vector<IRenderEngine*, 2>) and an IRenderData* (_pData) for reading console state.
EnablePainting() launches a dedicated render thread (s_renderThread / _renderThread). The thread loop is:
wait for _enable event
loop:
_waitUntilCanRender() ← WaitUntilCanRender() on each engine
_waitUntilTimerOrRedraw() ← WaitOnAddress on _redraw, with timer deadline
if !_threadKeepRunning: break
PaintFrame()
NotifyPaintFrame() sets the _redraw atomic and calls til::atomic_notify_one(_redraw) to wake the thread. It is called from TriggerRedraw, TriggerSelection, TriggerScroll, etc.
TriggerTeardown() sets _threadKeepRunning = false, notifies the thread, and waits for it to exit.
Sources: src/renderer/base/renderer.cpp55-137 src/renderer/base/renderer.hpp100-151
The Renderer includes a general-purpose timer facility used internally for cursor blinking and rendition (text-blink) cycling:
| Timer | Field | Purpose |
|---|---|---|
_cursorBlinker | TimerHandle | Toggles _cursorBlinkerOn to show/hide cursor |
_renditionBlinker | TimerHandle | Calls ToggleBlinkRendition() + TriggerRedrawAll() |
RegisterTimer, StartTimer, StartRepeatingTimer, and StopTimer are public so that callers can register their own timer callbacks. Time is tracked with QueryUnbiasedInterruptTime in 100-nanosecond units.
Sources: src/renderer/base/renderer.cpp147-320 src/renderer/base/renderer.hpp27-31
AddRenderEngine(pEngine) and RemoveRenderEngine(pEngine) manage the _engines list. It is valid to have multiple engines registered simultaneously. The primary use case is registering both a visual engine (e.g., AtlasEngine) and a UiaEngine side-by-side so that UI Automation events are fired for every frame.
Sources: src/renderer/base/renderer.hpp69-70 src/renderer/base/renderer.cpp406-416
Per-frame paint sequence
Sources: src/renderer/base/renderer.cpp328-479
Before a frame is requested, the console state machine calls Trigger* methods on Renderer, which forward to the engines' Invalidate* methods:
Renderer method | Engine method(s) called |
|---|---|
TriggerRedraw(region) | engine->Invalidate(&srUpdateRegion) |
TriggerRedrawAll() | engine->InvalidateAll() |
TriggerSelection() | engine->InvalidateSelection(old), engine->InvalidateSelection(new) |
TriggerSearchHighlight(old) | engine->InvalidateHighlight(old, buffer), engine->InvalidateHighlight(new, buffer) |
TriggerScroll(delta) | engine->InvalidateScroll(delta) |
TriggerTitleChange() | engine->InvalidateTitle(newTitle) |
TriggerSystemRedraw(prcDirty) | engine->InvalidateSystem(prcDirty) |
TriggerFontChange(dpi, ...) | engine->UpdateDpi(dpi), engine->UpdateFont(...) |
Each of these also calls NotifyPaintFrame() to wake the render thread.
Sources: src/renderer/base/renderer.cpp580-912
Engine class hierarchy and file locations
Sources: src/renderer/inc/IRenderEngine.hpp src/renderer/atlas/AtlasEngine.h src/renderer/gdi/gdirenderer.hpp src/renderer/uia/UiaRenderer.hpp src/renderer/wddmcon/WddmConRenderer.hpp src/interactivity/onecore/BgfxEngine.hpp
| Class | Location | Used by | Notes |
|---|---|---|---|
AtlasEngine | src/renderer/atlas/ | Windows Terminal | DirectX 11 + Direct2D, primary visual renderer. See Atlas Engine |
GdiEngine | src/renderer/gdi/ | conhost (Win32 window) | GDI double-buffered, uses PolyTextOutW batching |
UiaEngine | src/renderer/uia/ | TermControl, conhost | Fires UI Automation events; no visual output |
WddmConEngine | src/renderer/wddmcon/ | conhost (WDDM session) | Writes to WDDM console display via WDDMCon* API |
BgfxEngine | src/interactivity/onecore/ | OneCore conhost | Writes character cells to a shared memory buffer via ConIoSrv |
XtermEngine / Xterm256Engine | src/renderer/vt/ | ConPTY | Emits VT escape sequences; see VT Output Renderer |
GdiEngine in src/renderer/gdi/gdirenderer.hpp uses a double-buffered GDI approach:
StartPaint() calls _PrepareMemoryBitmap() to size the in-memory HDC / HBITMAP, then opens the window DC.POLYTEXTW structs (_pPolyText, flushed via _FlushBufferLines()).EndPaint() BitBlts the memory surface to the real window and releases the DC.Present() returns S_FALSE (GDI does all work within StartPaint/EndPaint).SetWorldTransform).Sources: src/renderer/gdi/paint.cpp28-261 src/renderer/gdi/state.cpp21-67
UiaEngine in src/renderer/uia/UiaRenderer.hpp does not draw pixels. It tracks changes during the paint cycle (_textBufferChanged, _selectionChanged, _cursorChanged) and fires UI Automation events via IUiaEventDispatcher in EndPaint(). It can be Enable()d or Disable()d independently so that only the focused control fires events.
Sources: src/renderer/uia/UiaRenderer.cpp1-60
Both of these engines serve non-interactive or embedded console scenarios:
WddmConEngine: Used when conhost runs under a WDDM graphics session. It allocates display state arrays (CD_IO_CHARACTER) and writes character cells via WDDMConWriteOutput. Present() returns S_FALSE.BgfxEngine: Used on OneCore systems. It writes character data into a shared memory region managed by ConIoSrvComm and calls RequestUpdateDisplay() in EndPaint().Sources: src/renderer/wddmcon/WddmConRenderer.cpp23-95 src/interactivity/onecore/BgfxEngine.cpp23-95
PaintFrame() wraps _PaintFrame() in a retry loop:
maxRetriesForRenderEngine (5) retries with exponential backoff (100, 200, 400, 800, 1600 ms)._disablePainting() is called and the registered _pfnRendererEnteredErrorState callback is fired.AtlasEngine, device-removed conditions (DXGI_ERROR_DEVICE_REMOVED, DXGI_ERROR_DEVICE_RESET, D2DERR_RECREATE_TARGET) return E_PENDING, which triggers the retry and causes the DirectX device to be recreated.Sources: src/renderer/base/renderer.cpp328-368 src/renderer/atlas/AtlasEngine.r.cpp32-81
Refresh this wiki
This wiki was recently refreshed. Please wait 4 days to refresh again.