using System; using System.Collections.Generic; using Artemis.Core.SkiaSharp; using HPPH; using RGB.NET.Core; using RGB.NET.Presets.Extensions; using SkiaSharp; namespace Artemis.Core; /// /// Represents a SkiaSharp-based RGB.NET PixelTexture /// public sealed class SKTexture : ITexture, IDisposable { #region Constructors internal SKTexture(IManagedGraphicsContext? graphicsContext, int width, int height, float scale, IReadOnlyCollection devices) { RenderScale = scale; Size = new Size(width, height); ImageInfo = new SKImageInfo(width, height); Surface = graphicsContext == null ? SKSurface.Create(ImageInfo) : SKSurface.Create(graphicsContext.GraphicsContext, true, ImageInfo); _readback = new SKBitmap(new SKImageInfo(width, height, SKColorType.Bgra8888, SKAlphaType.Unpremul)); foreach (ArtemisDevice artemisDevice in devices) { foreach (ArtemisLed artemisLed in artemisDevice.Leds) _ledRects[artemisLed.RgbLed] = CreateSampleRect(artemisLed.RgbLed.AbsoluteBoundary, scale, width, height); } } #endregion private static SKRectI CreateSampleRect(Rectangle boundary, float scale, int width, int height) { (int left, int right) = ScaleSpan(boundary.Location.X, boundary.Size.Width, scale, width); (int top, int bottom) = ScaleSpan(boundary.Location.Y, boundary.Size.Height, scale, height); return new SKRectI(left, top, right, bottom); } private static (int Start, int End) ScaleSpan(float location, float size, float scale, int max) { float start = location * scale; float end = (location + size) * scale; if (end <= 0 || start >= max) return (0, 0); // Always sample at least one pixel int startPixel = Math.Clamp(start.RoundToInt(), 0, max - 1); int endPixel = Math.Clamp(end.RoundToInt(), startPixel + 1, max); return (startPixel, endPixel); } internal Color GetColorAtRenderTarget(in RenderTarget renderTarget) { if (!_hasPixels) return Color.Transparent; SKRectI skRectI = _ledRects[renderTarget.Led]; if (skRectI.Width <= 0 || skRectI.Height <= 0) return Color.Transparent; return AveragePixels(_readback, skRectI); } internal static unsafe Color AveragePixels(SKBitmap bitmap, SKRectI rectangle) { int left = Math.Max(0, rectangle.Left); int top = Math.Max(0, rectangle.Top); int right = Math.Min(bitmap.Width, rectangle.Right); int bottom = Math.Min(bitmap.Height, rectangle.Bottom); if (left >= right || top >= bottom) return Color.Transparent; byte* pixels = (byte*)bitmap.GetPixels().ToPointer(); if (pixels == null) return Color.Transparent; long blue = 0; long green = 0; long red = 0; long alpha = 0; int rowBytes = bitmap.RowBytes; for (int y = top; y < bottom; y++) { byte* pixel = pixels + y * rowBytes + left * 4; for (int x = left; x < right; x++, pixel += 4) { blue += pixel[0]; green += pixel[1]; red += pixel[2]; alpha += pixel[3]; } } float count = (right - left) * (bottom - top); return new ColorBGRA( (byte)MathF.Round(blue / count), (byte)MathF.Round(green / count), (byte)MathF.Round(red / count), (byte)MathF.Round(alpha / count)).ToColor(); } /// ~SKTexture() { Dispose(); } /// public void Dispose() { _readback.Dispose(); Surface.Dispose(); GC.SuppressFinalize(this); } #region Methods /// /// Invalidates the texture /// public void Invalidate() { IsInvalid = true; } internal void CopyPixelData() { _hasPixels = Surface.ReadPixels(_readback.Info, _readback.GetPixels(), _readback.RowBytes, 0, 0); } #endregion #region Properties & Fields private readonly SKBitmap _readback; private bool _hasPixels; private readonly Dictionary _ledRects = []; /// public Size Size { get; } /// public Color this[Point point] => Color.Transparent; /// public Color this[Rectangle rectangle] => Color.Transparent; /// /// Gets the SKBitmap backing this texture /// public SKSurface Surface { get; } /// /// Gets the image info used to create the /// public SKImageInfo ImageInfo { get; } /// /// Gets the render scale of the texture /// public float RenderScale { get; } /// /// Gets a boolean indicating whether has been called on this texture, indicating it should /// be replaced /// public bool IsInvalid { get; private set; } #endregion }