using System.Drawing;
using System.Diagnostics;
using Cosmos.Kernel.HAL.Devices.Display;
using Cosmos.Kernel.System.Graphics;
using Cosmos.Kernel.System.Graphics.Fonts;
using XFESystem.Desktop;
namespace XFESystem.Kernel;
/// <summary>Native pixels, cached glyph coverage, scene restoration and bounded damaged scanout.</summary>
internal sealed class CanvasAdapter : IBrandedDesktopCanvas, IDesktopIconCanvas, IDesktopCoverageCanvas, IDesktopSceneMotionCanvas, IDesktopWebCanvas, IDesktopWebFrameCache, IDesktopRenderYieldCanvas, IDesktopPresentedPixelsCanvas, IEditorPreparationCanvasHandoff
{
private readonly Canvas canvas;
private readonly bool preparationOnly;
private CanvasAdapter? preparationHandoffTarget;
private readonly TrueTypeFont font;
private readonly int scale;
private readonly int[] pixels, scene;
private readonly DesktopSceneMotionBuffer motion;
private readonly DesktopWebFrameCache webFrames = new();
private readonly byte[] unicode = ImmutableAssets.Unicode;
private readonly byte[] brand = ImmutableAssets.Brand;
private readonly byte[] atlas = ImmutableAssets.Atlas;
private readonly Dictionary<char, Glyph> glyphs = [];
private readonly Dictionary<(int Icon, int Size), int[]> iconCache = [];
private readonly IDisplay? scanout;
private readonly DisplayDevice? device;
private PixelRect clip;
private readonly Stack<PixelRect> webClips = [];
private readonly Dictionary<(char Character, int Size, int Face, bool Bold), WebGlyphCoverage> webGlyphs = [];
private readonly Dictionary<int, WebFontFace> webFonts = [];
private readonly Dictionary<(string Family,int Weight),WebFontFace> webFontRequests=[];
private readonly Dictionary<(int Primary,char Character,bool Bold),WebFontFace> webFallbackFaces=[];
private readonly Dictionary<(int Face,char Character),bool> webGlyphPresence=[];
private readonly Dictionary<(int Face,bool Bold,char Character,double Size),double> webAdvances=[];
private readonly Dictionary<(int Face,char Left,char Right,int Size),double> webKernings=[];
private readonly Dictionary<(string Text,double Size,string Family,int Weight),double> webTextWidths=[];
private readonly Dictionary<(string Text,double Size,string Family,int Weight),WebTextCoverage> webTextMasks=[];
private int webMeasuredCharacters,webMaskBytes;
private sealed record WebFontFace(int Id,string Family,TrueTypeFont Font,NativeWebFontMetrics Metrics,bool SyntheticBold);
private sealed record WebGlyphCoverage(int Width,int Height,int OffsetX,byte[] Alpha);
private sealed record WebTextCoverage(int Width,int Height,byte[] Alpha);
private DesktopBounds unchangedScanout;
public long FramesPresented { get; private set; }
public long PixelsPresented { get; private set; }
public long PartialFrames { get; private set; }
public long FullCopyFallbackFrames { get; private set; }
public long LastPresentTicks { get; private set; }
public int CachedGlyphCount => glyphs.Count;
public int CachedWebTextMaskCount => webTextMasks.Count;
public int CachedWebTextMaskBytes => webMaskBytes;
public long WebTextMeasureCacheHits { get; private set; }
public long WebTextMaskCacheHits { get; private set; }
public int CachedWebFrameCount => webFrames.Count;
public long CachedWebFrameBytes => webFrames.CachedBytes;
public long WebFrameRestoreHits => webFrames.RestoreHits;
public long WebFramePixelsRestored => webFrames.PixelsRestored;
public long WebFrameCaptures => webFrames.Captures;
public bool RenderYieldRequested => !preparationOnly && (NativeKeyboardCapture.HasPendingReports || NativePointerCapture.HasButtonTransitionPending);
public long MotionReuseCount => motion.ReuseCount;
public long MotionPixelsReused => motion.PixelsReused;
public long ScanoutPixelsSaved { get; private set; }
private sealed record Glyph(int Width, int Height, byte[] Alpha);
private readonly record struct PixelRect(int X, int Y, int Width, int Height);
private static readonly (int First, int Last, int Offset)[] GlyphRanges =
[
(128, 2047, 0), (8192, 8303, 63360), (8592, 8703, 67056),
(12288, 12351, 70752), (13312, 40959, 72864), (65280, 65519, 985248),
(65533, 65533, 993168)
];
// The pinned LunarFonts parser only reads its input bytes. Cache these
// immutable resource bytes before the heap becomes fragmented; every
// canvas still owns separate TrueTypeFont, metrics and raster dictionaries.
// In particular a later CJK canvas must not request another contiguous
// 9,868,836-byte array after long-running interpreter allocations.
private static class ImmutableAssets
{
internal static readonly byte[] Unicode = ResourceBytes("XFESystem.Kernel.Unifont.bin");
internal static readonly byte[] Brand = ResourceBytes("XFESystem.Kernel.Icon.rgba");
internal static readonly byte[] Atlas = ResourceBytes("XFESystem.Kernel.DesktopIcons.rgba");
internal static readonly byte[] CascadiaMono = ResourceBytes("XFESystem.Kernel.CascadiaMono.ttf");
internal static readonly byte[] LatoRegular = ResourceBytes("XFESystem.Kernel.LatoRegular.ttf");
internal static readonly byte[] LatoBold = ResourceBytes("XFESystem.Kernel.LatoBold.ttf");
internal static readonly byte[] WebCjkRegular = ResourceBytes("XFESystem.Kernel.WebCjkRegular.ttf");
internal static readonly byte[] WebCjkBold = ResourceBytes("XFESystem.Kernel.WebCjkBold.ttf");
internal static readonly byte[] BootstrapIcons = ResourceBytes("XFESystem.Kernel.BootstrapIcons.ttf");
}
public CanvasAdapter() : this(Canvas.GetFullScreen(), 0, false) { }
internal static CanvasAdapter CreateEditorPreparation(int layoutScale) => new(new Canvas(layoutScale, layoutScale), layoutScale, true);
private CanvasAdapter(Canvas target, int scaleOverride, bool preparationOnly)
{
canvas = target;
this.preparationOnly = preparationOnly;
scale = scaleOverride > 0 ? scaleOverride : canvas.Width >= 3000 || canvas.Height >= 1800 ? 2 : 1;
font = new TrueTypeFont(ImmutableAssets.CascadiaMono, 13 * scale);
pixels = canvas.GetBuffer() ?? throw new InvalidOperationException("The desktop canvas is not buffered.");
scene = new int[pixels.Length];
motion = new(pixels, scene, canvas.Width, canvas.Height);
clip = new(0, 0, canvas.Width, canvas.Height);
device = preparationOnly ? null : DisplayManager.Primary;
if (device is { } primary && primary.TryGetFacet<IDisplay>(out var display)) scanout = display;
if (atlas.Length != 9 * 64 * 64 * 4) throw new InvalidOperationException("Desktop icon atlas is invalid.");
}
public int Width => canvas.Width / scale;
public int Height => canvas.Height / scale;
public int NativeWidth => canvas.Width;
public int NativeHeight => canvas.Height;
public int ScalePercent => scale * 100;
// Cosmos firmware fabricates a fallback 60 Hz and GOP provides no active
// scanout clock. Only a real mode-setting driver's report can be confirmed.
public int DriverRefreshHz => device?.IsFirmware == false ? scanout?.Mode.RefreshRate ?? 0 : 0;
public bool SupportsSceneCache => true;
public int ToLayout(int nativePosition) => nativePosition / scale;
public void PrepareEditorText(string text, double fontSize, string fontFamily, int fontWeight)
{
if (!preparationOnly || preparationHandoffTarget is not null)
throw new InvalidOperationException("Editor text preparation requires an exclusively owned preparation canvas.");
double size = Math.Clamp(fontSize, 1, 128);
int nativeSize = Math.Max(1, (int)Math.Round(size * scale));
var face = WebFace(fontFamily, fontWeight);
char previous = '\0';
foreach (char character in text)
{
if (previous != '\0' && WebHasGlyph(face, previous) && WebHasGlyph(face, character)) WebKerning(face, previous, character, size);
WebAdvance(face, character, size);
WebGlyph(character, nativeSize, face);
previous = character;
}
}
public bool TryCompleteEditorPreparationHandoff(IDesktopCanvas target)
{
// This method is called on the UI only after the worker's release
// publication. No worker can access the page or these caches afterward.
// CanvasAdapter is sealed; equal runtime type and scale also identify
// the same immutable embedded font resources and font-mapping policy.
if (!preparationOnly || target is not CanvasAdapter destination || destination.preparationOnly || destination.scale != scale) return false;
if (preparationHandoffTarget is not null) return ReferenceEquals(preparationHandoffTarget, destination);
CopyBounded(webFonts, destination.webFonts, 32);
CopyBounded(webFontRequests, destination.webFontRequests, 256);
CopyBounded(webFallbackFaces, destination.webFallbackFaces, 8192);
CopyBounded(webGlyphPresence, destination.webGlyphPresence, 8192);
CopyBounded(webAdvances, destination.webAdvances, 8192);
CopyBounded(webKernings, destination.webKernings, 4096);
CopyBounded(webGlyphs, destination.webGlyphs, 4096);
foreach (var item in webTextWidths)
{
if (destination.webTextWidths.ContainsKey(item.Key)) continue;
if (destination.webTextWidths.Count >= 2048 || destination.webMeasuredCharacters + item.Key.Text.Length > 131072) break;
destination.webTextWidths.Add(item.Key, item.Value);
destination.webMeasuredCharacters += item.Key.Text.Length;
}
foreach (var item in webTextMasks)
{
if (destination.webTextMasks.ContainsKey(item.Key)) continue;
if (destination.webTextMasks.Count >= 1024 || destination.webMaskBytes + item.Value.Alpha.Length > 2097152) break;
destination.webTextMasks.Add(item.Key, item.Value);
destination.webMaskBytes += item.Value.Alpha.Length;
}
preparationHandoffTarget = destination;
return true;
}
private static void CopyBounded<TKey, TValue>(Dictionary<TKey, TValue> source, Dictionary<TKey, TValue> destination, int maximum) where TKey : notnull
{
foreach (var item in source)
{
if (destination.ContainsKey(item.Key)) continue;
if (destination.Count >= maximum) break;
destination.Add(item.Key, item.Value);
}
}
public int TextWidth(string text)
{
int width = 0;
foreach (char character in text) width += character < 128 ? 8 : unicode[GlyphOffset(character)];
return width;
}
public void BeginScene(DesktopBounds damage) { motion.End(); webClips.Clear(); clip = NativeRect(damage); }
public void PushClip(DesktopBounds bounds) { webClips.Push(clip); clip = Intersect(clip, NativeRect(bounds)); }
public void PopClip() { if (!webClips.TryPop(out clip)) throw new InvalidOperationException("Unbalanced web clipping."); }
public DesktopBounds WebPaintClip
{
get
{
int left=clip.X/scale,top=clip.Y/scale;
int right=(int)(((long)clip.X+clip.Width+scale-1)/scale),bottom=(int)(((long)clip.Y+clip.Height+scale-1)/scale);
return new(left,top,Math.Max(0,right-left),Math.Max(0,bottom-top));
}
}
public bool TryRestoreWebFrame(long pageId,long revision,long layoutVersion,DesktopBounds bounds,double scrollX,double scrollY)
{
if(!WebFrameGeometry(bounds,out int x,out int y,out int width,out int height))return false;
return webFrames.TryRestore(new(pageId,revision,layoutVersion,width,height,ScalePercent,scrollX,scrollY),pixels,NativeWidth,NativeHeight,x,y,
new(clip.X,clip.Y,clip.Width,clip.Height),motion.PreservedBounds);
}
public void StoreWebFrame(long pageId,long revision,long layoutVersion,DesktopBounds bounds,double scrollX,double scrollY)
{
if(!WebFrameGeometry(bounds,out int x,out int y,out int width,out int height))return;
webFrames.Store(new(pageId,revision,layoutVersion,width,height,ScalePercent,scrollX,scrollY),pixels,NativeWidth,NativeHeight,x,y,
new(clip.X,clip.Y,clip.Width,clip.Height),motion.PreservedBounds);
}
private bool WebFrameGeometry(DesktopBounds bounds,out int x,out int y,out int width,out int height)
{
long nx=(long)bounds.X*scale,ny=(long)bounds.Y*scale,nw=(long)bounds.Width*scale,nh=(long)bounds.Height*scale;
x=y=width=height=0;
if(nx<int.MinValue||nx>int.MaxValue||ny<int.MinValue||ny>int.MaxValue||nw<=0||nw>int.MaxValue||nh<=0||nh>int.MaxValue)return false;
x=(int)nx;y=(int)ny;width=(int)nw;height=(int)nh;return true;
}
public double MeasureWebText(string text, double fontSize, string fontFamily, int fontWeight)
{
double size=Math.Clamp(fontSize,1,256);var key=(text,size,fontFamily,fontWeight);
if(webTextWidths.TryGetValue(key,out var cached)){WebTextMeasureCacheHits++;return cached;}
var face=WebFace(fontFamily,fontWeight);double width=0;char previous='\0';
foreach(char ch in text)
{
if(previous!='\0'&&WebHasGlyph(face,previous)&&WebHasGlyph(face,ch))width+=WebKerning(face,previous,ch,size);
width+=WebAdvance(face,ch,size);previous=ch;
}
if(webTextWidths.Count<2048&&text.Length<=4096&&fontFamily.Length<=256&&webMeasuredCharacters+text.Length<=131072)
{webTextWidths[key]=width;webMeasuredCharacters+=text.Length;}
return width;
}
public double MeasureWebLineHeight(double fontSize,string fontFamily,int fontWeight)=>WebFace(fontFamily,fontWeight).Metrics.NaturalLineToEm*Math.Clamp(fontSize,1,256);
public string DescribeWebFont(string requested,int weight)=>requested+" -> "+WebFace(requested,weight).Family;
public string WebFontMetricsEvidence=>"Sans="+DescribeWebFont("system-ui",400)+";SansW="+MeasureWebText("WWW",16,"system-ui",400)+";SansI="+MeasureWebText("iii",16,"system-ui",400)+";Mono="+DescribeWebFont("Consolas",400)+";MonoW="+MeasureWebText("WWW",16,"Consolas",400)+";MonoI="+MeasureWebText("iii",16,"Consolas",400)+";Cjk="+WebCjkFace(false).Family+";CjkGlyph="+WebCjkFace(false).Font.HasGlyph('文')+";CjkBoldGlyph="+WebCjkFace(true).Font.HasGlyph('文')+";CjkAdvance="+MeasureWebText("中文",16,"system-ui",400);
public (int Width, int Height, byte[] Alpha) RasterizeWebText(string text, double fontSize, string fontFamily, int fontWeight)
{
if (text.Length > 4096) throw new ArgumentOutOfRangeException(nameof(text));
var key=(text,fontSize,fontFamily,fontWeight);
if(webTextMasks.TryGetValue(key,out var cached))
{WebTextMaskCacheHits++;return(cached.Width,cached.Height,(byte[])cached.Alpha.Clone());}
// Canvas bitmaps use CSS pixels, independently of desktop UI scale.
int size = Math.Max(1, (int)Math.Round(Math.Clamp(fontSize, 1, 256)));var face=WebFace(fontFamily,fontWeight);
int width = Math.Max(1, (int)Math.Ceiling(MeasureWebText(text, fontSize, fontFamily, fontWeight)));
int height = Math.Max(1, (int)Math.Ceiling(MeasureWebLineHeight(size,fontFamily,fontWeight)));
var alpha = new byte[checked(width * height)]; double left = 0;char previous='\0';
foreach (char ch in text)
{
if(previous!='\0'&&WebHasGlyph(face,previous)&&WebHasGlyph(face,ch))left+=WebKerning(face,previous,ch,size);
var glyph = WebGlyph(ch, size, face); int x = (int)Math.Round(left)+glyph.OffsetX;
for (int row = 0; row < Math.Min(height, glyph.Height); row++) for (int col = Math.Max(0,-x); col < Math.Min(glyph.Width, width - x); col++)
alpha[row * width + x + col] = Math.Max(alpha[row * width + x + col], glyph.Alpha[row * glyph.Width + col]);
left += WebAdvance(face,ch,size);previous=ch;
}
// Callers own a writable mask (opacity clipping may modify it). Retain a
// separate copy with a strict byte/entry bound, never the caller's array.
if(webTextMasks.Count<1024&&fontFamily.Length<=256&&alpha.Length<=262144&&webMaskBytes+alpha.Length<=2097152)
{webTextMasks[key]=new(width,height,(byte[])alpha.Clone());webMaskBytes+=alpha.Length;}
return (width, height, alpha);
}
public void WebText(string text, uint rgb, double x, double y, double fontSize, string fontFamily, int fontWeight, double opacity = 1)
{
double size = Math.Clamp(fontSize, 1, 128), left = x * scale;var face=WebFace(fontFamily,fontWeight);char previous='\0';
int nativeSize = Math.Max(1, (int)Math.Round(size * scale)), top = (int)Math.Round(y * scale);
foreach (char ch in text)
{
if(previous!='\0'&&WebHasGlyph(face,previous)&&WebHasGlyph(face,ch))left+=WebKerning(face,previous,ch,size)*scale;
double advance = WebAdvance(face,ch,size)*scale;
if (left >= clip.X + clip.Width) break;
if (left + advance > clip.X)
{
var glyph = WebGlyph(ch, nativeSize, face);
if (opacity >= 1) BlendCoverage(rgb, (int)Math.Round(left)+glyph.OffsetX, top, glyph.Width, glyph.Height, glyph.Alpha);
else if (opacity > 0)
{
var alpha = new byte[glyph.Alpha.Length];
for (int i = 0; i < alpha.Length; i++) alpha[i] = (byte)(glyph.Alpha[i] * opacity);
BlendCoverage(rgb, (int)Math.Round(left)+glyph.OffsetX, top, glyph.Width, glyph.Height, alpha);
}
}
left += advance;previous=ch;
}
}
private WebFontFace WebFace(string family,int weight)
{
if(webFontRequests.TryGetValue((family,weight),out var cached))return cached;
int id=1;
foreach(string raw in family.Split(','))
{
string name=raw.Trim(' ','\'','"').ToLowerInvariant();
if(name=="bootstrap-icons"){id=20;break;}
if(name is "monospace" or "ui-monospace" or "cascadia mono" or "cascadia code" or "consolas" or "sfmono-regular" or "sf mono" or "menlo" or "liberation mono"){id=0;break;}
if(name is "system-ui" or "sans-serif" or "-apple-system" or "segoe ui" or "lato" or "arial" or "helvetica neue" or "roboto" or "noto sans" or "liberation sans"){id=weight>=600?2:1;break;}
}
if(webFonts.TryGetValue(id,out var face))
{if(face.SyntheticBold!=(id==0&&weight>=600))face=face with{SyntheticBold=id==0&&weight>=600};if(webFontRequests.Count<256)webFontRequests[(family,weight)]=face;return face;}
byte[] bytes=id==20?ImmutableAssets.BootstrapIcons:id==0?ImmutableAssets.CascadiaMono:id==2?ImmutableAssets.LatoBold:ImmutableAssets.LatoRegular;
face=new(id,id==20?"bootstrap-icons 1.11.3 (MIT full glyf font)":id==0?"Cascadia Mono Regular (OFL fallback)":id==2?"Lato Bold (OFL fallback)":"Lato Regular (OFL fallback)",new TrueTypeFont(bytes),new(bytes),id==0&&weight>=600);
webFonts[id]=face;if(webFontRequests.Count<256)webFontRequests[(family,weight)]=face;return face;
}
private WebFontFace WebCjkFace(bool bold)
{
int id=bold?11:10;
if(webFonts.TryGetValue(id,out var face))return face;
byte[] bytes=bold?ImmutableAssets.WebCjkBold:ImmutableAssets.WebCjkRegular;
face=new(id,"XFE Web CJK "+(bold?"Bold":"Regular")+" (Noto Sans SC 2.004 OFL glyf subset)",new TrueTypeFont(bytes),new(bytes),false);
webFonts[id]=face;return face;
}
private WebFontFace ResolveWebGlyphFace(WebFontFace primary,char ch)
{
if(ch<128||WebHasGlyph(primary,ch))return primary;
var key=(primary.Id,ch,primary.SyntheticBold);
if(webFallbackFaces.TryGetValue(key,out var found))return found;
var candidate=WebCjkFace(primary.Id==2||primary.SyntheticBold);
var resolved=WebHasGlyph(candidate,ch)?candidate:primary;
if(webFallbackFaces.Count<8192)webFallbackFaces[key]=resolved;return resolved;
}
private double WebAdvance(WebFontFace face,char ch,double cssSize)
{
if(char.IsControl(ch))return ch=='\t'?WebAdvance(face,' ',cssSize)*4:0;
var key=(face.Id,face.SyntheticBold,ch,cssSize);
if(webAdvances.TryGetValue(key,out var cached))return cached;
var selected=ResolveWebGlyphFace(face,ch);
double value=WebHasGlyph(selected,ch)?selected.Font.GetAdvance(ch,(int)Math.Clamp(Math.Round(cssSize*64*selected.Metrics.LineToEm),1,65535))/64d:unicode[GlyphOffset(ch)]*cssSize/16d;
if(webAdvances.Count<8192)webAdvances[key]=value;return value;
}
private double WebKerning(WebFontFace face,char left,char right,double cssSize)
{
int size=(int)Math.Clamp(Math.Round(cssSize*64*face.Metrics.LineToEm),1,65535);var key=(face.Id,left,right,size);
if(webKernings.TryGetValue(key,out var cached))return cached;
double value=face.Font.GetKerning(left,right,size)/64d;
if(webKernings.Count<4096)webKernings[key]=value;return value;
}
private bool WebHasGlyph(WebFontFace face,char character)
{
var key=(face.Id,character);if(webGlyphPresence.TryGetValue(key,out bool cached))return cached;
bool value=face.Font.HasGlyph(character);if(webGlyphPresence.Count<8192)webGlyphPresence[key]=value;return value;
}
private WebGlyphCoverage WebGlyph(char ch, int nativeSize, WebFontFace face)
{
var key = (ch, nativeSize, face.Id,face.SyntheticBold);
if (webGlyphs.TryGetValue(key, out var value)) return value;
var selected=ResolveWebGlyphFace(face,ch);
int padding=Math.Max(1,nativeSize/4);
int width = Math.Max(1, (int)Math.Ceiling(WebAdvance(face,ch,nativeSize)))+padding*2, height = Math.Max(1, (int)Math.Ceiling(face.Metrics.NaturalLineToEm*nativeSize));
var alpha = new byte[width * height];
if (WebHasGlyph(selected,ch))
{
int rasterHeight=Math.Max(1,(int)Math.Round(nativeSize*selected.Metrics.LineToEm));
int baselineOffset=selected.Id==face.Id?0:(int)Math.Round(face.Metrics.AscentToEm*nativeSize)-selected.Font.GetAscent(rasterHeight);
var tile = new Canvas(width, height); tile.Clear(0); tile.DrawString(ch.ToString(), selected.Font, rasterHeight,Color.White, padding, baselineOffset);
var source = tile.GetBuffer()!;
for (int i = 0; i < source.Length; i++) alpha[i] = (byte)((uint)source[i] >> 24);
}
else
{
int offset=GlyphOffset(ch),glyphWidth=Math.Max(1,(int)Math.Ceiling(unicode[offset]*nativeSize/16d)),top=Math.Max(0,(int)Math.Round((face.Metrics.AscentToEm-.85)*nativeSize));
for(int row=0;row<nativeSize&&row+top<height;row++)for(int col=0;col<glyphWidth;col++)
{int sourceRow=Math.Min(15,row*16/nativeSize),sourceCol=Math.Min(unicode[offset]-1,col*unicode[offset]/glyphWidth);int bits=unicode[offset+1+sourceRow*2]<<8|unicode[offset+2+sourceRow*2];if((bits&(0x8000>>sourceCol))!=0)alpha[(row+top)*width+padding+col]=255;}
}
if (face.SyntheticBold&&selected.Id==face.Id) for (int row = 0; row < height; row++) for (int col = width - 1; col > 0; col--) alpha[row * width + col] = Math.Max(alpha[row * width + col], alpha[row * width + col - 1]);
value = new(width, height,-padding,alpha); if (webGlyphs.Count < 4096) webGlyphs[key] = value; return value;
}
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)
{
motion.Preserve(new(source.X * scale, source.Y * scale, source.Width * scale, source.Height * scale), destinationX * scale, destinationY * scale, unchangedInScanout);
unchangedScanout = unchangedInScanout ? motion.PreservedBounds : default;
}
public void EndScenePreservation() => motion.End();
public void AbortScenePreservation() { motion.End(); unchangedScanout = default; }
public void Fill(uint rgb, int x, int y, int width, int height)
{
var rectangle = Intersect(NativeRect(new(x, y, width, height)), clip);
int argb = unchecked((int)(0xFF000000u | rgb));
for (int row = rectangle.Y; row < rectangle.Y + rectangle.Height; row++)
{
motion.RowSegments(rectangle.X, rectangle.Width, row, out int leftEnd, out int rightStart);
Array.Fill(pixels, argb, row * NativeWidth + rectangle.X, leftEnd - rectangle.X);
Array.Fill(pixels, argb, row * NativeWidth + rightStart, rectangle.X + rectangle.Width - rightStart);
}
}
public void BlendCoverage(uint rgb, int nativeX, int nativeY, int width, int height, ReadOnlySpan<byte> coverage)
{
if (width <= 0 || height <= 0) return;
if ((long)width * height != coverage.Length) throw new ArgumentException("Coverage dimensions do not match the mask.", nameof(coverage));
int left = (int)Math.Max(nativeX, (long)clip.X), top = (int)Math.Max(nativeY, (long)clip.Y);
long right = Math.Min((long)nativeX + width, (long)clip.X + clip.Width);
long bottom = Math.Min((long)nativeY + height, (long)clip.Y + clip.Height);
if (right <= left || bottom <= top) return;
if (motion.Contains(new(left, top, (int)right - left, (int)bottom - top))) return;
for (int row = top; row < bottom; row++)
{
if(RenderYieldRequested)throw new DesktopRenderYieldException();
motion.RowSegments(left, (int)right - left, row, out int leftEnd, out int rightStart);
for (int segment = 0; segment < 2; segment++)
{
int start=segment==0?left:rightStart,end=segment==0?leftEnd:(int)right;
int offset=(row-nativeY)*width-nativeX,frameOffset=row*NativeWidth;
for(int col=start;col<end;)
{
byte alpha=coverage[offset+col];
if(alpha==0){do{col++;}while(col<end&&coverage[offset+col]==0);}
else if(alpha==255)
{
int first=col;do{col++;}while(col<end&&coverage[offset+col]==255);
Array.Fill(pixels,unchecked((int)(0xFF000000u|rgb&0xFFFFFF)),frameOffset+first,col-first);
}
else{Blend(frameOffset+col,rgb&0xFFFFFF,alpha);col++;}
}
}
}
}
public void Line(uint rgb, int x1, int y1, int x2, int y2) => DesktopChromeRenderer.Line(this, rgb, x1, y1, x2, y2);
public void Text(string text, uint rgb, int x, int y)
{
int left = x * scale, top = y * scale;
if (top >= clip.Y + clip.Height || top + 20 * scale <= clip.Y) return;
foreach (char character in text)
{
int width = (character < 128 ? 8 : unicode[GlyphOffset(character)]) * scale;
if (left >= clip.X + clip.Width) break;
if (left + width > clip.X && !motion.Contains(new(left, top, width, 20 * scale)))
{
var glyph = GlyphOf(character);
var visible = Intersect(new(left, top, glyph.Width, glyph.Height), clip);
for (int row = visible.Y; row < visible.Y + visible.Height; row++)
{
motion.RowSegments(visible.X, visible.Width, row, out int leftEnd, out int rightStart);
int right = visible.X + visible.Width;
for (int col = visible.X; col < right; col = col + 1 == leftEnd ? rightStart : col + 1)
{
if (col == leftEnd && col < rightStart) { col = rightStart; if (col >= right) break; }
byte alpha = glyph.Alpha[(row - top) * glyph.Width + col - left];
if (alpha != 0) Blend(row * NativeWidth + col, rgb, alpha);
}
}
}
left += width;
}
}
private Glyph GlyphOf(char character)
{
if (glyphs.TryGetValue(character, out var cached)) return cached;
int width = (character < 128 ? 8 : unicode[GlyphOffset(character)]) * scale, height = 20 * scale;
var alpha = new byte[width * height];
if (character < 128)
{
var tile = new Canvas(width, height);
tile.Clear(0);
tile.DrawString(character.ToString(), font, Color.White, 0, 0);
int[] source = tile.GetBuffer()!;
for (int i = 0; i < source.Length; i++) alpha[i] = (byte)((uint)source[i] >> 24);
}
else
{
int offset = GlyphOffset(character), logicalWidth = width / scale;
for (int row = 0; row < 16; row++)
{
int bits = unicode[offset + 1 + row * 2] << 8 | unicode[offset + 2 + row * 2];
for (int col = 0; col < logicalWidth; col++)
if ((bits & (0x8000 >> col)) != 0)
for (int sy = 0; sy < scale; sy++)
Array.Fill(alpha, (byte)255, (row * scale + sy) * width + col * scale, scale);
}
}
var glyph = new Glyph(width, height, alpha);
if (glyphs.Count < 4096) glyphs.Add(character, glyph);
return glyph;
}
public void DrawIcon(DesktopIcon icon, int x, int y, int size)
{
if (icon == DesktopIcon.Settings)
{
if (size <= 0 || size > 256) return;
// Code-native sliders are distinct from the nine generated atlas
// entries. In particular Settings=9 must never index past atlas.
int margin = Math.Max(2, size / 5), knob = Math.Max(3, size / 4);
int radius = Math.Max(1, knob / 2);
for (int row = 0; row < 3; row++)
{
int cy = y + margin + row * Math.Max(1, (size - 2 * margin) / 2);
int cx = x + (row == 1 ? size * 2 / 3 : size / 3);
DesktopChromeRenderer.Line(this, 0x4086B8, x + margin, cy, x + size - margin, cy);
DesktopChromeRenderer.RoundedFill(this, 0x4086B8, cx - radius, cy - radius, knob, knob, radius);
if (knob >= 5)
DesktopChromeRenderer.RoundedFill(this, 0x75C9F5, cx - radius + 1, cy - radius + 1, knob - 2, knob - 2, Math.Max(1, radius - 1));
}
return;
}
if ((uint)icon >= 9) throw new ArgumentOutOfRangeException(nameof(icon));
DrawRaster((int)icon, atlas, (int)icon * 64 * 64 * 4, x, y, size);
}
public void BrandIcon(int x, int y, int size) => DrawRaster(10, brand, 0, x, y, size);
private void DrawRaster(int icon, byte[] source, int offset, int x, int y, int size)
{
int edge = size * scale;
if (edge <= 0 || edge > 512) return;
int left = x * scale, top = y * scale;
var visible = Intersect(new(left, top, edge, edge), clip);
if (visible.Width <= 0 || visible.Height <= 0) return;
if (motion.Contains(new(visible.X, visible.Y, visible.Width, visible.Height))) return;
if (!iconCache.TryGetValue((icon, edge), out var scaled))
{
scaled = new int[edge * edge];
for (int row = 0; row < edge; row++)
for (int col = 0; col < edge; col++)
{
int at = offset + ((row * 64 / edge) * 64 + col * 64 / edge) * 4;
scaled[row * edge + col] = source[at + 3] << 24 | source[at] << 16 | source[at + 1] << 8 | source[at + 2];
}
if (iconCache.Count < 64) iconCache.Add((icon, edge), scaled);
}
for (int row = visible.Y; row < visible.Y + visible.Height; row++)
{
motion.RowSegments(visible.X, visible.Width, row, out int leftEnd, out int rightStart);
int right = visible.X + visible.Width;
for (int col = visible.X; col < right; col = col + 1 == leftEnd ? rightStart : col + 1)
{
if (col == leftEnd && col < rightStart) { col = rightStart; if (col >= right) break; }
uint argb = (uint)scaled[(row - top) * edge + col - left];
if (argb >> 24 != 0) Blend(row * NativeWidth + col, argb & 0xFFFFFF, (byte)(argb >> 24));
}
}
}
private void Blend(int index, uint rgb, byte alpha)
{
if (alpha == 255) { pixels[index] = unchecked((int)(0xFF000000 | rgb)); return; }
uint old = (uint)pixels[index];
uint r = ((rgb >> 16 & 255) * alpha + (old >> 16 & 255) * (255u - alpha) + 127) / 255;
uint g = ((rgb >> 8 & 255) * alpha + (old >> 8 & 255) * (255u - alpha) + 127) / 255;
uint b = ((rgb & 255) * alpha + (old & 255) * (255u - alpha) + 127) / 255;
pixels[index] = unchecked((int)(0xFF000000 | r << 16 | g << 8 | b));
}
public void SaveScene() { motion.End(); pixels.AsSpan().CopyTo(scene); }
public void SaveScene(DesktopBounds damage) { motion.End(); CopyRegion(pixels, scene, NativeRect(damage)); }
public void RestoreSceneRegion(DesktopBounds bounds) => CopyRegion(scene, pixels, NativeRect(bounds));
private void CopyRegion(int[] source, int[] target, PixelRect rectangle)
{
for (int row = rectangle.Y; row < rectangle.Y + rectangle.Height; row++)
source.AsSpan(row * NativeWidth + rectangle.X, rectangle.Width).CopyTo(target.AsSpan(row * NativeWidth + rectangle.X, rectangle.Width));
}
public void Present() => Present(new(0, 0, Width, Height));
public bool TryFingerprintPresentedPixels(DesktopBounds nativeBounds, out DesktopPresentedPixelsFingerprint result)
{
result = default;
// Read the display's mapped framebuffer, rather than the software
// working buffer that a later or aborted frame may already modify.
if (FramesPresented <= 0 || device?.IsWithdrawn == true || scanout is null || scanout.Framebuffer is not { } framebuffer)
return false;
var mode = scanout.Mode;
if (mode.Width != NativeWidth || mode.Height != NativeHeight || mode.BitsPerPixel != 32 || mode.Pitch % 4 != 0 ||
mode.Pitch < NativeWidth * 4 || framebuffer.Length < (ulong)mode.Pitch * (ulong)NativeHeight)
return false;
return DesktopPresentedPixels.TryFingerprint(framebuffer.As<int>(), NativeWidth, NativeHeight, mode.Pitch / 4,
nativeBounds, FramesPresented, out result);
}
public void Present(DesktopBounds damage)
{
if (preparationOnly) throw new InvalidOperationException("The editor preparation canvas cannot present to a display.");
long begin = Stopwatch.GetTimestamp();
var rectangle = NativeRect(damage);
var unchanged = unchangedScanout; unchangedScanout = default;
if (rectangle.Width <= 0 || rectangle.Height <= 0) return;
if (device?.IsWithdrawn == true) return;
if (scanout is not null && scanout.Framebuffer is { } framebuffer && scanout.Mode is var mode &&
mode.Width == NativeWidth && mode.Height == NativeHeight && mode.BitsPerPixel == 32 && mode.Pitch % 4 == 0 && mode.Pitch >= NativeWidth * 4 &&
framebuffer.Length >= (ulong)mode.Pitch * (ulong)NativeHeight)
{
Span<int> target = framebuffer.As<int>();
int stride = mode.Pitch / 4;
long copied = 0;
for (int row = rectangle.Y; row < rectangle.Y + rectangle.Height; row++)
{
DesktopSceneMotionBuffer.RowSegments(unchanged, rectangle.X, rectangle.Width, row, out int leftEnd, out int rightStart);
int leftCount = leftEnd - rectangle.X, rightCount = rectangle.X + rectangle.Width - rightStart;
pixels.AsSpan(row * NativeWidth + rectangle.X, leftCount).CopyTo(target.Slice(row * stride + rectangle.X, leftCount));
pixels.AsSpan(row * NativeWidth + rightStart, rightCount).CopyTo(target.Slice(row * stride + rightStart, rightCount));
copied += leftCount + rightCount;
}
scanout.Flush(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
PixelsPresented += copied;
ScanoutPixelsSaved += (long)rectangle.Width * rectangle.Height - copied;
if (rectangle.Width < NativeWidth || rectangle.Height < NativeHeight) PartialFrames++;
}
else
{
canvas.Display();
PixelsPresented += (long)NativeWidth * NativeHeight;
FullCopyFallbackFrames++;
}
FramesPresented++;
LastPresentTicks = Stopwatch.GetTimestamp() - begin;
}
private PixelRect NativeRect(DesktopBounds value)
{
int x = Math.Clamp(value.X * scale, 0, NativeWidth), y = Math.Clamp(value.Y * scale, 0, NativeHeight);
int right = Math.Clamp((value.X + Math.Max(0, value.Width)) * scale, x, NativeWidth);
int bottom = Math.Clamp((value.Y + Math.Max(0, value.Height)) * scale, y, NativeHeight);
return new(x, y, right - x, bottom - y);
}
private static PixelRect Intersect(PixelRect a, PixelRect b)
{
int x = Math.Max(a.X, b.X), y = Math.Max(a.Y, b.Y);
return new(x, y, Math.Max(0, Math.Min(a.X + a.Width, b.X + b.Width) - x), Math.Max(0, Math.Min(a.Y + a.Height, b.Y + b.Height) - y));
}
private static byte[] ResourceBytes(string name)
{
using var resource = typeof(CanvasAdapter).Assembly.GetManifestResourceStream(name)
?? throw new InvalidOperationException("An embedded desktop asset is missing: " + name);
var data = new byte[resource.Length]; resource.ReadExactly(data); return data;
}
private int GlyphOffset(int character)
{
foreach (var range in GlyphRanges)
if (character >= range.First && character <= range.Last) return range.Offset + (character - range.First) * 33;
return unicode.Length - 33;
}
}
using System.Drawing;
using System.Diagnostics;
using Cosmos.Kernel.HAL.Devices.Display;
using Cosmos.Kernel.System.Graphics;
using Cosmos.Kernel.System.Graphics.Fonts;
using XFESystem.Desktop;
namespace XFESystem.Kernel;
/// <summary>Native pixels, cached glyph coverage, scene restoration and bounded damaged scanout.</summary>
internal sealed class CanvasAdapter : IBrandedDesktopCanvas, IDesktopIconCanvas, IDesktopCoverageCanvas, IDesktopSceneMotionCanvas, IDesktopWebCanvas, IDesktopWebFrameCache, IDesktopRenderYieldCanvas, IDesktopPresentedPixelsCanvas, IEditorPreparationCanvasHandoff
{
private readonly Canvas canvas;
private readonly bool preparationOnly;
private CanvasAdapter? preparationHandoffTarget;
private readonly TrueTypeFont font;
private readonly int scale;
private readonly int[] pixels, scene;
private readonly DesktopSceneMotionBuffer motion;
private readonly DesktopWebFrameCache webFrames = new();
private readonly byte[] unicode = ImmutableAssets.Unicode;
private readonly byte[] brand = ImmutableAssets.Brand;
private readonly byte[] atlas = ImmutableAssets.Atlas;
private readonly Dictionary<char, Glyph> glyphs = [];
private readonly Dictionary<(int Icon, int Size), int[]> iconCache = [];
private readonly IDisplay? scanout;
private readonly DisplayDevice? device;
private PixelRect clip;
private readonly Stack<PixelRect> webClips = [];
private readonly Dictionary<(char Character, int Size, int Face, bool Bold), WebGlyphCoverage> webGlyphs = [];
private readonly Dictionary<int, WebFontFace> webFonts = [];
private readonly Dictionary<(string Family,int Weight),WebFontFace> webFontRequests=[];
private readonly Dictionary<(int Primary,char Character,bool Bold),WebFontFace> webFallbackFaces=[];
private readonly Dictionary<(int Face,char Character),bool> webGlyphPresence=[];
private readonly Dictionary<(int Face,bool Bold,char Character,double Size),double> webAdvances=[];
private readonly Dictionary<(int Face,char Left,char Right,int Size),double> webKernings=[];
private readonly Dictionary<(string Text,double Size,string Family,int Weight),double> webTextWidths=[];
private readonly Dictionary<(string Text,double Size,string Family,int Weight),WebTextCoverage> webTextMasks=[];
private int webMeasuredCharacters,webMaskBytes;
private sealed record WebFontFace(int Id,string Family,TrueTypeFont Font,NativeWebFontMetrics Metrics,bool SyntheticBold);
private sealed record WebGlyphCoverage(int Width,int Height,int OffsetX,byte[] Alpha);
private sealed record WebTextCoverage(int Width,int Height,byte[] Alpha);
private DesktopBounds unchangedScanout;
public long FramesPresented { get; private set; }
public long PixelsPresented { get; private set; }
public long PartialFrames { get; private set; }
public long FullCopyFallbackFrames { get; private set; }
public long LastPresentTicks { get; private set; }
public int CachedGlyphCount => glyphs.Count;
public int CachedWebTextMaskCount => webTextMasks.Count;
public int CachedWebTextMaskBytes => webMaskBytes;
public long WebTextMeasureCacheHits { get; private set; }
public long WebTextMaskCacheHits { get; private set; }
public int CachedWebFrameCount => webFrames.Count;
public long CachedWebFrameBytes => webFrames.CachedBytes;
public long WebFrameRestoreHits => webFrames.RestoreHits;
public long WebFramePixelsRestored => webFrames.PixelsRestored;
public long WebFrameCaptures => webFrames.Captures;
public bool RenderYieldRequested => !preparationOnly && (NativeKeyboardCapture.HasPendingReports || NativePointerCapture.HasButtonTransitionPending);
public long MotionReuseCount => motion.ReuseCount;
public long MotionPixelsReused => motion.PixelsReused;
public long ScanoutPixelsSaved { get; private set; }
private sealed record Glyph(int Width, int Height, byte[] Alpha);
private readonly record struct PixelRect(int X, int Y, int Width, int Height);
private static readonly (int First, int Last, int Offset)[] GlyphRanges =
[
(128, 2047, 0), (8192, 8303, 63360), (8592, 8703, 67056),
(12288, 12351, 70752), (13312, 40959, 72864), (65280, 65519, 985248),
(65533, 65533, 993168)
];
// The pinned LunarFonts parser only reads its input bytes. Cache these
// immutable resource bytes before the heap becomes fragmented; every
// canvas still owns separate TrueTypeFont, metrics and raster dictionaries.
// In particular a later CJK canvas must not request another contiguous
// 9,868,836-byte array after long-running interpreter allocations.
private static class ImmutableAssets
{
internal static readonly byte[] Unicode = ResourceBytes("XFESystem.Kernel.Unifont.bin");
internal static readonly byte[] Brand = ResourceBytes("XFESystem.Kernel.Icon.rgba");
internal static readonly byte[] Atlas = ResourceBytes("XFESystem.Kernel.DesktopIcons.rgba");
internal static readonly byte[] CascadiaMono = ResourceBytes("XFESystem.Kernel.CascadiaMono.ttf");
internal static readonly byte[] LatoRegular = ResourceBytes("XFESystem.Kernel.LatoRegular.ttf");
internal static readonly byte[] LatoBold = ResourceBytes("XFESystem.Kernel.LatoBold.ttf");
internal static readonly byte[] WebCjkRegular = ResourceBytes("XFESystem.Kernel.WebCjkRegular.ttf");
internal static readonly byte[] WebCjkBold = ResourceBytes("XFESystem.Kernel.WebCjkBold.ttf");
internal static readonly byte[] BootstrapIcons = ResourceBytes("XFESystem.Kernel.BootstrapIcons.ttf");
}
public CanvasAdapter() : this(Canvas.GetFullScreen(), 0, false) { }
internal static CanvasAdapter CreateEditorPreparation(int layoutScale) => new(new Canvas(layoutScale, layoutScale), layoutScale, true);
private CanvasAdapter(Canvas target, int scaleOverride, bool preparationOnly)
{
canvas = target;
this.preparationOnly = preparationOnly;
scale = scaleOverride > 0 ? scaleOverride : canvas.Width >= 3000 || canvas.Height >= 1800 ? 2 : 1;
font = new TrueTypeFont(ImmutableAssets.CascadiaMono, 13 * scale);
pixels = canvas.GetBuffer() ?? throw new InvalidOperationException("The desktop canvas is not buffered.");
scene = new int[pixels.Length];
motion = new(pixels, scene, canvas.Width, canvas.Height);
clip = new(0, 0, canvas.Width, canvas.Height);
device = preparationOnly ? null : DisplayManager.Primary;
if (device is { } primary && primary.TryGetFacet<IDisplay>(out var display)) scanout = display;
if (atlas.Length != 9 * 64 * 64 * 4) throw new InvalidOperationException("Desktop icon atlas is invalid.");
}
public int Width => canvas.Width / scale;
public int Height => canvas.Height / scale;
public int NativeWidth => canvas.Width;
public int NativeHeight => canvas.Height;
public int ScalePercent => scale * 100;
// Cosmos firmware fabricates a fallback 60 Hz and GOP provides no active
// scanout clock. Only a real mode-setting driver's report can be confirmed.
public int DriverRefreshHz => device?.IsFirmware == false ? scanout?.Mode.RefreshRate ?? 0 : 0;
public bool SupportsSceneCache => true;
public int ToLayout(int nativePosition) => nativePosition / scale;
public void PrepareEditorText(string text, double fontSize, string fontFamily, int fontWeight)
{
if (!preparationOnly || preparationHandoffTarget is not null)
throw new InvalidOperationException("Editor text preparation requires an exclusively owned preparation canvas.");
double size = Math.Clamp(fontSize, 1, 128);
int nativeSize = Math.Max(1, (int)Math.Round(size * scale));
var face = WebFace(fontFamily, fontWeight);
char previous = '\0';
foreach (char character in text)
{
if (previous != '\0' && WebHasGlyph(face, previous) && WebHasGlyph(face, character)) WebKerning(face, previous, character, size);
WebAdvance(face, character, size);
WebGlyph(character, nativeSize, face);
previous = character;
}
}
public bool TryCompleteEditorPreparationHandoff(IDesktopCanvas target)
{
// This method is called on the UI only after the worker's release
// publication. No worker can access the page or these caches afterward.
// CanvasAdapter is sealed; equal runtime type and scale also identify
// the same immutable embedded font resources and font-mapping policy.
if (!preparationOnly || target is not CanvasAdapter destination || destination.preparationOnly || destination.scale != scale) return false;
if (preparationHandoffTarget is not null) return ReferenceEquals(preparationHandoffTarget, destination);
CopyBounded(webFonts, destination.webFonts, 32);
CopyBounded(webFontRequests, destination.webFontRequests, 256);
CopyBounded(webFallbackFaces, destination.webFallbackFaces, 8192);
CopyBounded(webGlyphPresence, destination.webGlyphPresence, 8192);
CopyBounded(webAdvances, destination.webAdvances, 8192);
CopyBounded(webKernings, destination.webKernings, 4096);
CopyBounded(webGlyphs, destination.webGlyphs, 4096);
foreach (var item in webTextWidths)
{
if (destination.webTextWidths.ContainsKey(item.Key)) continue;
if (destination.webTextWidths.Count >= 2048 || destination.webMeasuredCharacters + item.Key.Text.Length > 131072) break;
destination.webTextWidths.Add(item.Key, item.Value);
destination.webMeasuredCharacters += item.Key.Text.Length;
}
foreach (var item in webTextMasks)
{
if (destination.webTextMasks.ContainsKey(item.Key)) continue;
if (destination.webTextMasks.Count >= 1024 || destination.webMaskBytes + item.Value.Alpha.Length > 2097152) break;
destination.webTextMasks.Add(item.Key, item.Value);
destination.webMaskBytes += item.Value.Alpha.Length;
}
preparationHandoffTarget = destination;
return true;
}
private static void CopyBounded<TKey, TValue>(Dictionary<TKey, TValue> source, Dictionary<TKey, TValue> destination, int maximum) where TKey : notnull
{
foreach (var item in source)
{
if (destination.ContainsKey(item.Key)) continue;
if (destination.Count >= maximum) break;
destination.Add(item.Key, item.Value);
}
}
public int TextWidth(string text)
{
int width = 0;
foreach (char character in text) width += character < 128 ? 8 : unicode[GlyphOffset(character)];
return width;
}
public void BeginScene(DesktopBounds damage) { motion.End(); webClips.Clear(); clip = NativeRect(damage); }
public void PushClip(DesktopBounds bounds) { webClips.Push(clip); clip = Intersect(clip, NativeRect(bounds)); }
public void PopClip() { if (!webClips.TryPop(out clip)) throw new InvalidOperationException("Unbalanced web clipping."); }
public DesktopBounds WebPaintClip
{
get
{
int left=clip.X/scale,top=clip.Y/scale;
int right=(int)(((long)clip.X+clip.Width+scale-1)/scale),bottom=(int)(((long)clip.Y+clip.Height+scale-1)/scale);
return new(left,top,Math.Max(0,right-left),Math.Max(0,bottom-top));
}
}
public bool TryRestoreWebFrame(long pageId,long revision,long layoutVersion,DesktopBounds bounds,double scrollX,double scrollY)
{
if(!WebFrameGeometry(bounds,out int x,out int y,out int width,out int height))return false;
return webFrames.TryRestore(new(pageId,revision,layoutVersion,width,height,ScalePercent,scrollX,scrollY),pixels,NativeWidth,NativeHeight,x,y,
new(clip.X,clip.Y,clip.Width,clip.Height),motion.PreservedBounds);
}
public void StoreWebFrame(long pageId,long revision,long layoutVersion,DesktopBounds bounds,double scrollX,double scrollY)
{
if(!WebFrameGeometry(bounds,out int x,out int y,out int width,out int height))return;
webFrames.Store(new(pageId,revision,layoutVersion,width,height,ScalePercent,scrollX,scrollY),pixels,NativeWidth,NativeHeight,x,y,
new(clip.X,clip.Y,clip.Width,clip.Height),motion.PreservedBounds);
}
private bool WebFrameGeometry(DesktopBounds bounds,out int x,out int y,out int width,out int height)
{
long nx=(long)bounds.X*scale,ny=(long)bounds.Y*scale,nw=(long)bounds.Width*scale,nh=(long)bounds.Height*scale;
x=y=width=height=0;
if(nx<int.MinValue||nx>int.MaxValue||ny<int.MinValue||ny>int.MaxValue||nw<=0||nw>int.MaxValue||nh<=0||nh>int.MaxValue)return false;
x=(int)nx;y=(int)ny;width=(int)nw;height=(int)nh;return true;
}
public double MeasureWebText(string text, double fontSize, string fontFamily, int fontWeight)
{
double size=Math.Clamp(fontSize,1,256);var key=(text,size,fontFamily,fontWeight);
if(webTextWidths.TryGetValue(key,out var cached)){WebTextMeasureCacheHits++;return cached;}
var face=WebFace(fontFamily,fontWeight);double width=0;char previous='\0';
foreach(char ch in text)
{
if(previous!='\0'&&WebHasGlyph(face,previous)&&WebHasGlyph(face,ch))width+=WebKerning(face,previous,ch,size);
width+=WebAdvance(face,ch,size);previous=ch;
}
if(webTextWidths.Count<2048&&text.Length<=4096&&fontFamily.Length<=256&&webMeasuredCharacters+text.Length<=131072)
{webTextWidths[key]=width;webMeasuredCharacters+=text.Length;}
return width;
}
public double MeasureWebLineHeight(double fontSize,string fontFamily,int fontWeight)=>WebFace(fontFamily,fontWeight).Metrics.NaturalLineToEm*Math.Clamp(fontSize,1,256);
public string DescribeWebFont(string requested,int weight)=>requested+" -> "+WebFace(requested,weight).Family;
public string WebFontMetricsEvidence=>"Sans="+DescribeWebFont("system-ui",400)+";SansW="+MeasureWebText("WWW",16,"system-ui",400)+";SansI="+MeasureWebText("iii",16,"system-ui",400)+";Mono="+DescribeWebFont("Consolas",400)+";MonoW="+MeasureWebText("WWW",16,"Consolas",400)+";MonoI="+MeasureWebText("iii",16,"Consolas",400)+";Cjk="+WebCjkFace(false).Family+";CjkGlyph="+WebCjkFace(false).Font.HasGlyph('文')+";CjkBoldGlyph="+WebCjkFace(true).Font.HasGlyph('文')+";CjkAdvance="+MeasureWebText("中文",16,"system-ui",400);
public (int Width, int Height, byte[] Alpha) RasterizeWebText(string text, double fontSize, string fontFamily, int fontWeight)
{
if (text.Length > 4096) throw new ArgumentOutOfRangeException(nameof(text));
var key=(text,fontSize,fontFamily,fontWeight);
if(webTextMasks.TryGetValue(key,out var cached))
{WebTextMaskCacheHits++;return(cached.Width,cached.Height,(byte[])cached.Alpha.Clone());}
// Canvas bitmaps use CSS pixels, independently of desktop UI scale.
int size = Math.Max(1, (int)Math.Round(Math.Clamp(fontSize, 1, 256)));var face=WebFace(fontFamily,fontWeight);
int width = Math.Max(1, (int)Math.Ceiling(MeasureWebText(text, fontSize, fontFamily, fontWeight)));
int height = Math.Max(1, (int)Math.Ceiling(MeasureWebLineHeight(size,fontFamily,fontWeight)));
var alpha = new byte[checked(width * height)]; double left = 0;char previous='\0';
foreach (char ch in text)
{
if(previous!='\0'&&WebHasGlyph(face,previous)&&WebHasGlyph(face,ch))left+=WebKerning(face,previous,ch,size);
var glyph = WebGlyph(ch, size, face); int x = (int)Math.Round(left)+glyph.OffsetX;
for (int row = 0; row < Math.Min(height, glyph.Height); row++) for (int col = Math.Max(0,-x); col < Math.Min(glyph.Width, width - x); col++)
alpha[row * width + x + col] = Math.Max(alpha[row * width + x + col], glyph.Alpha[row * glyph.Width + col]);
left += WebAdvance(face,ch,size);previous=ch;
}
// Callers own a writable mask (opacity clipping may modify it). Retain a
// separate copy with a strict byte/entry bound, never the caller's array.
if(webTextMasks.Count<1024&&fontFamily.Length<=256&&alpha.Length<=262144&&webMaskBytes+alpha.Length<=2097152)
{webTextMasks[key]=new(width,height,(byte[])alpha.Clone());webMaskBytes+=alpha.Length;}
return (width, height, alpha);
}
public void WebText(string text, uint rgb, double x, double y, double fontSize, string fontFamily, int fontWeight, double opacity = 1)
{
double size = Math.Clamp(fontSize, 1, 128), left = x * scale;var face=WebFace(fontFamily,fontWeight);char previous='\0';
int nativeSize = Math.Max(1, (int)Math.Round(size * scale)), top = (int)Math.Round(y * scale);
foreach (char ch in text)
{
if(previous!='\0'&&WebHasGlyph(face,previous)&&WebHasGlyph(face,ch))left+=WebKerning(face,previous,ch,size)*scale;
double advance = WebAdvance(face,ch,size)*scale;
if (left >= clip.X + clip.Width) break;
if (left + advance > clip.X)
{
var glyph = WebGlyph(ch, nativeSize, face);
if (opacity >= 1) BlendCoverage(rgb, (int)Math.Round(left)+glyph.OffsetX, top, glyph.Width, glyph.Height, glyph.Alpha);
else if (opacity > 0)
{
var alpha = new byte[glyph.Alpha.Length];
for (int i = 0; i < alpha.Length; i++) alpha[i] = (byte)(glyph.Alpha[i] * opacity);
BlendCoverage(rgb, (int)Math.Round(left)+glyph.OffsetX, top, glyph.Width, glyph.Height, alpha);
}
}
left += advance;previous=ch;
}
}
private WebFontFace WebFace(string family,int weight)
{
if(webFontRequests.TryGetValue((family,weight),out var cached))return cached;
int id=1;
foreach(string raw in family.Split(','))
{
string name=raw.Trim(' ','\'','"').ToLowerInvariant();
if(name=="bootstrap-icons"){id=20;break;}
if(name is "monospace" or "ui-monospace" or "cascadia mono" or "cascadia code" or "consolas" or "sfmono-regular" or "sf mono" or "menlo" or "liberation mono"){id=0;break;}
if(name is "system-ui" or "sans-serif" or "-apple-system" or "segoe ui" or "lato" or "arial" or "helvetica neue" or "roboto" or "noto sans" or "liberation sans"){id=weight>=600?2:1;break;}
}
if(webFonts.TryGetValue(id,out var face))
{if(face.SyntheticBold!=(id==0&&weight>=600))face=face with{SyntheticBold=id==0&&weight>=600};if(webFontRequests.Count<256)webFontRequests[(family,weight)]=face;return face;}
byte[] bytes=id==20?ImmutableAssets.BootstrapIcons:id==0?ImmutableAssets.CascadiaMono:id==2?ImmutableAssets.LatoBold:ImmutableAssets.LatoRegular;
face=new(id,id==20?"bootstrap-icons 1.11.3 (MIT full glyf font)":id==0?"Cascadia Mono Regular (OFL fallback)":id==2?"Lato Bold (OFL fallback)":"Lato Regular (OFL fallback)",new TrueTypeFont(bytes),new(bytes),id==0&&weight>=600);
webFonts[id]=face;if(webFontRequests.Count<256)webFontRequests[(family,weight)]=face;return face;
}
private WebFontFace WebCjkFace(bool bold)
{
int id=bold?11:10;
if(webFonts.TryGetValue(id,out var face))return face;
byte[] bytes=bold?ImmutableAssets.WebCjkBold:ImmutableAssets.WebCjkRegular;
face=new(id,"XFE Web CJK "+(bold?"Bold":"Regular")+" (Noto Sans SC 2.004 OFL glyf subset)",new TrueTypeFont(bytes),new(bytes),false);
webFonts[id]=face;return face;
}
private WebFontFace ResolveWebGlyphFace(WebFontFace primary,char ch)
{
if(ch<128||WebHasGlyph(primary,ch))return primary;
var key=(primary.Id,ch,primary.SyntheticBold);
if(webFallbackFaces.TryGetValue(key,out var found))return found;
var candidate=WebCjkFace(primary.Id==2||primary.SyntheticBold);
var resolved=WebHasGlyph(candidate,ch)?candidate:primary;
if(webFallbackFaces.Count<8192)webFallbackFaces[key]=resolved;return resolved;
}
private double WebAdvance(WebFontFace face,char ch,double cssSize)
{
if(char.IsControl(ch))return ch=='\t'?WebAdvance(face,' ',cssSize)*4:0;
var key=(face.Id,face.SyntheticBold,ch,cssSize);
if(webAdvances.TryGetValue(key,out var cached))return cached;
var selected=ResolveWebGlyphFace(face,ch);
double value=WebHasGlyph(selected,ch)?selected.Font.GetAdvance(ch,(int)Math.Clamp(Math.Round(cssSize*64*selected.Metrics.LineToEm),1,65535))/64d:unicode[GlyphOffset(ch)]*cssSize/16d;
if(webAdvances.Count<8192)webAdvances[key]=value;return value;
}
private double WebKerning(WebFontFace face,char left,char right,double cssSize)
{
int size=(int)Math.Clamp(Math.Round(cssSize*64*face.Metrics.LineToEm),1,65535);var key=(face.Id,left,right,size);
if(webKernings.TryGetValue(key,out var cached))return cached;
double value=face.Font.GetKerning(left,right,size)/64d;
if(webKernings.Count<4096)webKernings[key]=value;return value;
}
private bool WebHasGlyph(WebFontFace face,char character)
{
var key=(face.Id,character);if(webGlyphPresence.TryGetValue(key,out bool cached))return cached;
bool value=face.Font.HasGlyph(character);if(webGlyphPresence.Count<8192)webGlyphPresence[key]=value;return value;
}
private WebGlyphCoverage WebGlyph(char ch, int nativeSize, WebFontFace face)
{
var key = (ch, nativeSize, face.Id,face.SyntheticBold);
if (webGlyphs.TryGetValue(key, out var value)) return value;
var selected=ResolveWebGlyphFace(face,ch);
int padding=Math.Max(1,nativeSize/4);
int width = Math.Max(1, (int)Math.Ceiling(WebAdvance(face,ch,nativeSize)))+padding*2, height = Math.Max(1, (int)Math.Ceiling(face.Metrics.NaturalLineToEm*nativeSize));
var alpha = new byte[width * height];
if (WebHasGlyph(selected,ch))
{
int rasterHeight=Math.Max(1,(int)Math.Round(nativeSize*selected.Metrics.LineToEm));
int baselineOffset=selected.Id==face.Id?0:(int)Math.Round(face.Metrics.AscentToEm*nativeSize)-selected.Font.GetAscent(rasterHeight);
var tile = new Canvas(width, height); tile.Clear(0); tile.DrawString(ch.ToString(), selected.Font, rasterHeight,Color.White, padding, baselineOffset);
var source = tile.GetBuffer()!;
for (int i = 0; i < source.Length; i++) alpha[i] = (byte)((uint)source[i] >> 24);
}
else
{
int offset=GlyphOffset(ch),glyphWidth=Math.Max(1,(int)Math.Ceiling(unicode[offset]*nativeSize/16d)),top=Math.Max(0,(int)Math.Round((face.Metrics.AscentToEm-.85)*nativeSize));
for(int row=0;row<nativeSize&&row+top<height;row++)for(int col=0;col<glyphWidth;col++)
{int sourceRow=Math.Min(15,row*16/nativeSize),sourceCol=Math.Min(unicode[offset]-1,col*unicode[offset]/glyphWidth);int bits=unicode[offset+1+sourceRow*2]<<8|unicode[offset+2+sourceRow*2];if((bits&(0x8000>>sourceCol))!=0)alpha[(row+top)*width+padding+col]=255;}
}
if (face.SyntheticBold&&selected.Id==face.Id) for (int row = 0; row < height; row++) for (int col = width - 1; col > 0; col--) alpha[row * width + col] = Math.Max(alpha[row * width + col], alpha[row * width + col - 1]);
value = new(width, height,-padding,alpha); if (webGlyphs.Count < 4096) webGlyphs[key] = value; return value;
}
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)
{
motion.Preserve(new(source.X * scale, source.Y * scale, source.Width * scale, source.Height * scale), destinationX * scale, destinationY * scale, unchangedInScanout);
unchangedScanout = unchangedInScanout ? motion.PreservedBounds : default;
}
public void EndScenePreservation() => motion.End();
public void AbortScenePreservation() { motion.End(); unchangedScanout = default; }
public void Fill(uint rgb, int x, int y, int width, int height)
{
var rectangle = Intersect(NativeRect(new(x, y, width, height)), clip);
int argb = unchecked((int)(0xFF000000u | rgb));
for (int row = rectangle.Y; row < rectangle.Y + rectangle.Height; row++)
{
motion.RowSegments(rectangle.X, rectangle.Width, row, out int leftEnd, out int rightStart);
Array.Fill(pixels, argb, row * NativeWidth + rectangle.X, leftEnd - rectangle.X);
Array.Fill(pixels, argb, row * NativeWidth + rightStart, rectangle.X + rectangle.Width - rightStart);
}
}
public void BlendCoverage(uint rgb, int nativeX, int nativeY, int width, int height, ReadOnlySpan<byte> coverage)
{
if (width <= 0 || height <= 0) return;
if ((long)width * height != coverage.Length) throw new ArgumentException("Coverage dimensions do not match the mask.", nameof(coverage));
int left = (int)Math.Max(nativeX, (long)clip.X), top = (int)Math.Max(nativeY, (long)clip.Y);
long right = Math.Min((long)nativeX + width, (long)clip.X + clip.Width);
long bottom = Math.Min((long)nativeY + height, (long)clip.Y + clip.Height);
if (right <= left || bottom <= top) return;
if (motion.Contains(new(left, top, (int)right - left, (int)bottom - top))) return;
for (int row = top; row < bottom; row++)
{
if(RenderYieldRequested)throw new DesktopRenderYieldException();
motion.RowSegments(left, (int)right - left, row, out int leftEnd, out int rightStart);
for (int segment = 0; segment < 2; segment++)
{
int start=segment==0?left:rightStart,end=segment==0?leftEnd:(int)right;
int offset=(row-nativeY)*width-nativeX,frameOffset=row*NativeWidth;
for(int col=start;col<end;)
{
byte alpha=coverage[offset+col];
if(alpha==0){do{col++;}while(col<end&&coverage[offset+col]==0);}
else if(alpha==255)
{
int first=col;do{col++;}while(col<end&&coverage[offset+col]==255);
Array.Fill(pixels,unchecked((int)(0xFF000000u|rgb&0xFFFFFF)),frameOffset+first,col-first);
}
else{Blend(frameOffset+col,rgb&0xFFFFFF,alpha);col++;}
}
}
}
}
public void Line(uint rgb, int x1, int y1, int x2, int y2) => DesktopChromeRenderer.Line(this, rgb, x1, y1, x2, y2);
public void Text(string text, uint rgb, int x, int y)
{
int left = x * scale, top = y * scale;
if (top >= clip.Y + clip.Height || top + 20 * scale <= clip.Y) return;
foreach (char character in text)
{
int width = (character < 128 ? 8 : unicode[GlyphOffset(character)]) * scale;
if (left >= clip.X + clip.Width) break;
if (left + width > clip.X && !motion.Contains(new(left, top, width, 20 * scale)))
{
var glyph = GlyphOf(character);
var visible = Intersect(new(left, top, glyph.Width, glyph.Height), clip);
for (int row = visible.Y; row < visible.Y + visible.Height; row++)
{
motion.RowSegments(visible.X, visible.Width, row, out int leftEnd, out int rightStart);
int right = visible.X + visible.Width;
for (int col = visible.X; col < right; col = col + 1 == leftEnd ? rightStart : col + 1)
{
if (col == leftEnd && col < rightStart) { col = rightStart; if (col >= right) break; }
byte alpha = glyph.Alpha[(row - top) * glyph.Width + col - left];
if (alpha != 0) Blend(row * NativeWidth + col, rgb, alpha);
}
}
}
left += width;
}
}
private Glyph GlyphOf(char character)
{
if (glyphs.TryGetValue(character, out var cached)) return cached;
int width = (character < 128 ? 8 : unicode[GlyphOffset(character)]) * scale, height = 20 * scale;
var alpha = new byte[width * height];
if (character < 128)
{
var tile = new Canvas(width, height);
tile.Clear(0);
tile.DrawString(character.ToString(), font, Color.White, 0, 0);
int[] source = tile.GetBuffer()!;
for (int i = 0; i < source.Length; i++) alpha[i] = (byte)((uint)source[i] >> 24);
}
else
{
int offset = GlyphOffset(character), logicalWidth = width / scale;
for (int row = 0; row < 16; row++)
{
int bits = unicode[offset + 1 + row * 2] << 8 | unicode[offset + 2 + row * 2];
for (int col = 0; col < logicalWidth; col++)
if ((bits & (0x8000 >> col)) != 0)
for (int sy = 0; sy < scale; sy++)
Array.Fill(alpha, (byte)255, (row * scale + sy) * width + col * scale, scale);
}
}
var glyph = new Glyph(width, height, alpha);
if (glyphs.Count < 4096) glyphs.Add(character, glyph);
return glyph;
}
public void DrawIcon(DesktopIcon icon, int x, int y, int size)
{
if (icon == DesktopIcon.Settings)
{
if (size <= 0 || size > 256) return;
// Code-native sliders are distinct from the nine generated atlas
// entries. In particular Settings=9 must never index past atlas.
int margin = Math.Max(2, size / 5), knob = Math.Max(3, size / 4);
int radius = Math.Max(1, knob / 2);
for (int row = 0; row < 3; row++)
{
int cy = y + margin + row * Math.Max(1, (size - 2 * margin) / 2);
int cx = x + (row == 1 ? size * 2 / 3 : size / 3);
DesktopChromeRenderer.Line(this, 0x4086B8, x + margin, cy, x + size - margin, cy);
DesktopChromeRenderer.RoundedFill(this, 0x4086B8, cx - radius, cy - radius, knob, knob, radius);
if (knob >= 5)
DesktopChromeRenderer.RoundedFill(this, 0x75C9F5, cx - radius + 1, cy - radius + 1, knob - 2, knob - 2, Math.Max(1, radius - 1));
}
return;
}
if ((uint)icon >= 9) throw new ArgumentOutOfRangeException(nameof(icon));
DrawRaster((int)icon, atlas, (int)icon * 64 * 64 * 4, x, y, size);
}
public void BrandIcon(int x, int y, int size) => DrawRaster(10, brand, 0, x, y, size);
private void DrawRaster(int icon, byte[] source, int offset, int x, int y, int size)
{
int edge = size * scale;
if (edge <= 0 || edge > 512) return;
int left = x * scale, top = y * scale;
var visible = Intersect(new(left, top, edge, edge), clip);
if (visible.Width <= 0 || visible.Height <= 0) return;
if (motion.Contains(new(visible.X, visible.Y, visible.Width, visible.Height))) return;
if (!iconCache.TryGetValue((icon, edge), out var scaled))
{
scaled = new int[edge * edge];
for (int row = 0; row < edge; row++)
for (int col = 0; col < edge; col++)
{
int at = offset + ((row * 64 / edge) * 64 + col * 64 / edge) * 4;
scaled[row * edge + col] = source[at + 3] << 24 | source[at] << 16 | source[at + 1] << 8 | source[at + 2];
}
if (iconCache.Count < 64) iconCache.Add((icon, edge), scaled);
}
for (int row = visible.Y; row < visible.Y + visible.Height; row++)
{
motion.RowSegments(visible.X, visible.Width, row, out int leftEnd, out int rightStart);
int right = visible.X + visible.Width;
for (int col = visible.X; col < right; col = col + 1 == leftEnd ? rightStart : col + 1)
{
if (col == leftEnd && col < rightStart) { col = rightStart; if (col >= right) break; }
uint argb = (uint)scaled[(row - top) * edge + col - left];
if (argb >> 24 != 0) Blend(row * NativeWidth + col, argb & 0xFFFFFF, (byte)(argb >> 24));
}
}
}
private void Blend(int index, uint rgb, byte alpha)
{
if (alpha == 255) { pixels[index] = unchecked((int)(0xFF000000 | rgb)); return; }
uint old = (uint)pixels[index];
uint r = ((rgb >> 16 & 255) * alpha + (old >> 16 & 255) * (255u - alpha) + 127) / 255;
uint g = ((rgb >> 8 & 255) * alpha + (old >> 8 & 255) * (255u - alpha) + 127) / 255;
uint b = ((rgb & 255) * alpha + (old & 255) * (255u - alpha) + 127) / 255;
pixels[index] = unchecked((int)(0xFF000000 | r << 16 | g << 8 | b));
}
public void SaveScene() { motion.End(); pixels.AsSpan().CopyTo(scene); }
public void SaveScene(DesktopBounds damage) { motion.End(); CopyRegion(pixels, scene, NativeRect(damage)); }
public void RestoreSceneRegion(DesktopBounds bounds) => CopyRegion(scene, pixels, NativeRect(bounds));
private void CopyRegion(int[] source, int[] target, PixelRect rectangle)
{
for (int row = rectangle.Y; row < rectangle.Y + rectangle.Height; row++)
source.AsSpan(row * NativeWidth + rectangle.X, rectangle.Width).CopyTo(target.AsSpan(row * NativeWidth + rectangle.X, rectangle.Width));
}
public void Present() => Present(new(0, 0, Width, Height));
public bool TryFingerprintPresentedPixels(DesktopBounds nativeBounds, out DesktopPresentedPixelsFingerprint result)
{
result = default;
// Read the display's mapped framebuffer, rather than the software
// working buffer that a later or aborted frame may already modify.
if (FramesPresented <= 0 || device?.IsWithdrawn == true || scanout is null || scanout.Framebuffer is not { } framebuffer)
return false;
var mode = scanout.Mode;
if (mode.Width != NativeWidth || mode.Height != NativeHeight || mode.BitsPerPixel != 32 || mode.Pitch % 4 != 0 ||
mode.Pitch < NativeWidth * 4 || framebuffer.Length < (ulong)mode.Pitch * (ulong)NativeHeight)
return false;
return DesktopPresentedPixels.TryFingerprint(framebuffer.As<int>(), NativeWidth, NativeHeight, mode.Pitch / 4,
nativeBounds, FramesPresented, out result);
}
public void Present(DesktopBounds damage)
{
if (preparationOnly) throw new InvalidOperationException("The editor preparation canvas cannot present to a display.");
long begin = Stopwatch.GetTimestamp();
var rectangle = NativeRect(damage);
var unchanged = unchangedScanout; unchangedScanout = default;
if (rectangle.Width <= 0 || rectangle.Height <= 0) return;
if (device?.IsWithdrawn == true) return;
if (scanout is not null && scanout.Framebuffer is { } framebuffer && scanout.Mode is var mode &&
mode.Width == NativeWidth && mode.Height == NativeHeight && mode.BitsPerPixel == 32 && mode.Pitch % 4 == 0 && mode.Pitch >= NativeWidth * 4 &&
framebuffer.Length >= (ulong)mode.Pitch * (ulong)NativeHeight)
{
Span<int> target = framebuffer.As<int>();
int stride = mode.Pitch / 4;
long copied = 0;
for (int row = rectangle.Y; row < rectangle.Y + rectangle.Height; row++)
{
DesktopSceneMotionBuffer.RowSegments(unchanged, rectangle.X, rectangle.Width, row, out int leftEnd, out int rightStart);
int leftCount = leftEnd - rectangle.X, rightCount = rectangle.X + rectangle.Width - rightStart;
pixels.AsSpan(row * NativeWidth + rectangle.X, leftCount).CopyTo(target.Slice(row * stride + rectangle.X, leftCount));
pixels.AsSpan(row * NativeWidth + rightStart, rightCount).CopyTo(target.Slice(row * stride + rightStart, rightCount));
copied += leftCount + rightCount;
}
scanout.Flush(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
PixelsPresented += copied;
ScanoutPixelsSaved += (long)rectangle.Width * rectangle.Height - copied;
if (rectangle.Width < NativeWidth || rectangle.Height < NativeHeight) PartialFrames++;
}
else
{
canvas.Display();
PixelsPresented += (long)NativeWidth * NativeHeight;
FullCopyFallbackFrames++;
}
FramesPresented++;
LastPresentTicks = Stopwatch.GetTimestamp() - begin;
}
private PixelRect NativeRect(DesktopBounds value)
{
int x = Math.Clamp(value.X * scale, 0, NativeWidth), y = Math.Clamp(value.Y * scale, 0, NativeHeight);
int right = Math.Clamp((value.X + Math.Max(0, value.Width)) * scale, x, NativeWidth);
int bottom = Math.Clamp((value.Y + Math.Max(0, value.Height)) * scale, y, NativeHeight);
return new(x, y, right - x, bottom - y);
}
private static PixelRect Intersect(PixelRect a, PixelRect b)
{
int x = Math.Max(a.X, b.X), y = Math.Max(a.Y, b.Y);
return new(x, y, Math.Max(0, Math.Min(a.X + a.Width, b.X + b.Width) - x), Math.Max(0, Math.Min(a.Y + a.Height, b.Y + b.Height) - y));
}
private static byte[] ResourceBytes(string name)
{
using var resource = typeof(CanvasAdapter).Assembly.GetManifestResourceStream(name)
?? throw new InvalidOperationException("An embedded desktop asset is missing: " + name);
var data = new byte[resource.Length]; resource.ReadExactly(data); return data;
}
private int GlyphOffset(int character)
{
foreach (var range in GlyphRanges)
if (character >= range.First && character <= range.Last) return range.Offset + (character - range.First) * 33;
return unicode.Length - 33;
}
}