Skip to content

Commit

Permalink
✨ Work on the embedded monitor feature.
Browse files Browse the repository at this point in the history
Nothing big here, but work towards being able to configure images in the UI.
  • Loading branch information
hexawyz committed Jan 13, 2025
1 parent d05fdc1 commit 24d261f
Show file tree
Hide file tree
Showing 10 changed files with 305 additions and 15 deletions.
45 changes: 38 additions & 7 deletions src/Exo/Core/Exo.Core/Features/MonitorFeatures.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Collections.Immutable;
using System.Diagnostics.Contracts;
using DeviceTools.DisplayDevices;
using DeviceTools.DisplayDevices.Mccs;
using Exo.Images;
using Exo.Monitors;

namespace Exo.Features.Monitors;

Expand Down Expand Up @@ -119,15 +121,44 @@ public interface IMonitorInputLagFeature : IMonitorDeviceFeature, INonContinuous
public interface IMonitorBlueLightFilterLevelFeature : IMonitorDeviceFeature, IContinuousVcpFeature { }
public interface IMonitorPowerIndicatorToggleFeature : IMonitorDeviceFeature, IBooleanVcpFeature { }

public interface IEmbeddedMonitorInformationFeature : IMonitorDeviceFeature
public interface IEmbeddedMonitorFeature : IEmbeddedMonitor, IMonitorDeviceFeature
{
}

/// <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>
/// <para>THis feature is exclusive with <see cref="IEmbeddedMonitorFeature"/>.</para>
/// </remarks>
public interface IEmbeddedMonitorControllerFeature : IMonitorDeviceFeature
{
/// <summary>Gets a list of the embedded monitors exposed by this device.</summary>
ImmutableArray<IEmbeddedMonitor> EmbeddedMonitors { get; }
}

public interface IEmbeddedMonitorScreenSaverFeature : IEmbeddedMonitor, IMonitorDeviceFeature
{
MonitorShape Shape { get; }
Size ImageSize { get; }
}

public enum MonitorShape : byte
public interface IEmbeddedMonitor
{
Rectangle = 0,
Square = 1,
Circle = 2,
/// <summary>Gets the monitor ID.</summary>
/// <remarks>This property is especially important for devices exposing multiple monitors.</remarks>
Guid MonitorId { get; }
/// <summary>Gets the shape of the monitor.</summary>
/// <remarks>
/// Some AIO devices will expose a circular screen, but most embedded monitors are expected to be of rectangular shape.
/// The shape of the monitor might mainly be used to optimize image compression if the monitor is non-rectangular.
/// </remarks>
MonitorShape Shape { get; }
/// <summary>Gets the image size of the monitor.</summary>
Size ImageSize { get; }
/// <summary>Gets the effective pixel format of the monitor.</summary>
/// <remarks>
/// Monitors should generally support a 32 bits RGB(A) format, but this information is needed in order to feed acceptable images to the device.
/// This is especially important in case of raw images, but it will matter in other situations, such as when only a reduced number of colors is supported.
/// </remarks>
PixelFormat PixelFormat { get; }
/// <summary>Gets a description of the image formats that are directly supported by the embedded monitor.</summary>
ImageFormats SupportedImageFormats { get; }
}
9 changes: 9 additions & 0 deletions src/Exo/Core/Exo.Core/Images/ColorFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Exo.Images;

public enum ColorFormat
{
Palette = 0,
SRGB = 1,
RGB = 2,
CMYK = 3,
}
12 changes: 12 additions & 0 deletions src/Exo/Core/Exo.Core/Images/ImageFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Exo.Images;

/// <summary>Describe know supported image formats.</summary>
public enum ImageFormat
{
// NB: Enum must be kept in sync with ImageFormats
Raw = 0,
Bitmap = 1,
Gif = 2,
Jpeg = 3,
Png = 4,
}
13 changes: 13 additions & 0 deletions src/Exo/Core/Exo.Core/Images/ImageFormats.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Exo.Images;

/// <summary>Describe known supported image formats.</summary>
[Flags]
public enum ImageFormats
{
// NB: Enum must be kept in sync with ImageFormats
Raw = 0x00000001,
Bitmap = 0x00000010,
Gif = 0x00000100,
Jpeg = 0x00001000,
Png = 0x00010000,
}
37 changes: 37 additions & 0 deletions src/Exo/Core/Exo.Core/Images/PixelComponentFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace Exo.Images;

public readonly struct PixelComponentFormat : IEquatable<PixelComponentFormat>
{
public static PixelComponentFormat Empty => default;
public static PixelComponentFormat Color1Bit => new PixelComponentFormat(0x01);
public static PixelComponentFormat Color2Bit => new PixelComponentFormat(0x02);
public static PixelComponentFormat Color3Bit => new PixelComponentFormat(0x03);
public static PixelComponentFormat Color4Bit => new PixelComponentFormat(0x04);
public static PixelComponentFormat Color5Bit => new PixelComponentFormat(0x05);
public static PixelComponentFormat Color6Bit => new PixelComponentFormat(0x06);
public static PixelComponentFormat Color7Bit => new PixelComponentFormat(0x07);
public static PixelComponentFormat Color8Bit => new PixelComponentFormat(0x08);
public static PixelComponentFormat Color10Bit => new PixelComponentFormat(0x09);
public static PixelComponentFormat Color12Bit => new PixelComponentFormat(0x0A);
public static PixelComponentFormat Color16Bit => new PixelComponentFormat(0x0B);
public static PixelComponentFormat Color16BitFloat => new PixelComponentFormat(0x1B);
public static PixelComponentFormat Color32Bit => new PixelComponentFormat(0x0D);
public static PixelComponentFormat Color32BitFloat => new PixelComponentFormat(0x1D);

private static ReadOnlySpan<byte> BitsPerComponentTable => [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 16, 24, 32, 48, 64];

private readonly byte _rawValue;

public byte BitsPerComponent => BitsPerComponentTable[_rawValue & 0xF];
public bool IsFloatingPoint => (_rawValue & 0x10) != 0;
public bool IsEmpty => _rawValue == 0;

private PixelComponentFormat(byte rawValue) => _rawValue = rawValue;

public override bool Equals(object? obj) => obj is PixelComponentFormat format && Equals(format);
public bool Equals(PixelComponentFormat other) => _rawValue == other._rawValue;
public override int GetHashCode() => HashCode.Combine(_rawValue);

public static bool operator ==(PixelComponentFormat left, PixelComponentFormat right) => left.Equals(right);
public static bool operator !=(PixelComponentFormat left, PixelComponentFormat right) => !(left == right);
}
84 changes: 84 additions & 0 deletions src/Exo/Core/Exo.Core/Images/PixelFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.Numerics;
using System.Runtime.CompilerServices;

namespace Exo.Images;

public readonly struct PixelFormat : IEquatable<PixelFormat>
{
// As enumerating all possible color formats is a lost fight, we will instead use a compact system to describe pixel formats.
// This representation is internal and as such, can evolve to fit more needs.

// Supported bitness for each component: 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 16, 24, 32, 48, 64 (Hopefully that's exhaustive enough; it fits fine in 4 bits)
// Number of components: 5; Disabled components must be 0, non empty components must be packed together.
// Component format: Integer / Float (16 bits+)
// Little endian: yes / no (For when components are split over multiple bytes; when components are 8 bits, this must never be yes)
// Transparency: yes / no
// Color systems: Palette, sRGB, RGB, CMYK, … (3bits reserved)
// Permutations: (Normal order, Reversed)x(Alpha first, Alpha last)

public static PixelFormat R8G8B8 => new(0b_00000_00000_01000_01000_01000_0_0_0_0_001);
public static PixelFormat B8G8R8 => new(0b_00000_00000_01000_01000_01000_0_0_0_1_001);
public static PixelFormat X8R8G8B8 => new(0b_00000_01000_01000_01000_01000_0_0_0_0_001);
public static PixelFormat X8B8G8R8 => new(0b_00000_01000_01000_01000_01000_0_0_0_1_001);
public static PixelFormat R8G8B8X8 => new(0b_00000_01000_01000_01000_01000_0_1_0_0_001);
public static PixelFormat B8G8R8X8 => new(0b_00000_01000_01000_01000_01000_0_1_0_1_001);
public static PixelFormat A8R8G8B8 => new(0b_00000_01000_01000_01000_01000_0_0_1_0_001);
public static PixelFormat A8B8G8R8 => new(0b_00000_01000_01000_01000_01000_0_0_1_1_001);
public static PixelFormat R8G8B8A8 => new(0b_00000_01000_01000_01000_01000_0_1_1_0_001);
public static PixelFormat B8G8R8A8 => new(0b_00000_01000_01000_01000_01000_0_1_1_1_001);

private readonly uint _rawValue;

private PixelFormat(uint rawValue) => _rawValue = rawValue;

/// <summary>Indicates the format of the first color component.</summary>
/// <remarks>
/// When the color format is palette, this is the palette index.
/// When the color format is RGB, this is red.
/// When the color format is CMYK, this is cyan.
/// </remarks>
public PixelComponentFormat Component1 => Unsafe.BitCast<byte, PixelComponentFormat>((byte)((_rawValue >>> 7) & 0x1F));
/// <summary>Indicates the format of the second color component.</summary>
/// <remarks>
/// This must be unused if the color format is palette.
/// When the color format is RGB, this is green.
/// When the color format is CMYK, this is magenta.
/// </remarks>
public PixelComponentFormat Component2 => Unsafe.BitCast<byte, PixelComponentFormat>((byte)((_rawValue >>> 12) & 0x1F));
/// <summary>Indicates the format of the third color component.</summary>
/// <remarks>
/// This must be unused if the color format is palette.
/// When the color format is RGB, this is green.
/// When the color format is CMYK, this is yellow.
/// </remarks>
public PixelComponentFormat Component3 => Unsafe.BitCast<byte, PixelComponentFormat>((byte)((_rawValue >>> 17) & 0x1F));
/// <summary>Indicates the format of the fourth color component.</summary>
/// <remarks>
/// This must be unused if the color format is palette.
/// When the color format is RGB, this can be alpha.
/// When the color format is CMYK, this is black.
/// </remarks>
public PixelComponentFormat Component4 => Unsafe.BitCast<byte, PixelComponentFormat>((byte)((_rawValue >>> 22) & 0x1F));
/// <summary>Indicates the format of the fifth color component.</summary>
/// <remarks>
/// This must be unused if the color format is not CMYK.
/// When the color format is CMYK, this can be alpha.
/// </remarks>
public PixelComponentFormat Component5 => Unsafe.BitCast<byte, PixelComponentFormat>((byte)((_rawValue >>> 27) & 0x1F));

/// <summary>Gets the number of color components that are defined.</summary>
public uint ComponentCount => 5 - (uint)BitOperations.LeadingZeroCount(_rawValue) / 5;

public ColorFormat ColorFormat => (ColorFormat)(_rawValue & 0x07);
public bool IsComponentOrderReversed => (_rawValue & 0x08) != 0;
public bool IsTransparent => (_rawValue & 0x10) != 0;
public bool IsAlphaLast => (_rawValue & 0x20) == 0;
public bool IsLittleEndian => (_rawValue & 0x40) != 0;

public override bool Equals(object? obj) => obj is PixelFormat format && Equals(format);
public bool Equals(PixelFormat other) => _rawValue == other._rawValue;
public override int GetHashCode() => HashCode.Combine(_rawValue);

public static bool operator ==(PixelFormat left, PixelFormat right) => left.Equals(right);
public static bool operator !=(PixelFormat left, PixelFormat right) => !(left == right);
}
8 changes: 8 additions & 0 deletions src/Exo/Core/Exo.Core/Monitors/MonitorShape.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Exo.Monitors;

public enum MonitorShape : byte
{
Rectangle = 0,
Square = 1,
Circle = 2,
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System;
using System.Buffers.Binary;
using System.Collections;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using DeviceTools;
using DeviceTools.HumanInterfaceDevices;
using DeviceTools.HumanInterfaceDevices.Usages;
using Exo.Discovery;
using Exo.Features;
using Exo.Features.Monitors;
using Exo.Images;
using Exo.Monitors;
using Microsoft.Extensions.Logging;

namespace Exo.Devices.Elgato.StreamDeck;
Expand All @@ -26,8 +31,50 @@ namespace Exo.Devices.Elgato.StreamDeck;
// Or all buttons could be exposed as a generic feature… Depends on what we want to do.
// Generally the idea would be to expose stuff as close to what the hardware support, but that would require very elgato-specific stuff and restrict a bit the fun that can be implemented.
// The first part may be fine, but the second would be a bit more annoying.
public sealed class StreamDeckDeviceDriver : Driver, IDeviceDriver<IGenericDeviceFeature>, IDeviceIdFeature, IDeviceSerialNumberFeature
public sealed class StreamDeckDeviceDriver :
Driver,
IDeviceDriver<IGenericDeviceFeature>,
IDeviceDriver<IMonitorDeviceFeature>,
IDeviceIdFeature,
IDeviceSerialNumberFeature,
IEmbeddedMonitorControllerFeature,
IMonitorBrightnessFeature
{
private static readonly Guid[] StreamDeckXlButtonIds = [
new(0x903959D8, 0x4449, 0x4081, 0x92, 0x24, 0x60, 0x54, 0x55, 0x26, 0xAB, 0xE6),
new(0xCF380166, 0x0805, 0x4BE7, 0xBF, 0xAD, 0x10, 0x44, 0x9E, 0xD1, 0x63, 0x9E),
new(0xE412EB19, 0x58A1, 0x4F26, 0xAE, 0x7A, 0xFE, 0x8D, 0xF3, 0x18, 0xF5, 0x85),
new(0x82994C85, 0x3CEF, 0x4093, 0xA0, 0x31, 0x0B, 0xF8, 0x81, 0x8A, 0x71, 0x1F),
new(0x211D7055, 0xAFCE, 0x4EA6, 0x90, 0xBE, 0x36, 0xDE, 0xAB, 0xBE, 0x0F, 0xBC),
new(0xD7ED1BE9, 0xBE25, 0x4007, 0x84, 0x3E, 0x02, 0xD5, 0xEF, 0xC4, 0xE2, 0x9E),
new(0xB3D52EEB, 0x3833, 0x4E84, 0xAF, 0x91, 0x19, 0xA4, 0xE7, 0x92, 0xAB, 0x44),
new(0x3E5F4DFF, 0x3723, 0x4DFB, 0xA4, 0x56, 0xFA, 0x84, 0xA0, 0x23, 0x2E, 0xC5),
new(0xC94DDD1D, 0x6D5C, 0x4295, 0xB5, 0x9F, 0x84, 0x5B, 0xBF, 0x6F, 0xD5, 0x40),
new(0x3D9F4285, 0x8BD1, 0x423B, 0xB7, 0xC4, 0xD8, 0x3C, 0x6A, 0x72, 0x57, 0x18),
new(0xAF42B08F, 0x409C, 0x4B96, 0xBE, 0x89, 0xB9, 0xB8, 0xA3, 0x32, 0x85, 0x0C),
new(0x7608B456, 0xA90B, 0x49AF, 0x96, 0xA2, 0x94, 0x78, 0xBC, 0x7C, 0x77, 0x70),
new(0x9DEBCE69, 0x469A, 0x480A, 0xBE, 0x34, 0x46, 0xA7, 0xA6, 0x10, 0x61, 0x78),
new(0x0F9C3B6E, 0xAF86, 0x4A04, 0x89, 0x13, 0x39, 0x33, 0xFD, 0xD6, 0xD9, 0x4A),
new(0xC8A60D4B, 0xB6D8, 0x40AF, 0x82, 0xB4, 0xCF, 0xA8, 0xB2, 0xD0, 0x8F, 0x6E),
new(0xD7CE48B8, 0x5807, 0x4F28, 0xA6, 0xEA, 0x2E, 0x42, 0x26, 0xC5, 0x5B, 0x30),
new(0xFC472091, 0x5D34, 0x4FC1, 0xA7, 0xA9, 0xA6, 0xF6, 0xC8, 0xCD, 0x4C, 0x47),
new(0x656C6B17, 0x371C, 0x4D4F, 0x92, 0x78, 0xCC, 0xF2, 0xF3, 0xFC, 0xD7, 0xA5),
new(0x72FCC17E, 0xF725, 0x4D2A, 0xB3, 0x7A, 0xF4, 0x31, 0x26, 0x91, 0x05, 0x11),
new(0x363A6A93, 0xAAB4, 0x4CE5, 0xB7, 0xF0, 0x12, 0x4E, 0xE2, 0xDD, 0x23, 0xA4),
new(0x3C603EF9, 0xB96C, 0x4D75, 0x9B, 0x6C, 0xD6, 0xA7, 0x2A, 0x55, 0xCC, 0x7C),
new(0xF36CE9EC, 0xD225, 0x45C9, 0xB6, 0xF2, 0x21, 0x5C, 0x35, 0x6D, 0xC3, 0xA9),
new(0x9733A887, 0x7758, 0x4928, 0xB1, 0x08, 0x5A, 0x44, 0x08, 0xCD, 0x82, 0xC6),
new(0x28EEED2E, 0x9099, 0x4E50, 0x95, 0xCC, 0x39, 0xDC, 0xB3, 0x5A, 0x66, 0x78),
new(0xFC135451, 0x54D4, 0x42C8, 0x9B, 0x15, 0xCC, 0x59, 0x65, 0xFB, 0xA4, 0xA0),
new(0xC3DDEF57, 0x9D98, 0x4048, 0x93, 0xEE, 0xCA, 0x5F, 0x29, 0xEE, 0xE7, 0x93),
new(0xC0951F45, 0xD7B1, 0x44C7, 0x95, 0x74, 0x10, 0xEE, 0x90, 0x19, 0xC7, 0x25),
new(0xD33FFC43, 0xCDE4, 0x4F06, 0x8F, 0xB2, 0x3F, 0x91, 0x23, 0x6A, 0xC4, 0xDB),
new(0xA61978BF, 0x8AD6, 0x42DE, 0xBB, 0xA3, 0x0A, 0x31, 0x6D, 0x98, 0x90, 0x2F),
new(0xA75F7663, 0xEAB1, 0x4510, 0xBE, 0x6B, 0x49, 0x2C, 0x0B, 0x78, 0x3A, 0x7F),
new(0x01AA7856, 0xC94B, 0x4587, 0xA5, 0x31, 0x3B, 0xEA, 0xB2, 0xB3, 0x04, 0x26),
new(0x63AA0DA1, 0x17D9, 0x482E, 0x92, 0xAE, 0xD0, 0x3A, 0x89, 0x99, 0xA7, 0x65),
];

private const ushort ElgatoVendorId = 0x0FD9;

[DiscoverySubsystem<HidDiscoverySubsystem>]
Expand Down Expand Up @@ -93,13 +140,16 @@ CancellationToken cancellationToken
try
{
string serialNumber = await device.GetSerialNumberAsync(cancellationToken).ConfigureAwait(false);
var deviceInfo = await device.GetDeviceInfoAsync(cancellationToken).ConfigureAwait(false);

return new DriverCreationResult<SystemDevicePath>
(
keys,
new StreamDeckDeviceDriver
(
device,
StreamDeckXlButtonIds,
deviceInfo,
friendlyName,
productId,
version,
Expand All @@ -116,26 +166,43 @@ CancellationToken cancellationToken
}

private readonly StreamDeckDevice _device;
private readonly Guid[] _buttonIds;
private readonly StreamDeckDeviceInfo _deviceInfo;
private readonly ushort _productId;
private readonly ushort _versionNumber;
private readonly Button[] _buttons;
private readonly IDeviceFeatureSet<IGenericDeviceFeature> _genericFeatures;
private readonly IDeviceFeatureSet<IMonitorDeviceFeature> _monitorFeatures;

IDeviceFeatureSet<IGenericDeviceFeature> IDeviceDriver<IGenericDeviceFeature>.Features => _genericFeatures;
IDeviceFeatureSet<IMonitorDeviceFeature> IDeviceDriver<IMonitorDeviceFeature>.Features => _monitorFeatures;

private StreamDeckDeviceDriver
(
StreamDeckDevice device,
Guid[] buttonIds,
StreamDeckDeviceInfo deviceInfo,
string friendlyName,
ushort productId,
ushort versionNumber,
DeviceConfigurationKey configurationKey
) : base(friendlyName, configurationKey)
{
_device = device;
_buttonIds = buttonIds;
_deviceInfo = deviceInfo;
_productId = productId;
_versionNumber = versionNumber;

var buttons = new Button[deviceInfo.ButtonCount];
for (int i = 0; i < buttons.Length; i++)
{
buttons[i] = new(this, (byte)i);
}
_buttons = buttons;

_genericFeatures = FeatureSet.Create<IGenericDeviceFeature, StreamDeckDeviceDriver, IDeviceIdFeature, IDeviceSerialNumberFeature>(this);
_monitorFeatures = FeatureSet.Create<IMonitorDeviceFeature, StreamDeckDeviceDriver, IEmbeddedMonitorControllerFeature>(this);
}

public override DeviceCategory DeviceCategory => DeviceCategory.Keyboard;
Expand All @@ -145,7 +212,33 @@ public override async ValueTask DisposeAsync()
await _device.DisposeAsync().ConfigureAwait(false);
}

private Size ButtonImageSize => new(_deviceInfo.ButtonImageWidth, _deviceInfo.ButtonImageHeight);

DeviceId IDeviceIdFeature.DeviceId => DeviceId.ForUsb(ElgatoVendorId, _productId, _versionNumber);

string IDeviceSerialNumberFeature.SerialNumber => ConfigurationKey.UniqueId!;

ImmutableArray<IEmbeddedMonitor> IEmbeddedMonitorControllerFeature.EmbeddedMonitors => ImmutableCollectionsMarshal.AsImmutableArray(Unsafe.As<IEmbeddedMonitor[]>(_buttons));

// TODO: Must be able to read the value from the device.
ValueTask<ContinuousValue> IContinuousVcpFeature.GetValueAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
ValueTask IContinuousVcpFeature.SetValueAsync(ushort value, CancellationToken cancellationToken) => throw new NotImplementedException();

private sealed class Button : IEmbeddedMonitor
{
private readonly StreamDeckDeviceDriver _driver;
private readonly byte _keyIndex;

public Button(StreamDeckDeviceDriver driver, byte buttonId)
{
_driver = driver;
_keyIndex = buttonId;
}

Guid IEmbeddedMonitor.MonitorId => _driver._buttonIds[_keyIndex];
MonitorShape IEmbeddedMonitor.Shape => MonitorShape.Square;
Size IEmbeddedMonitor.ImageSize => _driver.ButtonImageSize;
PixelFormat IEmbeddedMonitor.PixelFormat => PixelFormat.R8G8B8X8;
ImageFormats IEmbeddedMonitor.SupportedImageFormats { get; }
}
}
Loading

0 comments on commit 24d261f

Please sign in to comment.