SharpConsoleUI Rendering Pipeline

Want to write a control? This document explains how frames are produced. For the practical, step-by-step authoring path — deriving from BaseControl and overriding MeasureDOM/PaintDOM — start at Creating Your Own Control, which links the composite-control and from-scratch (BadgeControl) tutorials.

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 — a per-window PendingWork intent accumulator (Repaint/Relayout) plus per-cell and per-line dirty tracking — 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)                    │
│  • Pre/PostBufferPaint hooks (compositor effects)           │
└──────────────────────┬──────────────────────────────────────┘
                       │
                       ▼ (Direct Cell Path — no ANSI strings)
┌─────────────────────────────────────────────────────────────┐
│              Console Driver (Screen Buffer)                 │
│  • ConsoleBuffer (screen-level double buffer)               │
│  • Direct cell copy from CharacterBuffer                    │
│  • Diff-based rendering (Cell/Line/Smart modes)             │
└──────────────────────┬──────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────────────────────┐
│                    Physical Console                         │
│  • Raw libc write (Unix) / Console.Write (Windows)          │
│  • ANSI codes generated once during final output            │
└─────────────────────────────────────────────────────────────┘

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 cell buffer, DOM paint target Layout/CharacterBuffer.cs
ConsoleBuffer Screen-level buffer, direct cell copy, 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 _shouldQuit flag 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 _needsFullRedraw or any window has NeedsRedraw flag 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 _needsDesktopClear flag 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.

Within each window, portal overlays (dropdowns, context menus, tooltips) render after all normal controls, appearing on top of the window's content.

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 ShowPerformanceMetrics property

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 (direct cell path)
    var buffer = window.EnsureContentReady(visibleRegions);
    if (buffer != null)
    {
        RenderVisibleWindowContentFromBuffer(window, buffer, visibleRegions);
    }
}

Note The direct cell path bypasses ANSI serialization entirely. EnsureContentReady() runs the DOM layout pipeline (Measure → Arrange → Paint) to populate the window's CharacterBuffer. Then RenderVisibleWindowContentFromBuffer() copies cells directly from CharacterBuffer to ConsoleBuffer using WriteBufferRegion(), avoiding the previous serialize-to-ANSI → clip-with-regex → parse-ANSI round-trip.

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.EnsureContentReady()#

File: Window.Rendering.cs

The single entry point for all window content rendering. Rebuilds the window's CharacterBuffer via the DOM pipeline without ANSI serialization. Used by the Renderer for both normal windows and overlay windows.

internal CharacterBuffer? EnsureContentReady(List<Rectangle>? visibleRegions = null)
{
    if (_state == WindowState.Minimized) return null;
    lock (_lock)
    {
        if (PendingWork != FrameWork.None)   // the window has work to do this frame
        {
            var availableWidth = Width - 2;
            var availableHeight = Height - 2;
            RebuildContentBufferOnly(availableWidth, availableHeight, visibleRegions);
            bool isInRenderingPipeline = visibleRegions != null && visibleRegions.Count > 0;
            // Off-screen (no viewport yet): re-raise so the window rebuilds once it gets one.
            if (!isInRenderingPipeline) Request(Invalidation.Repaint);
        }
        return _renderer?.Buffer;
    }
}

The window is rendered only when PendingWork != FrameWork.None. RebuildContentBufferOnly consumes the pending work — an atomic snapshot-and-clear back to None — so a request that arrives after the consume simply leaves PendingWork non-None again and is picked up on the next frame. See The Invalidation Model below.

A convenience wrapper RenderAndGetVisibleContent() exists for test code and diagnostics — it calls EnsureContentReady() internally then serializes the buffer to ANSI strings via buffer.ToLines(). No separate code path exists; the buffer is always the source of truth.

The Invalidation Model#

A window tracks pending work with a single signal: PendingWork. Callers never set it directly — they declare an intent through Invalidate:

Invalidation (the request) Meaning Cost on the next frame
Repaint Appearance only — a color, selection, or the content of existing cells changed. Layout is unaffected. Paint only; the Measure pass is skipped and the cached layout reused.
Relayout Size or position changed. Full Measure + Arrange + Paint.
public enum Invalidation { Repaint = 1, Relayout = 2 }       // what a caller requests
public enum FrameWork    { None = 0, Repaint = 1, Relayout = 2 } // the window's pending state
public FrameWork Window.PendingWork { get; }                  // read-only; the render engine consults this

Accumulation. Each Invalidate folds its intent into PendingWork through a lock-free monotone max-join: Relayout dominates Repaint dominates None. Many invalidations in a single frame therefore coalesce to the strongest intent requested — a burst of Repaints plus one Relayout resolves to Relayout, and nothing weaker can pull it back down. Because the fold is a single atomic operation, Invalidate is also the one call safe to make directly from a background thread (a playback or PTY-read loop can invalidate its control without marshalling to the UI thread).

Consume. The render reads PendingWork, does the work, and atomically resets it to None. A request that races the consume either lands before it (handled this frame) or after it (leaves PendingWork non-None, handled next frame) — it can never be lost.

Propagation. A control's Invalidate(work) calls Container.Invalidate(work, this), which bubbles up the container hierarchy to the Window. BaseControl.SetProperty<T> issues this automatically on every property change, so self-invalidation is built in across the control library.

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. Measure is gated on the frame's intent: it runs on a Relayout frame (or when a node is structurally dirty / NeedsMeasure) and is skipped on a Repaint frame, where the cached DesiredSize from the previous layout is reused. Arrange and Paint still run so the buffer stays correct. This is the payoff of the invalidation model: an appearance-only change never pays for a re-measure.

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, defaultForeground, defaultBackground);
    }
}

CharacterBuffer key methods:

// Full Cell (preserves flags: IsWideContinuation, Combiners, Decorations)
// Use for parsed markup cells and cell copies between buffers
void SetCell(int x, int y, Cell cell);

// Narrow character (clears all flags, assumes width-1)
// Use for literal characters: borders, padding, fill chars, track chars
void SetNarrowCell(int x, int y, char character, Color fg, Color bg);
void SetNarrowCell(int x, int y, Rune character, Color fg, Color bg);

// Text rendering with automatic wide character and combiner handling
void WriteString(int x, int y, string text, Color fg, Color bg);

// Bulk fill
void FillRect(LayoutRect rect, char c, Color fg, Color bg);

Note The old SetCell(x, y, Rune, Color, Color) overload was renamed to SetNarrowCell to make misuse a compile error. Use SetCell(Cell) for parsed/copied cells, SetNarrowCell for literal narrow characters.

Stage 3.5: Pre/Post Buffer Paint Hooks (Compositor Effects)

File: Windows/WindowRenderer.cs

Two compositor-style hooks allow buffer manipulation at different points in the rendering pipeline:

public bool RebuildContentBuffer(...)
{
    // Stage 1-2: Measure, Arrange
    RebuildDOMTree();
    PerformDOMLayout();

    // Clear buffer with background color
    _buffer.Clear(backgroundColor);

    // ◄── PRE-PAINT HOOK POINT (backgrounds, fractals)
    if (PreBufferPaint != null && _buffer != null)
    {
        var dirtyRegion = new LayoutRect(0, 0, _buffer.Width, _buffer.Height);
        PreBufferPaint.Invoke(_buffer, dirtyRegion, clipRect);
    }

    // Stage 3: Paint controls ON TOP of pre-paint content
    PaintDOMWithoutClear(clipRect);

    // ◄── POST-PAINT HOOK POINT (effects, overlays)
    if (PostBufferPaint != null && _buffer != null)
    {
        var dirtyRegion = new LayoutRect(0, 0, _buffer.Width, _buffer.Height);
        PostBufferPaint.Invoke(_buffer, dirtyRegion, clipRect);
    }

    // Buffer is now ready — cells flow directly to ConsoleBuffer
    // (ToLines() is only called when AnsiLines diagnostic layer is enabled)
    return true;
}

PreBufferPaint - Fires after buffer clear, before controls paint:

  • Custom backgrounds: Animated patterns, gradients
  • Full-buffer graphics: Fractals, visualizations
  • Controls render ON TOP of pre-paint content

PostBufferPaint - Fires after controls paint, before buffer is consumed by the driver:

  • Transitions: Fade in/out, slide, wipe effects
  • Filters: Blur, desaturate, brightness adjustments
  • Overlays: Glow effects, highlights, custom decorations
  • Capture: Screenshots, recording via BufferSnapshot

API:

// Pre-paint: custom backgrounds (controls render on top)
window.PreBufferPaint += (buffer, dirtyRegion, clipRect) =>
{
    // Draw animated fractal or pattern
    RenderFractal(buffer);
};

// Post-paint: effects applied to final content
window.PostBufferPaint += (buffer, dirtyRegion, clipRect) =>
{
    // Apply fade or blur effect
    ApplyFadeEffect(buffer, _fadeProgress);
};

Thread Safety: Both events fire within existing _renderLock, ensuring safe buffer manipulation.

Performance: Zero overhead when events 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

Window-level buffer storing character, foreground, and background color for each cell.

Buffer Structure#

public class CharacterBuffer
{
    private Cell[,] _cells;  // 2D array [col, row]
    private int _width;
    private int _height;
}

public struct Cell : IEquatable<Cell>
{
    public Rune Character;          // Unicode scalar value (supports emoji, CJK)
    public Color Foreground;
    public Color Background;
    public TextDecoration Decorations;
    public bool Dirty;              // Changed since last render
    public bool IsWideContinuation; // Right half of a wide character
    public string? Combiners;       // Zero-width combining marks (e.g., diacritics)
}

Key changes from the original char-based Cell:

  • Rune replaces char — a System.Text.Rune represents a full Unicode scalar value, enabling correct handling of emoji and characters above U+FFFF (which require surrogate pairs as char)
  • IsWideContinuation marks the right-half placeholder cell for CJK/emoji characters that occupy 2 terminal columns
  • Combiners stores zero-width combining marks (diacritics, variation selectors) attached to the base character
  • TextDecoration supports underline, strikethrough, and other text effects

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. SetCell / SetNarrowCell (Individual Cell Update)

File: CharacterBuffer.cs

Two write APIs with distinct purposes:

  • SetCell(int x, int y, Cell cell) — preserves all flags (IsWideContinuation, Combiners, Decorations). Use for cells from MarkupParser.Parse() or when copying cells between buffers.
  • SetNarrowCell(int x, int y, char/Rune, Color fg, Color bg) — clears all flags, assumes width-1. Use for literal narrow characters: border chars, padding spaces, fill chars.

The old SetCell(x, y, Rune, Color, Color) overload was renamed to SetNarrowCell to make misuse a compile error.

public void SetNarrowCell(int x, int y, Rune character, Color foreground, Color background)
{
    if (x < 0 || x >= Width || y < 0 || y >= Height)
        return;

    CleanupWideCharAt(x, y);  // Fix orphaned wide char pairs

    ref var cell = ref _cells[x, y];
    if (cell.Character != character ||
        !cell.Foreground.Equals(foreground) ||
        !cell.Background.Equals(background) ||
        cell.IsWideContinuation ||
        cell.Combiners != null)
    {
        cell.Character = character;
        cell.Foreground = foreground;
        cell.Background = background;
        cell.Decorations = TextDecoration.None;
        cell.IsWideContinuation = false;
        cell.Combiners = null;
        cell.Dirty = true;
    }
}

Optimizations:

  • Dirty tracking: only marks cell dirty if value actually changes
  • Wide character cleanup: when overwriting a cell that is part of a wide character pair, the orphaned partner cell is cleaned up automatically

2. WriteString (Text Rendering with Wide Character Support)

File: CharacterBuffer.cs

The primary text rendering method. Handles wide characters and zero-width combiners automatically:

public void WriteString(int x, int y, string text, Color foreground, Color background)
{
    int cx = x;
    foreach (var rune in text.EnumerateRunes())
    {
        int runeWidth = UnicodeWidth.GetRuneWidth(rune);

        if (runeWidth == 0)
        {
            // Zero-width: attach to previous cell as combiner
            // (skips past continuation cells to find the base cell)
        }
        else if (runeWidth == 2)
        {
            // Wide char: write base cell + continuation cell
            SetCell(cx, y, new Cell(rune, foreground, background));
            SetCell(cx + 1, y, continuation with IsWideContinuation = true);
            cx += 2;
        }
        else
        {
            SetCell(cx, y, new Cell(rune, foreground, background));
            cx++;
        }
    }
}

Wide character handling:

  • Uses UnicodeWidth.GetRuneWidth() (backed by Wcwidth library) to determine display width
  • Width 0: zero-width combiners attached to the preceding base cell via AppendCombiner()
  • Width 2: CJK/emoji characters write a base cell + a continuation cell marked IsWideContinuation = true
  • Width 1: standard characters, one cell each
  • If a wide char doesn't fit at the buffer edge, a space is written instead

3. FillRect (Bulk Operations)

File: CharacterBuffer.cs

Used for backgrounds, borders, clearing regions. Delegates to SetNarrowCell() per cell.

4. ToLines (ANSI Serialization — Diagnostic Only)

File: CharacterBuffer.cs

Converts buffer to ANSI-formatted strings. Not called during rendering. Cells flow directly from CharacterBuffer to ConsoleBuffer via SetCellsFromBuffer(). ToLines() is only invoked for:

  • The AnsiLines diagnostic layer (test snapshots and debugging)
  • The RenderAndGetVisibleContent() convenience API (used by tests to get string-based 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 (used in diagnostic and test output)

  • 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

Alpha Compositing#

Controls write cells with Color.Transparent as the background to indicate "show whatever is behind me." CharacterBuffer resolves this at paint time by using the background color already present at that position in the buffer.

Paint order matters:

  1. PreBufferPaint hooks run first — gradient backgrounds and custom fills land in the buffer.
  2. Controls paint on top via PaintDOM. A cell written with Color.Transparent background inherits the gradient color at that cell from step 1.
  3. PostBufferPaint hooks run last — can read or overwrite cells.
PreBufferPaint:  gradient fills every cell (e.g. DarkBlue→Black vertically)
PaintDOM:        SparklineControl writes bar chars with bg=Transparent
                 → those cells display with the gradient color as their background
PostBufferPaint: (optional effects)

Background resolution chain in controls:

Controls expose BackgroundColor as Color?. null means "use the theme default or Color.Transparent." Color.Transparent means "composite against the buffer." Any other Color means "fill with this solid color."

// PaintDOM — typical pattern in a control
Color bgColor = ColorResolver.ResolveControlBackground(_backgroundColorValue, Container);
// bgColor is Color.Transparent when no explicit color is set → gradient shows through

This design keeps controls unaware of what sits behind them. The gradient (or any PreBufferPaint content) shows through automatically for every control that does not set an explicit opaque background.


6. Console Driver Layer#

Screen-Level Buffering: ConsoleBuffer#

File: Drivers/ConsoleBuffer.cs

Second level of double buffering, operating at the screen level. ConsoleBuffer is a pure cell-level buffer — all writes go through cell-level methods, never ANSI string parsing.

public class ConsoleBuffer
{
    private Cell[,] _frontBuffer;  // Currently displayed
    private Cell[,] _backBuffer;   // Being rendered
    private int _width;
    private int _height;

    private struct Cell
    {
        public Rune Character;          // Unicode scalar value
        public string AnsiEscape;       // Pre-formatted ANSI color string
        public bool IsWideContinuation; // Right half of wide character
        public string? Combiners;       // Zero-width combining marks
    }
}

Three Write Methods (Unified Cell API)#

All data enters ConsoleBuffer through exactly three cell-level methods. Note the narrow-cell writers are named SetNarrowCell — they assume width-1 and clear cell flags, so they are only for literal narrow characters (borders, padding, fill).

1. SetNarrowCell (Single Cell)

Used by borders (vertical, scrollbar), invisible borders. Accepts both char and Rune.

public void SetNarrowCell(int x, int y, Rune character, Color fg, Color bg)
{
    // Fix wide char pair split: clean up orphaned base/continuation cells
    if (_backBuffer[x, y].IsWideContinuation && x > 0)
        _backBuffer[x - 1, y].Reset();  // Orphaned base
    if (x + 1 < _width && _backBuffer[x + 1, y].IsWideContinuation)
        _backBuffer[x + 1, y].Reset();  // Orphaned continuation

    string ansi = FormatCellAnsi(fg, bg);
    ref var cell = ref _backBuffer[x, y];
    if (cell.Character != character || cell.AnsiEscape != ansi
        || cell.IsWideContinuation || cell.Combiners != null)
    {
        cell.Character = character;
        cell.AnsiEscape = ansi;
        cell.IsWideContinuation = false;
        cell.Combiners = null;
    }
}

2. FillCells (Horizontal Run)

Used by Renderer.FillRect (background fills), invisible border rows. Accepts both char and Rune.

public void FillCells(int x, int y, int width, Rune character, Color fg, Color bg)
{
    // Fix wide char pair split at left and right boundaries
    if (x > 0 && _backBuffer[x, y].IsWideContinuation)
        _backBuffer[x - 1, y].Reset();
    int rightEdge = x + maxWidth;
    if (rightEdge < _width && _backBuffer[rightEdge, y].IsWideContinuation)
        _backBuffer[rightEdge, y].Reset();

    string ansi = FormatCellAnsi(fg, bg);
    for (int i = 0; i < maxWidth; i++)
    {
        ref var cell = ref _backBuffer[x + i, y];
        if (cell.Character != character || cell.AnsiEscape != ansi
            || cell.IsWideContinuation || cell.Combiners != null)
        {
            cell.Character = character;
            cell.AnsiEscape = ansi;
            cell.IsWideContinuation = false;
            cell.Combiners = null;
        }
    }
}

3. SetCellsFromBuffer (Bulk Cell Copy from CharacterBuffer)

Used by window content, overlay content, border lines (top/bottom), status bars. Preserves wide character pairs and combiners from the source buffer.

public void SetCellsFromBuffer(int destX, int destY, CharacterBuffer source,
    int srcX, int srcY, int width, Color fallbackBg)
{
    // Fix wide char pair split at left and right boundaries
    // (same orphan cleanup as FillCells)

    for (int i = 0; i < maxWidth; i++)
    {
        var srcCell = source.GetCell(srcX + i, srcY);
        string ansi = FormatCellAnsi(srcCell.Foreground, srcCell.Background);
        ref var destCell = ref _backBuffer[destX + i, destY];
        destCell.Character = srcCell.Character;
        destCell.AnsiEscape = ansi;
        destCell.IsWideContinuation = srcCell.IsWideContinuation;
        destCell.Combiners = srcCell.Combiners;
    }
}

ANSI color cache — exploits spatial locality (adjacent cells often share colors):

private string FormatCellAnsi(Color fg, Color bg)
{
    if (fg.Equals(_lastCellFg) && bg.Equals(_lastCellBg))
        return _lastCellAnsi;  // Cache hit
    _lastCellAnsi = $"\x1b[38;2;{fg.R};{fg.G};{fg.B};48;2;{bg.R};{bg.G};{bg.B}m";
    _lastCellFg = fg;
    _lastCellBg = bg;
    return _lastCellAnsi;
}

Driver Interface (Cell-Level Only)#

File: Drivers/IConsoleDriver.cs

The driver interface exposes three cell-level output methods. No ANSI string writes exist:

public interface IConsoleDriver
{
    void SetNarrowCell(int x, int y, char character, Color fg, Color bg);  // char convenience
    void FillCells(int x, int y, int width, char character, Color fg, Color bg);  // char convenience
    void WriteBufferRegion(int destX, int destY, CharacterBuffer source,
        int srcX, int srcY, int width, Color fallbackBg);
    // Note: char overloads wrap in Rune internally. All paths support
    // full Unicode including wide characters and combining marks.
}

In Buffer mode, all three delegate to the corresponding ConsoleBuffer methods. In Direct mode, they format inline ANSI strings and write immediately to stdout.

Render (Diff-Based Screen Output)#

File: ConsoleBuffer.cs

The final step: writing to the physical console. Builds the entire frame output as a single string for atomic write.

public void Render()
{
    lock (_consoleLock)
    {
        // 1. Pre-process wide character dirty pair coherence
        //    When either half of a wide char pair changes, force both dirty
        for (int y = 0; y < _height; y++)
            for (int x = 1; x < _width; x++)
            {
                // If continuation changed but base is clean → force base dirty
                // If front had continuation but back doesn't → force base dirty
            }

        // 2. Build entire screen in one string (atomic output)
        _screenBuilder.Clear();

        // 3. Choose rendering strategy per configured DirtyTrackingMode:
        //    - Cell:  render only changed regions within lines (minimal output)
        //    - Line:  render entire line when any cell changes
        //    - Smart: analyze each line and choose Cell vs Line dynamically
        for (int y = 0; y < _height; y++)
        {
            // ... strategy-specific rendering ...
        }

        // 4. Single atomic write via raw libc write() on Unix
        if (_screenBuilder.Length > 0)
            WriteOutput(_screenBuilder);
    }
}

Four levels of optimization:

  1. Wide character coherence: Pre-processes wide char pairs so both halves are re-emitted together
  2. Line-level dirty checking: Skip unchanged lines entirely
  3. Region diffing: Within dirty lines, only update changed regions (Cell/Smart modes)
  4. Atomic output: Single WriteOutput() call eliminates flicker from multiple cursor moves

Three dirty tracking modes (configurable via ConsoleWindowSystemOptions.DirtyTrackingMode):

Mode Strategy Best For
Cell Render only changed cell ranges per line Sparse updates (text input, cursor blink)
Line Render entire line when any cell changes Dense updates (full redraws, scrolling)
Smart Per-line analysis: coverage > threshold or fragmented runs → Line mode, else Cell mode General purpose (default)

AppendRegionToBuilder (Wide Character Aware Output)#

File: ConsoleBuffer.cs

Appends a dirty region to the output string, handling wide characters and combiners:

private void AppendRegionToBuilder(int y, int startX, int endX, StringBuilder builder)
{
    string lastOutputAnsi = string.Empty;

    for (int x = startX; x <= endX && x < _width; x++)
    {
        ref var backCell = ref _backBuffer[x, y];
        ref var frontCell = ref _frontBuffer[x, y];
        frontCell.CopyFrom(backCell);  // Sync buffers

        // Skip continuation cells — terminal auto-advances for wide chars
        if (backCell.IsWideContinuation) { /* emit combiners only */ continue; }

        // Wide char ghost cleanup: if terminal had different content at x+1,
        // emit space there first to clear old content, then reposition
        bool isWideChar = x + 1 < _width && _backBuffer[x + 1, y].IsWideContinuation;
        if (isWideChar) { /* clear ghost content at x+1 if needed */ }

        // Emit ANSI only when color changes
        if (backCell.AnsiEscape != lastOutputAnsi)
            builder.Append(backCell.AnsiEscape);

        builder.AppendRune(backCell.Character);  // Rune → UTF-16 encoding
        if (backCell.Combiners != null)
            builder.Append(backCell.Combiners);

        if (isWideChar) x++;  // Skip continuation in loop
    }
}

Wide character output handling:

  • Continuation cells are skipped (terminal auto-advances past them)
  • Ghost content at x+1 is cleared before emitting a wide char (prevents old content from persisting)
  • AppendRune() handles surrogate pair encoding for characters above U+FFFF
  • Combiners are appended after both base and continuation cells

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#

  1. Console initialization (colors, cursor visibility, buffer size)
  2. Double-buffered rendering (back buffer, dirty-region diff)
  3. Input handling (background thread with blocking read)
  4. Resize detection (background thread with polling)
  5. Screen coordinate mapping

Two Render Modes#

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

8. The loop and WithAsyncWindowThread#

Everything above runs on one thread: the UI thread, inside ConsoleWindowSystem.Run(). Input is polled, animations advance, dirty windows measure, arrange and paint, the compositor merges them, and the driver flushes the diff - in that order, synchronously, once per frame.

WithAsyncWindowThread does not add a second render loop. It starts a delegate on a thread-pool task (Task.Run) that belongs to one window and lives as long as that window does. Nothing in the frame sequence above waits for it.

That is what makes it useful and what makes it dangerous. A slow query in that delegate cannot stall the frame, because the frame never joins it. For the same reason the delegate must not touch control state directly: the render loop reads that state without a lock, so a background write is a data race.

.WithAsyncWindowThread(async (win, ct) =>
{
    var rows = await QueryAsync(ct);          // off the UI thread - fine

    windowSystem.EnqueueOnUIThread(() =>      // back on the UI thread
    {
        table.SetRows(rows);                  // control state: UI thread only
    });
})

EnqueueOnUIThread queues the action onto the loop; it runs at the top of the next frame, before measure. Invalidate is the one call that is safe to make directly from the delegate, because it folds into the window's pending-work accumulator atomically rather than touching layout.

See Threading & async for the full model.


9. File Reference Table#

Core Rendering Pipeline#

Component File Key Methods Line Range
Event Loop ConsoleWindowSystem.cs Run(), ProcessOnce() —
Render Coordinator Rendering/RenderCoordinator.cs UpdateDisplay(), coverage caching, status bar caching —
Window Renderer Renderer.cs RenderWindow(), RenderVisibleWindowContentFromBuffer() —
Overlay Renderer Renderer.cs RenderOverlayWindow(), RenderOverlayControlRegionsFromBuffer() —
Occlusion Culling VisibleRegions.cs CalculateVisibleRegions() 40-158
Window Content Window.Rendering.cs EnsureContentReady(), ContentBuffer, RenderAndGetVisibleContent() (test API) —
Layout Pipeline Window.Layout.cs RebuildContentBufferOnly() —
DOM Rendering Windows/WindowRenderer.cs RebuildContentBuffer() —
Character Buffer Layout/CharacterBuffer.cs SetCell(), SetNarrowCell(), GetCell(), ToLines() (test/diagnostic) —
Console Buffer Drivers/ConsoleBuffer.cs SetCell(), FillCells(), SetCellsFromBuffer(), Render() —
Console Driver Drivers/NetConsoleDriver.cs SetNarrowCell(), FillCells(), WriteBufferRegion(), Render() —
Driver Interface Drivers/IConsoleDriver.cs SetNarrowCell(), FillCells(), WriteBufferRegion() —

Supporting Systems#

Component File Key Methods Line Range
Input Handling NetConsoleDriver.cs InputLoop(), ProcessInput() 560-680
Resize Detection NetConsoleDriver.cs ResizeLoop() 720-800
Border Rendering Windows/BorderRenderer.cs RenderBorders(), BuildTopBorder(), BuildBottomBorder() —
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
Markup Parsing Parsing/MarkupParser.cs Markup parsing and text measurement
Unicode Width Helpers/UnicodeWidth.cs Display width detection via Wcwidth (0/1/2 columns)
Scrollbar Helper Helpers/ScrollbarHelper.cs Shared scrollbar geometry, drawing, and hit testing
StringBuilder Extensions Helpers/StringBuilderExtensions.cs AppendRune() for Rune → UTF-16 StringBuilder output

10. 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:

  1. Invalidate() sets window.NeedsRedraw = true
  2. Next frame: ProcessOnce() detects dirty window
  3. UpdateDisplay() called
  4. Window rendered with new content

Pattern 2: Custom Control Rendering#

Derive from BaseControl and override PaintDOM (declared by IDOMPaintable, not by IWindowControl) to render custom controls. See Creating Your Own Control for the full walkthrough.

public class CustomControl : BaseControl
{
    public override void PaintDOM(CharacterBuffer buffer, LayoutRect bounds, LayoutRect clipRect,
                                  Color defaultForeground, Color defaultBackground)
    {
        SetActualBounds(bounds);

        // Draw background
        buffer.FillRect(bounds, ' ', Color.White, Color.DarkBlue);

        // Draw text (supports full Unicode including CJK and emoji)
        buffer.WriteString(bounds.X + 2, bounds.Y + 1, "Custom Control",
                       Color.Yellow, Color.DarkBlue);

        // Draw border
        for (int x = bounds.X; x < bounds.Right; x++)
        {
            // Literal narrow chars → SetNarrowCell (there is no 5-arg SetCell overload).
            buffer.SetNarrowCell(x, bounds.Y, '─', Color.Gray, Color.DarkBlue);
            buffer.SetNarrowCell(x, bounds.Bottom - 1, '─', Color.Gray, Color.DarkBlue);
        }
    }
}

Best practices:

  • Respect clipRect for 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 MyListControl : BaseControl
{
    private List<string> _items = new();

    public void AddItem(string item)
    {
        _items.Add(item);
        Invalidate(Invalidation.Relayout);  // an added row changes size → re-layout
    }

    public void UpdateItem(int index, string newValue)
    {
        if (index < 0 || index >= _items.Count)
            return;

        _items[index] = newValue;
        Invalidate(Invalidation.Relayout);  // text width may change → re-layout
    }
}

Optimization:

  • Batch updates: modify multiple items, then call Invalidate() once — the max-join coalesces them.
  • Use Invalidate(Invalidation.Repaint) for appearance-only changes (e.g. a selection or color change that doesn't move anything): the layout is reused and the Measure pass is skipped.
  • A burst of Repaints plus one Relayout in the same frame resolves to Relayout — you never need to track "the strongest invalidation so far" yourself; the accumulator does it.

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.Placement = Placement.Center(40, 10);
windowSystem.AddWindow(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 : BaseControl
{
    private string _currentTime = "";

    public ClockControl()
    {
        // Update clock every second
        var timer = new System.Threading.Timer(_ =>
        {
            _currentTime = DateTime.Now.ToString("HH:mm:ss");
            // Fixed-width text → appearance-only; Repaint skips Measure. Safe from the timer thread.
            Invalidate(Invalidation.Repaint);
        }, null, 0, 1000);
    }

    public override void PaintDOM(CharacterBuffer buffer, LayoutRect bounds, LayoutRect clipRect,
                                  Color defaultForeground, Color defaultBackground)
    {
        SetActualBounds(bounds);
        buffer.WriteStringClipped(bounds.X, bounds.Y, _currentTime,
                                  Color.Green, Color.Black, clipRect);
    }
}

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 Width#

When calculating text widths for layout, account for both markup tags and wide characters:

using SharpConsoleUI.Parsing;
using SharpConsoleUI.Helpers;

public int MeasureMarkupWidth(string markupText)
{
    // Strip markup tags: "[red]Hello[/]" → "Hello"
    int visualWidth = MarkupParser.StripLength(markupText);
    return visualWidth;
}

public int MeasureStringWidth(string plainText)
{
    // Accounts for wide characters (CJK = 2 columns, combining marks = 0)
    return UnicodeWidth.GetStringWidth(plainText);
}

Why needed:

  • Markup tags don't consume screen space ("[red]Hi[/]" = 2 columns, not 10)
  • CJK characters occupy 2 terminal columns ("日本" = 4 columns, not 2)
  • Zero-width combiners don't consume space ("é" as e + combining accent = 1 column)
  • 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

11. Debugging & Troubleshooting#

Rendering Issues#

Problem: Window content not updating

Check:

  1. Is Invalidate() being called?
  2. Is window.Visible true?
  3. Is window minimized?
  4. 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:

  1. Excessive invalidations (batch updates)
  2. 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:

  1. Rendering is lazy: Only dirty windows render, only dirty lines write to console
  2. Occlusion is expensive but worth it: Rectangle subtraction adds complexity but saves 50%+ rendering
  3. Double buffering at two levels: Window buffers for composition, screen buffer for output
  4. Three-pass rendering guarantees order: Normal → Active → AlwaysOnTop
  5. 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 CharacterBuffer and ConsoleBuffer to understand buffering
  • Review VisibleRegions only if working on occlusion culling

For debugging:

  • Enable ShowPerformanceMetrics for 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 → RenderCoordinator orchestrates multi-pass rendering → Renderer calculates visible regions per window → DOM layout pipeline paints to CharacterBuffer (with full Unicode/wide character support) → All data enters ConsoleBuffer through cell-level methods only (SetCell, FillCells, SetCellsFromBuffer — no ANSI parsing) → Wide character coherence pre-processing ensures atomic pair updates → Smart dirty tracking selects optimal Cell/Line strategy per line → Single atomic write to console.