using XFESystem.Desktop;
namespace XFESystem.Platform;
public sealed record DesktopScanout(DesktopDisplay Display, IFramebufferPresenter Presenter);
/// <summary>One desktop scene, independently copied to each real output with its own pitch and format.</summary>
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<DesktopScanout> 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<int> 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<ushort> 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);
}
}
/// <summary>Applies the output topology and resizes the CPU canvas as one UI-thread operation.</summary>
public sealed class FramebufferDisplayManager : IDesktopDisplayManager
{
private CpuFramebufferCanvas? canvas;
public IReadOnlyList<DesktopDisplay> 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<DesktopScanout> 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. */ }
}
using XFESystem.Desktop;
namespace XFESystem.Platform;
public sealed record DesktopScanout(DesktopDisplay Display, IFramebufferPresenter Presenter);
/// <summary>One desktop scene, independently copied to each real output with its own pitch and format.</summary>
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<DesktopScanout> 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<int> 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<ushort> 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);
}
}
/// <summary>Applies the output topology and resizes the CPU canvas as one UI-thread operation.</summary>
public sealed class FramebufferDisplayManager : IDesktopDisplayManager
{
private CpuFramebufferCanvas? canvas;
public IReadOnlyList<DesktopDisplay> 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<DesktopScanout> 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. */ }
}