using System; using System.Buffers; using System.Runtime.InteropServices; using Artemis.Core.SkiaSharp; using RGB.NET.Core; using RGB.NET.Presets.Textures.Sampler; using SkiaSharp; namespace Artemis.Core { /// /// Represents a SkiaSharp-based RGB.NET PixelTexture /// public sealed class SKTexture : PixelTexture, IDisposable { private readonly bool _isScaledDown; private readonly SKPixmap _pixelData; private readonly IntPtr _pixelDataPtr; #region Constructors internal SKTexture(IManagedGraphicsContext? graphicsContext, int width, int height, float scale) : base(width, height, DATA_PER_PIXEL, new AverageByteSampler()) { ImageInfo = new SKImageInfo(width, height); Surface = graphicsContext == null ? SKSurface.Create(ImageInfo) : SKSurface.Create(graphicsContext.GraphicsContext, true, ImageInfo); RenderScale = scale; _isScaledDown = Math.Abs(scale - 1) > 0.001; _pixelDataPtr = Marshal.AllocHGlobal(ImageInfo.BytesSize); _pixelData = new SKPixmap(ImageInfo, _pixelDataPtr, ImageInfo.RowBytes); } #endregion private void ReleaseUnmanagedResources() { Marshal.FreeHGlobal(_pixelDataPtr); } /// ~SKTexture() { ReleaseUnmanagedResources(); } /// public void Dispose() { Surface.Dispose(); _pixelData.Dispose(); ReleaseUnmanagedResources(); GC.SuppressFinalize(this); } #region Constants private const int STACK_ALLOC_LIMIT = 1024; private const int DATA_PER_PIXEL = 4; #endregion #region Methods /// /// Invalidates the texture /// public void Invalidate() { IsInvalid = true; } internal void CopyPixelData() { using SKImage skImage = Surface.Snapshot(); skImage.ReadPixels(_pixelData); } /// protected override Color GetColor(in ReadOnlySpan pixel) { return new(pixel[2], pixel[1], pixel[0]); } /// public override Color this[in Rectangle rectangle] { get { if (Data.Length == 0) return Color.Transparent; SKRectI skRectI = CreatedFlooredRectI( Size.Width * rectangle.Location.X.Clamp(0, 1), Size.Height * rectangle.Location.Y.Clamp(0, 1), Size.Width * rectangle.Size.Width.Clamp(0, 1), Size.Height * rectangle.Size.Height.Clamp(0, 1) ); if (skRectI.Width == 0 || skRectI.Height == 0) return Color.Transparent; if (skRectI.Width == 1 && skRectI.Height == 1) return GetColor(GetPixelData(skRectI.Left, skRectI.Top)); int bufferSize = skRectI.Width * skRectI.Height * DATA_PER_PIXEL; if (bufferSize <= STACK_ALLOC_LIMIT) { Span buffer = stackalloc byte[bufferSize]; GetRegionData(skRectI.Left, skRectI.Top, skRectI.Width, skRectI.Height, buffer); Span pixelData = stackalloc byte[DATA_PER_PIXEL]; Sampler.Sample(new SamplerInfo(skRectI.Width, skRectI.Height, buffer), pixelData); return GetColor(pixelData); } else { byte[] rent = ArrayPool.Shared.Rent(bufferSize); Span buffer = new Span(rent).Slice(0, bufferSize); GetRegionData(skRectI.Left, skRectI.Top, skRectI.Width, skRectI.Height, buffer); Span pixelData = stackalloc byte[DATA_PER_PIXEL]; Sampler.Sample(new SamplerInfo(skRectI.Width, skRectI.Height, buffer), pixelData); ArrayPool.Shared.Return(rent); return GetColor(pixelData); } } } private static SKRectI CreatedFlooredRectI(float x, float y, float width, float height) { return new( width <= 0.0 ? checked((int) Math.Floor(x)) : checked((int) Math.Ceiling(x)), height <= 0.0 ? checked((int) Math.Floor(y)) : checked((int) Math.Ceiling(y)), width >= 0.0 ? checked((int) Math.Floor(x + width)) : checked((int) Math.Ceiling(x + width)), height >= 0.0 ? checked((int) Math.Floor(y + height)) : checked((int) Math.Ceiling(y + height)) ); } #endregion #region Properties & Fields /// /// Gets the SKBitmap backing this texture /// public SKSurface Surface { get; } /// /// Gets the image info used to create the /// public SKImageInfo ImageInfo { get; } /// /// Gets the color data in RGB format /// protected override ReadOnlySpan Data => _pixelData.GetPixelSpan(); /// /// 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 } }