using System.Numerics; using XFESystem.Desktop; namespace XFESystem.Platform; /// Present RGB24 CPU pixels to an actual scanout. A failed copy must throw. public interface IFramebufferPresenter { int Width { get; } int Height { get; } void Present(ReadOnlySpan rgbPixels, DesktopBounds nativeDamage); } /// A mapped, little-endian physical framebuffer. Ownership/mapping belongs to the kernel. public readonly record struct FramebufferSurface(nuint Address, nuint MappedBytes, int Width, int Height, int PitchBytes, int BitsPerPixel, uint RedMask, uint GreenMask, uint BlueMask); /// Writes the supplied framebuffer address directly, respecting pitch and RGB masks. public sealed unsafe class MemoryFramebufferPresenter : IFramebufferPresenter { private readonly FramebufferSurface surface; private readonly int bytesPerPixel; public int Width => surface.Width; public int Height => surface.Height; public MemoryFramebufferPresenter(FramebufferSurface surface) { if (surface.Address == 0 || surface.Width <= 0 || surface.Height <= 0 || surface.BitsPerPixel is not (16 or 24 or 32)) throw new ArgumentException("A mapped 16, 24 or 32 bit framebuffer is required.", nameof(surface)); bytesPerPixel = surface.BitsPerPixel / 8; if (surface.PitchBytes < (long)surface.Width * bytesPerPixel) throw new ArgumentException("Framebuffer pitch is smaller than a visible row.", nameof(surface)); ulong required = checked((ulong)(surface.Height - 1) * (uint)surface.PitchBytes + (ulong)surface.Width * (uint)bytesPerPixel); if (surface.PitchBytes < (long)surface.Width * bytesPerPixel || required > surface.MappedBytes || required > nuint.MaxValue - surface.Address || !ValidMask(surface.RedMask, surface.BitsPerPixel) || !ValidMask(surface.GreenMask, surface.BitsPerPixel) || !ValidMask(surface.BlueMask, surface.BitsPerPixel) || (surface.RedMask & surface.GreenMask) != 0 || (surface.RedMask & surface.BlueMask) != 0 || (surface.GreenMask & surface.BlueMask) != 0) throw new ArgumentException("Framebuffer geometry, mapping extent or RGB masks are invalid.", nameof(surface)); this.surface = surface; } public void Present(ReadOnlySpan rgbPixels, DesktopBounds nativeDamage) { if ((long)Width * Height > rgbPixels.Length) throw new ArgumentException("The CPU pixel buffer is incomplete.", nameof(rgbPixels)); var damage = PixelGeometry.Intersect(nativeDamage, new(0, 0, Width, Height)); for (int y = damage.Y; y < damage.Y + damage.Height; y++) { byte* output = (byte*)surface.Address + (nuint)y * (nuint)surface.PitchBytes + (nuint)damage.X * (uint)bytesPerPixel; for (int x = damage.X; x < damage.X + damage.Width; x++) { uint rgb = unchecked((uint)rgbPixels[y * Width + x]); uint packed = Channel((byte)(rgb >> 16), surface.RedMask) | Channel((byte)(rgb >> 8), surface.GreenMask) | Channel((byte)rgb, surface.BlueMask); for (int b = 0; b < bytesPerPixel; b++) *output++ = (byte)(packed >> (b * 8)); } } } private static bool ValidMask(uint mask, int bits) { if (mask == 0 || (bits < 32 && (mask >> bits) != 0)) return false; uint aligned = mask >> BitOperations.TrailingZeroCount(mask); return (aligned & (aligned + 1)) == 0; } private static uint Channel(byte value, uint mask) { int shift = BitOperations.TrailingZeroCount(mask); uint range = mask >> shift; return (uint)(((ulong)value * range + 127) / 255) << shift; } } /// Real host-memory scanout for rendering tests and offscreen consumers. public sealed class ArrayFramebufferPresenter : IFramebufferPresenter { private readonly int[] pixels; public int Width { get; } public int Height { get; } public ReadOnlyMemory Pixels => pixels; public long Presentations { get; private set; } public ArrayFramebufferPresenter(int width, int height) { PixelGeometry.ValidateSize(width, height); Width = width; Height = height; pixels = new int[checked(width * height)]; } public void Present(ReadOnlySpan rgbPixels, DesktopBounds nativeDamage) { if (rgbPixels.Length < pixels.Length) throw new ArgumentException("The CPU pixel buffer is incomplete.", nameof(rgbPixels)); var damage = PixelGeometry.Intersect(nativeDamage, new(0, 0, Width, Height)); for (int y = damage.Y; y < damage.Y + damage.Height; y++) rgbPixels.Slice(y * Width + damage.X, damage.Width).CopyTo(pixels.AsSpan(y * Width + damage.X, damage.Width)); Presentations++; } } internal static class PixelGeometry { // 8K fits; reject overflowing/accidental enormous managed allocations. internal const int MaximumPixels = 33_554_432; internal static void ValidateSize(int width, int height) { if (width <= 0 || height <= 0 || (long)width * height > MaximumPixels) throw new ArgumentOutOfRangeException(nameof(width)); } internal static DesktopBounds Intersect(DesktopBounds a, DesktopBounds b) { long x = Math.Max(a.X, b.X), y = Math.Max(a.Y, b.Y); long right = Math.Min((long)a.X + Math.Max(0, a.Width), (long)b.X + Math.Max(0, b.Width)); long bottom = Math.Min((long)a.Y + Math.Max(0, a.Height), (long)b.Y + Math.Max(0, b.Height)); if (right <= x || bottom <= y) return default; return new((int)x, (int)y, (int)(right - x), (int)(bottom - y)); } }