// 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; } } }