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

XFESystem

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

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Diagnostics;
using XFESystem.Desktop;

namespace XFESystem.Platform;

/// <summary>CPU renderer with platform-owned pixels, font caches and pointer-free scene snapshots.</summary>
public sealed class CpuFramebufferCanvas : IBrandedDesktopCanvas, IDesktopIconCanvas, IDesktopCoverageCanvas,
    IDesktopSceneMotionCanvas, IDesktopWebCanvas, IDesktopWebFrameCache, IDesktopPresentedPixelsCanvas,
    IDesktopRenderYieldCanvas, IEditorPreparationCanvasFactory, IEditorPreparationCanvasHandoff, IDesktopWebTextPreparationCanvas
{
    private readonly IFramebufferPresenter presenter;
    private readonly int[] pixels, scene, presented;
    private PlatformFonts fonts;
    private bool preparationOnly, preparationTransferred;
    private readonly DesktopSceneMotionBuffer motion;
    private readonly DesktopWebFrameCache webFrames = new();
    private readonly Stack<DesktopBounds> clips = [];
    private readonly Func<long> timestamp;
    private readonly Func<bool>? yieldRequested;
    private DesktopBounds clip;
    private readonly Dictionary<(string Text, double Size, string Family, int Weight), PlatformTextMask> textMasks = [];
    private long maskBytes;

    public int NativeWidth => presenter.Width;
    public int NativeHeight => presenter.Height;
    public int Width => (int)((long)NativeWidth * 100 / ScalePercent);
    public int Height => (int)((long)NativeHeight * 100 / ScalePercent);
    public int ScalePercent { get; }
    public bool SupportsSceneCache => true;
    public bool RenderYieldRequested => yieldRequested?.Invoke() ?? false;
    public long FramesPresented { get; private set; }
    public long LastPresentTicks { get; private set; }
    public ReadOnlyMemory<int> Pixels => pixels;
    public PlatformFonts Fonts => fonts;
    public int CachedTextMaskCount => textMasks.Count;
    public long CachedTextMaskBytes => maskBytes;

    public CpuFramebufferCanvas(IFramebufferPresenter presenter, PlatformFonts? fonts = null, int scalePercent = 100,
        Func<long>? timestamp = null, Func<bool>? yieldRequested = null)
    {
        ArgumentNullException.ThrowIfNull(presenter);
        PixelGeometry.ValidateSize(presenter.Width, presenter.Height);
        if (scalePercent is < 100 or > 400 || presenter.Width * 100L / scalePercent < 1 || presenter.Height * 100L / scalePercent < 1)
            throw new ArgumentOutOfRangeException(nameof(scalePercent));
        this.presenter = presenter; this.fonts = fonts ?? PlatformFonts.CreateBundled();
        ScalePercent = scalePercent; this.timestamp = timestamp ?? Stopwatch.GetTimestamp; this.yieldRequested = yieldRequested;
        pixels = new int[checked(NativeWidth * NativeHeight)]; scene = new int[pixels.Length]; presented = new int[pixels.Length];
        motion = new(pixels, scene, NativeWidth, NativeHeight); clip = new(0, 0, NativeWidth, NativeHeight);
    }

    public int TextWidth(string text) => (int)Math.Ceiling(fonts.Measure(text, 13, "monospace", 400));
    public void Text(string text, uint rgb, int x, int y)
    {
        ArgumentNullException.ThrowIfNull(text);
        foreach (string line in text.Replace("\r", "", StringComparison.Ordinal).Split('\n'))
        { WebText(line, rgb, x, y, 13, "monospace", 400); y += (int)Math.Ceiling(MeasureWebLineHeight(13, "monospace", 400)); }
    }
    public void Fill(uint rgb, int x, int y, int width, int height)
    {
        var rect = PixelGeometry.Intersect(NativeRect(new(x, y, width, height)), clip);
        for (int row = rect.Y; row < rect.Y + rect.Height; row++)
        {
            motion.RowSegments(rect.X, rect.Width, row, out int leftEnd, out int rightStart);
            pixels.AsSpan(row * NativeWidth + rect.X, leftEnd - rect.X).Fill((int)(rgb & 0xFFFFFF));
            pixels.AsSpan(row * NativeWidth + rightStart, rect.X + rect.Width - rightStart).Fill((int)(rgb & 0xFFFFFF));
        }
    }
    public void Line(uint rgb, int x1, int y1, int x2, int y2)
    {
        double ax = x1 * (ScalePercent / 100d), ay = y1 * (ScalePercent / 100d);
        double bx = x2 * (ScalePercent / 100d), by = y2 * (ScalePercent / 100d);
        if (!ClipLine(ref ax, ref ay, ref bx, ref by)) return;
        int x = (int)Math.Round(ax), y = (int)Math.Round(ay), endX = (int)Math.Round(bx), endY = (int)Math.Round(by);
        int dx = Math.Abs(endX - x), sx = x < endX ? 1 : -1, dy = -Math.Abs(endY - y), sy = y < endY ? 1 : -1, error = dx + dy;
        while (true)
        {
            SetPixel(rgb, x, y, 255);
            if (x == endX && y == endY) break;
            int twice = error * 2;
            if (twice >= dy) { error += dy; x += sx; }
            if (twice <= dx) { error += dx; y += sy; }
        }
    }
    public void BlendCoverage(uint rgb, int nativeX, int nativeY, int width, int height, ReadOnlySpan<byte> coverage)
    {
        if (width <= 0 || height <= 0 || (long)width * height != coverage.Length)
            throw new ArgumentException("Coverage must contain exactly width * height bytes.", nameof(coverage));
        var rect = PixelGeometry.Intersect(new(nativeX, nativeY, width, height), clip);
        for (int y = rect.Y; y < rect.Y + rect.Height; y++)
            for (int x = rect.X; x < rect.X + rect.Width; x++)
                SetPixel(rgb, x, y, coverage[checked((int)(((long)y - nativeY) * width + x - nativeX))]);
    }
    public void BeginScene(DesktopBounds damage) { motion.End(); clips.Clear(); clip = NativeRect(damage); }
    public void PushClip(DesktopBounds bounds) { if (clips.Count >= 128) throw new InvalidOperationException("Clip nesting budget exceeded."); clips.Push(clip); clip = PixelGeometry.Intersect(clip, NativeRect(bounds)); }
    public void PopClip() { if (!clips.TryPop(out clip)) throw new InvalidOperationException("Unbalanced web clipping."); }
    public void SaveScene() => SaveScene(new(0, 0, Width, Height));
    public void SaveScene(DesktopBounds damage) => Copy(pixels, scene, NativeRect(damage));
    public void RestoreSceneRegion(DesktopBounds bounds) => Copy(scene, pixels, NativeRect(bounds));
    public void PreserveTranslatedSceneRegion(DesktopBounds source, int destinationX, int destinationY) => PreserveTranslatedSceneRegion(source, destinationX, destinationY, false);
    public void PreserveTranslatedSceneRegion(DesktopBounds source, int destinationX, int destinationY, bool unchangedInScanout)
    {
        // Clip source and destination together in the shared, tested motion buffer.
        int sx = ScaledCoordinate(source.X), sy = ScaledCoordinate(source.Y);
        int sw = ScaledLength(source.Width), sh = ScaledLength(source.Height);
        motion.Preserve(new(sx, sy, sw, sh), ScaledCoordinate(destinationX), ScaledCoordinate(destinationY), unchangedInScanout);
    }
    public void EndScenePreservation() => motion.End();
    public void AbortScenePreservation() => motion.End();
    public void Present() => Present(new(0, 0, Width, Height));
    public void Present(DesktopBounds damage)
    {
        var native = NativeRect(damage);
        presenter.Present(pixels, native);
        Copy(pixels, presented, native);
        LastPresentTicks = timestamp(); FramesPresented++;
    }
    public bool TryFingerprintPresentedPixels(DesktopBounds nativeBounds, out DesktopPresentedPixelsFingerprint result)
        => DesktopPresentedPixels.TryFingerprint(presented, NativeWidth, NativeHeight, NativeWidth, nativeBounds, FramesPresented, out result);
    public double MeasureWebText(string text, double fontSize, string fontFamily, int fontWeight) => fonts.Measure(text, fontSize, fontFamily, fontWeight);
    public double MeasureWebLineHeight(double fontSize, string fontFamily, int fontWeight) => fonts.LineHeight(fontSize, fontFamily, fontWeight);
    public (int Width, int Height, byte[] Alpha) RasterizeWebText(string text, double fontSize, string fontFamily, int fontWeight)
    { var mask = Mask(text, fontSize, fontFamily, fontWeight); return (mask.Width, mask.Height, (byte[])mask.Alpha.Clone()); }
    public IDesktopPreparedWebText PrepareWebText(string text, double fontSize, string fontFamily, int fontWeight, double opacity)
    {
        if (!double.IsFinite(opacity) || opacity < 0 || opacity > 1) throw new ArgumentOutOfRangeException(nameof(opacity));
        var mask = Mask(text, fontSize * ScalePercent / 100, fontFamily, fontWeight);
        // Neither the cache nor this mask is mutated after construction. The
        // worker retains its fonts; publication transfers only opaque coverage.
        byte[] alpha = mask.Alpha;
        if (opacity != 1)
        {
            alpha = new byte[mask.Alpha.Length];
            for (int index = 0; index < alpha.Length; index++) alpha[index] = (byte)Math.Round(mask.Alpha[index] * opacity);
        }
        return new PreparedText(ScalePercent, fonts.LineHeight(fontSize, fontFamily, fontWeight), mask.Width, mask.Height, alpha);
    }
    private sealed class PreparedText(int scalePercent, double lineHeight, int width, int height, byte[] coverage) : IDesktopPreparedWebText
    {
        public int ScalePercent => scalePercent;
        public double LineHeight => lineHeight;
        public void Paint(IDesktopCoverageCanvas target, uint rgb, double x, double y)
        {
            if (target.ScalePercent != scalePercent) throw new ArgumentException("Prepared text scale differs from the destination.", nameof(target));
            double nativeX = x * scalePercent / 100, nativeY = y * scalePercent / 100;
            if (!double.IsFinite(nativeX) || !double.IsFinite(nativeY) || nativeX < int.MinValue || nativeX > int.MaxValue || nativeY < int.MinValue || nativeY > int.MaxValue) return;
            target.BlendCoverage(rgb, (int)Math.Round(nativeX), (int)Math.Round(nativeY), width, height, coverage);
        }
    }
    public void WebText(string text, uint color, double x, double y, double fontSize, string fontFamily, int fontWeight, double opacity = 1)
    {
        if (!double.IsFinite(x) || !double.IsFinite(y) || !double.IsFinite(opacity) || opacity < 0 || opacity > 1)
            throw new ArgumentOutOfRangeException(nameof(opacity));
        if (text.Length == 0 || opacity == 0) return;
        double nx = x * ScalePercent / 100, ny = y * ScalePercent / 100;
        if (nx < int.MinValue || nx > int.MaxValue || ny < int.MinValue || ny > int.MaxValue) return;
        var mask = Mask(text, fontSize * ScalePercent / 100, fontFamily, fontWeight);
        int px = (int)Math.Round(nx), py = (int)Math.Round(ny);
        var rect = PixelGeometry.Intersect(new(px, py, mask.Width, mask.Height), clip);
        for (int row = rect.Y; row < rect.Y + rect.Height; row++)
            for (int col = rect.X; col < rect.X + rect.Width; col++)
                SetPixel(color, col, row, (byte)Math.Round(mask.Alpha[(row - py) * mask.Width + col - px] * opacity));
    }
    public DesktopBounds WebPaintClip => new(clip.X * 100 / ScalePercent, clip.Y * 100 / ScalePercent,
        (int)Math.Ceiling((clip.X + clip.Width) * 100d / ScalePercent) - clip.X * 100 / ScalePercent,
        (int)Math.Ceiling((clip.Y + clip.Height) * 100d / ScalePercent) - clip.Y * 100 / ScalePercent);
    public bool TryRestoreWebFrame(long pageId, long revision, long layoutVersion, DesktopBounds bounds, double scrollX, double scrollY)
    {
        if (!RawNativeRect(bounds, out var native)) return false;
        return webFrames.TryRestore(new(pageId, revision, layoutVersion, native.Width, native.Height, ScalePercent, scrollX, scrollY),
            pixels, NativeWidth, NativeHeight, native.X, native.Y, clip, motion.PreservedBounds);
    }
    public void StoreWebFrame(long pageId, long revision, long layoutVersion, DesktopBounds bounds, double scrollX, double scrollY)
    {
        if (!RawNativeRect(bounds, out var native)) return;
        webFrames.Store(new(pageId, revision, layoutVersion, native.Width, native.Height, ScalePercent, scrollX, scrollY),
            pixels, NativeWidth, NativeHeight, native.X, native.Y, clip, motion.PreservedBounds);
    }
    public void BrandIcon(int x, int y, int size) => Image(Images.Brand, 0, x, y, size);
    public void DrawIcon(DesktopIcon icon, int x, int y, int size)
    {
        if (icon == DesktopIcon.Settings)
        {
            int middleX = x + size / 2, middleY = y + size / 2;
            for (int i = 0; i < 16; i++)
            {
                double angle = Math.PI * i / 8, next = Math.PI * (i + 1) / 8;
                int radius = i % 2 == 0 ? size * 2 / 5 : size / 3;
                int nextRadius = i % 2 == 0 ? size / 3 : size * 2 / 5;
                Line(0x91BFFF, middleX + (int)(Math.Cos(angle) * radius), middleY + (int)(Math.Sin(angle) * radius),
                    middleX + (int)(Math.Cos(next) * nextRadius), middleY + (int)(Math.Sin(next) * nextRadius));
            }
            for (int i = 0; i < 24; i++)
            {
                double angle = Math.PI * i / 12, next = Math.PI * (i + 1) / 12;
                Line(0x91BFFF, middleX + (int)(Math.Cos(angle) * size / 8), middleY + (int)(Math.Sin(angle) * size / 8),
                    middleX + (int)(Math.Cos(next) * size / 8), middleY + (int)(Math.Sin(next) * size / 8));
            }
            return;
        }
        if ((int)icon is < 0 or >= 9) throw new ArgumentOutOfRangeException(nameof(icon));
        Image(Images.Atlas, (int)icon * 64 * 64 * 4, x, y, size);
    }
    public IDesktopCanvas CreateEditorPreparationCanvas()
    {
        // The worker needs real fonts/metrics; it does not need another full-screen scanout/scene allocation.
        int edge = (ScalePercent + 99) / 100;
        return new CpuFramebufferCanvas(new ArrayFramebufferPresenter(edge, edge), fonts.CreateIndependent(), ScalePercent, timestamp) { preparationOnly = true };
    }
    public void PrepareEditorText(string text, double fontSize, string fontFamily, int fontWeight)
    {
        if (!preparationOnly || preparationTransferred) throw new InvalidOperationException("Font preparation requires an exclusively owned preparation canvas.");
        _ = Mask(text, fontSize * ScalePercent / 100, fontFamily, fontWeight);
    }
    public bool TryCompleteEditorPreparationHandoff(IDesktopCanvas target)
    {
        if (!preparationOnly || preparationTransferred || target is not CpuFramebufferCanvas destination || destination.preparationOnly ||
            ScalePercent != destination.ScalePercent || !fonts.HasSameAssets(destination.fonts)) return false;
        // Publication/ownership transfer is supplied by the existing editor service before this UI-only call.
        destination.fonts = fonts;
        foreach (var entry in textMasks)
        {
            if (destination.textMasks.ContainsKey(entry.Key)) continue;
            if (destination.textMasks.Count == 1024 || destination.maskBytes + entry.Value.Alpha.Length > 2 * 1024 * 1024) break;
            destination.textMasks.Add(entry.Key, entry.Value); destination.maskBytes += entry.Value.Alpha.Length;
        }
        textMasks.Clear(); maskBytes = 0; preparationTransferred = true;
        // The transferred font cache is no longer usable by this preparation canvas.
        fonts = fonts.CreateIndependent();
        return true;
    }

    private static class Images
    {
        internal static readonly byte[] Brand = PlatformResources.Read("PlatformImages/Brand.rgba");
        internal static readonly byte[] Atlas = PlatformResources.Read("PlatformImages/Icons.rgba");
    }
    private void Image(byte[] rgba, int offset, int x, int y, int size)
    {
        if (size <= 0) return;
        if (offset < 0 || offset + 64 * 64 * 4 > rgba.Length) throw new InvalidDataException("The icon asset is incomplete.");
        int px = ScaledCoordinate(x), py = ScaledCoordinate(y), edge = ScaledLength(size);
        if (edge <= 0) return;
        var rect = PixelGeometry.Intersect(new(px, py, edge, edge), clip);
        for (int row = rect.Y; row < rect.Y + rect.Height; row++)
            for (int col = rect.X; col < rect.X + rect.Width; col++)
            {
                int at = offset + ((int)((long)(row - py) * 64 / edge) * 64 + (int)((long)(col - px) * 64 / edge)) * 4;
                SetPixel((uint)(rgba[at] << 16 | rgba[at + 1] << 8 | rgba[at + 2]), col, row, rgba[at + 3]);
            }
    }
    private PlatformTextMask Mask(string text, double size, string family, int weight)
    {
        var key = (text, size, family, weight); if (textMasks.TryGetValue(key, out var cached)) return cached;
        var mask = fonts.Rasterize(text, size, family, weight);
        if (text.Length <= 4096 && textMasks.Count < 1024 && maskBytes + mask.Alpha.Length <= 2 * 1024 * 1024)
        { textMasks.Add(key, mask); maskBytes += mask.Alpha.Length; }
        return mask;
    }
    private void SetPixel(uint rgb, int x, int y, byte alpha)
    {
        if (alpha == 0 || !clip.Contains(x, y) || motion.PreservedBounds.Contains(x, y)) return;
        int at = y * NativeWidth + x;
        if (alpha == 255) { pixels[at] = (int)(rgb & 0xFFFFFF); return; }
        uint before = (uint)pixels[at]; int inverse = 255 - alpha;
        uint r = (uint)(((rgb >> 16 & 255) * alpha + (before >> 16 & 255) * inverse + 127) / 255);
        uint g = (uint)(((rgb >> 8 & 255) * alpha + (before >> 8 & 255) * inverse + 127) / 255);
        uint b = (uint)(((rgb & 255) * alpha + (before & 255) * inverse + 127) / 255);
        pixels[at] = (int)(r << 16 | g << 8 | b);
    }
    private void Copy(int[] source, int[] target, DesktopBounds rect)
    {
        for (int row = rect.Y; row < rect.Y + rect.Height; row++)
            source.AsSpan(row * NativeWidth + rect.X, rect.Width).CopyTo(target.AsSpan(row * NativeWidth + rect.X, rect.Width));
    }
    private int ScaledCoordinate(int value) => (int)Math.Clamp((long)value * ScalePercent / 100, int.MinValue, int.MaxValue);
    private int ScaledLength(int value) => (int)Math.Clamp((long)Math.Max(0, value) * ScalePercent / 100, 0, int.MaxValue);
    private DesktopBounds NativeRect(DesktopBounds bounds)
    {
        if (bounds.Width <= 0 || bounds.Height <= 0) return default;
        long left = (long)bounds.X * ScalePercent / 100, top = (long)bounds.Y * ScalePercent / 100;
        long right = ((long)bounds.X + Math.Max(0, bounds.Width)) * ScalePercent / 100;
        long bottom = ((long)bounds.Y + Math.Max(0, bounds.Height)) * ScalePercent / 100;
        int x = (int)Math.Clamp(left, 0, NativeWidth), y = (int)Math.Clamp(top, 0, NativeHeight);
        if ((long)bounds.X + Math.Max(0, bounds.Width) >= Width) right = NativeWidth;
        if ((long)bounds.Y + Math.Max(0, bounds.Height) >= Height) bottom = NativeHeight;
        int r = (int)Math.Clamp(right, 0, NativeWidth), b = (int)Math.Clamp(bottom, 0, NativeHeight);
        return new(x, y, Math.Max(0, r - x), Math.Max(0, b - y));
    }
    private bool RawNativeRect(DesktopBounds bounds, out DesktopBounds native)
    {
        long x = (long)bounds.X * ScalePercent / 100, y = (long)bounds.Y * ScalePercent / 100;
        long w = (long)bounds.Width * ScalePercent / 100, h = (long)bounds.Height * ScalePercent / 100;
        native = default;
        if (x < int.MinValue || x > int.MaxValue || y < int.MinValue || y > int.MaxValue || w < 1 || w > int.MaxValue || h < 1 || h > int.MaxValue) return false;
        native = new((int)x, (int)y, (int)w, (int)h); return true;
    }
    private bool ClipLine(ref double ax, ref double ay, ref double bx, ref double by)
    {
        if (clip.Width == 0 || clip.Height == 0) return false;
        double dx = bx - ax, dy = by - ay, begin = 0, end = 1;
        bool Edge(double p, double q)
        {
            if (p == 0) return q >= 0;
            double ratio = q / p;
            if (p < 0) { if (ratio > end) return false; begin = Math.Max(begin, ratio); }
            else { if (ratio < begin) return false; end = Math.Min(end, ratio); }
            return true;
        }
        if (!Edge(-dx, ax - clip.X) || !Edge(dx, clip.X + clip.Width - 1 - ax) ||
            !Edge(-dy, ay - clip.Y) || !Edge(dy, clip.Y + clip.Height - 1 - ay)) return false;
        bx = ax + end * dx; by = ay + end * dy; ax += begin * dx; ay += begin * dy;
        return true;
    }
}