namespace XFESystem.Desktop; /// /// A canvas-local cache of actual opaque web viewport pixels. Keys contain only /// values, never a page/runtime reference. Position is deliberately excluded so /// an unchanged viewport can be restored while its window moves. /// public interface IDesktopWebFrameCache { DesktopBounds WebPaintClip { get; } bool TryRestoreWebFrame(long pageId, long revision, long layoutVersion, DesktopBounds bounds, double scrollX, double scrollY); void StoreWebFrame(long pageId, long revision, long layoutVersion, DesktopBounds bounds, double scrollX, double scrollY); } public readonly record struct DesktopWebFrameKey(long PageId, long Revision, long LayoutVersion, int NativeWidth, int NativeHeight, int ScalePercent, double ScrollX, double ScrollY); /// /// Bounded LRU framebuffer storage used by native canvases and host pixel tests. /// Capture requires the entire viewport to be painted; restore copies only the /// intersection of the requested destination, framebuffer and current damage. /// All coordinates and dimensions in this class are native pixels. /// public sealed class DesktopWebFrameCache { public const int DefaultMaximumEntries = 6; public const long DefaultMaximumBytes = 32L * 1024 * 1024; // Cache capture can occur after a large interpreter has fragmented the // native heap. Never require one contiguous allocation for a viewport. // The payload bound excludes the managed array's small object header. public const int MaximumPixelChunkBytes = 64 * 1024; private sealed class PixelStorage { private const int ChunkPixels = MaximumPixelChunkBytes / sizeof(int); private readonly int[][] chunks; public int Length { get; } public int ChunkCount => chunks.Length; public int LargestChunkBytes => Math.Min(Length, ChunkPixels) * sizeof(int); public PixelStorage(int length) { Length = length; chunks = new int[(length + ChunkPixels - 1) / ChunkPixels][]; for (int index = 0; index < chunks.Length; index++) chunks[index] = new int[Math.Min(ChunkPixels, length - index * ChunkPixels)]; } public void CopyTo(int source, Span destination) { if (source < 0 || destination.Length > Length - source) throw new ArgumentOutOfRangeException(nameof(source)); while (!destination.IsEmpty) { int offset = source % ChunkPixels; int count = Math.Min(destination.Length, ChunkPixels - offset); chunks[source / ChunkPixels].AsSpan(offset, count).CopyTo(destination); source += count; destination = destination[count..]; } } public void CopyFrom(int destination, ReadOnlySpan source) { if (destination < 0 || source.Length > Length - destination) throw new ArgumentOutOfRangeException(nameof(destination)); while (!source.IsEmpty) { int offset = destination % ChunkPixels; int count = Math.Min(source.Length, ChunkPixels - offset); source[..count].CopyTo(chunks[destination / ChunkPixels].AsSpan(offset, count)); destination += count; source = source[count..]; } } } private sealed record Entry(DesktopWebFrameKey Key, PixelStorage Pixels); private readonly List entries = []; public DesktopWebFrameCache(long maximumBytes = DefaultMaximumBytes, int maximumEntries = DefaultMaximumEntries) { if (maximumBytes <= 0 || maximumBytes > DefaultMaximumBytes) throw new ArgumentOutOfRangeException(nameof(maximumBytes)); if (maximumEntries <= 0 || maximumEntries > DefaultMaximumEntries) throw new ArgumentOutOfRangeException(nameof(maximumEntries)); MaximumBytes = maximumBytes; MaximumEntries = maximumEntries; } public long MaximumBytes { get; } public int MaximumEntries { get; } public int Count => entries.Count; public long CachedBytes { get; private set; } public long RestoreHits { get; private set; } public long PixelsRestored { get; private set; } public long Captures { get; private set; } public long Evictions { get; private set; } public long PixelChunkAllocations { get; private set; } public long PixelStorageReuses { get; private set; } public int LargestPixelChunkAllocatedBytes { get; private set; } public bool TryRestore(DesktopWebFrameKey key, Span framebuffer, int framebufferWidth, int framebufferHeight, int destinationX, int destinationY, DesktopBounds clip, DesktopBounds preserved = default) { ValidateFramebuffer(framebuffer.Length, framebufferWidth, framebufferHeight); if (!ValidKey(key)) return false; int index = Find(key); if (index < 0) return false; var entry = entries[index]; Touch(index); RestoreHits++; long left = Math.Max(0L, Math.Max((long)destinationX, clip.X)); long top = Math.Max(0L, Math.Max((long)destinationY, clip.Y)); long right = Math.Min(framebufferWidth, Math.Min((long)destinationX + key.NativeWidth, (long)clip.X + Math.Max(0, clip.Width))); long bottom = Math.Min(framebufferHeight, Math.Min((long)destinationY + key.NativeHeight, (long)clip.Y + Math.Max(0, clip.Height))); if (right <= left || bottom <= top) return true; int width = (int)(right - left); int protectedX=Math.Clamp(preserved.X,0,framebufferWidth),protectedY=Math.Clamp(preserved.Y,0,framebufferHeight); int protectedRight=(int)Math.Clamp((long)preserved.X+Math.Max(0,preserved.Width),0,framebufferWidth); int protectedBottom=(int)Math.Clamp((long)preserved.Y+Math.Max(0,preserved.Height),0,framebufferHeight); preserved=new(protectedX,protectedY,Math.Max(0,protectedRight-protectedX),Math.Max(0,protectedBottom-protectedY)); for (int y = (int)top; y < bottom; y++) { int source = checked((int)(((long)y - destinationY) * key.NativeWidth + left - destinationX)); DesktopSceneMotionBuffer.RowSegments(preserved,(int)left,width,y,out int leftEnd,out int rightStart); int leftCount=leftEnd-(int)left,rightCount=(int)right-rightStart,frameOffset=checked(y*framebufferWidth); entry.Pixels.CopyTo(source,framebuffer.Slice(frameOffset+(int)left,leftCount)); entry.Pixels.CopyTo(source+rightStart-(int)left,framebuffer.Slice(frameOffset+rightStart,rightCount)); PixelsRestored+=leftCount+rightCount; } return true; } public bool Store(DesktopWebFrameKey key, ReadOnlySpan framebuffer, int framebufferWidth, int framebufferHeight, int sourceX, int sourceY, DesktopBounds clip, DesktopBounds preserved = default) { ValidateFramebuffer(framebuffer.Length, framebufferWidth, framebufferHeight); if (!ValidKey(key)) return false; long bytes = (long)key.NativeWidth * key.NativeHeight * sizeof(int); if (bytes > MaximumBytes || sourceX < 0 || sourceY < 0 || (long)sourceX + key.NativeWidth > framebufferWidth || (long)sourceY + key.NativeHeight > framebufferHeight || clip.Width <= 0 || clip.Height <= 0 || clip.X > sourceX || clip.Y > sourceY || (long)clip.X + clip.Width < (long)sourceX + key.NativeWidth || (long)clip.Y + clip.Height < (long)sourceY + key.NativeHeight) return false; // The protected pixels may belong to a foreground window. A lower // browser's paint deliberately skipped them, so they are not its image. if(preserved.Width>0&&preserved.Height>0&&(long)sourceX+key.NativeWidth>preserved.X&& (long)preserved.X+preserved.Width>sourceX&&(long)sourceY+key.NativeHeight>preserved.Y&& (long)preserved.Y+preserved.Height>sourceY)return false; int index = Find(key); Entry entry; if (index >= 0) { entry = entries[index]; Touch(index); } else { int pixelCount = checked(key.NativeWidth * key.NativeHeight); PixelStorage? reusedPixels = null; while (entries.Count > 0 && (entries.Count >= MaximumEntries || CachedBytes + bytes > MaximumBytes)) { var evicted = entries[0]; CachedBytes -= (long)evicted.Pixels.Length * sizeof(int); entries.RemoveAt(0); Evictions++; // New revisions and scroll positions usually retain the same // viewport geometry. Reuse only a buffer the normal LRU/byte // policy already evicted; every pixel is overwritten below. // This preserves all surviving keys and avoids another large // contiguous native allocation on every editor update. if (reusedPixels is null && evicted.Pixels.Length == pixelCount) reusedPixels = evicted.Pixels; } if (reusedPixels is null) { reusedPixels = new(pixelCount); PixelChunkAllocations += reusedPixels.ChunkCount; LargestPixelChunkAllocatedBytes = Math.Max(LargestPixelChunkAllocatedBytes, reusedPixels.LargestChunkBytes); } else PixelStorageReuses++; entry = new(key, reusedPixels); entries.Add(entry); CachedBytes += bytes; } for (int y = 0; y < key.NativeHeight; y++) entry.Pixels.CopyFrom(y * key.NativeWidth, framebuffer.Slice(checked((sourceY + y) * framebufferWidth + sourceX), key.NativeWidth)); Captures++; return true; } private static bool ValidKey(DesktopWebFrameKey key) => key.NativeWidth > 0 && key.NativeHeight > 0 && key.ScalePercent > 0 && (long)key.NativeWidth * key.NativeHeight <= int.MaxValue / sizeof(int) && double.IsFinite(key.ScrollX) && double.IsFinite(key.ScrollY); private static void ValidateFramebuffer(int length, int width, int height) { if (width <= 0 || height <= 0 || (long)width * height > length) throw new ArgumentException("Invalid framebuffer dimensions."); } private int Find(DesktopWebFrameKey key) { for (int i = 0; i < entries.Count; i++) if (entries[i].Key == key) return i; return -1; } private void Touch(int index) { if (index == entries.Count - 1) return; var entry = entries[index]; entries.RemoveAt(index); entries.Add(entry); } }