XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
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); }
}