// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Rendering; /// /// Caches rendered content for views that don't change frequently. /// Improves performance by avoiding redundant rendering. /// public class RenderCache : IDisposable { private readonly Dictionary _cache = new(); private readonly object _lock = new(); private long _maxCacheSize = 50 * 1024 * 1024; // 50 MB default private long _currentCacheSize; private bool _disposed; /// /// Gets or sets the maximum cache size in bytes. /// public long MaxCacheSize { get => _maxCacheSize; set { _maxCacheSize = Math.Max(1024 * 1024, value); // Minimum 1 MB TrimCache(); } } /// /// Gets the current cache size in bytes. /// public long CurrentCacheSize => _currentCacheSize; /// /// Gets the number of cached items. /// public int CachedItemCount { get { lock (_lock) { return _cache.Count; } } } /// /// Tries to get a cached bitmap for the given key. /// public bool TryGet(string key, out SKBitmap? bitmap) { lock (_lock) { if (_cache.TryGetValue(key, out var entry)) { entry.LastAccessed = DateTime.UtcNow; entry.AccessCount++; bitmap = entry.Bitmap; return true; } } bitmap = null; return false; } /// /// Caches a bitmap with the given key. /// public void Set(string key, SKBitmap bitmap) { if (bitmap == null) return; long bitmapSize = bitmap.ByteCount; // Don't cache if bitmap is larger than max size if (bitmapSize > _maxCacheSize) { return; } lock (_lock) { // Remove existing entry if present if (_cache.TryGetValue(key, out var existing)) { _currentCacheSize -= existing.Size; existing.Bitmap?.Dispose(); } // Create copy of bitmap for cache var cachedBitmap = bitmap.Copy(); if (cachedBitmap == null) return; var entry = new CacheEntry { Key = key, Bitmap = cachedBitmap, Size = bitmapSize, Created = DateTime.UtcNow, LastAccessed = DateTime.UtcNow, AccessCount = 1 }; _cache[key] = entry; _currentCacheSize += bitmapSize; // Trim cache if needed TrimCache(); } } /// /// Invalidates a cached entry. /// public void Invalidate(string key) { lock (_lock) { if (_cache.TryGetValue(key, out var entry)) { _currentCacheSize -= entry.Size; entry.Bitmap?.Dispose(); _cache.Remove(key); } } } /// /// Invalidates all entries matching a prefix. /// public void InvalidatePrefix(string prefix) { lock (_lock) { var keysToRemove = _cache.Keys .Where(k => k.StartsWith(prefix, StringComparison.Ordinal)) .ToList(); foreach (var key in keysToRemove) { if (_cache.TryGetValue(key, out var entry)) { _currentCacheSize -= entry.Size; entry.Bitmap?.Dispose(); _cache.Remove(key); } } } } /// /// Clears all cached content. /// public void Clear() { lock (_lock) { foreach (var entry in _cache.Values) { entry.Bitmap?.Dispose(); } _cache.Clear(); _currentCacheSize = 0; } } /// /// Renders content with caching. /// public SKBitmap GetOrCreate(string key, int width, int height, Action render) { // Check cache first if (TryGet(key, out var cached) && cached != null && cached.Width == width && cached.Height == height) { return cached; } // Create new bitmap var bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul); using (var canvas = new SKCanvas(bitmap)) { canvas.Clear(SKColors.Transparent); render(canvas); } // Cache it Set(key, bitmap); return bitmap; } private void TrimCache() { if (_currentCacheSize <= _maxCacheSize) return; // Remove least recently used entries until under limit var entries = _cache.Values .OrderBy(e => e.LastAccessed) .ThenBy(e => e.AccessCount) .ToList(); foreach (var entry in entries) { if (_currentCacheSize <= _maxCacheSize * 0.8) // Target 80% usage { break; } _currentCacheSize -= entry.Size; entry.Bitmap?.Dispose(); _cache.Remove(entry.Key); } } public void Dispose() { if (_disposed) return; _disposed = true; Clear(); } private class CacheEntry { public string Key { get; set; } = string.Empty; public SKBitmap? Bitmap { get; set; } public long Size { get; set; } public DateTime Created { get; set; } public DateTime LastAccessed { get; set; } public int AccessCount { get; set; } } } /// /// Provides layered rendering for separating static and dynamic content. /// public class LayeredRenderer : IDisposable { private readonly Dictionary _layers = new(); private readonly object _lock = new(); private bool _disposed; /// /// Gets or creates a render layer. /// public RenderLayer GetLayer(int zIndex) { lock (_lock) { if (!_layers.TryGetValue(zIndex, out var layer)) { layer = new RenderLayer(zIndex); _layers[zIndex] = layer; } return layer; } } /// /// Removes a render layer. /// public void RemoveLayer(int zIndex) { lock (_lock) { if (_layers.TryGetValue(zIndex, out var layer)) { layer.Dispose(); _layers.Remove(zIndex); } } } /// /// Composites all layers onto the target canvas. /// public void Composite(SKCanvas canvas, SKRect bounds) { lock (_lock) { foreach (var layer in _layers.Values.OrderBy(l => l.ZIndex)) { layer.DrawTo(canvas, bounds); } } } /// /// Invalidates all layers. /// public void InvalidateAll() { lock (_lock) { foreach (var layer in _layers.Values) { layer.Invalidate(); } } } public void Dispose() { if (_disposed) return; _disposed = true; lock (_lock) { foreach (var layer in _layers.Values) { layer.Dispose(); } _layers.Clear(); } } } /// /// Represents a single render layer with its own bitmap buffer. /// public class RenderLayer : IDisposable { private SKBitmap? _bitmap; private SKCanvas? _canvas; private bool _isDirty = true; private SKRect _bounds; private bool _disposed; /// /// Gets the Z-index of this layer. /// public int ZIndex { get; } /// /// Gets whether this layer needs to be redrawn. /// public bool IsDirty => _isDirty; /// /// Gets or sets whether this layer is visible. /// public bool IsVisible { get; set; } = true; /// /// Gets or sets the layer opacity (0-1). /// public float Opacity { get; set; } = 1f; public RenderLayer(int zIndex) { ZIndex = zIndex; } /// /// Prepares the layer for rendering. /// public SKCanvas BeginDraw(SKRect bounds) { if (_bitmap == null || _bounds != bounds) { _bitmap?.Dispose(); _canvas?.Dispose(); int width = Math.Max(1, (int)bounds.Width); int height = Math.Max(1, (int)bounds.Height); _bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul); _canvas = new SKCanvas(_bitmap); _bounds = bounds; } _canvas!.Clear(SKColors.Transparent); _isDirty = false; return _canvas; } /// /// Marks the layer as needing redraw. /// public void Invalidate() { _isDirty = true; } /// /// Draws this layer to the target canvas. /// public void DrawTo(SKCanvas canvas, SKRect bounds) { if (!IsVisible || _bitmap == null) return; using var paint = new SKPaint { Color = SKColors.White.WithAlpha((byte)(Opacity * 255)) }; canvas.DrawBitmap(_bitmap, bounds.Left, bounds.Top, paint); } public void Dispose() { if (_disposed) return; _disposed = true; _canvas?.Dispose(); _bitmap?.Dispose(); } } /// /// Provides text rendering optimization with glyph caching. /// public class TextRenderCache : IDisposable { private readonly Dictionary _cache = new(); private readonly object _lock = new(); private int _maxEntries = 500; private bool _disposed; /// /// Gets or sets the maximum number of cached text entries. /// public int MaxEntries { get => _maxEntries; set => _maxEntries = Math.Max(10, value); } /// /// Gets a cached text bitmap or creates one. /// public SKBitmap GetOrCreate(string text, SKPaint paint) { var key = new TextCacheKey(text, paint); lock (_lock) { if (_cache.TryGetValue(key, out var cached)) { return cached; } // Create text bitmap var bounds = new SKRect(); paint.MeasureText(text, ref bounds); int width = Math.Max(1, (int)Math.Ceiling(bounds.Width) + 2); int height = Math.Max(1, (int)Math.Ceiling(bounds.Height) + 2); var bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul); using (var canvas = new SKCanvas(bitmap)) { canvas.Clear(SKColors.Transparent); canvas.DrawText(text, -bounds.Left + 1, -bounds.Top + 1, paint); } // Trim cache if needed if (_cache.Count >= _maxEntries) { var oldest = _cache.First(); oldest.Value.Dispose(); _cache.Remove(oldest.Key); } _cache[key] = bitmap; return bitmap; } } /// /// Clears all cached text. /// public void Clear() { lock (_lock) { foreach (var entry in _cache.Values) { entry.Dispose(); } _cache.Clear(); } } public void Dispose() { if (_disposed) return; _disposed = true; Clear(); } private readonly struct TextCacheKey : IEquatable { private readonly string _text; private readonly float _textSize; private readonly SKColor _color; private readonly int _weight; private readonly int _hashCode; public TextCacheKey(string text, SKPaint paint) { _text = text; _textSize = paint.TextSize; _color = paint.Color; _weight = paint.Typeface?.FontWeight ?? (int)SKFontStyleWeight.Normal; _hashCode = HashCode.Combine(_text, _textSize, _color, _weight); } public bool Equals(TextCacheKey other) { return _text == other._text && Math.Abs(_textSize - other._textSize) < 0.001f && _color == other._color && _weight == other._weight; } public override bool Equals(object? obj) => obj is TextCacheKey other && Equals(other); public override int GetHashCode() => _hashCode; } }