using XFESystem.Desktop; namespace XFESystem.Platform; public sealed record DesktopScanout(DesktopDisplay Display, IFramebufferPresenter Presenter); /// One desktop scene, independently copied to each real output with its own pitch and format. public sealed class MultiDisplayPresenter : IFramebufferPresenter { private readonly DesktopScanout[] outputs; private readonly int[][] scanoutPixels; private readonly int scalePercent; private DesktopDisplayLayout layout; private bool fullDamage = true, identifying; public int Width => layout.Width * scalePercent / 100; public int Height => layout.Height * scalePercent / 100; public DesktopDisplayLayout Layout => layout; public MultiDisplayPresenter(IEnumerable outputs, DesktopDisplayLayout layout, int scalePercent) { this.outputs = outputs.ToArray(); this.layout = layout; this.scalePercent = scalePercent; if (this.outputs.Length is < 1 or > 8) throw new ArgumentException("One to eight outputs are supported.", nameof(outputs)); foreach (var output in this.outputs) if (output.Display.NativeWidth != output.Presenter.Width || output.Display.NativeHeight != output.Presenter.Height) throw new ArgumentException("The display and scanout geometries differ.", nameof(outputs)); scanoutPixels = this.outputs.Select(output => new int[checked(output.Presenter.Width * output.Presenter.Height)]).ToArray(); PixelGeometry.ValidateSize(Width, Height); } public void Configure(DesktopDisplayLayout next) { layout = next; fullDamage = true; } public void Identify(bool visible) { if (identifying != visible) { identifying = visible; fullDamage = true; } } public void Present(ReadOnlySpan rgbPixels, DesktopBounds nativeDamage) { if (rgbPixels.Length < (long)Width * Height) throw new ArgumentException("The virtual desktop buffer is incomplete.", nameof(rgbPixels)); bool full = fullDamage || identifying; for (int index = 0; index < outputs.Length; index++) { var output = outputs[index]; int width = output.Presenter.Width, height = output.Presenter.Height; int[] target = scanoutPixels[index]; DesktopDisplayScreen? screen = null; for (int candidate = 0; candidate < layout.Screens.Count; candidate++) if (layout.Screens[candidate].Display.Id == output.Display.Id) { screen = layout.Screens[candidate]; break; } var damage = new DesktopBounds(0, 0, width, height); if (screen is null) { if (!full) continue; target.AsSpan().Clear(); } else { var b = screen.Bounds; var source = new DesktopBounds(b.X * scalePercent / 100, b.Y * scalePercent / 100, b.Width * scalePercent / 100, b.Height * scalePercent / 100); if (!identifying && source == new DesktopBounds(0, 0, width, height) && Width == width && Height == height) { // The normal one-screen and equal-size mirror paths keep // their direct scanout, with no extra full-frame copy. output.Presenter.Present(rgbPixels, full ? damage : nativeDamage); continue; } int destinationWidth = width, destinationHeight = height; if ((long)width * source.Height > (long)height * source.Width) destinationWidth = Math.Max(1, (int)((long)height * source.Width / source.Height)); else destinationHeight = Math.Max(1, (int)((long)width * source.Height / source.Width)); var destination = new DesktopBounds((width - destinationWidth) / 2, (height - destinationHeight) / 2, destinationWidth, destinationHeight); var inputDamage = full ? source : PixelGeometry.Intersect(nativeDamage, source); if (inputDamage.Width == 0 || inputDamage.Height == 0) continue; if (full) target.AsSpan().Clear(); int left = Math.Max(destination.X, destination.X + (int)((long)(inputDamage.X - source.X) * destinationWidth / source.Width) - 1); int top = Math.Max(destination.Y, destination.Y + (int)((long)(inputDamage.Y - source.Y) * destinationHeight / source.Height) - 1); int right = Math.Min(destination.X + destinationWidth, destination.X + (int)(((long)inputDamage.X + inputDamage.Width - source.X) * destinationWidth / source.Width) + 1); int bottom = Math.Min(destination.Y + destinationHeight, destination.Y + (int)(((long)inputDamage.Y + inputDamage.Height - source.Y) * destinationHeight / source.Height) + 1); for (int y = top; y < bottom; y++) { int sy = source.Y + (int)((long)(y - destination.Y) * source.Height / destinationHeight); if (destinationWidth == source.Width) rgbPixels.Slice(sy * Width + source.X + left - destination.X, right - left).CopyTo(target.AsSpan(y * width + left, right - left)); else for (int x = left; x < right; x++) { int sx = source.X + (int)((long)(x - destination.X) * source.Width / destinationWidth); target[y * width + x] = rgbPixels[sy * Width + sx]; } } if (!full) damage = new(left, top, right - left, bottom - top); } if (identifying) DrawIdentity(target, width, height, index + 1); output.Presenter.Present(target, damage); } fullDamage = false; } // A small dependency-free digit mask remains distinct on mirrored outputs. private static void DrawIdentity(int[] pixels, int width, int height, int number) { ReadOnlySpan glyphs = [0, 0x2492, 0x73e7, 0x73cf, 0x5bc9, 0x79cf, 0x79ef, 0x7249, 0x7bef]; const int cell = 14, boxWidth = 112, boxHeight = 110; int left = Math.Max(0, (width - boxWidth) / 2), top = Math.Max(0, (height - boxHeight) / 2); for (int y = top; y < top + boxHeight && y < height; y++) pixels.AsSpan(y * width + left, Math.Min(boxWidth, width - left)).Fill(0x17394f); for (int row = 0; row < 5; row++) for (int column = 0; column < 3; column++) if ((glyphs[number] & 1 << (14 - row * 3 - column)) != 0) for (int y = top + 20 + row * cell; y < top + 20 + (row + 1) * cell && y < height; y++) pixels.AsSpan(y * width + left + 35 + column * cell, cell).Fill(0xe7f3fc); } } /// Applies the output topology and resizes the CPU canvas as one UI-thread operation. public sealed class FramebufferDisplayManager : IDesktopDisplayManager { private CpuFramebufferCanvas? canvas; public IReadOnlyList Displays { get; } public DesktopDisplayConfiguration Current { get; private set; } public DesktopDisplayLayout Layout => Presenter.Layout; public int ScalePercent { get; } public MultiDisplayPresenter Presenter { get; } public string DetectionMessage => "Firmware outputs detected at startup. Reconnect displays, then restart to detect changes."; public event Action? Changed; public FramebufferDisplayManager(IEnumerable scanouts, int scalePercent) { if (scalePercent is < 100 or > 400) throw new ArgumentOutOfRangeException(nameof(scalePercent)); var outputs = scanouts.ToArray(); Displays = Array.AsReadOnly(outputs.Select(s => s.Display).ToArray()); ScalePercent = scalePercent; if (outputs.Length is < 1 or > 8) throw new ArgumentException("No supported display outputs.", nameof(scanouts)); Current = DesktopDisplayLayout.Default(Displays, scalePercent); Presenter = new(outputs, DesktopDisplayLayout.Create(Displays, Current, scalePercent), scalePercent); } public void AttachCanvas(CpuFramebufferCanvas canvas) => this.canvas = canvas; public bool TryApply(DesktopDisplayConfiguration configuration, out string? error) { error = null; DesktopDisplayLayout previous = Layout; try { var next = DesktopDisplayLayout.Create(Displays, configuration, ScalePercent); Presenter.Configure(next); try { canvas?.Resize(); } catch { Presenter.Configure(previous); throw; } Current = configuration; } catch (Exception failure) when (failure is ArgumentException or OverflowException or OutOfMemoryException) { error = failure is OutOfMemoryException ? "There is not enough memory for this display arrangement." : failure.Message; return false; } Changed?.Invoke(); return true; } public void Identify(bool visible) => Presenter.Identify(visible); public void Detect() { /* This backend has an immutable firmware snapshot, not a hotplug driver. */ } }