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;
}
}
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;
}
}