Skip to content

Commit

Permalink
✨ V0 embedded monitor image conversion.
Browse files Browse the repository at this point in the history
Very rough but it finally works, so it can be improved upon later.
  • Loading branch information
hexawyz committed Feb 7, 2025
1 parent 0db0580 commit 68a2e96
Show file tree
Hide file tree
Showing 8 changed files with 763 additions and 21 deletions.
8 changes: 8 additions & 0 deletions src/Exo/Core/Exo.Core/LittleEndian.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,12 @@ public static void Write(ref byte source, ushort value)
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static void Write(ref byte source, uint value)
=> Unsafe.WriteUnaligned(ref source, BitConverter.IsLittleEndian ? value : BinaryPrimitives.ReverseEndianness(value));

[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static void Write(ref byte source, ulong value)
=> Unsafe.WriteUnaligned(ref source, BitConverter.IsLittleEndian ? value : BinaryPrimitives.ReverseEndianness(value));

[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static void Write(ref byte source, UInt128 value)
=> Unsafe.WriteUnaligned(ref source, BitConverter.IsLittleEndian ? value : BinaryPrimitives.ReverseEndianness(value));
}
5 changes: 1 addition & 4 deletions src/Exo/Core/Exo.Memory/MemoryMappedFileMemoryManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,11 @@ public MemoryMappedFileMemoryManager(MemoryMappedFile memoryMappedFile, nint off
_viewHandle = _viewAccessor.SafeMemoryMappedViewHandle;
_offset = offset;
_length = length;
bool success = false;
_viewHandle.DangerousAddRef(ref success);
}

protected override void Dispose(bool disposing)
{
if (Interlocked.Exchange(ref _viewAccessor, null) is { } accessor)
accessor.Dispose();
if (Interlocked.Exchange(ref _viewAccessor, null) is { } accessor) accessor.Dispose();
}

public override Span<byte> GetSpan() => new((byte*)_viewHandle.DangerousGetHandle(), _length);
Expand Down
310 changes: 310 additions & 0 deletions src/Exo/Service/Exo.Service.Core/CircleCroppingProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors;

namespace Exo.Service;

internal sealed class CircleCroppingProcessor : IImageProcessor
{
private readonly Color _color;
private readonly byte _tileShift;

public CircleCroppingProcessor(Color color, byte tileShift)
{
_color = color;
_tileShift = tileShift;
}

public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(SixLabors.ImageSharp.Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)
where TPixel : unmanaged, IPixel<TPixel>
=> new CircleCroppingProcessor<TPixel>(configuration, source, sourceRectangle, _color.ToPixel<TPixel>(), _tileShift);
}

internal sealed class CircleCroppingProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly TPixel _color;
private readonly byte _tileShift;

public CircleCroppingProcessor(SixLabors.ImageSharp.Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle, TPixel color, byte tileShift)
: base(configuration, source, sourceRectangle)
{
if (tileShift > 8) throw new ArgumentOutOfRangeException(nameof(tileShift));
_color = color;
_tileShift = tileShift;
}

protected override void OnFrameApply(ImageFrame<TPixel> source)
{
// Most operations are optimized for circle and not for ellipse because we don't really care that much about ellipses (at least yet)
if (source.Width == source.Height)
{
if (_tileShift == 0)
{
if (SourceRectangle == new Rectangle(0, 0, source.Width, source.Height))
{
var operation = new UncroppedCircleRowIntervalOperation(source.PixelBuffer, _color);
ParallelRowIterator.IterateRowIntervals(Configuration, SourceRectangle, in operation);
}
else
{
var operation = new CircleRowIntervalOperation(SourceRectangle, source.PixelBuffer, _color);
ParallelRowIterator.IterateRowIntervals(Configuration, SourceRectangle, in operation);
}
}
else
{
if (SourceRectangle == new Rectangle(0, 0, source.Width, source.Height))
{
var operation = new UncroppedTiledCircleRowIntervalOperation(source.PixelBuffer, _color, _tileShift);
ParallelRowIterator.IterateRowIntervals(Configuration, SourceRectangle, in operation);
}
else
{
var operation = new TiledCircleRowIntervalOperation(SourceRectangle, source.PixelBuffer, _color, _tileShift);
ParallelRowIterator.IterateRowIntervals(Configuration, SourceRectangle, in operation);
}
}
}
else
{
var operation = new TiledEllipseRowIntervalOperation(SourceRectangle, source.PixelBuffer, _color, _tileShift);

ParallelRowIterator.IterateRowIntervals(Configuration, SourceRectangle, in operation);
}
}

// This is the reference algorithm.
// All other versions are simplified from this one.
private readonly struct TiledEllipseRowIntervalOperation : IRowIntervalOperation
{
private readonly Rectangle _bounds;
private readonly Buffer2D<TPixel> _pixels;
private readonly TPixel _color;
private readonly double _ratio;
private readonly int _maxX;
private readonly int _midPointY;
private readonly int _tileSize;
private readonly int _tileMask;

public TiledEllipseRowIntervalOperation(Rectangle bounds, Buffer2D<TPixel> pixels, TPixel color, byte tileShift)
{
_bounds = bounds;
_pixels = pixels;
_color = color;
_maxX = _pixels.Width - 1;
// The vertical radius of should be exactly H/2
_midPointY = _pixels.Height >>> 1;
_tileSize = (1 << tileShift);
_tileMask = _tileSize - 1;
_ratio = (double)_pixels.Width / _pixels.Height;
}

public void Invoke(in RowInterval rows)
{
for (int i = rows.Min; i < rows.Max;)
{
// We can avoid doing the circle computation for every row in the tile.
// To that effect, it is easy to compute the upper bound of the tile.
int tileMaxY = (i + _tileSize) & ~_tileMask;

// The most important thing to do is to make y closer to the center.
// The reason behind that is that we want to be optimistic towards the fact that the point would be "inside".
// For that reason, every "pixel" or "tile" should use the coordinate that points towards the center.
// Remember that pixels are considered to be rectangles of width 1x1. Tiles are larger. If more than 0% of the rectangle overlaps the circle, then it is "inside".
// NB: We may want to handle the boundary condition at the edges. Basically, if the corner is exactly on the circle, we want to consider the rectangle "outside".
// Regarding the computation below, an easy way to compute the lower coordinate of the tile is to add 2^(N-1) before masking out the bits of the tile.
// However, because in the upper half, we already want to adjust the pixel coordinate by 1 below, the "-1" is nullified.
int y;
if (i < _midPointY) y = Math.Min(tileMaxY, _midPointY);
// In the lower half case, the value needs to be reflected around the middle, in order to obtain the equivalent y in the upper quadrant.
else if (i > _midPointY) y = _pixels.Height - Math.Max((i - _tileMask) & ~_tileMask, _midPointY);
else y = i;

// The below formula relies on basic trigonometry (sin² + cos² = 1) but adapted for the specifics of the current computation.
// This does use a few FP operations (although the minimum possible), and it might be possible to switch to a full integer algorithm at some point.
// But for now, this will do.
int x0 = (_pixels.Width - (int)(Math.Floor(Math.Sqrt((y * (_pixels.Height - y)) << 2) * _ratio))) >> 1;
int x1 = (_maxX - x0 + _tileMask) & ~_tileMask;
int w0 = Math.Max(0, (x0 & ~_tileMask) - _bounds.Left);
int w1 = Math.Max(0, _bounds.Right - x1);
x0 = _bounds.Left;

// We shall now mask out the pixels of each row in bulk.
for (; i < tileMaxY; i++)
{
var row = _pixels.DangerousGetRowSpan(i);
row.Slice(x0, w0).Fill(_color);
row.Slice(x1, w1).Fill(_color);
}
}
}
}

private readonly struct TiledCircleRowIntervalOperation : IRowIntervalOperation
{
private readonly Rectangle _bounds;
private readonly Buffer2D<TPixel> _pixels;
private readonly TPixel _color;
private readonly int _maxX;
private readonly int _midPointY;
private readonly int _tileSize;
private readonly int _tileMask;

public TiledCircleRowIntervalOperation(Rectangle bounds, Buffer2D<TPixel> pixels, TPixel color, byte tileShift)
{
_bounds = bounds;
_pixels = pixels;
_color = color;
_maxX = _pixels.Width - 1;
_midPointY = _pixels.Height >>> 1;
_tileSize = 1 << tileShift;
_tileMask = _tileSize - 1;
}

public void Invoke(in RowInterval rows)
{
for (int i = rows.Min; i < rows.Max;)
{
int tileMaxY = (i + _tileSize) & ~_tileMask;

int y;
if (i < _midPointY) y = Math.Min(tileMaxY, _midPointY);
else if (i > _midPointY) y = _pixels.Height - Math.Max((i - _tileMask) & ~_tileMask, _midPointY);
else y = i;

int x0 = (_pixels.Width - (int)Math.Floor(Math.Sqrt((y * (_pixels.Height - y)) << 2))) >> 1;
int x1 = (_maxX - x0 + _tileMask) & ~_tileMask;
int w0 = Math.Max(0, (x0 & ~_tileMask) - _bounds.Left);
int w1 = Math.Max(0, _bounds.Right - x1);
x0 = _bounds.Left;

for (; i < tileMaxY; i++)
{
var row = _pixels.DangerousGetRowSpan(i);
row.Slice(x0, w0).Fill(_color);
row.Slice(x1, w1).Fill(_color);
}
}
}
}

private readonly struct CircleRowIntervalOperation : IRowIntervalOperation
{
private readonly Rectangle _bounds;
private readonly Buffer2D<TPixel> _pixels;
private readonly TPixel _color;
private readonly int _maxX;
private readonly int _midPointY;

public CircleRowIntervalOperation(Rectangle bounds, Buffer2D<TPixel> pixels, TPixel color)
{
_bounds = bounds;
_pixels = pixels;
_color = color;
_maxX = _pixels.Width - 1;
_midPointY = _pixels.Height >>> 1;
}

public void Invoke(in RowInterval rows)
{
for (int i = rows.Min; i < rows.Max; i++)
{
int y;
if (i < _midPointY) y = i + 1;
else if (i > _midPointY) y = _pixels.Height - i;
else y = i;

int x0 = (_pixels.Width - (int)Math.Floor(Math.Sqrt((y * (_pixels.Height - y)) << 2))) >> 1;
int x1 = _maxX - x0;
int w0 = Math.Max(0, x0 - _bounds.Left);
int w1 = Math.Max(0, _bounds.Right - x1);
x0 = _bounds.Left;

var row = _pixels.DangerousGetRowSpan(i);
row.Slice(x0, w0).Fill(_color);
row.Slice(x1, w1).Fill(_color);
}
}
}

private readonly struct UncroppedTiledCircleRowIntervalOperation : IRowIntervalOperation
{
private readonly Buffer2D<TPixel> _pixels;
private readonly TPixel _color;
private readonly int _maxX;
private readonly int _midPointY;
private readonly int _tileSize;
private readonly int _tileMask;

public UncroppedTiledCircleRowIntervalOperation(Buffer2D<TPixel> pixels, TPixel color, byte tileShift)
{
_pixels = pixels;
_color = color;
_maxX = _pixels.Width - 1;
_midPointY = _pixels.Height >>> 1;
_tileSize = 1 << tileShift;
_tileMask = _tileSize - 1;
}

public void Invoke(in RowInterval rows)
{
for (int i = rows.Min; i < rows.Max;)
{
int tileMaxY = (i + _tileSize) & ~_tileMask;

int y;
if (i < _midPointY) y = Math.Min(tileMaxY, _midPointY);
else if (i > _midPointY) y = _pixels.Height - Math.Max((i - _tileMask) & ~_tileMask, _midPointY);
else y = i;

int x0 = (_pixels.Width - (int)Math.Floor(Math.Sqrt((y * (_pixels.Height - y)) << 2))) >> 1;
int x1 = (_maxX - x0 + _tileMask) & ~_tileMask;
x0 &= ~_tileMask;
int w1 = _pixels.Width - x1;

for (; i < tileMaxY; i++)
{
var row = _pixels.DangerousGetRowSpan(i);
row[..x0].Fill(_color);
row.Slice(x1, w1).Fill(_color);
}
}
}
}

private readonly struct UncroppedCircleRowIntervalOperation : IRowIntervalOperation
{
private readonly Buffer2D<TPixel> _pixels;
private readonly TPixel _color;
private readonly int _maxX;
private readonly int _midPointY;

public UncroppedCircleRowIntervalOperation(Buffer2D<TPixel> pixels, TPixel color)
{
_pixels = pixels;
_color = color;
_maxX = _pixels.Width - 1;
_midPointY = _pixels.Height >>> 1;
}

public void Invoke(in RowInterval rows)
{
for (int i = rows.Min; i < rows.Max; i++)
{
int y;
if (i < _midPointY) y = i + 1;
else if (i > _midPointY) y = _pixels.Height - i;
else y = i;

int x = (_pixels.Width - (int)Math.Floor(Math.Sqrt((y * (_pixels.Height - y)) << 2))) >> 1;

var row = _pixels.DangerousGetRowSpan(i);
row[..x].Fill(_color);
row.Slice(_pixels.Width - x, x).Fill(_color);
}
}
}
}
26 changes: 20 additions & 6 deletions src/Exo/Service/Exo.Service.Core/EmbeddedMonitorService.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Reflection.Metadata;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Channels;
Expand Down Expand Up @@ -261,14 +262,26 @@ public async ValueTask<bool> SetBuiltInGraphicsAsync(Guid graphicsId, Cancellati
return false;
}

public async ValueTask<bool> SetImageAsync(UInt128 imageId, Rectangle region, CancellationToken cancellationToken)
public async ValueTask<bool> SetImageAsync(ImageStorageService imageStorageService, UInt128 imageId, Rectangle region, CancellationToken cancellationToken)
{
if (imageId != _currentImageId || region.Left != _currentRegionLeft || region.Top != _currentRegionTop || region.Width != _currentRegionWidth || region.Height != _currentRegionHeight)
{
if (_monitor is not null)
{
// TODO: Implement generation in the image storage.
//await _monitor.SetImageAsync();
var (physicalImageId, imageFormat, imageFile) = imageStorageService.GetTransformedImage
(
imageId,
new(region.Left, region.Top, region.Width, region.Height),
_imageFormats,
(_capabilities & EmbeddedMonitorCapabilities.AnimatedImages) != 0 ? _imageFormats & ImageFormats.Gif : 0,
new(_width, _height),
_shape == MonitorShape.Circle
);
using (imageFile)
using (var memoryManager = imageFile.CreateMemoryManager())
{
await _monitor.SetImageAsync(physicalImageId, imageFormat, memoryManager.Memory, cancellationToken);
}
}

_currentGraphics = default;
Expand Down Expand Up @@ -663,10 +676,11 @@ public async ValueTask SetBuiltInGraphicsAsync(Guid deviceId, Guid monitorId, Gu
public async ValueTask SetImageAsync(Guid deviceId, Guid monitorId, UInt128 imageId, Rectangle imageRegion, CancellationToken cancellationToken)
{
if (!_embeddedMonitorDeviceStates.TryGetValue(deviceId, out var deviceState)) throw new InvalidOperationException("Device not found.");

if ((uint)imageRegion.Left > ushort.MaxValue ||
(uint)imageRegion.Top > ushort.MaxValue ||
(uint)imageRegion.Width > ushort.MaxValue ||
(uint)imageRegion.Height > ushort.MaxValue)
(uint)(imageRegion.Width - 1) > ushort.MaxValue - 1 ||
(uint)(imageRegion.Height - 1) > ushort.MaxValue - 1)
{
throw new ArgumentException("Invalid crop region.");
}
Expand All @@ -677,7 +691,7 @@ public async ValueTask SetImageAsync(Guid deviceId, Guid monitorId, UInt128 imag
{
if (!deviceState.EmbeddedMonitors.TryGetValue(monitorId, out var monitorState)) throw new InvalidOperationException("Embedded monitor not found.");

if (!await monitorState.SetImageAsync(imageId, imageRegion, cancellationToken).ConfigureAwait(false)) return;
if (!await monitorState.SetImageAsync(_imageStorageService, imageId, imageRegion, cancellationToken).ConfigureAwait(false)) return;

configuration = monitorState.CreatePersistedConfiguration();
}
Expand Down
Loading

0 comments on commit 68a2e96

Please sign in to comment.