// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using SkiaSharp; using Microsoft.Maui.Platform.Linux.Window; using Microsoft.Maui.Platform; using System.Runtime.InteropServices; namespace Microsoft.Maui.Platform.Linux.Rendering; /// /// Manages Skia rendering to an X11 window with dirty region optimization. /// public class SkiaRenderingEngine : IDisposable { private readonly X11Window _window; private SKBitmap? _bitmap; private SKBitmap? _backBuffer; private SKCanvas? _canvas; private SKImageInfo _imageInfo; private bool _disposed; private bool _fullRedrawNeeded = true; // Dirty region tracking for optimized rendering private readonly List _dirtyRegions = new(); private readonly object _dirtyLock = new(); private const int MaxDirtyRegions = 32; private const float RegionMergeThreshold = 0.3f; // Merge if overlap > 30% public static SkiaRenderingEngine? Current { get; private set; } public ResourceCache ResourceCache { get; } public int Width => _imageInfo.Width; public int Height => _imageInfo.Height; /// /// Gets or sets whether dirty region optimization is enabled. /// When disabled, full redraws occur (useful for debugging). /// public bool EnableDirtyRegionOptimization { get; set; } = true; /// /// Gets the number of dirty regions in the current frame. /// public int DirtyRegionCount { get { lock (_dirtyLock) return _dirtyRegions.Count; } } public SkiaRenderingEngine(X11Window window) { _window = window; ResourceCache = new ResourceCache(); Current = this; CreateSurface(window.Width, window.Height); _window.Resized += OnWindowResized; _window.Exposed += OnWindowExposed; } private void CreateSurface(int width, int height) { _bitmap?.Dispose(); _backBuffer?.Dispose(); _canvas?.Dispose(); _imageInfo = new SKImageInfo( Math.Max(1, width), Math.Max(1, height), SKColorType.Bgra8888, SKAlphaType.Premul); _bitmap = new SKBitmap(_imageInfo); _backBuffer = new SKBitmap(_imageInfo); _canvas = new SKCanvas(_bitmap); _fullRedrawNeeded = true; lock (_dirtyLock) { _dirtyRegions.Clear(); } } private void OnWindowResized(object? sender, (int Width, int Height) size) { CreateSurface(size.Width, size.Height); } private void OnWindowExposed(object? sender, EventArgs e) { _fullRedrawNeeded = true; } /// /// Marks the entire surface as needing redraw. /// public void InvalidateAll() { _fullRedrawNeeded = true; } /// /// Marks a specific region as needing redraw. /// Multiple regions are tracked and merged for efficiency. /// public void InvalidateRegion(SKRect region) { if (region.IsEmpty || region.Width <= 0 || region.Height <= 0) return; // Clamp to surface bounds region = SKRect.Intersect(region, new SKRect(0, 0, Width, Height)); if (region.IsEmpty) return; lock (_dirtyLock) { // If we have too many regions, just do a full redraw if (_dirtyRegions.Count >= MaxDirtyRegions) { _fullRedrawNeeded = true; _dirtyRegions.Clear(); return; } // Try to merge with existing regions for (int i = 0; i < _dirtyRegions.Count; i++) { var existing = _dirtyRegions[i]; if (ShouldMergeRegions(existing, region)) { _dirtyRegions[i] = SKRect.Union(existing, region); return; } } _dirtyRegions.Add(region); } } private bool ShouldMergeRegions(SKRect a, SKRect b) { // Check if regions overlap var intersection = SKRect.Intersect(a, b); if (intersection.IsEmpty) { // Check if they're adjacent (within a few pixels) var expanded = new SKRect(a.Left - 4, a.Top - 4, a.Right + 4, a.Bottom + 4); return expanded.IntersectsWith(b); } // Merge if intersection is significant relative to either region var intersectionArea = intersection.Width * intersection.Height; var aArea = a.Width * a.Height; var bArea = b.Width * b.Height; var minArea = Math.Min(aArea, bArea); return intersectionArea / minArea >= RegionMergeThreshold; } /// /// Renders the view tree, optionally using dirty region optimization. /// public void Render(SkiaView rootView) { if (_canvas == null || _bitmap == null) return; // Measure and arrange var availableSize = new SKSize(Width, Height); rootView.Measure(availableSize); rootView.Arrange(new SKRect(0, 0, Width, Height)); // Determine what to redraw List regionsToRedraw; bool isFullRedraw = _fullRedrawNeeded || !EnableDirtyRegionOptimization; lock (_dirtyLock) { if (isFullRedraw) { regionsToRedraw = new List { new SKRect(0, 0, Width, Height) }; _dirtyRegions.Clear(); _fullRedrawNeeded = false; } else if (_dirtyRegions.Count == 0) { // Nothing to redraw return; } else { regionsToRedraw = MergeOverlappingRegions(_dirtyRegions.ToList()); _dirtyRegions.Clear(); } } // Render dirty regions foreach (var region in regionsToRedraw) { RenderRegion(rootView, region, isFullRedraw); } // Draw popup overlays (always on top, full redraw) SkiaView.DrawPopupOverlays(_canvas); // Draw modal dialogs on top of everything if (LinuxDialogService.HasActiveDialog) { LinuxDialogService.DrawDialogs(_canvas, new SKRect(0, 0, Width, Height)); } _canvas.Flush(); // Present to X11 window PresentToWindow(); } private void RenderRegion(SkiaView rootView, SKRect region, bool isFullRedraw) { if (_canvas == null) return; _canvas.Save(); if (!isFullRedraw) { // Clip to dirty region for partial updates _canvas.ClipRect(region); } // Clear the region using var clearPaint = new SKPaint { Color = SKColors.White, Style = SKPaintStyle.Fill }; _canvas.DrawRect(region, clearPaint); // Draw the view tree (views will naturally clip to their bounds) rootView.Draw(_canvas); _canvas.Restore(); } private List MergeOverlappingRegions(List regions) { if (regions.Count <= 1) return regions; var merged = new List(); var used = new bool[regions.Count]; for (int i = 0; i < regions.Count; i++) { if (used[i]) continue; var current = regions[i]; used[i] = true; // Keep merging until no more merges possible bool didMerge; do { didMerge = false; for (int j = i + 1; j < regions.Count; j++) { if (used[j]) continue; if (ShouldMergeRegions(current, regions[j])) { current = SKRect.Union(current, regions[j]); used[j] = true; didMerge = true; } } } while (didMerge); merged.Add(current); } return merged; } private void PresentToWindow() { if (_bitmap == null) return; var pixels = _bitmap.GetPixels(); if (pixels == IntPtr.Zero) return; _window.DrawPixels(pixels, _imageInfo.Width, _imageInfo.Height, _imageInfo.RowBytes); } public SKCanvas? GetCanvas() => _canvas; protected virtual void Dispose(bool disposing) { if (!_disposed) { if (disposing) { _window.Resized -= OnWindowResized; _window.Exposed -= OnWindowExposed; _canvas?.Dispose(); _bitmap?.Dispose(); _backBuffer?.Dispose(); ResourceCache.Dispose(); if (Current == this) Current = null; } _disposed = true; } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } public class ResourceCache : IDisposable { private readonly Dictionary _typefaces = new(); private bool _disposed; public SKTypeface GetTypeface(string fontFamily, SKFontStyle style) { var key = $"{fontFamily}_{style.Weight}_{style.Width}_{style.Slant}"; if (!_typefaces.TryGetValue(key, out var typeface)) { typeface = SKTypeface.FromFamilyName(fontFamily, style) ?? SKTypeface.Default; _typefaces[key] = typeface; } return typeface; } public void Clear() { foreach (var tf in _typefaces.Values) tf.Dispose(); _typefaces.Clear(); } public void Dispose() { if (!_disposed) { Clear(); _disposed = true; } } }