SharpConsoleUI Rendering Pipeline
Overview
SharpConsoleUI uses a sophisticated multi-stage rendering pipeline optimized for console applications. The architecture employs double buffering at two levels (window and screen), dirty tracking at three levels (window, cell, and line), and occlusion culling to minimize unnecessary rendering work.
High-Level Architecture
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
│ • Window creation and updates │
│ • Control invalidation (AddControl, SetTitle, etc.) │
└──────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ ConsoleWindowSystem (Coordinator) │
│ • Event loop (Run) │
│ • Frame limiting and dirty checking │
│ • UpdateDisplay() orchestration │
└──────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Renderer (Multi-Pass) │
│ • Build render list (Z-order, occlusion) │
│ • Three passes: Normal → Active → AlwaysOnTop │
│ • Per-window visible regions calculation │
└──────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Window Content Rendering (DOM-based) │
│ • Measure → Arrange → Paint (layout stages) │
│ • CharacterBuffer (window-level buffer) │
│ • PostBufferPaint hook (compositor effects) │
│ • ANSI serialization with color optimization │
└──────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Console Driver (Screen Buffer) │
│ • ConsoleBuffer (screen-level double buffer) │
│ • ANSI parsing and storage │
│ • Diff-based line rendering │
└──────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Physical Console │
│ • Console.SetCursorPosition() │
│ • Console.Write() with ANSI codes │
└─────────────────────────────────────────────────────────────┘
Key Components
| Component | Responsibility | Location |
|---|---|---|
| ConsoleWindowSystem | Main event loop, rendering coordinator | ConsoleWindowSystem.cs |
| Renderer | Multi-pass rendering, occlusion culling | Renderer.cs |
| VisibleRegions | Rectangle subtraction, overlap detection | VisibleRegions.cs |
| Window | Content rendering, DOM rebuilding | Window.cs |
| CharacterBuffer | Window-level buffer, ANSI serialization | Layout/CharacterBuffer.cs |
| ConsoleBuffer | Screen-level buffer, diff-based rendering | Drivers/ConsoleBuffer.cs |
| NetConsoleDriver | Console abstraction, I/O handling | Drivers/NetConsoleDriver.cs |
1. Entry Points
Main Event Loop: ConsoleWindowSystem.Run()
File: ConsoleWindowSystem.cs:822-975
The primary entry point for application rendering. Runs continuously until the application exits.
public void Run()
{
while (!_shouldQuit)
{
ProcessOnce(); // Single frame processing
// Frame limiting (default: ~60 FPS)
int sleepTime = Math.Max(1, FrameDelayMilliseconds - (int)_stopwatch.ElapsedMilliseconds);
if (sleepTime > 0)
Thread.Sleep(sleepTime);
_stopwatch.Restart();
}
}
Key behaviors:
- Infinite loop until
_shouldQuitflag set - Calls
ProcessOnce()for each frame - Frame rate limiting via
FrameDelayMilliseconds(default: 16ms ≈ 60 FPS) - Precise timing using
Stopwatch
Single Frame Processing: ProcessOnce()
File: ConsoleWindowSystem.cs:977-1002
Handles input, processes events, and conditionally renders.
public void ProcessOnce()
{
ProcessInput(); // Handle keyboard/mouse input
ProcessPendingEvents(); // Execute queued events
// Only render if something changed
if (_needsFullRedraw || _windows.Any(w => w.NeedsRedraw))
{
UpdateDisplay();
_needsFullRedraw = false;
}
}
Optimization: Dirty Checking
- Rendering only occurs when
_needsFullRedrawor any window hasNeedsRedrawflag set - Avoids wasting CPU cycles when UI is static
- Typical frame: ~99% of frames skip rendering in idle applications
Manual Rendering: UpdateDisplay()
File: ConsoleWindowSystem.cs:2522-2766
The central rendering coordinator. Can also be called manually for immediate updates.
2. Core Rendering Coordinator: UpdateDisplay()
File: ConsoleWindowSystem.cs:2522-2766
The heart of the rendering pipeline, orchestrating all rendering phases.
The Five Phases
private void UpdateDisplay()
{
lock (_renderLock)
{
var startTime = DateTime.UtcNow;
// PHASE 1: Atomic desktop clearing
if (_needsDesktopClear)
{
_driver.ClearScreen();
_needsDesktopClear = false;
}
// PHASE 2: Build render list (occlusion culling)
var renderList = BuildRenderList();
// PHASE 3: Render passes (Normal → Active → AlwaysOnTop)
foreach (var pass in new[] { WindowPass.Normal,
WindowPass.Active,
WindowPass.AlwaysOnTop })
{
var windowsInPass = renderList.Where(w => GetWindowPass(w) == pass);
foreach (var window in windowsInPass)
{
_renderer.RenderWindow(window, renderList);
}
}
// PHASE 4: Flush to screen
_driver.Render();
// PHASE 5: Status bar rendering
if (_statusBar != null)
{
RenderStatusBar(DateTime.UtcNow - startTime);
}
}
}
Phase Breakdown
Phase 1: Atomic Desktop Clearing
- Clears entire screen when requested (window resize, theme change)
- Sets
_needsDesktopClearflag on major layout changes - Atomic operation: No partial clearing
Phase 2: Build Render List
File: ConsoleWindowSystem.cs:2634-2670
Determines which windows to render and in what order:
private List<Window> BuildRenderList()
{
return _windows
.Where(w => w.Visible && !w.IsMinimized)
.OrderBy(w => w.ZOrder) // Back-to-front ordering
.ToList();
}
Z-Order management:
- Lower
ZOrder= rendered first (background) - Higher
ZOrder= rendered last (foreground) - Modal windows automatically get highest Z-order
Phase 3: Three-Pass Rendering
| Pass | Windows Rendered | Purpose |
|---|---|---|
| Normal | Standard windows | Base UI layer |
| Active | Focused/Active window | Ensure focus visible |
| AlwaysOnTop | Notifications, tooltips | UI chrome that must be visible |
This ensures correct visual stacking regardless of Z-order values.
Phase 4: Screen Flush
- Calls
_driver.Render()to commit all buffered changes - Single flush per frame: All windows rendered before screen update
- Diff-based rendering minimizes console I/O
Phase 5: Status Bar
- Rendered last, overlays all content
- Shows FPS, dirty characters, memory usage
- Optional, controlled by
ShowPerformanceMetricsproperty
3. Window Rendering Pipeline
Entry: Renderer.RenderWindow()
File: Renderer.cs:211-270
Renders a single window with occlusion culling.
public void RenderWindow(Window window, List<Window> allWindows)
{
// Calculate visible regions (occlusion culling)
var visibleRegions = VisibleRegions.CalculateVisibleRegions(
window.Bounds,
allWindows.Where(w => w.ZOrder > window.ZOrder).Select(w => w.Bounds)
);
if (visibleRegions.Count == 0)
return; // Completely occluded, skip rendering
// Render background fill
foreach (var region in visibleRegions)
{
FillRegion(region, window.BackgroundColor);
}
// Render border (if present)
if (window.HasBorder)
{
RenderBorder(window, visibleRegions);
}
// Render window content
var contentLines = window.RenderAndGetVisibleContent(visibleRegions);
foreach (var line in contentLines)
{
_driver.AddContent(line.X, line.Y, line.Content);
}
}
Visible Regions Calculation
File: VisibleRegions.cs:40-158
The most complex optimization: determining which parts of a window are actually visible.
Algorithm: Rectangle Subtraction
Original Window Bounds:
┌─────────────────┐
│ │
│ │
│ │
└─────────────────┘
Overlapping Window (higher Z-order):
┌─────────┐
│ Overlap │
└─────────┘
Result (visible regions):
┌───────┐ ┌──┐
│ R1 │ │R2│
├───────┴─────────┤ │
│ R3 │ │
└─────────────────┴──┘
Implementation highlights:
- Dual-buffer pooling: Reuses
List<LayoutRect>to avoid allocations - Early exit: Returns empty list if completely occluded
- Iterative subtraction: Each overlapping window subtracts from remaining regions
public static List<LayoutRect> CalculateVisibleRegions(
LayoutRect targetBounds,
IEnumerable<LayoutRect> overlappingBounds)
{
var visible = new List<LayoutRect> { targetBounds };
foreach (var overlap in overlappingBounds)
{
var temp = new List<LayoutRect>();
foreach (var region in visible)
{
if (!region.IntersectsWith(overlap))
{
temp.Add(region); // No overlap, keep as-is
}
else
{
// Subtract overlap, potentially creating 4 new rectangles
temp.AddRange(SubtractRectangle(region, overlap));
}
}
visible = temp;
if (visible.Count == 0)
break; // Completely occluded
}
return visible;
}
Performance impact:
- Worst case: Window completely visible = 1 region
- Best case: Window completely occluded = 0 regions (skip rendering)
- Typical case: 2-5 regions per partially occluded window
4. Window Content Rendering (DOM-Based)
Entry: Window.RenderAndGetVisibleContent()
File: Window.cs:2295-2430
Converts window controls into ANSI-formatted strings for display.
public List<(int X, int Y, string Content)> RenderAndGetVisibleContent(
List<LayoutRect> visibleRegions)
{
lock (_lock)
{
// Rebuild DOM if needed (controls added/removed/invalidated)
if (NeedsRedraw)
{
RebuildContentCacheDOM();
NeedsRedraw = false;
}
// Extract lines from character buffer
var allLines = _contentBuffer.ToLines();
// Clip to visible regions only
return ClipLinesToVisibleRegions(allLines, visibleRegions);
}
}
The Three-Stage Layout Pipeline
File: Window.cs:2500-2803
DOM-based rendering follows the classic layout model:
Stage 1: Measure
File: Window.cs:2550-2600
Each control calculates its desired size given available space.
private void MeasureStage(LayoutRect availableSpace)
{
foreach (var control in _controls)
{
var desiredSize = control.Measure(availableSpace.Width, availableSpace.Height);
control._measuredSize = desiredSize;
}
}
Considerations:
- Text controls measure string lengths (with ANSI stripping)
- Container controls recursively measure children
- Scrollable controls report virtual size vs viewport size
Stage 2: Arrange
File: Window.cs:2602-2670
Assigns final positions and sizes to controls based on layout rules.
private void ArrangeStage(LayoutRect finalBounds)
{
int currentY = finalBounds.Y;
foreach (var control in _controls)
{
var controlBounds = new LayoutRect(
finalBounds.X,
currentY,
finalBounds.Width,
control._measuredSize.Height
);
control.Arrange(controlBounds);
currentY += control._measuredSize.Height;
}
}
Layout strategies:
- Stack layout: Controls stacked vertically (default)
- Column layout: Side-by-side using
ColumnContainer - Absolute positioning: Fixed X/Y coordinates
- Fill: Control expands to fill available space
Stage 3: Paint
File: Window.cs:2672-2803
Controls draw themselves into the window's CharacterBuffer.
private void PaintStage(CharacterBuffer buffer)
{
foreach (var control in _controls)
{
var controlBounds = control.ArrangedBounds;
control.PaintDOM(buffer, controlBounds, _clipRect);
}
}
CharacterBuffer interface:
public interface ICharacterBuffer
{
void SetChar(int x, int y, char c, Color fg, Color bg);
void FillRect(LayoutRect rect, char c, Color fg, Color bg);
void DrawText(int x, int y, string text, Color fg, Color bg);
}
Stage 3.5: PostBufferPaint Hook (Compositor Effects)
File: Windows/WindowRenderer.cs
NEW: After painting controls but before ANSI conversion, the PostBufferPaint event fires, allowing compositor-style buffer manipulation.
private List<string> RebuildContentCacheDOM(...)
{
// Stage 1-3: Measure, Arrange, Paint
RebuildDOMTree();
PerformDOMLayout();
PaintDOM(clipRect, backgroundColor);
// ◄── POST-PAINT HOOK POINT
if (PostBufferPaint != null && _buffer != null)
{
var dirtyRegion = new LayoutRect(0, 0, _buffer.Width, _buffer.Height);
PostBufferPaint.Invoke(_buffer, dirtyRegion, clipRect);
}
// Continue to ANSI serialization
return BufferToLines(foregroundColor, backgroundColor);
}
Purpose: Enables advanced visual effects without modifying the core rendering pipeline:
- Transitions: Fade in/out, slide, wipe effects
- Filters: Blur, desaturate, brightness adjustments
- Overlays: Glow effects, highlights, custom decorations
- Capture: Screenshots, recording via
BufferSnapshot
API:
// Subscribe to event
window.Renderer.PostBufferPaint += (buffer, dirtyRegion, clipRect) =>
{
// Apply custom effects to buffer
for (int y = 0; y < buffer.Height; y++)
{
for (int x = 0; x < buffer.Width; x++)
{
var cell = buffer.GetCell(x, y);
// Modify cell colors, characters, etc.
buffer.SetCell(x, y, cell.Character, modifiedFg, modifiedBg);
}
}
};
Thread Safety: Event fires within existing _renderLock, ensuring safe buffer manipulation.
Performance: Zero overhead when event not subscribed. Only process dirty regions for optimal performance.
See Compositor Effects for comprehensive examples and best practices.
5. Character Buffer System
File: Layout/CharacterBuffer.cs:23-505
Window-level buffer storing character, foreground, and background color for each cell.
Buffer Structure
public class CharacterBuffer
{
private Cell[,] _buffer; // 2D array [row, col]
private int _width;
private int _height;
private struct Cell
{
public char Character;
public Color Foreground;
public Color Background;
public bool Dirty; // Changed since last render
}
}
Memory layout example (3x2 buffer):
Col 0 Col 1 Col 2
┌───────────┐ ┌───────────┐ ┌───────────┐
R0 │ 'H', W, B │ │ 'e', W, B │ │ 'l', W, B │
└───────────┘ └───────────┘ └───────────┘
┌───────────┐ ┌───────────┐ ┌───────────┐
R1 │ 'l', W, B │ │ 'o', W, B │ │ ' ', W, B │
└───────────┘ └───────────┘ └───────────┘
(W = White foreground, B = Black background)
Key Operations
1. SetChar (Individual Cell Update)
File: CharacterBuffer.cs:145-165
public void SetChar(int x, int y, char c, Color fg, Color bg)
{
if (x < 0 || x >= _width || y < 0 || y >= _height)
return; // Out of bounds, silently ignore
ref var cell = ref _buffer[y, x];
// Only mark dirty if actually changed
if (cell.Character != c || cell.Foreground != fg || cell.Background != bg)
{
cell.Character = c;
cell.Foreground = fg;
cell.Background = bg;
cell.Dirty = true;
}
}
Optimization: Dirty tracking
- Only marks cell dirty if value actually changes
- Avoids redundant ANSI generation for unchanged cells
2. FillRect (Bulk Operations)
File: CharacterBuffer.cs:200-235
Used for backgrounds, borders, clearing regions.
public void FillRect(LayoutRect rect, char c, Color fg, Color bg)
{
var clipped = ClipRect(rect);
for (int y = clipped.Y; y < clipped.Bottom; y++)
{
for (int x = clipped.X; x < clipped.Right; x++)
{
SetChar(x, y, c, fg, bg);
}
}
}
3. ToLines (ANSI Serialization)
File: CharacterBuffer.cs:350-505
Converts buffer to ANSI-formatted strings for console output.
public List<string> ToLines()
{
var lines = new List<string>();
for (int y = 0; y < _height; y++)
{
var sb = new StringBuilder();
Color? currentFg = null;
Color? currentBg = null;
for (int x = 0; x < _width; x++)
{
var cell = _buffer[y, x];
// Optimization: Only emit ANSI codes when colors change
if (cell.Foreground != currentFg)
{
sb.Append(AnsiCodes.Foreground(cell.Foreground));
currentFg = cell.Foreground;
}
if (cell.Background != currentBg)
{
sb.Append(AnsiCodes.Background(cell.Background));
currentBg = cell.Background;
}
sb.Append(cell.Character);
}
lines.Add(sb.ToString());
}
return lines;
}
ANSI Optimization: Color Runs
- Tracks current foreground/background colors
- Only emits ANSI escape codes when colors change
- Typical reduction: 80% fewer ANSI codes vs naive approach
Example output:
\x1b[37;40mHello \x1b[31mWorld\x1b[37m!
^^^^^^^^^^ ^^^^
Set colors Change FG only
6. Console Driver Layer
Screen-Level Buffering: ConsoleBuffer
File: Drivers/ConsoleBuffer.cs:23-343
Second level of double buffering, operating at the screen level.
public class ConsoleBuffer
{
private BufferLine[] _frontBuffer; // Currently displayed
private BufferLine[] _backBuffer; // Being rendered
private int _width;
private int _height;
private class BufferLine
{
public Cell[] Cells;
public bool Dirty; // Line changed since last render
}
private struct Cell
{
public char Character;
public Color Foreground;
public Color Background;
}
}
AddContent (ANSI Parsing)
File: ConsoleBuffer.cs:180-245
Receives ANSI-formatted strings from window buffers, parses, and stores.
public void AddContent(int x, int y, string ansiContent)
{
if (y < 0 || y >= _height)
return;
var line = _backBuffer[y];
int currentX = x;
Color currentFg = Color.White;
Color currentBg = Color.Black;
// Parse ANSI string
int i = 0;
while (i < ansiContent.Length)
{
if (ansiContent[i] == '\x1b') // ANSI escape sequence
{
i = ParseAnsiSequence(ansiContent, i, ref currentFg, ref currentBg);
}
else
{
// Regular character
if (currentX < _width)
{
line.Cells[currentX] = new Cell
{
Character = ansiContent[i],
Foreground = currentFg,
Background = currentBg
};
currentX++;
}
i++;
}
}
line.Dirty = true;
}
ANSI parsing:
- Inline parsing during storage (no separate pass)
- Extracts RGB colors from
\x1b[38;2;R;G;Bmsequences - Maintains color state across escape sequences
Render (Diff-Based Screen Output)
File: ConsoleBuffer.cs:270-343
The final step: writing to the physical console.
public void Render()
{
for (int y = 0; y < _height; y++)
{
var backLine = _backBuffer[y];
var frontLine = _frontBuffer[y];
// Only render dirty lines
if (!backLine.Dirty)
continue;
// Diff: Find changed regions within line
var changedRegions = FindChangedRegions(backLine, frontLine);
foreach (var region in changedRegions)
{
RenderLineRegion(y, region);
}
// Swap buffers for this line
(_frontBuffer[y], _backBuffer[y]) = (_backBuffer[y], _frontBuffer[y]);
_frontBuffer[y].Dirty = false;
}
}
Three levels of optimization:
- Line-level dirty checking: Skip unchanged lines entirely
- Region diffing: Within dirty lines, only update changed regions
- Cursor movement optimization: Minimize
SetCursorPositioncalls
RenderLine (Cursor Movement Optimization)
File: ConsoleBuffer.cs:345-420
private void RenderLineRegion(int y, Region region)
{
Console.SetCursorPosition(region.StartX, y);
Color? currentFg = null;
Color? currentBg = null;
for (int x = region.StartX; x <= region.EndX; x++)
{
var cell = _backBuffer[y].Cells[x];
// Emit ANSI codes only when colors change
if (cell.Foreground != currentFg)
{
Console.Write(AnsiCodes.Foreground(cell.Foreground));
currentFg = cell.Foreground;
}
if (cell.Background != currentBg)
{
Console.Write(AnsiCodes.Background(cell.Background));
currentBg = cell.Background;
}
Console.Write(cell.Character);
}
}
Typical performance:
- Idle frame: 0 lines rendered (all clean)
- Text input: 1-2 lines rendered (input line + cursor)
- Window drag: 10-50 lines rendered (only affected regions)
- Full redraw: All lines rendered (rare, only on resize/theme change)
7. Console Driver Abstraction: NetConsoleDriver
File: Drivers/NetConsoleDriver.cs:65-1027
Abstracts platform-specific console operations and manages console state.
Responsibilities
- Console initialization (colors, cursor visibility, buffer size)
- Render mode management (Buffer vs Direct)
- Input handling (background thread with blocking read)
- Resize detection (background thread with polling)
- Screen coordinate mapping
Two Render Modes
Buffer Mode (Recommended)
File: NetConsoleDriver.cs:145-200
Uses ConsoleBuffer for double-buffered rendering.
public void Render() // Buffer mode
{
if (_renderMode == RenderMode.Buffer)
{
_consoleBuffer.Render();
}
}
Advantages:
- Diff-based rendering (minimal console I/O)
- Flicker-free updates
- Handles overlapping windows gracefully
- Best performance for complex UIs
Use when:
- Multiple overlapping windows
- Frequent UI updates
- Production applications
Direct Mode
File: NetConsoleDriver.cs:202-250
Immediate rendering without buffering.
public void AddContent(int x, int y, string content) // Direct mode
{
if (_renderMode == RenderMode.Direct)
{
Console.SetCursorPosition(x, y);
Console.Write(content);
}
else
{
_consoleBuffer.AddContent(x, y, content);
}
}
Advantages:
- Immediate visual feedback
- Simpler debugging (see what's being drawn)
- Lower memory usage
Use when:
- Debugging rendering issues
- Simple single-window applications
- Testing
Background Threads
Input Loop
File: NetConsoleDriver.cs:560-680
Continuously reads keyboard/mouse input without blocking rendering.
private void InputLoop()
{
while (!_shouldStop)
{
if (Console.KeyAvailable)
{
var keyInfo = Console.ReadKey(intercept: true);
lock (_inputQueue)
{
_inputQueue.Enqueue(keyInfo);
}
}
else
{
Thread.Sleep(10); // Small sleep to avoid busy-waiting
}
}
}
Design rationale:
Console.ReadKey()is blocking, would freeze rendering- Background thread allows input + rendering concurrently
- Queue-based communication with main thread
Resize Loop
File: NetConsoleDriver.cs:720-800
Detects console window resizes and triggers full redraw.
private void ResizeLoop()
{
int lastWidth = Console.WindowWidth;
int lastHeight = Console.WindowHeight;
while (!_shouldStop)
{
Thread.Sleep(250); // Check 4 times per second
int currentWidth = Console.WindowWidth;
int currentHeight = Console.WindowHeight;
if (currentWidth != lastWidth || currentHeight != lastHeight)
{
OnConsoleResized(new Size(currentWidth, currentHeight));
lastWidth = currentWidth;
lastHeight = currentHeight;
}
}
}
Platform limitations:
- .NET Console API has no resize event
- Polling is necessary (expensive, but infrequent operation)
- 250ms polling interval = imperceptible lag
8. Complete Data Flow Diagram
APPLICATION LAYER
═════════════════════════════════════════════════════════════════
User calls:
• window.AddControl(new MarkupControl("text"))
• window.Title = "New Title"
• window.BackgroundColor = Color.Blue
│
▼
Window.Invalidate() sets window.NeedsRedraw = true
│
│
MAIN EVENT LOOP (ConsoleWindowSystem.Run)
═════════════════════════════════════════════════════════════════
┌────────────────────────────────────────────────┐
│ 1. ProcessInput() │ ← Input from background thread
│ • Keyboard/mouse from NetConsoleDriver │
│ • Updates control states │
└────────────────────┬───────────────────────────┘
│
┌────────────────────▼───────────────────────────┐
│ 2. ProcessPendingEvents() │
│ • Timers, deferred actions │
└────────────────────┬───────────────────────────┘
│
┌────────────────────▼───────────────────────────┐
│ 3. Dirty Check │
│ if (_needsFullRedraw || │
│ _windows.Any(w => w.NeedsRedraw)) │
└────────────────────┬───────────────────────────┘
│ YES
▼
RENDERING COORDINATOR (UpdateDisplay)
═════════════════════════════════════════════════════════════════
┌────────────────────────────────────────────────┐
│ Phase 1: Desktop Clear (if needed) │
│ _driver.ClearScreen() │
└────────────────────┬───────────────────────────┘
│
┌────────────────────▼───────────────────────────┐
│ Phase 2: Build Render List │
│ • Z-order sorting (back-to-front) │
│ • Filter visible, non-minimized windows │
└────────────────────┬───────────────────────────┘
│
┌────────────────────▼───────────────────────────┐
│ Phase 3: Multi-Pass Rendering │
│ For each pass (Normal, Active, AlwaysOnTop): │
│ For each window in pass: │
│ Renderer.RenderWindow(window, renderList) │
└────────────────────┬───────────────────────────┘
│
│
WINDOW RENDERER (Renderer.RenderWindow)
═════════════════════════════════════════════════════════════════
┌────────────────────▼───────────────────────────┐
│ 4. Calculate Visible Regions │
│ VisibleRegions.CalculateVisibleRegions(...) │ ← Occlusion culling
│ • Rectangle subtraction algorithm │
│ • Returns list of visible rectangles │
└────────────────────┬───────────────────────────┘
│
┌────────────────────▼───────────────────────────┐
│ 5. Render Background │
│ For each visible region: │
│ _driver.FillRect(region, backgroundColor) │
└────────────────────┬───────────────────────────┘
│
┌────────────────────▼───────────────────────────┐
│ 6. Render Border (if present) │
│ Using cached border strings │ ← Border cache
└────────────────────┬───────────────────────────┘
│
┌────────────────────▼───────────────────────────┐
│ 7. Render Content │
│ var lines = │
│ window.RenderAndGetVisibleContent(regions) │
│ For each line: │
│ _driver.AddContent(line.X, line.Y, line.Txt) │
└────────────────────┬───────────────────────────┘
│
│
WINDOW CONTENT (Window.RenderAndGetVisibleContent)
═════════════════════════════════════════════════════════════════
┌────────────────────▼───────────────────────────┐
│ 8. Rebuild DOM (if needed) │
│ if (NeedsRedraw) │
│ RebuildContentCacheDOM() │
└────────────────────┬───────────────────────────┘
│
┌────────────────────▼───────────────────────────┐
│ 9. Three-Stage Layout │
│ • MEASURE: Calculate desired sizes │ ← DOM layout
│ • ARRANGE: Assign final positions │
│ • PAINT: Draw to CharacterBuffer │
└────────────────────┬───────────────────────────┘
│
┌────────────────────▼───────────────────────────┐
│ 9.5 PostBufferPaint Event (Optional) │
│ • Fire compositor effects hook │ ← COMPOSITOR EFFECTS
│ • Buffer manipulation allowed │
│ • Transitions, filters, overlays, snapshots │
└────────────────────┬───────────────────────────┘
│
┌────────────────────▼───────────────────────────┐
│ 10. Serialize to ANSI │
│ _contentBuffer.ToLines() │ ← ANSI generation
│ • Color optimization (minimize escape codes) │
│ • Returns List<string> with ANSI formatting │
└────────────────────┬───────────────────────────┘
│
┌────────────────────▼───────────────────────────┐
│ 11. Clip to Visible Regions │
│ ClipLinesToVisibleRegions(lines, regions) │
└────────────────────┬───────────────────────────┘
│
│
CONSOLE DRIVER (NetConsoleDriver + ConsoleBuffer)
═════════════════════════════════════════════════════════════════
┌────────────────────▼───────────────────────────┐
│ 12. Parse and Buffer │
│ ConsoleBuffer.AddContent(x, y, ansiString) │
│ • Parse ANSI escape sequences │ ← Screen buffer
│ • Extract RGB colors │
│ • Store in back buffer (Cell[,] array) │
│ • Mark line as dirty │
└────────────────────┬───────────────────────────┘
│ (Repeat for all windows)
│
┌────────────────────▼───────────────────────────┐
│ 13. Diff and Render │
│ ConsoleBuffer.Render() │
│ • Compare back buffer vs front buffer │ ← Diff-based I/O
│ • Identify changed regions per line │
│ • Console.SetCursorPosition() + Write() │
│ • Swap front/back buffers │
└────────────────────┬───────────────────────────┘
│
▼
═════════════════════════════════════════════════════════════════
PHYSICAL CONSOLE OUTPUT
(User sees updated display)
═════════════════════════════════════════════════════════════════
9. Key Optimizations
1. Double Buffering (Two Levels)
Window Level: CharacterBuffer
- Each window has its own character buffer
- Allows independent window rendering
- Occlusion culling operates at this level
Screen Level: ConsoleBuffer
- Entire screen double-buffered
- Front buffer = currently displayed
- Back buffer = being rendered
- Swap on
Render()call
Impact:
- Eliminates flicker completely
- Enables diff-based rendering
- Allows overlapping windows without corruption
2. Dirty Tracking (Three Levels)
Level 1: Window Dirty Flag
public bool NeedsRedraw { get; private set; }
public void Invalidate()
{
NeedsRedraw = true;
// Frame check: if (any window dirty) → UpdateDisplay()
}
Level 2: Cell Dirty Flag
private struct Cell
{
public bool Dirty; // Character/color changed
}
// Only mark dirty if value actually changes
if (cell.Character != newChar)
{
cell.Character = newChar;
cell.Dirty = true;
}
Level 3: Line Dirty Flag
private class BufferLine
{
public bool Dirty; // Any cell in line changed
}
// Skip entire line if not dirty
if (!line.Dirty)
continue;
Impact:
- Idle frame: 0 console I/O operations
- Text input: 1-2 lines updated
- Window drag: 10-50 lines updated (only borders/overlaps)
- Full redraw: All lines (only on resize/theme change)
3. Occlusion Culling
Problem: Without culling, background windows are fully rendered even when completely hidden.
Solution: Rectangle subtraction algorithm calculates visible regions.
Impact:
- Scenario: 3 overlapping windows (A behind B behind C)
- Without culling: All 3 rendered fully = 3x work
- With culling: Only visible parts of A/B rendered ≈ 1.2x work
- 50-70% reduction in rendering work for typical desktop layouts
4. Border Caching
File: Window.cs:1850-1920
Pre-renders window borders as strings, reused each frame.
private string[] _cachedBorderLines;
private void RebuildBorderCache()
{
_cachedBorderLines = new string[Height];
// Top border
_cachedBorderLines[0] = "┌" + new string('─', Width - 2) + "┐";
// Side borders
for (int i = 1; i < Height - 1; i++)
_cachedBorderLines[i] = "│" + new string(' ', Width - 2) + "│";
// Bottom border
_cachedBorderLines[Height - 1] = "└" + new string('─', Width - 2) + "┘";
}
// Usage: Just write cached strings
foreach (var line in _cachedBorderLines)
_driver.AddContent(x, y++, line);
Invalidation:
- Window resize
- Border style change
- Theme change
Impact:
- Without caching: Build border strings every frame (~5-10% CPU)
- With caching: Simple string copy (<1% CPU)
5. ANSI Optimization
Color Run Compression
Instead of emitting ANSI codes for every character:
❌ BAD (naive approach):
\x1b[37;40mH\x1b[37;40me\x1b[37;40ml\x1b[37;40ml\x1b[37;40mo
(240 bytes for "Hello")
✅ GOOD (optimized):
\x1b[37;40mHello
(14 bytes for "Hello")
Implementation:
Color? currentFg = null;
for (int x = 0; x < width; x++)
{
if (cell.Foreground != currentFg)
{
sb.Append(AnsiCodes.Foreground(cell.Foreground));
currentFg = cell.Foreground;
}
sb.Append(cell.Character);
}
Impact:
- Typical reduction: 70-80% fewer ANSI escape codes
- Example: 1000-character line with 10 color changes
- Naive: 1000 × 12 bytes = 12,000 bytes
- Optimized: 1000 + (10 × 12) = 1,120 bytes (90% reduction)
6. Z-Order Rendering (Three Passes)
Ensures correct visual stacking without sorting overhead.
Pass 1: Normal Windows
┌──────────┐
│ Window A │
└──────────┘
Pass 2: Active Window (overlays normal)
┌──────────┐
│ Window B │
└──────────┘
Pass 3: AlwaysOnTop (overlays all)
┌────────────┐
│ Tooltip/ │
│ Notification│
└────────────┘
Impact:
- Guarantees correct visual order
- Avoids complex Z-order sorting
- Predictable rendering behavior
7. Compositor Effects Hook
NEW: Event-based buffer manipulation for post-processing effects.
// Zero-cost when not used
if (PostBufferPaint != null && _buffer != null)
{
PostBufferPaint.Invoke(_buffer, dirtyRegion, clipRect);
}
Benefits:
- Enables transitions, filters, and overlays without core changes
- Fires within render lock (thread-safe)
- Only processes subscribed windows
- Provides dirty region for optimization
Use Cases:
- Fade in/out transitions
- Blur effects for modal backgrounds
- Glow effects around focused controls
- Screenshot capture via
BufferSnapshot
Performance:
- Zero overhead when event not subscribed
- Event fires after painting, before ANSI conversion (optimal timing)
- Can use dirty region to minimize processing area
See Compositor Effects for comprehensive guide.
10. Threading & Locking
Lock Hierarchy (Deadlock Prevention)
The library uses multiple locks with strict ordering:
1. _renderLock (ConsoleWindowSystem)
• Protects rendering pipeline
• Must be acquired first if multiple locks needed
2. window._lock (Window)
• Protects window state
• Acquired inside _renderLock
3. _consoleLock (NetConsoleDriver)
• Protects console I/O
• Acquired last, held briefly
RULE: Always acquire in order 1 → 2 → 3
NEVER acquire in reverse order (deadlock risk)
Critical Sections
_renderLock (Rendering)
File: ConsoleWindowSystem.cs:2525
private void UpdateDisplay()
{
lock (_renderLock) // Entire rendering pipeline protected
{
// Phase 1-5: All rendering happens here
}
}
Protects:
_windowslist iteration- Renderer state
- Driver calls
Duration: Entire frame (typically 1-5ms)
window._lock (Window State)
File: Window.cs:350
public void AddControl(IWindowControl control)
{
lock (_lock)
{
_controls.Add(control);
Invalidate();
}
}
Protects:
_controlslist- Window properties (title, colors, size)
- Content buffer
Duration: Brief (microseconds)
_consoleLock (Console I/O)
File: NetConsoleDriver.cs:180
public void Render()
{
lock (_consoleLock)
{
Console.SetCursorPosition(x, y);
Console.Write(text);
}
}
Protects:
- Console.* API calls
- Cursor position consistency
Duration: Very brief (single I/O operation)
Background Threads
| Thread | Purpose | Frequency | Synchronization |
|---|---|---|---|
| InputLoop | Read keyboard/mouse | Continuous (10ms sleep) | Queue + lock |
| ResizeLoop | Detect window resize | 4 Hz (250ms sleep) | Event callback |
Communication:
// Input thread → Main thread
lock (_inputQueue)
{
_inputQueue.Enqueue(keyInfo);
}
// Main thread reads during ProcessInput()
lock (_inputQueue)
{
while (_inputQueue.TryDequeue(out var keyInfo))
HandleInput(keyInfo);
}
Safety guarantees:
- No shared mutable state between threads (except queues)
- All rendering occurs on main thread
- Background threads only feed data to main thread
11. Performance Metrics
Frame Time Tracking
File: ConsoleWindowSystem.cs:2750-2766
private void UpdateDisplay()
{
var startTime = DateTime.UtcNow;
// ... rendering ...
var frameTime = DateTime.UtcNow - startTime;
_recentFrameTimes.Enqueue(frameTime);
if (_recentFrameTimes.Count > 60)
_recentFrameTimes.Dequeue();
}
Status Bar Display
File: ConsoleWindowSystem.cs:2800-2850
Shows real-time performance data:
═════════════════════════════════════════════════════════════
FPS: 58.3 | Frame: 2.1ms | Dirty: 143 chars | Mem: 42 MB
═════════════════════════════════════════════════════════════
Metrics:
| Metric | Calculation | Meaning |
|---|---|---|
| FPS | 1000 / avg(frameTimes) |
Frames per second |
| Frame Time | DateTime.UtcNow - startTime |
Milliseconds per frame |
| Dirty Chars | Count of dirty cells in buffers | Characters changed this frame |
| Memory | GC.GetTotalMemory(false) |
Managed heap size |
Typical values:
| Scenario | FPS | Frame Time | Dirty Chars |
|---|---|---|---|
| Idle | 60 | 0.5ms | 0 |
| Text input | 60 | 1-2ms | 50-100 |
| Window drag | 45-55 | 3-5ms | 500-2000 |
| Full redraw | 30-40 | 10-20ms | 5000-20000 |
Enabling Metrics
var windowSystem = new ConsoleWindowSystem(RenderMode.Buffer);
windowSystem.ShowPerformanceMetrics = true;
12. Render Modes
Buffer Mode (Default, Recommended)
var windowSystem = new ConsoleWindowSystem(RenderMode.Buffer);
Characteristics:
- Double-buffered at screen level
- Diff-based rendering
- Minimal console I/O
- Flicker-free
Use for:
- ✅ Production applications
- ✅ Multiple overlapping windows
- ✅ Frequent UI updates
- ✅ Complex layouts
Performance:
- Idle: 0 console writes per frame
- Active: 1-50 console writes per frame
- Full redraw: 100-200 console writes
Direct Mode
var windowSystem = new ConsoleWindowSystem(RenderMode.Direct);
Characteristics:
- No screen buffering
- Immediate console I/O
- Visible rendering order
- Potential flicker
Use for:
- ✅ Debugging rendering issues
- ✅ Understanding rendering order
- ✅ Simple single-window apps
- ❌ Production (too much flicker)
Performance:
- Every
AddContent()call = immediateConsole.Write() - 10-100× more console I/O than Buffer mode
- Useful for seeing exactly what's being drawn when
Comparison
| Aspect | Buffer Mode | Direct Mode |
|---|---|---|
| Performance | Excellent | Poor |
| Flicker | None | Significant |
| Debugging | Harder (buffered) | Easier (immediate) |
| Memory | Higher | Lower |
| Complexity | Higher | Lower |
| Production | ✅ Yes | ❌ No |
13. File Reference Table
Core Rendering Pipeline
| Component | File | Key Methods | Line Range |
|---|---|---|---|
| Event Loop | ConsoleWindowSystem.cs |
Run(), ProcessOnce() |
822-1002 |
| Render Coordinator | ConsoleWindowSystem.cs |
UpdateDisplay() |
2522-2766 |
| Window Renderer | Renderer.cs |
RenderWindow() |
211-270 |
| Occlusion Culling | VisibleRegions.cs |
CalculateVisibleRegions() |
40-158 |
| Window Content | Window.cs |
RenderAndGetVisibleContent() |
2295-2430 |
| Layout Pipeline | Window.cs |
RebuildContentCacheDOM() |
2500-2803 |
| Character Buffer | Layout/CharacterBuffer.cs |
ToLines(), SetChar() |
145-505 |
| Console Buffer | Drivers/ConsoleBuffer.cs |
AddContent(), Render() |
23-343 |
| Console Driver | Drivers/NetConsoleDriver.cs |
Render(), ClearScreen() |
65-1027 |
Supporting Systems
| Component | File | Key Methods | Line Range |
|---|---|---|---|
| Input Handling | NetConsoleDriver.cs |
InputLoop(), ProcessInput() |
560-680 |
| Resize Detection | NetConsoleDriver.cs |
ResizeLoop() |
720-800 |
| Border Caching | Window.cs |
RebuildBorderCache() |
1850-1920 |
| Z-Order Management | ConsoleWindowSystem.cs |
BuildRenderList() |
2634-2670 |
| Performance Metrics | ConsoleWindowSystem.cs |
RenderStatusBar() |
2800-2850 |
Helper Classes
| Component | File | Purpose |
|---|---|---|
| ANSI Codes | Rendering/AnsiCodes.cs |
ANSI escape sequence generation |
| Color Helpers | Helpers/ColorResolver.cs |
Color resolution and inheritance |
| Layout Rect | Models/ImmutableModels.cs |
Immutable rectangle structure |
| Spectre Integration | Helpers/AnsiConsoleHelper.cs |
Spectre.Console markup parsing |
14. Common Patterns & Examples
Pattern 1: Invalidating Content
When you modify a window's content, call Invalidate() to trigger a redraw:
// Simple invalidation
window.AddControl(new MarkupControl("Hello"));
// AddControl() automatically calls Invalidate()
// Manual invalidation
window.Title = "New Title"; // Property setter calls Invalidate()
// Explicit invalidation
window.BackgroundColor = Color.Blue;
window.Invalidate(); // Force redraw
What happens:
Invalidate()setswindow.NeedsRedraw = true- Next frame:
ProcessOnce()detects dirty window UpdateDisplay()called- Window rendered with new content
Pattern 2: Custom Control Rendering
Implement IWindowControl.PaintDOM() to render custom controls:
public class CustomControl : IWindowControl
{
public void PaintDOM(CharacterBuffer buffer, LayoutRect bounds, LayoutRect clipRect)
{
// Draw background
buffer.FillRect(bounds, ' ', Color.White, Color.DarkBlue);
// Draw text
buffer.DrawText(bounds.X + 2, bounds.Y + 1, "Custom Control",
Color.Yellow, Color.DarkBlue);
// Draw border
for (int x = bounds.X; x < bounds.Right; x++)
{
buffer.SetChar(x, bounds.Y, '─', Color.Gray, Color.DarkBlue);
buffer.SetChar(x, bounds.Bottom - 1, '─', Color.Gray, Color.DarkBlue);
}
}
}
Best practices:
- Respect
clipRectfor scrollable containers - Use
FillRect()for backgrounds (faster than per-char) - Cache expensive calculations (text measurements, layouts)
- Only draw within
bounds
Pattern 3: Efficient List Updates
When updating list items, use targeted invalidation:
public class ListControl : IWindowControl
{
private List<string> _items = new();
public void AddItem(string item)
{
_items.Add(item);
Container?.Invalidate(true); // Invalidate parent window
}
public void UpdateItem(int index, string newValue)
{
if (index < 0 || index >= _items.Count)
return;
_items[index] = newValue;
Container?.Invalidate(true); // Only this window redraws
}
}
Optimization:
- Batch updates: Modify multiple items, then call
Invalidate()once - Use
Container?.Invalidate(false)for minor changes (no border redraw) - Consider
InvalidationManagerfor coalescing multiple invalidations
Pattern 4: Modal Windows
Display modal windows that block interaction with other windows:
// Create and show modal
var modal = new Window(windowSystem)
{
Title = "Confirmation",
Width = 40,
Height = 10,
IsModal = true,
AlwaysOnTop = true
};
modal.AddControl(new MarkupControl("[yellow]Are you sure?[/]"));
modal.CenterOnScreen();
windowSystem.OpenWindow(modal);
// Check if modals active
if (windowSystem.ModalStateService.HasModals)
{
// Background windows won't receive input
}
// Close modal
windowSystem.CloseWindow(modal);
Behavior:
- Modal windows have highest Z-order
- Input blocked to non-modal windows
- Escape key typically closes modal (customizable)
Pattern 5: Real-Time Updates
For controls that update continuously (clocks, progress bars):
public class ClockControl : IWindowControl
{
private string _currentTime;
public ClockControl()
{
// Update clock every second
var timer = new System.Threading.Timer(_ =>
{
_currentTime = DateTime.Now.ToString("HH:mm:ss");
Container?.Invalidate(true);
}, null, 0, 1000);
}
public void PaintDOM(CharacterBuffer buffer, LayoutRect bounds, LayoutRect clipRect)
{
buffer.DrawText(bounds.X, bounds.Y, _currentTime, Color.Green, Color.Black);
}
}
Considerations:
- Timer callbacks run on background thread
Invalidate()is thread-safe- Rendering occurs on main thread (thread-safe by design)
- Avoid excessive update rates (>60 FPS wasted)
Pattern 6: Measuring Text with ANSI
When calculating text widths for layout, strip ANSI codes:
using SharpConsoleUI.Helpers;
public int MeasureTextWidth(string markupText)
{
// Strip Spectre.Console markup: "[red]Hello[/]" → "Hello"
int visualWidth = AnsiConsoleHelper.StripSpectreLength(markupText);
return visualWidth;
}
Why needed:
- ANSI escape codes don't consume screen space
"[red]Hi[/]"displays as 2 characters, not 10- Use for centering, alignment, truncation
Pattern 7: Handling Overlapping Windows
The system handles overlaps automatically, but you can optimize:
// Windows with AlwaysOnTop don't participate in occlusion culling
var tooltip = new Window(windowSystem)
{
AlwaysOnTop = true, // Rendered last, always visible
IsModal = false // Doesn't block input
};
// Background windows can be skipped if fully occluded
// System calculates this automatically via VisibleRegions
Automatic optimizations:
- Fully occluded windows skip content rendering
- Partially occluded windows render only visible regions
- Z-order ensures correct visual stacking
15. Debugging & Troubleshooting
Rendering Issues
Problem: Window content not updating
Check:
- Is
Invalidate()being called? - Is
window.Visibletrue? - Is window minimized?
- Is window fully occluded by others?
Debug:
// Force redraw
window.Invalidate();
windowSystem.FullRedraw();
// Check state using LogService (NEVER use Console.WriteLine - it corrupts UI!)
var logService = windowSystem.LogService;
logService.Log(LogLevel.Debug, "Window",
$"NeedsRedraw: {window.NeedsRedraw}, Visible: {window.Visible}, Z-Order: {window.ZOrder}");
// Or write to a debug window
var debugWindow = new Window(windowSystem) { Title = "Debug Info" };
debugWindow.AddControl(new MarkupControl($"NeedsRedraw: {window.NeedsRedraw}"));
debugWindow.AddControl(new MarkupControl($"Visible: {window.Visible}"));
debugWindow.AddControl(new MarkupControl($"Z-Order: {window.ZOrder}"));
Problem: Flickering
Likely causes:
- Using
RenderMode.Direct(switch toBuffer) - Excessive invalidations (batch updates)
- Console output interference (check for
Console.WriteLine()or console logging providers - these WILL corrupt the UI!)
Problem: Performance degradation
Enable metrics:
windowSystem.ShowPerformanceMetrics = true;
Watch for:
- FPS < 30: Too much rendering work
- Dirty chars > 10,000: Excessive invalidations
- Frame time > 16ms: Missing 60 FPS target
Logging
Method 1: Environment Variables
Enable debug logging via environment:
export SHARPCONSOLEUI_DEBUG_LOG=/tmp/consoleui.log
export SHARPCONSOLEUI_DEBUG_LEVEL=Debug
dotnet run
Method 2: Programmatic Initialization (Recommended for Debugging)
Initialize logging directly in your code:
var windowSystem = new ConsoleWindowSystem(RenderMode.Buffer);
// Enable file logging programmatically
windowSystem.LogService.EnableFileLogging("/tmp/debug.log");
windowSystem.LogService.MinimumLevel = LogLevel.Debug; // or LogLevel.Trace for maximum detail
// Your application code...
var window = new Window(windowSystem) { Title = "Test" };
// Log custom debug information
windowSystem.LogService.Log(LogLevel.Debug, "MyApp", "Window created");
// Access logs programmatically
var recentLogs = windowSystem.LogService.GetRecentLogs(50);
foreach (var entry in recentLogs)
{
// Process log entries (store, analyze, etc.)
}
Method 3: Subscribe to Log Events
Real-time log monitoring:
windowSystem.LogService.LogAdded += (sender, entry) =>
{
// Write to your own file, database, or monitoring system
File.AppendAllText("/tmp/myapp.log",
$"[{entry.Timestamp:HH:mm:ss.fff}] [{entry.Level}] {entry.Category}: {entry.Message}\n");
};
Log Categories to Search For:
[System]- Overall system lifecycle[Render]- Rendering operations[Window]- Window lifecycle (create, close, show, hide)[WindowState]- Window state management[Focus]- Focus changes[Modal]- Modal window handling[Interaction]- Drag/resize events[Input]- Keyboard/mouse input
Example Log Output:
[14:23:45.123] [Debug] Window: Window created - ID: win_001, Title: 'Main Window'
[14:23:45.134] [Trace] Render: UpdateDisplay started
[14:23:45.136] [Debug] Render: Building render list - 3 windows
[14:23:45.138] [Trace] Render: Rendering window 'Main Window' (Z-Order: 0)
[14:23:45.142] [Trace] Render: Visible regions calculated - 2 regions
[14:23:45.145] [Debug] Render: Frame completed - 2.1ms, 143 dirty chars
Conclusion
The SharpConsoleUI rendering pipeline is designed for performance, correctness, and maintainability:
- Performance: Double buffering, dirty tracking, occlusion culling minimize CPU and I/O
- Correctness: Multi-pass rendering, locking hierarchy, diff-based output ensure visual accuracy
- Maintainability: Clear separation of concerns (Coordinator → Renderer → Window → Driver)
Key takeaways:
- Rendering is lazy: Only dirty windows render, only dirty lines write to console
- Occlusion is expensive but worth it: Rectangle subtraction adds complexity but saves 50%+ rendering
- Double buffering at two levels: Window buffers for composition, screen buffer for output
- Three-pass rendering guarantees order: Normal → Active → AlwaysOnTop
- Locking is simple: One lock per layer, strict hierarchy, brief critical sections
For new contributors:
- Start at
ConsoleWindowSystem.UpdateDisplay()to understand flow - Read
Renderer.RenderWindow()to see per-window logic - Study
CharacterBufferandConsoleBufferto understand buffering - Review
VisibleRegionsonly if working on occlusion culling
For debugging:
- Use
RenderMode.Directto see immediate rendering - Enable
ShowPerformanceMetricsfor real-time stats - Enable debug logging for detailed operation traces
- Check lock contention if experiencing stuttering
The pipeline in one sentence:
Application invalidates windows → Event loop detects dirty windows → Multi-pass renderer calculates visible regions and draws to window buffers → Window buffers serialize to ANSI → Screen buffer diffs and writes only changed lines to console.