namespace XFESystem.Desktop;
/// <summary>
/// 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.
/// </summary>
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);
/// <summary>
/// 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.
/// </summary>
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<int> 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<int> 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<Entry> 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<int> 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<int> 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); }
}
namespace XFESystem.Desktop;
/// <summary>
/// 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.
/// </summary>
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);
/// <summary>
/// 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.
/// </summary>
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<int> 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<int> 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<Entry> 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<int> 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<int> 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); }
}