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; private sealed record Entry(DesktopWebFrameKey Key, int[] 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 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.AsSpan(source,leftCount).CopyTo(framebuffer.Slice(frameOffset+(int)left,leftCount)); entry.Pixels.AsSpan(source+rightStart-(int)left,rightCount).CopyTo(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 { while (entries.Count > 0 && (entries.Count >= MaximumEntries || CachedBytes + bytes > MaximumBytes)) { CachedBytes -= (long)entries[0].Pixels.Length * sizeof(int); entries.RemoveAt(0); Evictions++; } entry = new(key, new int[checked(key.NativeWidth * key.NativeHeight)]); entries.Add(entry); CachedBytes += bytes; } for (int y = 0; y < key.NativeHeight; y++) framebuffer.Slice(checked((sourceY + y) * framebufferWidth + sourceX), key.NativeWidth) .CopyTo(entry.Pixels.AsSpan(y * key.NativeWidth, 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); } }