Editor/HumanoidMocap/Video/VideoFrameLayout.cs
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;

namespace HumanoidMocapX.EditorTools.Video;

/// <summary>Visible RGB32 aperture and display orientation, independent of coded padding.
/// Rotation undoes the counter-clockwise content rotation reported by Media Foundation.</summary>
internal sealed class VideoFrameLayout
{
    internal const int MaximumDimension=8192,MaximumPixels=33_177_600;
    const int MaximumRowPadding=4096;
    readonly int bufferWidth,bufferHeight,x,y,width,height,rotation;
    public int Width=>rotation is 90 or 270?height:width;
    public int Height=>rotation is 90 or 270?width:height;

    public VideoFrameLayout(int bufferWidth,int bufferHeight,int x,int y,int width,int height,int rotation)
    {
        ValidateDimensions(bufferWidth,bufferHeight);
        if(x<0||y<0||width<=0||height<=0||(long)x+width>bufferWidth||(long)y+height>bufferHeight)
            throw new InvalidDataException("Video display aperture is outside its decoded frame.");
        if(rotation is not (0 or 90 or 180 or 270))throw new InvalidDataException("Unsupported video rotation metadata.");
        this.bufferWidth=bufferWidth;this.bufferHeight=bufferHeight;this.x=x;this.y=y;
        this.width=width;this.height=height;this.rotation=rotation;
    }

    public byte[] CopyRgba(IntPtr data,int length,int stride,CancellationToken token)
    {
        ValidateStride(bufferWidth,bufferHeight,stride);
        var pitch=Math.Abs((long)stride);
        if(data==IntPtr.Zero||pitch*bufferHeight>length)
            throw new InvalidDataException("Decoded buffer is shorter than its frame dimensions or stride.");
        var result=new byte[checked(Width*Height*4)];
        var row=rotation==0?null:new byte[checked(width*4)];
        for(var sourceY=0;sourceY<height;sourceY++)
        {
            token.ThrowIfCancellationRequested();
            var bufferY=stride<0?bufferHeight-1-y-sourceY:y+sourceY;
            var source=IntPtr.Add(data,checked((int)(bufferY*pitch+x*4L)));
            if(rotation==0)Marshal.Copy(source,result,sourceY*width*4,width*4);
            else
            {
                Marshal.Copy(source,row!,0,width*4);
                for(var sourceX=0;sourceX<width;sourceX++)
                {
                    var destination=rotation switch
                    {
                        90=>(sourceX*Width+height-1-sourceY)*4,
                        180=>((height-1-sourceY)*Width+width-1-sourceX)*4,
                        _=>((width-1-sourceX)*Width+sourceY)*4
                    };
                    var offset=sourceX*4;
                    result[destination]=row![offset];result[destination+1]=row[offset+1];
                    result[destination+2]=row[offset+2];
                }
            }
        }
        for(var i=0;i<result.Length;i+=4){(result[i],result[i+2])=(result[i+2],result[i]);result[i+3]=255;}
        return result;
    }

    internal static void ValidateDimensions(int width,int height)
    {
        if(width<=0||height<=0||width>MaximumDimension||height>MaximumDimension||(long)width*height>MaximumPixels)
            throw new InvalidDataException("Video exceeds the decode limit of 8192 pixels per side or 33,177,600 pixels per frame.");
    }

    internal static void ValidateStride(int width,int height,int stride)
    {
        ValidateDimensions(width,height);
        var pitch=Math.Abs((long)stride);
        // Allow a page of native row alignment, but reject malicious multi-gigabyte pitches.
        if(pitch<width*4L||pitch>width*4L+MaximumRowPadding)
            throw new InvalidDataException("Video row stride is outside the decoded width and alignment limit.");
    }
}