@@ -254,6 +254,13 @@ public void Render()
254254 var metrics = _diagnostics ? . IsEnabled == true ? new Diagnostics . RenderingMetrics ( ) : null ;
255255 var renderStartTime = metrics != null ? DateTime . UtcNow : default ;
256256
257+ // Diagnostics: Capture dirty count BEFORE rendering (before buffers sync)
258+ int dirtyCountBeforeRender = 0 ;
259+ if ( metrics != null )
260+ {
261+ dirtyCountBeforeRender = GetDirtyCharacterCount ( ) ;
262+ }
263+
257264 // Diagnostics: Capture console buffer state before rendering
258265 if ( _diagnostics ? . IsEnabled == true && _diagnostics . EnabledLayers . HasFlag ( Configuration . DiagnosticsLayers . ConsoleBuffer ) )
259266 {
@@ -331,23 +338,83 @@ public void Render()
331338 // This eliminates flickering by doing a single write instead of multiple cursor moves
332339 var screenBuilder = new StringBuilder ( ) ;
333340
334- // FIX14: Track rendering statistics
335- int linesRendered = 0 ;
341+ // FIX14: Track rendering statistics
342+ int linesRendered = 0 ;
343+ int cellsRendered = 0 ;
344+
345+ // Choose rendering strategy based on configured dirty tracking mode
346+ if ( _options . DirtyTrackingMode == Configuration . DirtyTrackingMode . Cell )
347+ {
348+ // CELL-LEVEL: Always render only changed regions within lines (minimal output)
349+ for ( int y = 0 ; y < Math . Min ( _height , Console . WindowHeight ) ; y ++ )
350+ {
351+ var dirtyRegions = GetDirtyRegionsInLine ( y ) ;
352+ if ( dirtyRegions . Count == 0 )
353+ continue ;
354+
355+ linesRendered ++ ;
356+
357+ foreach ( var ( startX , endX ) in dirtyRegions )
358+ {
359+ // Position cursor at start of dirty region
360+ screenBuilder . Append ( $ "\x1b [{ y + 1 } ;{ startX + 1 } H") ;
361+
362+ // Append only the dirty region
363+ AppendRegionToBuilder ( y , startX , endX , screenBuilder ) ;
336364
365+ cellsRendered += ( endX - startX + 1 ) ;
366+ }
367+ }
368+ }
369+ else if ( _options . DirtyTrackingMode == Configuration . DirtyTrackingMode . Line )
370+ {
371+ // LINE-LEVEL: Always render entire line when any cell changes
337372 for ( int y = 0 ; y < Math . Min ( _height , Console . WindowHeight ) ; y ++ )
338373 {
339374 if ( ! IsLineDirty ( y ) )
340375 continue ;
341376
342- // FIX14: Count lines rendered
343377 linesRendered ++ ;
344378
345379 // Add ANSI absolute positioning: ESC[row;colH (1-based)
346380 screenBuilder . Append ( $ "\x1b [{ y + 1 } ;1H") ;
347381
348382 // Append this line's content to the screen builder
349383 AppendLineToBuilder ( y , screenBuilder ) ;
384+
385+ cellsRendered += _width ;
386+ }
387+ }
388+ else // DirtyTrackingMode.Smart
389+ {
390+ // SMART MODE: Analyze each line and choose optimal strategy per line
391+ for ( int y = 0 ; y < Math . Min ( _height , Console . WindowHeight ) ; y ++ )
392+ {
393+ var ( isDirty , useLineMode , dirtyRegions ) = AnalyzeLine ( y ) ;
394+ if ( ! isDirty )
395+ continue ;
396+
397+ linesRendered ++ ;
398+
399+ if ( useLineMode )
400+ {
401+ // Use LINE strategy for this line (high coverage or fragmented)
402+ screenBuilder . Append ( $ "\x1b [{ y + 1 } ;1H") ;
403+ AppendLineToBuilder ( y , screenBuilder ) ;
404+ cellsRendered += _width ;
405+ }
406+ else
407+ {
408+ // Use CELL strategy for this line (low coverage, not fragmented)
409+ foreach ( var ( startX , endX ) in dirtyRegions )
410+ {
411+ screenBuilder . Append ( $ "\x1b [{ y + 1 } ;{ startX + 1 } H") ;
412+ AppendRegionToBuilder ( y , startX , endX , screenBuilder ) ;
413+ cellsRendered += ( endX - startX + 1 ) ;
414+ }
415+ }
350416 }
417+ }
351418
352419
353420 // Single atomic write of entire screen - no cursor jumps, no flicker!
@@ -363,8 +430,9 @@ public void Render()
363430 metrics . BytesWritten = output . Length ;
364431 metrics . AnsiEscapeSequences = CountAnsiSequences ( output ) ;
365432 metrics . CursorMovements = CountCursorMoves ( output ) ;
366- metrics . CellsActuallyRendered = linesRendered * _width ; // Approximate
367- metrics . DirtyCellsMarked = GetDirtyCharacterCount ( ) ;
433+ metrics . CellsActuallyRendered = cellsRendered ; // Actual cells rendered (mode-aware)
434+ metrics . DirtyCellsMarked = dirtyCountBeforeRender ; // Captured before rendering
435+ metrics . CharactersChanged = dirtyCountBeforeRender ; // Number of characters that changed
368436
369437 // Capture output snapshot
370438 if ( _diagnostics ? . EnabledLayers . HasFlag ( Configuration . DiagnosticsLayers . ConsoleOutput ) == true )
@@ -443,8 +511,150 @@ private bool IsLineDirty(int y)
443511 return false ;
444512 }
445513
446- private bool IsValidPosition ( int x , int y )
447- => x >= 0 && x < _width && y >= 0 && y < _height ;
514+
515+ /// <summary>
516+ /// Gets dirty regions (contiguous changed cells) within a line.
517+ /// Returns list of (startX, endX) tuples representing dirty regions.
518+ /// Used for cell-level dirty tracking mode.
519+ /// </summary>
520+ private List < ( int startX , int endX ) > GetDirtyRegionsInLine ( int y )
521+ {
522+ var regions = new List < ( int , int ) > ( ) ;
523+ int ? regionStart = null ;
524+
525+ for ( int x = 0 ; x < _width ; x ++ )
526+ {
527+ ref readonly var frontCell = ref _frontBuffer [ x , y ] ;
528+ ref readonly var backCell = ref _backBuffer [ x , y ] ;
529+
530+ bool isDirty = ! frontCell . Equals ( backCell ) ;
531+
532+ if ( isDirty )
533+ {
534+ // Start new region or continue existing
535+ regionStart ??= x ;
536+ }
537+ else if ( regionStart . HasValue )
538+ {
539+ // End of dirty region
540+ regions . Add ( ( regionStart . Value , x - 1 ) ) ;
541+ regionStart = null ;
542+ }
543+ }
544+
545+ // Close final region if line ends dirty
546+ if ( regionStart . HasValue )
547+ {
548+ regions . Add ( ( regionStart . Value , _width - 1 ) ) ;
549+ }
550+
551+ return regions ;
552+ }
553+
554+ /// <summary>
555+ /// Smart mode: Analyzes a line in a single pass to determine:
556+ /// 1. Is the line dirty?
557+ /// 2. If dirty, should we use LINE or CELL rendering strategy?
558+ /// Returns (isDirty, useLineMode, dirtyRegions).
559+ /// Optimized to avoid double-scanning the line.
560+ /// </summary>
561+ private ( bool isDirty , bool useLineMode , List < ( int startX , int endX ) > dirtyRegions ) AnalyzeLine ( int y )
562+ {
563+ var regions = new List < ( int startX , int endX ) > ( ) ;
564+ int ? regionStart = null ;
565+ int dirtyCells = 0 ;
566+ int dirtyRuns = 0 ;
567+
568+ for ( int x = 0 ; x < _width ; x ++ )
569+ {
570+ ref readonly var frontCell = ref _frontBuffer [ x , y ] ;
571+ ref readonly var backCell = ref _backBuffer [ x , y ] ;
572+
573+ bool isDirty = ! frontCell . Equals ( backCell ) ;
574+
575+ if ( isDirty )
576+ {
577+ dirtyCells ++ ;
578+ if ( ! regionStart . HasValue )
579+ {
580+ // Start new dirty region
581+ regionStart = x ;
582+ dirtyRuns ++ ;
583+ }
584+ }
585+ else if ( regionStart . HasValue )
586+ {
587+ // End of dirty region
588+ regions . Add ( ( regionStart . Value , x - 1 ) ) ;
589+ regionStart = null ;
590+ }
591+ }
592+
593+ // Close final region if line ends dirty
594+ if ( regionStart . HasValue )
595+ {
596+ regions . Add ( ( regionStart . Value , _width - 1 ) ) ;
597+ }
598+
599+ // No dirty cells? Return early
600+ if ( dirtyCells == 0 )
601+ return ( false , false , regions ) ;
602+
603+ // Decision heuristics for Smart mode:
604+ float coverage = ( float ) dirtyCells / _width ;
605+
606+ // 1. High coverage (>threshold%) → LINE mode (too much to render cell-by-cell)
607+ if ( coverage > _options . SmartModeCoverageThreshold )
608+ return ( true , true , regions ) ;
609+
610+ // 2. Highly fragmented (>threshold separate runs) → LINE mode (too many cursor moves)
611+ if ( dirtyRuns > _options . SmartModeFragmentationThreshold )
612+ return ( true , true , regions ) ;
613+
614+ // 3. Full line dirty → LINE mode (same output, fewer cursor moves)
615+ if ( dirtyCells == _width )
616+ return ( true , true , regions ) ;
617+
618+ // 4. Low coverage + low fragmentation → CELL mode (minimal output)
619+ return ( true , false , regions ) ;
620+ }
621+
622+ private bool IsValidPosition ( int x , int y )
623+ => x >= 0 && x < _width && y >= 0 && y < _height ;
624+
625+ /// <summary>
626+ /// Appends a specific region of a line to the builder (cell-level tracking).
627+ /// Only outputs cells from startX to endX (inclusive).
628+ /// </summary>
629+ private void AppendRegionToBuilder ( int y , int startX , int endX , StringBuilder builder )
630+ {
631+ string lastOutputAnsi = string . Empty ;
632+
633+ for ( int x = startX ; x <= endX && x < _width ; x ++ )
634+ {
635+ ref var backCell = ref _backBuffer [ x , y ] ;
636+ ref var frontCell = ref _frontBuffer [ x , y ] ;
637+
638+ // Output ANSI only if it changed
639+ if ( backCell . AnsiEscape != lastOutputAnsi )
640+ {
641+ builder . Append ( backCell . AnsiEscape ) ;
642+ lastOutputAnsi = backCell . AnsiEscape ;
643+ }
644+
645+ // Output character
646+ builder . Append ( backCell . Character ) ;
647+
648+ // Sync buffers
649+ frontCell . CopyFrom ( backCell ) ;
650+ }
651+
652+ // Reset ANSI at end of region
653+ if ( ! string . IsNullOrEmpty ( lastOutputAnsi ) )
654+ {
655+ builder . Append ( "\x1b [0m" ) ;
656+ }
657+ }
448658
449659 private void AppendLineToBuilder ( int y , StringBuilder builder )
450660 {
0 commit comments