Skip to content

Commit

Permalink
✨ Add built-in graphics support for embedded monitors.
Browse files Browse the repository at this point in the history
  • Loading branch information
hexawyz committed Jan 25, 2025
1 parent 8443603 commit 02634c0
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 11 deletions.
2 changes: 1 addition & 1 deletion src/Exo/Core/Exo.Core/Features/BaseFeatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public interface ILightingDeviceFeature : IDeviceFeature
{
}

[TypeId(0xA6A121D7, 0xE5A3, 0x49A6, 0x88, 0xBE, 0xE7, 0x52, 0x39, 0xED, 0x9E, 0x3A )]
[TypeId(0xA6A121D7, 0xE5A3, 0x49A6, 0x88, 0xBE, 0xE7, 0x52, 0x39, 0xED, 0x9E, 0x3A)]
public interface IMotherboardDeviceFeature : IDeviceFeature
{
}
Expand Down
75 changes: 72 additions & 3 deletions src/Exo/Core/Exo.Core/Features/EmbeddedMonitorFeatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ namespace Exo.Features.EmbeddedMonitors;
public interface IEmbeddedMonitorFeature : IEmbeddedMonitor, IEmbeddedMonitorDeviceFeature
{
}

/// <summary>To be used for a device exposing multiple embedded monitors.</summary>
/// <remarks>
/// <para>This feature is necessary to support devices such as the various Elgato StreamDecks.</para>
Expand All @@ -19,7 +18,7 @@ public interface IEmbeddedMonitorControllerFeature : IEmbeddedMonitorDeviceFeatu
ImmutableArray<IEmbeddedMonitor> EmbeddedMonitors { get; }
}

/// <summary>A feaure to implement for a device supporting an automatic screensaver.</summary>
/// <summary>A feature to implement for a device supporting an automatic screensaver.</summary>
/// <remarks></remarks>
public interface IEmbeddedMonitorScreenSaverFeature : IEmbeddedMonitor, IEmbeddedMonitorDeviceFeature
{
Expand Down Expand Up @@ -52,16 +51,21 @@ public interface IEmbeddedMonitor
/// A 128 bit value is used in order to easily allow for callers to implement a more complex hashing system, for example using XXH128, in order to optimize operations on their side.
/// This is intended to be done in the image service of Exo.
/// </para>
/// <para>
/// Calling this method will switch the monitor to custom graphics and set <see cref="IEmbeddedMonitorBuiltInGraphicModes.CurrentModeId"/> to <see cref="string.Empty"/>, if the
/// monitor supports built-in modes.
/// </para>
/// </remarks>
/// <param name="imageId">An opaque image ID used to identify an image.</param>
/// <param name="imageFormat">The image format of the specified data.</param>
/// <param name="data">Valid image data in the format specified by <paramref name="imageFormat"/>.</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="NotSupportedException">If the monitor only supports built-in graphics modes, offering no ability to show a custom image.</exception>
ValueTask SetImageAsync(UInt128 imageId, ImageFormat imageFormat, ReadOnlyMemory<byte> data, CancellationToken cancellationToken);
}

public interface IDrawableEmbeddedMonitor
public interface IDrawableEmbeddedMonitor : IEmbeddedMonitor
{
/// <summary>Draws the specified image in the specified region of a monitor.</summary>
/// <remarks>
Expand Down Expand Up @@ -89,6 +93,31 @@ public interface IDrawableEmbeddedMonitor
ValueTask DrawImageAsync(Point position, Size size, UInt128 imageId, ImageFormat imageFormat, ReadOnlyMemory<byte> data, CancellationToken cancellationToken);
}

/// <summary>A feature to implement for an embedded monitor supporting built-in graphics.</summary>
/// <remarks>In absence of this interface, embedded monitors are assumed to support only <see cref="EmbeddedMonitorGraphicsDescription.CustomGraphics"/>.</remarks>
public interface IEmbeddedMonitorBuiltInGraphics : IEmbeddedMonitor
{
/// <summary>Gets the array of built-in graphics supported by this monitor.</summary>
/// <remarks>
/// The returned array must contain at least one element and must not contain duplicate mode IDs.
/// If the monitor supports showing custom images, as would typically be the case, this must explicitly contain <see cref="EmbeddedMonitorGraphicsDescription.CustomGraphics"/>.
/// </remarks>
ImmutableArray<EmbeddedMonitorGraphicsDescription> SupportedGraphics { get; }

/// <summary>Gets the ID of the currently displayed graphics.</summary>
Guid CurrentGraphicsId { get; }

/// <summary>Sets the current built-in graphics mode on the monitor.</summary>
/// <remarks>
/// This method can not be used to switch to the custom graphics.
/// To that effect, one must call <see cref="IEmbeddedMonitor.SetImageAsync(UInt128, ImageFormat, ReadOnlyMemory{byte}, CancellationToken)"/>.
/// </remarks>
/// <param name="modeId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
ValueTask SetCurrentModeAsync(Guid modeId, CancellationToken cancellationToken);
}

public readonly record struct EmbeddedMonitorInformation
{
public EmbeddedMonitorInformation(MonitorShape shape, Size imageSize, PixelFormat pixelFormat, ImageFormats supportedImageFormats, bool hasAnimationSupport)
Expand Down Expand Up @@ -120,3 +149,43 @@ public EmbeddedMonitorInformation(MonitorShape shape, Size imageSize, PixelForma
/// <remarks>Animated images are only expected to be supported for full refreshes.</remarks>
public bool HasAnimationSupport { get; }
}

public readonly struct EmbeddedMonitorGraphicsDescription
{
/// <summary>Gets a default definition of the custom graphics.</summary>
/// <remarks>
/// Unless it is not supported by the monitor, custom graphics is always assumed to be the default mode.
/// To that effect, the <see cref="Guid"/> associated with it is <see cref="Guid.Empty"/>.
/// </remarks>
public static EmbeddedMonitorGraphicsDescription CustomGraphics => new(default, new Guid(0x2F538961, 0xDAF9, 0x4664, 0x87, 0xFA, 0x22, 0xB8, 0x48, 0xDF, 0x0E, 0xEC));

public static Guid OffId => new(0xA93C0E79, 0xB47E, 0x4542, 0xBF, 0x77, 0xC0, 0x06, 0xE4, 0xFF, 0xFB, 0x6D);

/// <summary>Gets a definition to represent a graphics off mode.</summary>
/// <remarks>Embedded monitors supporting a built-in "graphics off" mode can add this to the list of modes.</remarks>
public static EmbeddedMonitorGraphicsDescription Off => new(OffId);

/// <summary>Gets the ID used to describe these graphics.</summary>
/// <remarks>
/// This property can be <see cref="Guid.Empty"/> to represent custom graphics, which is assumed to be default in all cases where it is supported.
/// </remarks>
public Guid GraphicsId { get; }
/// <summary>Gets the name string ID for these graphics.</summary>
public Guid NameStringId { get; }

/// <summary>Initializes the structure using the same <see cref="Guid"/> for both graphics ID and name string ID.</summary>
/// <remarks>
/// In many cases, graphics ID would be unique GUIDs, so it is not reasonable to use the same <see cref="Guid"/> value for both the name and the graphics themselves.
/// From a logic POV it does not change much, but it is more convenient for implementation to just require a single GUID.
/// Of course, the constructor allowing for the use of two different GUIDs is still available for implementation that want more complex binding.
/// </remarks>
/// <param name="modeAndNameStringId">The unique ID that will be used to reference both the graphics and the name.</param>
public EmbeddedMonitorGraphicsDescription(Guid modeAndNameStringId)
: this(modeAndNameStringId, modeAndNameStringId) { }

public EmbeddedMonitorGraphicsDescription(Guid modeId, Guid nameStringId)
{
GraphicsId = modeId;
NameStringId = nameStringId;
}
}
12 changes: 12 additions & 0 deletions src/Exo/Devices/Exo.Devices.Nzxt.Kraken/KrakenDisplayMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Exo.Devices.Nzxt.Kraken;

/// <summary>Represents a display mode supported by the device.</summary>
/// <remarks><see cref=" KrakenPresetVisual"/> is a subset of this enum containing only the preset visual modes.</remarks>
internal enum KrakenDisplayMode : byte
{
Off = 0,
Animation = 1,
LiquidTemperature = 2,
StoredImage = 4,
QuickImage = 5,
}
63 changes: 56 additions & 7 deletions src/Exo/Devices/Exo.Devices.Nzxt.Kraken/KrakenDriver.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
Expand Down Expand Up @@ -33,6 +32,7 @@ public class KrakenDriver :
ISensorsGroupedQueryFeature,
ICoolingControllerFeature,
IEmbeddedMonitorFeature,
IEmbeddedMonitorBuiltInGraphics,
IMonitorBrightnessFeature
{
private static readonly Guid LiquidTemperatureSensorId = new(0x8E880DE1, 0x2A45, 0x400D, 0xA9, 0x0F, 0x42, 0xE8, 0x9B, 0xF9, 0x50, 0xDB);
Expand All @@ -44,6 +44,9 @@ public class KrakenDriver :

private static readonly Guid MonitorId = new (0xAB1C8580, 0x9FC4, 0x4BB6, 0xB9, 0xC7, 0x02, 0xF1, 0x81, 0x81, 0x68, 0xB6);

private static readonly Guid BootAnimationGraphicsId = new (0xE4A8CC79, 0x1062, 0x4E85, 0x97, 0x72, 0xB4, 0xBF, 0xFF, 0x66, 0x74, 0xB3);
private static readonly Guid LiquidTemperatureGraphicsId = new (0x9AA7AF98, 0x19D2, 0x4F98, 0x96, 0x90, 0xAD, 0xF9, 0xA5, 0x90, 0x8B, 0xC3);

// Both cooling curves taken out of NZXT CAM when creating a new cooling profile. No idea if they are the default HW curve.
// Points from CAM are the first column, others are interpolated.
private static readonly byte[] DefaultPumpCurve = [
Expand Down Expand Up @@ -152,6 +155,7 @@ await KrakenWinUsbImageTransport.CreateAsync(winUsbDevice, cancellationToken).Co
string? serialNumber = await hidStream.GetSerialNumberAsync(cancellationToken).ConfigureAwait(false);
var hidTransport = new KrakenHidTransport(hidStream);
var screenInfo = await hidTransport.GetScreenInformationAsync(cancellationToken).ConfigureAwait(false);
var currentDisplayMode = KrakenDisplayMode.Off;
var storageManager = imageTransport is not null ?
await KrakenImageStorageManager.CreateAsync(screenInfo.ImageCount, screenInfo.MemoryBlockCount, hidTransport, imageTransport, cancellationToken).ConfigureAwait(false) :
null;
Expand All @@ -162,6 +166,7 @@ await KrakenImageStorageManager.CreateAsync(screenInfo.ImageCount, screenInfo.Me
if (storageManager is not null)
{
await hidTransport.DisplayPresetVisualAsync(KrakenPresetVisual.LiquidTemperature, cancellationToken).ConfigureAwait(false);
currentDisplayMode = KrakenDisplayMode.LiquidTemperature;
KrakenImageFormat imageFormat;
byte[] imageData;
try
Expand All @@ -178,6 +183,7 @@ await KrakenImageStorageManager.CreateAsync(screenInfo.ImageCount, screenInfo.Me
}
await storageManager.UploadImageAsync(0, imageFormat, imageData, cancellationToken).ConfigureAwait(false);
await hidTransport.DisplayImageAsync(0, cancellationToken).ConfigureAwait(false);
currentDisplayMode = KrakenDisplayMode.StoredImage;
}

return new DriverCreationResult<SystemDevicePath>
Expand All @@ -188,6 +194,7 @@ await KrakenImageStorageManager.CreateAsync(screenInfo.ImageCount, screenInfo.Me
logger,
hidTransport,
storageManager,
currentDisplayMode,
screenInfo.Width,
screenInfo.Height,
productId,
Expand Down Expand Up @@ -254,20 +261,25 @@ static uint GetColor(byte r, byte g, byte b)
private readonly IDeviceFeatureSet<IMonitorDeviceFeature> _monitorFeatures;
private readonly IDeviceFeatureSet<IEmbeddedMonitorDeviceFeature> _embeddedMonitorFeatures;

private int _groupQueriedSensorCount;
private readonly ushort _productId;
private readonly ushort _versionNumber;
private readonly ushort _imageWidth;
private readonly ushort _imageHeight;

private byte _pumpSpeedTarget;
private byte _pumpState;
private byte _fanSpeedTarget;
private byte _fanState;

private byte _groupQueriedSensorCount;

private KrakenDisplayMode _currentDisplayMode;

private readonly ushort _productId;
private readonly ushort _versionNumber;
private readonly ushort _imageWidth;
private readonly ushort _imageHeight;

private readonly byte[] _pumpCoolingCurve;
private readonly byte[] _fanCoolingCurve;

private readonly ImmutableArray<EmbeddedMonitorGraphicsDescription> _embeddedMonitorGraphicsDescriptions;

public override DeviceCategory DeviceCategory => DeviceCategory.Cooler;
DeviceId IDeviceIdFeature.DeviceId => DeviceId.ForUsb(NzxtVendorId, _productId, _versionNumber);
string IDeviceSerialNumberFeature.SerialNumber => ConfigurationKey.UniqueId!;
Expand All @@ -289,6 +301,7 @@ private KrakenDriver
ILogger<KrakenDriver> logger,
KrakenHidTransport transport,
KrakenImageStorageManager? storageManager,
KrakenDisplayMode currentDisplayMode,
ushort imageWidth,
ushort imageHeight,
ushort productId,
Expand All @@ -307,6 +320,16 @@ DeviceConfigurationKey configurationKey
_fanCoolingCurve = (byte[])DefaultFanCurve.Clone();
_sensors = [new LiquidTemperatureSensor(this), new PumpSpeedSensor(this), new FanSpeedSensor(this)];
_coolers = [new PumpCooler(this), new FanCooler(this)];
_currentDisplayMode = currentDisplayMode;
_imageWidth = imageWidth;
_imageHeight = imageHeight;
_embeddedMonitorGraphicsDescriptions =
[
EmbeddedMonitorGraphicsDescription.Off,
new(BootAnimationGraphicsId),
new(LiquidTemperatureGraphicsId),
EmbeddedMonitorGraphicsDescription.CustomGraphics,
];
_genericFeatures = ConfigurationKey.UniqueId is not null ?
FeatureSet.Create<IGenericDeviceFeature, KrakenDriver, IDeviceIdFeature, IDeviceSerialNumberFeature>(this) :
FeatureSet.Create<IGenericDeviceFeature, KrakenDriver, IDeviceIdFeature>(this);
Expand Down Expand Up @@ -334,6 +357,30 @@ async ValueTask IContinuousVcpFeature.SetValueAsync(ushort value, CancellationTo
await _hidTransport.SetBrightnessAsync((byte)value, cancellationToken).ConfigureAwait(false);
}

ImmutableArray<EmbeddedMonitorGraphicsDescription> IEmbeddedMonitorBuiltInGraphics.SupportedGraphics => _embeddedMonitorGraphicsDescriptions;

Guid IEmbeddedMonitorBuiltInGraphics.CurrentGraphicsId
=> _currentDisplayMode switch
{
KrakenDisplayMode.Off => EmbeddedMonitorGraphicsDescription.OffId,
KrakenDisplayMode.Animation => BootAnimationGraphicsId,
KrakenDisplayMode.LiquidTemperature => LiquidTemperatureGraphicsId,
KrakenDisplayMode.StoredImage => default,
// Fallback to reporting the monitor as being off.
_ => EmbeddedMonitorGraphicsDescription.OffId
};

async ValueTask IEmbeddedMonitorBuiltInGraphics.SetCurrentModeAsync(Guid modeId, CancellationToken cancellationToken)
{
KrakenPresetVisual presetVisual;
if (modeId == EmbeddedMonitorGraphicsDescription.OffId) presetVisual = KrakenPresetVisual.Off;
else if (modeId == BootAnimationGraphicsId) presetVisual = KrakenPresetVisual.Animation;
else if (modeId == LiquidTemperatureGraphicsId) presetVisual = KrakenPresetVisual.LiquidTemperature;
else throw new ArgumentException();
await _hidTransport.DisplayPresetVisualAsync(presetVisual, cancellationToken).ConfigureAwait(false);
_currentDisplayMode = (KrakenDisplayMode)presetVisual;
}

void ISensorsGroupedQueryFeature.AddSensor(IPolledSensor sensor)
{
if (sensor is not Sensor s || s.Driver != this) throw new InvalidOperationException();
Expand All @@ -358,6 +405,8 @@ void ISensorsGroupedQueryFeature.RemoveSensor(IPolledSensor sensor)

async ValueTask ISensorsGroupedQueryFeature.QueryValuesAsync(CancellationToken cancellationToken)
{
if (_groupQueriedSensorCount == 0) return;

var readings = await _hidTransport.GetRecentReadingsAsync(cancellationToken).ConfigureAwait(false);

foreach (var sensor in _sensors)
Expand Down

0 comments on commit 02634c0

Please sign in to comment.