using XFESystem.Core;
using XFESystem.Desktop;
namespace XFESystem.Platform.Tests;
public sealed class MultiDisplayTests
{
private static readonly DesktopDisplay First = new("first", "Display 1", 640, 480);
private static readonly DesktopDisplay Second = new("second", "Display 2", 640, 480);
private static (FramebufferDisplayManager Manager, ArrayFramebufferPresenter First, ArrayFramebufferPresenter Second, CpuFramebufferCanvas Canvas) Create(DesktopDisplay? second = null)
{
second ??= Second;
var a = new ArrayFramebufferPresenter(First.NativeWidth, First.NativeHeight);
var b = new ArrayFramebufferPresenter(second.NativeWidth, second.NativeHeight);
var manager = new FramebufferDisplayManager([new(First, a), new(second, b)], 100);
var canvas = new CpuFramebufferCanvas(manager.Presenter); manager.AttachCanvas(canvas);
return (manager, a, b, canvas);
}
[Fact]
public void DuplicatePresentsIdenticalPixelsAndDamageToIndependentOutputs()
{
var (manager, a, b, canvas) = Create();
canvas.Fill(0x123456, 0, 0, 640, 480); canvas.Present();
Assert.Equal(a.Pixels.ToArray(), b.Pixels.ToArray());
canvas.Fill(0xff1234, 170, 82, 7, 9); canvas.Present(new(170, 82, 7, 9));
Assert.Equal(0xff1234, b.Pixels.Span[82 * 640 + 170]);
Assert.Equal(a.Pixels.ToArray(), b.Pixels.ToArray());
Assert.Equal(0x123456, b.Pixels.Span[81 * 640 + 170]);
Assert.Equal(DesktopDisplayMode.Duplicate, manager.Current.Mode);
}
[Fact]
public void DuplicateFitsDifferentAspectRatiosWithoutStretchingAndKeepsBlackBars()
{
var (_, _, b, canvas) = Create(Second with { NativeWidth = 800, NativeHeight = 480 });
canvas.Fill(0x238acd, 0, 0, 640, 480); canvas.Present();
Assert.Equal(0, b.Pixels.Span[240 * 800 + 79]);
Assert.Equal(0x238acd, b.Pixels.Span[240 * 800 + 80]);
Assert.Equal(0x238acd, b.Pixels.Span[240 * 800 + 719]);
Assert.Equal(0, b.Pixels.Span[240 * 800 + 720]);
}
[Fact]
public void ExtendedDesktopRoutesRegionsAndPointerDamageToTheCorrectScanout()
{
var (manager, a, b, canvas) = Create();
Assert.True(manager.TryApply(manager.Current.WithMode(DesktopDisplayMode.Extend), out var error), error);
Assert.Equal(1280, canvas.Width);
canvas.Fill(0xcc2211, 0, 0, 640, 480); canvas.Fill(0x1133cc, 640, 0, 640, 480); canvas.Present();
Assert.All(a.Pixels.ToArray(), p => Assert.Equal(0xcc2211, p));
Assert.All(b.Pixels.ToArray(), p => Assert.Equal(0x1133cc, p));
long firstPresents = a.Presentations;
canvas.Fill(0xffffff, 650, 10, 3, 3); canvas.Present(new(650, 10, 3, 3));
Assert.Equal(firstPresents, a.Presentations);
Assert.Equal(0xffffff, b.Pixels.Span[10 * 640 + 10]);
}
[Fact]
public void OnlySecondBlacksFirstAndSwitchingBackRestoresBothOutputs()
{
var (manager, a, b, canvas) = Create();
canvas.Fill(0xffffff, 0, 0, 640, 480); canvas.Present();
var previous = manager.Current;
Assert.True(manager.TryApply(previous.WithMode(DesktopDisplayMode.SelectedOnly).WithPrimary(Second.Id), out _));
canvas.Fill(0x3478ab, 0, 0, 640, 480); canvas.Present();
Assert.All(a.Pixels.ToArray(), p => Assert.Equal(0, p));
Assert.All(b.Pixels.ToArray(), p => Assert.Equal(0x3478ab, p));
Assert.True(manager.TryApply(previous, out _)); canvas.Present();
Assert.Equal(a.Pixels.ToArray(), b.Pixels.ToArray());
}
[Fact]
public void IdentifyNumbersDifferOnMirroredOutputsAndDisappearWithoutGhosts()
{
var (manager, a, b, canvas) = Create();
canvas.Fill(0x123456, 0, 0, 640, 480); canvas.Present();
manager.Identify(true); canvas.Present();
Assert.NotEqual(a.Pixels.ToArray(), b.Pixels.ToArray());
manager.Identify(false); canvas.Present();
Assert.All(a.Pixels.ToArray(), p => Assert.Equal(0x123456, p));
Assert.Equal(a.Pixels.ToArray(), b.Pixels.ToArray());
}
[Fact]
public void InvalidConfigurationLeavesLiveCanvasAndOutputsUntouched()
{
var (manager, _, _, canvas) = Create(); var original = manager.Current;
foreach (var bad in new[] {
original.WithPrimary("missing"), original.WithMode((DesktopDisplayMode)99),
original.WithMode(DesktopDisplayMode.Extend).Move(Second.Id, 100, 0),
original.WithMode(DesktopDisplayMode.Extend).Move(Second.Id, 700, 0),
original.Move(Second.Id, int.MaxValue, 0),
new DesktopDisplayConfiguration(DesktopDisplayMode.Extend, First.Id, [new(First.Id, 0, 0), new(First.Id, 640, 0)]) })
{
Assert.False(manager.TryApply(bad, out var error)); Assert.NotEmpty(error!);
Assert.Same(original, manager.Current); Assert.Equal(640, canvas.Width); Assert.Equal(480, canvas.Height);
}
}
[Fact]
public void PreviewTimeoutAndEscapeRestoreLayoutWhileOnlyConfirmationWritesSettings()
{
var (manager, _, _, canvas) = Create(); var storage = new MemoryWorkspace(); long now = 1000;
var desktop = new DesktopSession(canvas, storage, monotonicMilliseconds: () => now, displays: manager);
desktop.ShowDisplaySettings();
Assert.True(desktop.ApplyDisplayConfiguration(manager.Current.WithMode(DesktopDisplayMode.Extend)));
Assert.False(storage.Exists(DesktopSession.DisplaySettingsPath)); Assert.Equal(1280, canvas.Width);
now += 15001; desktop.AdvanceFeedback(now);
Assert.False(desktop.IsDisplayChangePending); Assert.Equal(640, canvas.Width);
Assert.True(desktop.ApplyDisplayConfiguration(manager.Current.WithMode(DesktopDisplayMode.Extend)));
desktop.Key(DesktopKey.Escape); Assert.Equal(640, canvas.Width);
Assert.True(desktop.ApplyDisplayConfiguration(manager.Current.WithMode(DesktopDisplayMode.Extend)));
desktop.KeepDisplayConfiguration();
Assert.Equal(DesktopDisplayMode.Extend, DesktopDisplayConfiguration.Parse(storage.ReadText(DesktopSession.DisplaySettingsPath)).Mode);
var next = Create(); _ = new DesktopSession(next.Canvas, storage, displays: next.Manager);
Assert.Equal(1280, next.Canvas.Width); Assert.Equal(DesktopDisplayMode.Extend, next.Manager.Current.Mode);
}
[Fact]
public void WindowsCanCrossDisplaysAndMaximizeWithinTheirOwnMonitor()
{
var (manager, _, _, canvas) = Create();
var desktop = new DesktopSession(canvas, new MemoryWorkspace(), displays: manager);
Assert.True(desktop.ApplyDisplayConfiguration(manager.Current.WithMode(DesktopDisplayMode.Extend)));
desktop.KeepDisplayConfiguration();
var window = desktop.Open(DesktopApplication.Files);
var before = window.Bounds;
desktop.Pointer(before.X + 60, before.Y + 16, true, timestampMs: 100);
desktop.Pointer(800, 40, true, timestampMs: 200); desktop.Pointer(800, 40, false, timestampMs: 210);
Assert.True(window.Bounds.X >= 640);
desktop.ToggleMaximize(window.Id);
Assert.Equal(new DesktopBounds(640, 0, 640, 480), window.Bounds);
Assert.True(desktop.ApplyDisplayConfiguration(manager.Current.WithPrimary(Second.Id))); desktop.KeepDisplayConfiguration();
Assert.Equal(new DesktopBounds(640, 0, 640, 432), desktop.WorkArea);
desktop.OpenSettings(); desktop.Render();
Assert.True(desktop.LastFrameCompleted);
}
[Fact]
public void NegativeAndVerticalLayoutsConstrainPointerToActualScreenRegions()
{
var outputs = new[] { First, Second with { NativeHeight = 400 } };
var config = new DesktopDisplayConfiguration(DesktopDisplayMode.Extend, First.Id, [new(First.Id, 0, 0), new(Second.Id, -640, 200)]);
var layout = DesktopDisplayLayout.Create(outputs, config, 100);
Assert.Equal(new DesktopBounds(640, 0, 640, 480), layout.Primary.Bounds);
var position = layout.ConstrainPointer(10, 5);
Assert.Equal((10, 200), position);
Assert.Equal((650, 5), layout.ConstrainPointer(650, 5));
}
[Fact]
public void ContinuousPointerRoutingAndOutputPresentationDoNotAllocate()
{
var (manager, _, _, _) = Create(Second with { NativeHeight = 400 });
var configuration = new DesktopDisplayConfiguration(DesktopDisplayMode.Extend, First.Id,
[new(First.Id, 0, 0), new(Second.Id, -640, 200)]);
Assert.True(manager.TryApply(configuration, out var error), error);
var pixels = new int[manager.Presenter.Width * manager.Presenter.Height];
void Motion()
{
_ = manager.Layout.Primary;
_ = manager.Layout.ConstrainPointer(650, 5);
_ = manager.Layout.ConstrainPointer(10, 5);
manager.Presenter.Present(pixels, new(640, 1, 8, 8));
}
for (int i = 0; i < 256; i++) Motion();
long before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 1024; i++) Motion();
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.Equal(0, allocated);
Assert.Equal((10, 200), manager.Layout.ConstrainPointer(10, 5));
}
[Theory]
[InlineData("{\"Version\":1,\"Version\":1,\"Mode\":\"Extend\",\"Primary\":\"first\",\"Positions\":[]}")]
[InlineData("{\"Version\":1,\"Mode\":\"Bogus\",\"Primary\":\"first\",\"Positions\":[]}")]
[InlineData("{\"Version\":1,\"Mode\":\"Extend\",\"Primary\":\"first\",\"Positions\":[{\"Id\":\"first\",\"X\":0,\"Y\":0,\"Unknown\":1}]}")]
public void MalformedSettingsAreRejected(string json) => Assert.ThrowsAny<Exception>(() => DesktopDisplayConfiguration.Parse(json));
}
using XFESystem.Core;
using XFESystem.Desktop;
namespace XFESystem.Platform.Tests;
public sealed class MultiDisplayTests
{
private static readonly DesktopDisplay First = new("first", "Display 1", 640, 480);
private static readonly DesktopDisplay Second = new("second", "Display 2", 640, 480);
private static (FramebufferDisplayManager Manager, ArrayFramebufferPresenter First, ArrayFramebufferPresenter Second, CpuFramebufferCanvas Canvas) Create(DesktopDisplay? second = null)
{
second ??= Second;
var a = new ArrayFramebufferPresenter(First.NativeWidth, First.NativeHeight);
var b = new ArrayFramebufferPresenter(second.NativeWidth, second.NativeHeight);
var manager = new FramebufferDisplayManager([new(First, a), new(second, b)], 100);
var canvas = new CpuFramebufferCanvas(manager.Presenter); manager.AttachCanvas(canvas);
return (manager, a, b, canvas);
}
[Fact]
public void DuplicatePresentsIdenticalPixelsAndDamageToIndependentOutputs()
{
var (manager, a, b, canvas) = Create();
canvas.Fill(0x123456, 0, 0, 640, 480); canvas.Present();
Assert.Equal(a.Pixels.ToArray(), b.Pixels.ToArray());
canvas.Fill(0xff1234, 170, 82, 7, 9); canvas.Present(new(170, 82, 7, 9));
Assert.Equal(0xff1234, b.Pixels.Span[82 * 640 + 170]);
Assert.Equal(a.Pixels.ToArray(), b.Pixels.ToArray());
Assert.Equal(0x123456, b.Pixels.Span[81 * 640 + 170]);
Assert.Equal(DesktopDisplayMode.Duplicate, manager.Current.Mode);
}
[Fact]
public void DuplicateFitsDifferentAspectRatiosWithoutStretchingAndKeepsBlackBars()
{
var (_, _, b, canvas) = Create(Second with { NativeWidth = 800, NativeHeight = 480 });
canvas.Fill(0x238acd, 0, 0, 640, 480); canvas.Present();
Assert.Equal(0, b.Pixels.Span[240 * 800 + 79]);
Assert.Equal(0x238acd, b.Pixels.Span[240 * 800 + 80]);
Assert.Equal(0x238acd, b.Pixels.Span[240 * 800 + 719]);
Assert.Equal(0, b.Pixels.Span[240 * 800 + 720]);
}
[Fact]
public void ExtendedDesktopRoutesRegionsAndPointerDamageToTheCorrectScanout()
{
var (manager, a, b, canvas) = Create();
Assert.True(manager.TryApply(manager.Current.WithMode(DesktopDisplayMode.Extend), out var error), error);
Assert.Equal(1280, canvas.Width);
canvas.Fill(0xcc2211, 0, 0, 640, 480); canvas.Fill(0x1133cc, 640, 0, 640, 480); canvas.Present();
Assert.All(a.Pixels.ToArray(), p => Assert.Equal(0xcc2211, p));
Assert.All(b.Pixels.ToArray(), p => Assert.Equal(0x1133cc, p));
long firstPresents = a.Presentations;
canvas.Fill(0xffffff, 650, 10, 3, 3); canvas.Present(new(650, 10, 3, 3));
Assert.Equal(firstPresents, a.Presentations);
Assert.Equal(0xffffff, b.Pixels.Span[10 * 640 + 10]);
}
[Fact]
public void OnlySecondBlacksFirstAndSwitchingBackRestoresBothOutputs()
{
var (manager, a, b, canvas) = Create();
canvas.Fill(0xffffff, 0, 0, 640, 480); canvas.Present();
var previous = manager.Current;
Assert.True(manager.TryApply(previous.WithMode(DesktopDisplayMode.SelectedOnly).WithPrimary(Second.Id), out _));
canvas.Fill(0x3478ab, 0, 0, 640, 480); canvas.Present();
Assert.All(a.Pixels.ToArray(), p => Assert.Equal(0, p));
Assert.All(b.Pixels.ToArray(), p => Assert.Equal(0x3478ab, p));
Assert.True(manager.TryApply(previous, out _)); canvas.Present();
Assert.Equal(a.Pixels.ToArray(), b.Pixels.ToArray());
}
[Fact]
public void IdentifyNumbersDifferOnMirroredOutputsAndDisappearWithoutGhosts()
{
var (manager, a, b, canvas) = Create();
canvas.Fill(0x123456, 0, 0, 640, 480); canvas.Present();
manager.Identify(true); canvas.Present();
Assert.NotEqual(a.Pixels.ToArray(), b.Pixels.ToArray());
manager.Identify(false); canvas.Present();
Assert.All(a.Pixels.ToArray(), p => Assert.Equal(0x123456, p));
Assert.Equal(a.Pixels.ToArray(), b.Pixels.ToArray());
}
[Fact]
public void InvalidConfigurationLeavesLiveCanvasAndOutputsUntouched()
{
var (manager, _, _, canvas) = Create(); var original = manager.Current;
foreach (var bad in new[] {
original.WithPrimary("missing"), original.WithMode((DesktopDisplayMode)99),
original.WithMode(DesktopDisplayMode.Extend).Move(Second.Id, 100, 0),
original.WithMode(DesktopDisplayMode.Extend).Move(Second.Id, 700, 0),
original.Move(Second.Id, int.MaxValue, 0),
new DesktopDisplayConfiguration(DesktopDisplayMode.Extend, First.Id, [new(First.Id, 0, 0), new(First.Id, 640, 0)]) })
{
Assert.False(manager.TryApply(bad, out var error)); Assert.NotEmpty(error!);
Assert.Same(original, manager.Current); Assert.Equal(640, canvas.Width); Assert.Equal(480, canvas.Height);
}
}
[Fact]
public void PreviewTimeoutAndEscapeRestoreLayoutWhileOnlyConfirmationWritesSettings()
{
var (manager, _, _, canvas) = Create(); var storage = new MemoryWorkspace(); long now = 1000;
var desktop = new DesktopSession(canvas, storage, monotonicMilliseconds: () => now, displays: manager);
desktop.ShowDisplaySettings();
Assert.True(desktop.ApplyDisplayConfiguration(manager.Current.WithMode(DesktopDisplayMode.Extend)));
Assert.False(storage.Exists(DesktopSession.DisplaySettingsPath)); Assert.Equal(1280, canvas.Width);
now += 15001; desktop.AdvanceFeedback(now);
Assert.False(desktop.IsDisplayChangePending); Assert.Equal(640, canvas.Width);
Assert.True(desktop.ApplyDisplayConfiguration(manager.Current.WithMode(DesktopDisplayMode.Extend)));
desktop.Key(DesktopKey.Escape); Assert.Equal(640, canvas.Width);
Assert.True(desktop.ApplyDisplayConfiguration(manager.Current.WithMode(DesktopDisplayMode.Extend)));
desktop.KeepDisplayConfiguration();
Assert.Equal(DesktopDisplayMode.Extend, DesktopDisplayConfiguration.Parse(storage.ReadText(DesktopSession.DisplaySettingsPath)).Mode);
var next = Create(); _ = new DesktopSession(next.Canvas, storage, displays: next.Manager);
Assert.Equal(1280, next.Canvas.Width); Assert.Equal(DesktopDisplayMode.Extend, next.Manager.Current.Mode);
}
[Fact]
public void WindowsCanCrossDisplaysAndMaximizeWithinTheirOwnMonitor()
{
var (manager, _, _, canvas) = Create();
var desktop = new DesktopSession(canvas, new MemoryWorkspace(), displays: manager);
Assert.True(desktop.ApplyDisplayConfiguration(manager.Current.WithMode(DesktopDisplayMode.Extend)));
desktop.KeepDisplayConfiguration();
var window = desktop.Open(DesktopApplication.Files);
var before = window.Bounds;
desktop.Pointer(before.X + 60, before.Y + 16, true, timestampMs: 100);
desktop.Pointer(800, 40, true, timestampMs: 200); desktop.Pointer(800, 40, false, timestampMs: 210);
Assert.True(window.Bounds.X >= 640);
desktop.ToggleMaximize(window.Id);
Assert.Equal(new DesktopBounds(640, 0, 640, 480), window.Bounds);
Assert.True(desktop.ApplyDisplayConfiguration(manager.Current.WithPrimary(Second.Id))); desktop.KeepDisplayConfiguration();
Assert.Equal(new DesktopBounds(640, 0, 640, 432), desktop.WorkArea);
desktop.OpenSettings(); desktop.Render();
Assert.True(desktop.LastFrameCompleted);
}
[Fact]
public void NegativeAndVerticalLayoutsConstrainPointerToActualScreenRegions()
{
var outputs = new[] { First, Second with { NativeHeight = 400 } };
var config = new DesktopDisplayConfiguration(DesktopDisplayMode.Extend, First.Id, [new(First.Id, 0, 0), new(Second.Id, -640, 200)]);
var layout = DesktopDisplayLayout.Create(outputs, config, 100);
Assert.Equal(new DesktopBounds(640, 0, 640, 480), layout.Primary.Bounds);
var position = layout.ConstrainPointer(10, 5);
Assert.Equal((10, 200), position);
Assert.Equal((650, 5), layout.ConstrainPointer(650, 5));
}
[Fact]
public void ContinuousPointerRoutingAndOutputPresentationDoNotAllocate()
{
var (manager, _, _, _) = Create(Second with { NativeHeight = 400 });
var configuration = new DesktopDisplayConfiguration(DesktopDisplayMode.Extend, First.Id,
[new(First.Id, 0, 0), new(Second.Id, -640, 200)]);
Assert.True(manager.TryApply(configuration, out var error), error);
var pixels = new int[manager.Presenter.Width * manager.Presenter.Height];
void Motion()
{
_ = manager.Layout.Primary;
_ = manager.Layout.ConstrainPointer(650, 5);
_ = manager.Layout.ConstrainPointer(10, 5);
manager.Presenter.Present(pixels, new(640, 1, 8, 8));
}
for (int i = 0; i < 256; i++) Motion();
long before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 1024; i++) Motion();
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.Equal(0, allocated);
Assert.Equal((10, 200), manager.Layout.ConstrainPointer(10, 5));
}
[Theory]
[InlineData("{\"Version\":1,\"Version\":1,\"Mode\":\"Extend\",\"Primary\":\"first\",\"Positions\":[]}")]
[InlineData("{\"Version\":1,\"Mode\":\"Bogus\",\"Primary\":\"first\",\"Positions\":[]}")]
[InlineData("{\"Version\":1,\"Mode\":\"Extend\",\"Primary\":\"first\",\"Positions\":[{\"Id\":\"first\",\"X\":0,\"Y\":0,\"Unknown\":1}]}")]
public void MalformedSettingsAreRejected(string json) => Assert.ThrowsAny<Exception>(() => DesktopDisplayConfiguration.Parse(json));
}