Skip to content

Commit

Permalink
✨ Implement built in graphics switch for embedded monitors.
Browse files Browse the repository at this point in the history
  • Loading branch information
hexawyz committed Feb 1, 2025
1 parent 7a1003f commit babc050
Show file tree
Hide file tree
Showing 9 changed files with 251 additions and 23 deletions.
151 changes: 137 additions & 14 deletions src/Exo/Service/Exo.Service.Core/EmbeddedMonitorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ private sealed class DeviceState
private readonly AsyncLock _lock;
public AsyncLock Lock => _lock;
public IConfigurationContainer DeviceConfigurationContainer { get; }
public IConfigurationContainer<Guid> EmbeddedMonitorsConfigurationContainer { get; }
public IConfigurationContainer<Guid> EmbeddedMonitorConfigurationContainer { get; }
private readonly Guid _id;
public Guid Id => _id;

Expand All @@ -39,7 +39,7 @@ Dictionary<Guid, EmbeddedMonitorState> embeddedMonitors
{
_id = id;
DeviceConfigurationContainer = deviceConfigurationContainer;
EmbeddedMonitorsConfigurationContainer = embeddedMonitorsConfigurationContainer;
EmbeddedMonitorConfigurationContainer = embeddedMonitorsConfigurationContainer;
EmbeddedMonitors = embeddedMonitors;
_lock = new();
}
Expand Down Expand Up @@ -93,6 +93,7 @@ private sealed class EmbeddedMonitorState
private ImageFormats _imageFormats;
private EmbeddedMonitorCapabilities _capabilities;
private ImmutableArray<EmbeddedMonitorGraphicsDescription> _supportedGraphics;
private HashSet<Guid>? _supportedBuiltInGraphicIds;

public EmbeddedMonitorState(Guid id)
{
Expand All @@ -109,7 +110,8 @@ public EmbeddedMonitorState
PixelFormat pixelFormat,
ImageFormats imageFormats,
EmbeddedMonitorCapabilities capabilities,
ImmutableArray<EmbeddedMonitorGraphicsDescription> supportedGraphics
ImmutableArray<EmbeddedMonitorGraphicsDescription> supportedGraphics,
PersistedMonitorConfiguration configuration
)
{
_id = id;
Expand All @@ -120,6 +122,23 @@ ImmutableArray<EmbeddedMonitorGraphicsDescription> supportedGraphics
_imageFormats = imageFormats;
_capabilities = capabilities;
_supportedGraphics = supportedGraphics;
if ((_capabilities & EmbeddedMonitorCapabilities.BuiltInGraphics) != 0 && supportedGraphics.Length > 0 && (supportedGraphics.Length > 1 || supportedGraphics[0].GraphicsId != default))
{
_supportedBuiltInGraphicIds = new();
foreach (var g in supportedGraphics)
{
if (g.GraphicsId == default) continue;
_supportedBuiltInGraphicIds.Add(g.GraphicsId);
}
// This is mostly a safeguard for bogus state. It should not be needed but it is better to guard against bugs in drivers.
if (_supportedBuiltInGraphicIds.Count == 0) _supportedBuiltInGraphicIds = null;
}
_currentGraphics = configuration.GraphicsId;
_currentImageId = configuration.ImageId;
_currentRegionLeft = (ushort)configuration.ImageRegion.Left;
_currentRegionTop = (ushort)configuration.ImageRegion.Top;
_currentRegionWidth = (ushort)configuration.ImageRegion.Width;
_currentRegionHeight = (ushort)configuration.ImageRegion.Height;
}

public bool SetOnline(IEmbeddedMonitor monitor)
Expand Down Expand Up @@ -194,6 +213,18 @@ public bool SetOnline(IEmbeddedMonitor monitor)
if (!supportedGraphics.SequenceEqual(_supportedGraphics))
{
_supportedGraphics = supportedGraphics;
_supportedBuiltInGraphicIds?.Clear();
if ((_capabilities & EmbeddedMonitorCapabilities.BuiltInGraphics) != 0 && supportedGraphics.Length > 0 && (supportedGraphics.Length > 1 || supportedGraphics[0].GraphicsId != default))
{
_supportedBuiltInGraphicIds ??= new();
foreach (var g in supportedGraphics)
{
if (g.GraphicsId == default) continue;
_supportedBuiltInGraphicIds.Add(g.GraphicsId);
}
}
// This is mostly a safeguard for bogus state. It should not be needed but it is better to guard against bugs in drivers.
if (_supportedBuiltInGraphicIds?.Count == 0) _supportedBuiltInGraphicIds = null;
hasChanged = true;
}

Expand All @@ -207,6 +238,44 @@ public void SetOffline()
_monitor = null;
}

public async ValueTask<bool> SetBuiltInGraphicsAsync(Guid graphicsId, CancellationToken cancellationToken)
{
if (_supportedBuiltInGraphicIds?.Contains(graphicsId) != true) throw new ArgumentOutOfRangeException(nameof(graphicsId));

if (graphicsId != _currentGraphics)
{
if (_monitor is IEmbeddedMonitorBuiltInGraphics builtInGraphics)
{
await builtInGraphics.SetCurrentModeAsync(graphicsId, cancellationToken).ConfigureAwait(false);
}

_currentGraphics = graphicsId;
_currentImageId = 0;
_currentRegionLeft = 0;
_currentRegionTop = 0;
_currentRegionWidth = 0;
_currentRegionHeight = 0;

return true;
}
return false;
}

public async ValueTask RestoreConfigurationAsync(ImageStorageService imageStorageService, CancellationToken cancellationToken)
{
if (_currentGraphics != default)
{
if (_monitor is IEmbeddedMonitorBuiltInGraphics builtInGraphics)
{
await builtInGraphics.SetCurrentModeAsync(_currentGraphics, cancellationToken).ConfigureAwait(false);
}
}
else if (_currentImageId != 0)
{
// TODO
}
}

public PersistedEmbeddedMonitorInformation CreatePersistedInformation()
=> new()
{
Expand All @@ -231,6 +300,14 @@ public EmbeddedMonitorInformation CreateInformation()
SupportedGraphics = _supportedGraphics,
};

public PersistedMonitorConfiguration CreatePersistedConfiguration()
=> new()
{
GraphicsId = _currentGraphics,
ImageId = _currentImageId,
ImageRegion = new(_currentRegionLeft, _currentRegionTop, _currentRegionWidth, _currentRegionHeight)
};

public EmbeddedMonitorConfigurationWatchNotification CreateConfigurationNotification(Guid deviceId)
=> new()
{
Expand Down Expand Up @@ -274,6 +351,7 @@ public static async ValueTask<EmbeddedMonitorService> CreateAsync
ILogger<EmbeddedMonitorService> logger,
IConfigurationContainer<Guid> devicesConfigurationContainer,
IDeviceWatcher deviceWatcher,
ImageStorageService imageStorageService,
CancellationToken cancellationToken
)
{
Expand Down Expand Up @@ -301,17 +379,30 @@ CancellationToken cancellationToken

foreach (var embeddedMonitorId in embeddedMonitorIds)
{
EmbeddedMonitorState state;
PersistedEmbeddedMonitorInformation info;
{
var result = await embeddedMonitorConfigurationContainer.ReadValueAsync<PersistedEmbeddedMonitorInformation>(embeddedMonitorId, cancellationToken).ConfigureAwait(false);
if (!result.Found) continue;
var info = result.Value;
state = new(embeddedMonitorId, info.Shape, info.Width, info.Height, info.PixelFormat, info.ImageFormats, info.Capabilities, info.SupportedGraphics);
info = result.Value;
}
PersistedMonitorConfiguration configuration;
{
var result = await embeddedMonitorConfigurationContainer.ReadValueAsync<PersistedMonitorConfiguration>(embeddedMonitorId, cancellationToken).ConfigureAwait(false);
if (!result.Found) continue;
if (!result.Found) configuration = default;
else configuration = result.Value;
}
var state = new EmbeddedMonitorState
(
embeddedMonitorId,
info.Shape,
info.Width,
info.Height,
info.PixelFormat,
info.ImageFormats,
info.Capabilities,
info.SupportedGraphics,
configuration
);
embeddedMonitors.Add(embeddedMonitorId, state);
}

Expand All @@ -331,15 +422,16 @@ CancellationToken cancellationToken
}
}

return new EmbeddedMonitorService(logger, devicesConfigurationContainer, deviceWatcher, deviceStates);
return new EmbeddedMonitorService(logger, devicesConfigurationContainer, deviceWatcher, imageStorageService, deviceStates);
}

private readonly IDeviceWatcher _deviceWatcher;
private readonly ConcurrentDictionary<Guid, DeviceState> _embeddedMonitorDeviceStates;
private readonly IConfigurationContainer<Guid> _devicesConfigurationContainer;
private readonly ImageStorageService _imageStorageService;
private readonly AsyncLock _lock;
private ChannelWriter<EmbeddedMonitorDeviceInformation>[]? _deviceListeners;
private ChannelWriter<EmbeddedMonitorConfigurationWatchNotification>[]? _configurationChangeListeners;
private readonly AsyncLock _lock;
private readonly IConfigurationContainer<Guid> _devicesConfigurationContainer;
private readonly ILogger<EmbeddedMonitorService> _logger;

private CancellationTokenSource? _cancellationTokenSource;
Expand All @@ -350,13 +442,15 @@ private EmbeddedMonitorService
ILogger<EmbeddedMonitorService> logger,
IConfigurationContainer<Guid> devicesConfigurationContainer,
IDeviceWatcher deviceWatcher,
ImageStorageService imageStorageService,
ConcurrentDictionary<Guid, DeviceState> embeddedMonitorDeviceStates
)
{
_lock = new();
_logger = logger;
_devicesConfigurationContainer = devicesConfigurationContainer;
_deviceWatcher = deviceWatcher;
_imageStorageService = imageStorageService;
_embeddedMonitorDeviceStates = embeddedMonitorDeviceStates;
_cancellationTokenSource = new();
_watchTask = WatchAsync(_cancellationTokenSource.Token);
Expand Down Expand Up @@ -466,7 +560,7 @@ private async ValueTask HandleArrivalAsync(DeviceWatchNotification notification,
{
if (deviceState.EmbeddedMonitors.Remove(deletedMonitorId))
{
await deviceState.EmbeddedMonitorsConfigurationContainer.DeleteValuesAsync(deletedMonitorId);
await deviceState.EmbeddedMonitorConfigurationContainer.DeleteValuesAsync(deletedMonitorId);
}
}
changedMonitors.Clear();
Expand All @@ -484,16 +578,25 @@ private async ValueTask HandleArrivalAsync(DeviceWatchNotification notification,
}
}

deviceState.SetOnline(notification.Driver!);

if (isNew)
{
_embeddedMonitorDeviceStates.TryAdd(deviceState.Id, deviceState);
}
else
{
foreach (var monitorState in deviceState.EmbeddedMonitors.Values)
{
await monitorState.RestoreConfigurationAsync(_imageStorageService, cancellationToken).ConfigureAwait(false);
}
}

foreach (var changedMonitorId in changedMonitors)
{
if (deviceState.EmbeddedMonitors.TryGetValue(changedMonitorId, out var monitorState))
{
await deviceState.EmbeddedMonitorsConfigurationContainer.WriteValueAsync(changedMonitorId, monitorState.CreatePersistedInformation(), cancellationToken).ConfigureAwait(false);
await deviceState.EmbeddedMonitorConfigurationContainer.WriteValueAsync(changedMonitorId, monitorState.CreatePersistedInformation(), cancellationToken).ConfigureAwait(false);
}
}

Expand All @@ -515,6 +618,26 @@ private async ValueTask OnDriverRemovedAsync(DeviceWatchNotification notificatio
}
}

public async ValueTask SetBuiltInGraphicsAsync(Guid deviceId, Guid monitorId, Guid graphicsId, CancellationToken cancellationToken)
{
ArgumentOutOfRangeException.ThrowIfEqual(graphicsId, default);

if (!_embeddedMonitorDeviceStates.TryGetValue(deviceId, out var deviceState)) throw new InvalidOperationException("Device not found.");

PersistedMonitorConfiguration configuration;

using (await deviceState.Lock.WaitAsync(cancellationToken).ConfigureAwait(false))
{
if (!deviceState.EmbeddedMonitors.TryGetValue(monitorId, out var monitorState)) throw new InvalidOperationException("Embedded monitor not found.");

if (!(await monitorState.SetBuiltInGraphicsAsync(graphicsId, cancellationToken).ConfigureAwait(false))) return;

configuration = monitorState.CreatePersistedConfiguration();
}

await PersistConfigurationAsync(deviceState.EmbeddedMonitorConfigurationContainer, monitorId, configuration, cancellationToken).ConfigureAwait(false);
}

public async IAsyncEnumerable<EmbeddedMonitorDeviceInformation> WatchDevicesAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
var channel = Watcher.CreateSingleWriterChannel<EmbeddedMonitorDeviceInformation>();
Expand Down Expand Up @@ -621,6 +744,6 @@ public readonly struct EmbeddedMonitorConfigurationWatchNotification
public required Guid DeviceId { get; init; }
public required Guid MonitorId { get; init; }
public required Guid GraphicsId { get; init; }
public required UInt128 ImageId { get; init; }
public required Rectangle ImageRegion { get; init; }
public UInt128 ImageId { get; init; }
public Rectangle ImageRegion { get; init; }
}
10 changes: 10 additions & 0 deletions src/Exo/Service/Exo.Service.Grpc/GrpcEmbeddedMonitorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,14 @@ public GrpcEmbeddedMonitorService(ILogger<GrpcDeviceService> logger, Configurati
_logger.GrpcSpecializedDeviceServiceWatchStop(GrpcService.EmbeddedMonitor);
}
}

public async ValueTask SetBuiltInGraphicsAsync(EmbeddedMonitorSetBuiltInGraphicsRequest request, CancellationToken cancellationToken)
{
await _embeddedMonitorService.SetBuiltInGraphicsAsync(request.DeviceId, request.MonitorId, request.GraphicsId, cancellationToken).ConfigureAwait(false);
}

public async ValueTask SetImageAsync(EmbeddedMonitorSetImageRequest request, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
1 change: 1 addition & 0 deletions src/Exo/Service/Exo.Service/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ public void ConfigureServices(IServiceCollection services)
sp.GetRequiredService<ILogger<EmbeddedMonitorService>>(),
sp.GetRequiredKeyedService<IConfigurationContainer<Guid>>(ConfigurationContainerNames.Devices),
sp.GetRequiredService<IDeviceWatcher>(),
sp.GetRequiredService<ImageStorageService>(),
default
).GetAwaiter().GetResult()
);
Expand Down
33 changes: 32 additions & 1 deletion src/Exo/Ui/Exo.Contracts.Ui.Settings/IEmbeddedMonitorService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ServiceModel;
using System.Runtime.Serialization;
using System.ServiceModel;

namespace Exo.Contracts.Ui.Settings;

Expand All @@ -11,4 +12,34 @@ public interface IEmbeddedMonitorService
/// <returns></returns>
[OperationContract(Name = "WatchEmbeddedMonitorDevices")]
IAsyncEnumerable<EmbeddedMonitorDeviceInformation> WatchEmbeddedMonitorDevicesAsync(CancellationToken cancellationToken);

[OperationContract(Name = "SetBuiltInGraphics")]
ValueTask SetBuiltInGraphicsAsync(EmbeddedMonitorSetBuiltInGraphicsRequest request, CancellationToken cancellationToken);

[OperationContract(Name = "SetImage")]
ValueTask SetImageAsync(EmbeddedMonitorSetImageRequest request, CancellationToken cancellationToken);
}

[DataContract]
public sealed class EmbeddedMonitorSetBuiltInGraphicsRequest
{
[DataMember(Order = 1)]
public Guid DeviceId { get; init; }
[DataMember(Order = 2)]
public Guid MonitorId { get; init; }
[DataMember(Order = 3)]
public Guid GraphicsId { get; init; }
}

[DataContract]
public sealed class EmbeddedMonitorSetImageRequest
{
[DataMember(Order = 1)]
public Guid DeviceId { get; init; }
[DataMember(Order = 2)]
public Guid MonitorId { get; init; }
[DataMember(Order = 3)]
public UInt128 ImageId { get; init; }
[DataMember(Order = 4)]
public Rectangle CropZone { get; init; }
}
16 changes: 16 additions & 0 deletions src/Exo/Ui/Exo.Contracts.Ui.Settings/Rectangle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Runtime.Serialization;

namespace Exo.Contracts.Ui.Settings;

[DataContract]
public readonly record struct Rectangle
{
[DataMember(Order = 1)]
public int Left { get; init; }
[DataMember(Order = 2)]
public int Top { get; init; }
[DataMember(Order = 3)]
public int Width { get; init; }
[DataMember(Order = 4)]
public int Height { get; init; }
}
2 changes: 1 addition & 1 deletion src/Exo/Ui/Exo.Contracts.Ui.Settings/Size.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Runtime.Serialization;
using System.Runtime.Serialization;

namespace Exo.Contracts.Ui.Settings;

Expand Down
3 changes: 2 additions & 1 deletion src/Exo/Ui/Exo.Settings.Ui/ViewModels/DeviceViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public DeviceViewModel
ISettingsMetadataService metadataService,
IPowerService powerService,
IMouseService mouseService,
IEmbeddedMonitorService embeddedMonitorService,
IRasterizationScaleProvider rasterizationScaleProvider,
DeviceInformation deviceInformation
)
Expand Down Expand Up @@ -46,7 +47,7 @@ DeviceInformation deviceInformation
}
else if (featureId == WellKnownGuids.EmbeddedMonitorDeviceFeature)
{
EmbeddedMonitorFeatures ??= new(this, availableImages, rasterizationScaleProvider, metadataService);
EmbeddedMonitorFeatures ??= new(this, availableImages, rasterizationScaleProvider, metadataService, embeddedMonitorService);
}
}
}
Expand Down
Loading

0 comments on commit babc050

Please sign in to comment.