using Ryujinx.Common.Memory;
using Ryujinx.Graphics.Nvdec.Vp9.Common;
using Ryujinx.Graphics.Nvdec.Vp9.Dsp;
using Ryujinx.Graphics.Nvdec.Vp9.Types;
using Ryujinx.Graphics.Video;
using System;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

namespace Ryujinx.Graphics.Nvdec.Vp9
{
    internal static class DecodeFrame
    {
        private static bool ReadIsValid(ArrayPtr<byte> start, int len)
        {
            return len != 0 && len <= start.Length;
        }

        private static void ReadTxModeProbs(ref Vp9EntropyProbs txProbs, ref Reader r)
        {
            Span<Array1<byte>> tx8x8ProbSpan1 = txProbs.Tx8x8Prob.AsSpan();
            Span<Array2<byte>> tx16x16ProbSpan1 = txProbs.Tx16x16Prob.AsSpan();
            Span<Array3<byte>> tx32x32ProbSpan1 = txProbs.Tx32x32Prob.AsSpan();
            
            for (int i = 0; i < EntropyMode.TxSizeContexts; ++i)
            {
                Span<byte> tx8x8ProbSpan2 = tx8x8ProbSpan1[i].AsSpan();
                
                for (int j = 0; j < (int)TxSize.TxSizes - 3; ++j)
                {
                    r.DiffUpdateProb(ref tx8x8ProbSpan2[j]);
                }
            }

            for (int i = 0; i < EntropyMode.TxSizeContexts; ++i)
            {
                Span<byte> tx16x16ProbSpan2 = tx16x16ProbSpan1[i].AsSpan();
                
                for (int j = 0; j < (int)TxSize.TxSizes - 2; ++j)
                {
                    r.DiffUpdateProb(ref tx16x16ProbSpan2[j]);
                }
            }

            for (int i = 0; i < EntropyMode.TxSizeContexts; ++i)
            {
                Span<byte> tx32x32ProbSpan2 = tx32x32ProbSpan1[i].AsSpan();
                
                for (int j = 0; j < (int)TxSize.TxSizes - 1; ++j)
                {
                    r.DiffUpdateProb(ref tx32x32ProbSpan2[j]);
                }
            }
        }

        private static void ReadSwitchableInterpProbs(ref Vp9EntropyProbs fc, ref Reader r)
        {
            Span<Array2<byte>> switchableInterpProbSpan1 = fc.SwitchableInterpProb.AsSpan();
            
            for (int i = 0; i < Constants.SwitchableFilterContexts; ++i)
            {
                Span<byte> switchableInterpProbSpan2 = switchableInterpProbSpan1[i].AsSpan();
                
                for (int j = 0; j < Constants.SwitchableFilters - 1; ++j)
                {
                    r.DiffUpdateProb(ref switchableInterpProbSpan2[j]);
                }
            }
        }

        private static void ReadInterModeProbs(ref Vp9EntropyProbs fc, ref Reader r)
        {
            Span<Array3<byte>> interModeProbSpan1 = fc.InterModeProb.AsSpan();
            
            for (int i = 0; i < Constants.InterModeContexts; ++i)
            {
                Span<byte> interModeProbSpan2 = interModeProbSpan1[i].AsSpan();
                
                for (int j = 0; j < Constants.InterModes - 1; ++j)
                {
                    r.DiffUpdateProb(ref interModeProbSpan2[j]);
                }
            }
        }

        private static void ReadMvProbs(ref Vp9EntropyProbs ctx, bool allowHp, ref Reader r)
        {
            r.UpdateMvProbs(ctx.Joints.AsSpan(), EntropyMv.Joints - 1);

            Span<byte> signSpan = ctx.Sign.AsSpan();
            Span<Array10<byte>> classesSpan = ctx.Classes.AsSpan();
            Span<Array1<byte>> class0Span = ctx.Class0.AsSpan();
            Span<Array10<byte>> bitsSpan = ctx.Bits.AsSpan();

            for (int i = 0; i < 2; ++i)
            {
                r.UpdateMvProbs(MemoryMarshal.CreateSpan(ref signSpan[i], 1), 1);
                r.UpdateMvProbs(classesSpan[i].AsSpan(), EntropyMv.Classes - 1);
                r.UpdateMvProbs(class0Span[i].AsSpan(), EntropyMv.Class0Size - 1);
                r.UpdateMvProbs(bitsSpan[i].AsSpan(), EntropyMv.OffsetBits);
            }
            
            Span<Array2<Array3<byte>>> class0FpSpan1 = ctx.Class0Fp.AsSpan();
            Span<Array3<byte>> fpSpan = ctx.Fp.AsSpan();

            for (int i = 0; i < 2; ++i)
            {
                Span<Array3<byte>> class0FpSpan2 = class0FpSpan1[i].AsSpan();
                
                for (int j = 0; j < EntropyMv.Class0Size; ++j)
                {
                    r.UpdateMvProbs(class0FpSpan2[j].AsSpan(), EntropyMv.FpSize - 1);
                }

                r.UpdateMvProbs(fpSpan[i].AsSpan(), 3);
            }

            if (allowHp)
            {
                Span<byte> class0HpSpan = ctx.Class0Hp.AsSpan();
                Span<byte> hpSpan = ctx.Hp.AsSpan();
                
                for (int i = 0; i < 2; ++i)
                {
                    r.UpdateMvProbs(MemoryMarshal.CreateSpan(ref class0HpSpan[i], 1), 1);
                    r.UpdateMvProbs(MemoryMarshal.CreateSpan(ref hpSpan[i], 1), 1);
                }
            }
        }

        private static void InverseTransformBlockInter(ref MacroBlockD xd, int plane, TxSize txSize, Span<byte> dst,
            int stride, int eob)
        {
            ref MacroBlockDPlane pd = ref xd.Plane[plane];
            ArrayPtr<int> dqcoeff = pd.DqCoeff;
            Debug.Assert(eob > 0);
            if (xd.CurBuf.HighBd)
            {
                Span<ushort> dst16 = MemoryMarshal.Cast<byte, ushort>(dst);
                if (xd.Lossless)
                {
                    Idct.HighbdIwht4x4Add(dqcoeff.AsSpan(), dst16, stride, eob, xd.Bd);
                }
                else
                {
                    switch (txSize)
                    {
                        case TxSize.Tx4x4:
                            Idct.HighbdIdct4x4Add(dqcoeff.AsSpan(), dst16, stride, eob, xd.Bd);
                            break;
                        case TxSize.Tx8x8:
                            Idct.HighbdIdct8x8Add(dqcoeff.AsSpan(), dst16, stride, eob, xd.Bd);
                            break;
                        case TxSize.Tx16x16:
                            Idct.HighbdIdct16x16Add(dqcoeff.AsSpan(), dst16, stride, eob, xd.Bd);
                            break;
                        case TxSize.Tx32x32:
                            Idct.HighbdIdct32x32Add(dqcoeff.AsSpan(), dst16, stride, eob, xd.Bd);
                            break;
                        default:
                            Debug.Assert(false, "Invalid transform size");
                            break;
                    }
                }
            }
            else
            {
                if (xd.Lossless)
                {
                    Idct.Iwht4x4Add(dqcoeff.AsSpan(), dst, stride, eob);
                }
                else
                {
                    switch (txSize)
                    {
                        case TxSize.Tx4x4:
                            Idct.Idct4x4Add(dqcoeff.AsSpan(), dst, stride, eob);
                            break;
                        case TxSize.Tx8x8:
                            Idct.Idct8x8Add(dqcoeff.AsSpan(), dst, stride, eob);
                            break;
                        case TxSize.Tx16x16:
                            Idct.Idct16x16Add(dqcoeff.AsSpan(), dst, stride, eob);
                            break;
                        case TxSize.Tx32x32:
                            Idct.Idct32x32Add(dqcoeff.AsSpan(), dst, stride, eob);
                            break;
                        default:
                            Debug.Assert(false, "Invalid transform size");
                            return;
                    }
                }
            }

            if (eob == 1)
            {
                dqcoeff.AsSpan()[0] = 0;
            }
            else
            {
                if (txSize <= TxSize.Tx16x16 && eob <= 10)
                {
                    dqcoeff.AsSpan().Slice(0, 4 * (4 << (int)txSize)).Clear();
                }
                else if (txSize == TxSize.Tx32x32 && eob <= 34)
                {
                    dqcoeff.AsSpan().Slice(0, 256).Clear();
                }
                else
                {
                    dqcoeff.AsSpan().Slice(0, 16 << ((int)txSize << 1)).Clear();
                }
            }
        }

        private static void InverseTransformBlockIntra(
            ref MacroBlockD xd,
            int plane,
            TxType txType,
            TxSize txSize,
            Span<byte> dst,
            int stride,
            int eob)
        {
            ref MacroBlockDPlane pd = ref xd.Plane[plane];
            ArrayPtr<int> dqcoeff = pd.DqCoeff;
            Debug.Assert(eob > 0);
            if (xd.CurBuf.HighBd)
            {
                Span<ushort> dst16 = MemoryMarshal.Cast<byte, ushort>(dst);
                if (xd.Lossless)
                {
                    Idct.HighbdIwht4x4Add(dqcoeff.AsSpan(), dst16, stride, eob, xd.Bd);
                }
                else
                {
                    switch (txSize)
                    {
                        case TxSize.Tx4x4:
                            Idct.HighbdIht4x4Add(txType, dqcoeff.AsSpan(), dst16, stride, eob, xd.Bd);
                            break;
                        case TxSize.Tx8x8:
                            Idct.HighbdIht8x8Add(txType, dqcoeff.AsSpan(), dst16, stride, eob, xd.Bd);
                            break;
                        case TxSize.Tx16x16:
                            Idct.HighbdIht16x16Add(txType, dqcoeff.AsSpan(), dst16, stride, eob, xd.Bd);
                            break;
                        case TxSize.Tx32x32:
                            Idct.HighbdIdct32x32Add(dqcoeff.AsSpan(), dst16, stride, eob, xd.Bd);
                            break;
                        default:
                            Debug.Assert(false, "Invalid transform size");
                            break;
                    }
                }
            }
            else
            {
                if (xd.Lossless)
                {
                    Idct.Iwht4x4Add(dqcoeff.AsSpan(), dst, stride, eob);
                }
                else
                {
                    switch (txSize)
                    {
                        case TxSize.Tx4x4:
                            Idct.Iht4x4Add(txType, dqcoeff.AsSpan(), dst, stride, eob);
                            break;
                        case TxSize.Tx8x8:
                            Idct.Iht8x8Add(txType, dqcoeff.AsSpan(), dst, stride, eob);
                            break;
                        case TxSize.Tx16x16:
                            Idct.Iht16x16Add(txType, dqcoeff.AsSpan(), dst, stride, eob);
                            break;
                        case TxSize.Tx32x32:
                            Idct.Idct32x32Add(dqcoeff.AsSpan(), dst, stride, eob);
                            break;
                        default:
                            Debug.Assert(false, "Invalid transform size");
                            return;
                    }
                }
            }

            if (eob == 1)
            {
                dqcoeff.AsSpan()[0] = 0;
            }
            else
            {
                if (txType == TxType.DctDct && txSize <= TxSize.Tx16x16 && eob <= 10)
                {
                    dqcoeff.AsSpan().Slice(0, 4 * (4 << (int)txSize)).Clear();
                }
                else if (txSize == TxSize.Tx32x32 && eob <= 34)
                {
                    dqcoeff.AsSpan().Slice(0, 256).Clear();
                }
                else
                {
                    dqcoeff.AsSpan().Slice(0, 16 << ((int)txSize << 1)).Clear();
                }
            }
        }

        private static unsafe void PredictAndReconstructIntraBlock(
            ref TileWorkerData twd,
            ref ModeInfo mi,
            int plane,
            int row,
            int col,
            TxSize txSize)
        {
            ref MacroBlockD xd = ref twd.Xd;
            ref MacroBlockDPlane pd = ref xd.Plane[plane];
            PredictionMode mode = plane == 0 ? mi.Mode : mi.UvMode;
            int dstOffset = (4 * row * pd.Dst.Stride) + (4 * col);
            byte* dst = &pd.Dst.Buf.ToPointer()[dstOffset];
            Span<byte> dstSpan = pd.Dst.Buf.AsSpan().Slice(dstOffset);

            if (mi.SbType < BlockSize.Block8x8)
            {
                if (plane == 0)
                {
                    mode = xd.Mi[0].Value.Bmi[(row << 1) + col].Mode;
                }
            }

            ReconIntra.PredictIntraBlock(ref xd, pd.N4Wl, txSize, mode, dst, pd.Dst.Stride, dst, pd.Dst.Stride, col,
                row, plane);

            if (mi.Skip == 0)
            {
                TxType txType =
                    plane != 0 || xd.Lossless ? TxType.DctDct : ReconIntra.IntraModeToTxTypeLookup[(int)mode];
                Luts.ScanOrder sc = plane != 0 || xd.Lossless
                    ? Luts.DefaultScanOrders[(int)txSize]
                    : Luts.ScanOrders[(int)txSize][(int)txType];
                int eob = Detokenize.DecodeBlockTokens(ref twd, plane, sc, col, row, txSize, mi.SegmentId);
                if (eob > 0)
                {
                    InverseTransformBlockIntra(ref xd, plane, txType, txSize, dstSpan, pd.Dst.Stride, eob);
                }
            }
        }

        private static int ReconstructInterBlock(
            ref TileWorkerData twd,
            ref ModeInfo mi,
            int plane,
            int row,
            int col,
            TxSize txSize)
        {
            ref MacroBlockD xd = ref twd.Xd;
            ref MacroBlockDPlane pd = ref xd.Plane[plane];
            Luts.ScanOrder sc = Luts.DefaultScanOrders[(int)txSize];
            int eob = Detokenize.DecodeBlockTokens(ref twd, plane, sc, col, row, txSize, mi.SegmentId);
            Span<byte> dst = pd.Dst.Buf.AsSpan().Slice((4 * row * pd.Dst.Stride) + (4 * col));

            if (eob > 0)
            {
                InverseTransformBlockInter(ref xd, plane, txSize, dst, pd.Dst.Stride, eob);
            }

            return eob;
        }

        private static unsafe void BuildMcBorder(
            byte* src,
            int srcStride,
            byte* dst,
            int dstStride,
            int x,
            int y,
            int bW,
            int bH,
            int w,
            int h)
        {
            // Get a pointer to the start of the real data for this row.
            byte* refRow = src - x - (y * srcStride);

            if (y >= h)
            {
                refRow += (h - 1) * srcStride;
            }
            else if (y > 0)
            {
                refRow += y * srcStride;
            }

            do
            {
                int right = 0, copy;
                int left = x < 0 ? -x : 0;

                if (left > bW)
                {
                    left = bW;
                }

                if (x + bW > w)
                {
                    right = x + bW - w;
                }

                if (right > bW)
                {
                    right = bW;
                }

                copy = bW - left - right;

                if (left != 0)
                {
                    MemoryUtil.Fill(dst, refRow[0], left);
                }

                if (copy != 0)
                {
                    MemoryUtil.Copy(dst + left, refRow + x + left, copy);
                }

                if (right != 0)
                {
                    MemoryUtil.Fill(dst + left + copy, refRow[w - 1], right);
                }

                dst += dstStride;
                ++y;

                if (y > 0 && y < h)
                {
                    refRow += srcStride;
                }
            } while (--bH != 0);
        }

        private static unsafe void HighBuildMcBorder(
            byte* src8,
            int srcStride,
            ushort* dst,
            int dstStride,
            int x,
            int y,
            int bW,
            int bH,
            int w,
            int h)
        {
            // Get a pointer to the start of the real data for this row.
            ushort* src = (ushort*)src8;
            ushort* refRow = src - x - (y * srcStride);

            if (y >= h)
            {
                refRow += (h - 1) * srcStride;
            }
            else if (y > 0)
            {
                refRow += y * srcStride;
            }

            do
            {
                int right = 0, copy;
                int left = x < 0 ? -x : 0;

                if (left > bW)
                {
                    left = bW;
                }

                if (x + bW > w)
                {
                    right = x + bW - w;
                }

                if (right > bW)
                {
                    right = bW;
                }

                copy = bW - left - right;

                if (left != 0)
                {
                    MemoryUtil.Fill(dst, refRow[0], left);
                }

                if (copy != 0)
                {
                    MemoryUtil.Copy(dst + left, refRow + x + left, copy);
                }

                if (right != 0)
                {
                    MemoryUtil.Fill(dst + left + copy, refRow[w - 1], right);
                }

                dst += dstStride;
                ++y;

                if (y > 0 && y < h)
                {
                    refRow += srcStride;
                }
            } while (--bH != 0);
        }

        [SkipLocalsInit]
        private static unsafe void ExtendAndPredict(
            byte* bufPtr1,
            int preBufStride,
            int x0,
            int y0,
            int bW,
            int bH,
            int frameWidth,
            int frameHeight,
            int borderOffset,
            byte* dst,
            int dstBufStride,
            int subpelX,
            int subpelY,
            Array8<short>[] kernel,
            ref ScaleFactors sf,
            ref MacroBlockD xd,
            int w,
            int h,
            int refr,
            int xs,
            int ys)
        {
            ushort* mcBufHigh = stackalloc ushort[80 * 2 * 80 * 2];
            if (xd.CurBuf.HighBd)
            {
                HighBuildMcBorder(bufPtr1, preBufStride, mcBufHigh, bW, x0, y0, bW, bH, frameWidth, frameHeight);
                ReconInter.HighbdInterPredictor(
                    mcBufHigh + borderOffset,
                    bW,
                    (ushort*)dst,
                    dstBufStride,
                    subpelX,
                    subpelY,
                    ref sf,
                    w,
                    h,
                    refr,
                    kernel,
                    xs,
                    ys,
                    xd.Bd);
            }
            else
            {
                BuildMcBorder(bufPtr1, preBufStride, (byte*)mcBufHigh, bW, x0, y0, bW, bH, frameWidth, frameHeight);
                ReconInter.InterPredictor(
                    (byte*)mcBufHigh + borderOffset,
                    bW,
                    dst,
                    dstBufStride,
                    subpelX,
                    subpelY,
                    ref sf,
                    w,
                    h,
                    refr,
                    kernel,
                    xs,
                    ys);
            }
        }

        private static unsafe void DecBuildInterPredictors(
            ref MacroBlockD xd,
            int plane,
            int bw,
            int bh,
            int x,
            int y,
            int w,
            int h,
            int miX,
            int miY,
            Array8<short>[] kernel,
            ref ScaleFactors sf,
            ref Buf2D preBuf,
            ref Buf2D dstBuf,
            ref Mv mv,
            ref Surface refFrameBuf,
            bool isScaled,
            int refr)
        {
            ref MacroBlockDPlane pd = ref xd.Plane[plane];
            byte* dst = dstBuf.Buf.ToPointer() + (dstBuf.Stride * y) + x;
            Mv32 scaledMv;
            int xs, ys, x0, y0, x016, y016, frameWidth, frameHeight, bufStride, subpelX, subpelY;
            byte* refFrame;
            byte* bufPtr;

            // Get reference frame pointer, width and height.
            if (plane == 0)
            {
                frameWidth = refFrameBuf.Width;
                frameHeight = refFrameBuf.Height;
                refFrame = refFrameBuf.YBuffer.ToPointer();
            }
            else
            {
                frameWidth = refFrameBuf.UvWidth;
                frameHeight = refFrameBuf.UvHeight;
                refFrame = plane == 1 ? refFrameBuf.UBuffer.ToPointer() : refFrameBuf.VBuffer.ToPointer();
            }

            if (isScaled)
            {
                Mv mvQ4 = ReconInter.ClampMvToUmvBorderSb(ref xd, ref mv, bw, bh, pd.SubsamplingX, pd.SubsamplingY);
                // Co-ordinate of containing block to pixel precision.
                int xStart = -xd.MbToLeftEdge >> (3 + pd.SubsamplingX);
                int yStart = -xd.MbToTopEdge >> (3 + pd.SubsamplingY);
                // Co-ordinate of the block to 1/16th pixel precision.
                x016 = (xStart + x) << Filter.SubpelBits;
                y016 = (yStart + y) << Filter.SubpelBits;

                // Co-ordinate of current block in reference frame
                // to 1/16th pixel precision.
                x016 = sf.ScaleValueX(x016);
                y016 = sf.ScaleValueY(y016);

                // Map the top left corner of the block into the reference frame.
                x0 = sf.ScaleValueX(xStart + x);
                y0 = sf.ScaleValueY(yStart + y);

                // Scale the MV and incorporate the sub-pixel offset of the block
                // in the reference frame.
                scaledMv = sf.ScaleMv(ref mvQ4, miX + x, miY + y);
                xs = sf.XStepQ4;
                ys = sf.YStepQ4;
            }
            else
            {
                // Co-ordinate of containing block to pixel precision.
                x0 = (-xd.MbToLeftEdge >> (3 + pd.SubsamplingX)) + x;
                y0 = (-xd.MbToTopEdge >> (3 + pd.SubsamplingY)) + y;

                // Co-ordinate of the block to 1/16th pixel precision.
                x016 = x0 << Filter.SubpelBits;
                y016 = y0 << Filter.SubpelBits;

                scaledMv.Row = mv.Row * (1 << (1 - pd.SubsamplingY));
                scaledMv.Col = mv.Col * (1 << (1 - pd.SubsamplingX));
                xs = ys = 16;
            }

            subpelX = scaledMv.Col & Filter.SubpelMask;
            subpelY = scaledMv.Row & Filter.SubpelMask;

            // Calculate the top left corner of the best matching block in the
            // reference frame.
            x0 += scaledMv.Col >> Filter.SubpelBits;
            y0 += scaledMv.Row >> Filter.SubpelBits;
            x016 += scaledMv.Col;
            y016 += scaledMv.Row;

            // Get reference block pointer.
            bufPtr = refFrame + (y0 * preBuf.Stride) + x0;
            bufStride = preBuf.Stride;

            // Do border extension if there is motion or the
            // width/height is not a multiple of 8 pixels.
            if (isScaled || scaledMv.Col != 0 || scaledMv.Row != 0 || (frameWidth & 0x7) != 0 ||
                (frameHeight & 0x7) != 0)
            {
                int y1 = ((y016 + ((h - 1) * ys)) >> Filter.SubpelBits) + 1;

                // Get reference block bottom right horizontal coordinate.
                int x1 = ((x016 + ((w - 1) * xs)) >> Filter.SubpelBits) + 1;
                int xPad = 0, yPad = 0;

                if (subpelX != 0 || sf.XStepQ4 != Filter.SubpelShifts)
                {
                    x0 -= Constants.InterpExtend - 1;
                    x1 += Constants.InterpExtend;
                    xPad = 1;
                }

                if (subpelY != 0 || sf.YStepQ4 != Filter.SubpelShifts)
                {
                    y0 -= Constants.InterpExtend - 1;
                    y1 += Constants.InterpExtend;
                    yPad = 1;
                }

                // Skip border extension if block is inside the frame.
                if (x0 < 0 || x0 > frameWidth - 1 || x1 < 0 || x1 > frameWidth - 1 ||
                    y0 < 0 || y0 > frameHeight - 1 || y1 < 0 || y1 > frameHeight - 1)
                {
                    // Extend the border.
                    byte* bufPtr1 = refFrame + (y0 * bufStride) + x0;
                    int bW = x1 - x0 + 1;
                    int bH = y1 - y0 + 1;
                    int borderOffset = (yPad * 3 * bW) + (xPad * 3);

                    ExtendAndPredict(
                        bufPtr1,
                        bufStride,
                        x0,
                        y0,
                        bW,
                        bH,
                        frameWidth,
                        frameHeight,
                        borderOffset,
                        dst,
                        dstBuf.Stride,
                        subpelX,
                        subpelY,
                        kernel,
                        ref sf,
                        ref xd,
                        w,
                        h,
                        refr,
                        xs,
                        ys);
                    return;
                }
            }

            if (xd.CurBuf.HighBd)
            {
                ReconInter.HighbdInterPredictor(
                    (ushort*)bufPtr,
                    bufStride,
                    (ushort*)dst,
                    dstBuf.Stride,
                    subpelX,
                    subpelY,
                    ref sf,
                    w,
                    h,
                    refr,
                    kernel,
                    xs,
                    ys,
                    xd.Bd);
            }
            else
            {
                ReconInter.InterPredictor(
                    bufPtr,
                    bufStride,
                    dst,
                    dstBuf.Stride,
                    subpelX,
                    subpelY,
                    ref sf,
                    w,
                    h,
                    refr,
                    kernel,
                    xs,
                    ys);
            }
        }

        private static void DecBuildInterPredictorsSb(ref Vp9Common cm, ref MacroBlockD xd, int miRow, int miCol)
        {
            int plane;
            int miX = miCol * Constants.MiSize;
            int miY = miRow * Constants.MiSize;
            ref ModeInfo mi = ref xd.Mi[0].Value;
            Array8<short>[] kernel = Luts.FilterKernels[mi.InterpFilter];
            BlockSize sbType = mi.SbType;
            int isCompound = mi.HasSecondRef() ? 1 : 0;
            int refr;
            bool isScaled;

            Span<sbyte> refFrameSpan = mi.RefFrame.AsSpan();
            Span<Mv> mvSpan = mi.Mv.AsSpan();
            Span<RefBuffer> frameRefsSpan = cm.FrameRefs.AsSpan();
            Span<Ptr<RefBuffer>> blockRefsSpan = xd.BlockRefs.AsSpan();
            Span<MacroBlockDPlane> planeSpan = xd.Plane.AsSpan();

            for (refr = 0; refr < 1 + isCompound; ++refr)
            {
                int frame = refFrameSpan[refr];
                ref RefBuffer refBuf = ref frameRefsSpan[frame - Constants.LastFrame];
                ref ScaleFactors sf = ref refBuf.Sf;
                ref Surface refFrameBuf = ref refBuf.Buf;

                if (!sf.IsValidScale())
                {
                    xd.ErrorInfo.Value.InternalError(CodecErr.UnsupBitstream,
                        "Reference frame has invalid dimensions");
                }

                isScaled = sf.IsScaled();
                ReconInter.SetupPrePlanes(ref xd, refr, ref refFrameBuf, miRow, miCol,
                    isScaled ? new Ptr<ScaleFactors>(ref sf) : Ptr<ScaleFactors>.Null);
                blockRefsSpan[refr] = new Ptr<RefBuffer>(ref refBuf);

                if (sbType < BlockSize.Block8x8)
                {
                    for (plane = 0; plane < Constants.MaxMbPlane; ++plane)
                    {
                        ref MacroBlockDPlane pd = ref planeSpan[plane];
                        ref Buf2D dstBuf = ref pd.Dst;
                        int num4x4W = pd.N4W;
                        int num4x4H = pd.N4H;
                        int n4Wx4 = 4 * num4x4W;
                        int n4Hx4 = 4 * num4x4H;
                        ref Buf2D preBuf = ref pd.Pre[refr];
                        int i = 0;
                        for (int y = 0; y < num4x4H; ++y)
                        {
                            for (int x = 0; x < num4x4W; ++x)
                            {
                                Mv mv = ReconInter.AverageSplitMvs(ref pd, ref mi, refr, i++);
                                DecBuildInterPredictors(
                                    ref xd,
                                    plane,
                                    n4Wx4,
                                    n4Hx4,
                                    4 * x,
                                    4 * y,
                                    4,
                                    4,
                                    miX,
                                    miY,
                                    kernel,
                                    ref sf,
                                    ref preBuf,
                                    ref dstBuf,
                                    ref mv,
                                    ref refFrameBuf,
                                    isScaled,
                                    refr);
                            }
                        }
                    }
                }
                else
                {
                    Mv mv = mvSpan[refr];
                    for (plane = 0; plane < Constants.MaxMbPlane; ++plane)
                    {
                        ref MacroBlockDPlane pd = ref planeSpan[plane];
                        ref Buf2D dstBuf = ref pd.Dst;
                        int num4x4W = pd.N4W;
                        int num4x4H = pd.N4H;
                        int n4Wx4 = 4 * num4x4W;
                        int n4Hx4 = 4 * num4x4H;
                        ref Buf2D preBuf = ref pd.Pre[refr];
                        DecBuildInterPredictors(
                            ref xd,
                            plane,
                            n4Wx4,
                            n4Hx4,
                            0,
                            0,
                            n4Wx4,
                            n4Hx4,
                            miX,
                            miY,
                            kernel,
                            ref sf,
                            ref preBuf,
                            ref dstBuf,
                            ref mv,
                            ref refFrameBuf,
                            isScaled,
                            refr);
                    }
                }
            }
        }

        private static void SetPlaneN4(ref MacroBlockD xd, int bw, int bh, int bwl, int bhl)
        {
            Span<MacroBlockDPlane> planeSpan = xd.Plane.AsSpan();
            
            for (int i = 0; i < Constants.MaxMbPlane; i++)
            {
                planeSpan[i].N4W = (ushort)((bw << 1) >> planeSpan[i].SubsamplingX);
                planeSpan[i].N4H = (ushort)((bh << 1) >> planeSpan[i].SubsamplingY);
                planeSpan[i].N4Wl = (byte)(bwl - planeSpan[i].SubsamplingX);
                planeSpan[i].N4Hl = (byte)(bhl - planeSpan[i].SubsamplingY);
            }
        }

        private static ref ModeInfo SetOffsets(
            ref Vp9Common cm,
            ref MacroBlockD xd,
            BlockSize bsize,
            int miRow,
            int miCol,
            int bw,
            int bh,
            int xMis,
            int yMis,
            int bwl,
            int bhl)
        {
            int offset = (miRow * cm.MiStride) + miCol;

            ref TileInfo tile = ref xd.Tile;

            xd.Mi = cm.MiGridVisible.Slice(offset);
            xd.Mi[0] = new Ptr<ModeInfo>(ref cm.Mi[offset]);
            xd.Mi[0].Value.SbType = bsize;
            for (int y = 0; y < yMis; ++y)
            {
                for (int x = y == 0 ? 1 : 0; x < xMis; ++x)
                {
                    xd.Mi[(y * cm.MiStride) + x] = xd.Mi[0];
                }
            }

            SetPlaneN4(ref xd, bw, bh, bwl, bhl);

            xd.SetSkipContext(miRow, miCol);

            // Distance of Mb to the various image edges. These are specified to 8th pel
            // as they are always compared to values that are in 1/8th pel units
            xd.SetMiRowCol(ref tile, miRow, bh, miCol, bw, cm.MiRows, cm.MiCols);

            ReconInter.SetupDstPlanes(xd.Plane.AsSpan(), ref xd.CurBuf, miRow, miCol);
            return ref xd.Mi[0].Value;
        }

        private static void DecodeBlock(
            ref TileWorkerData twd,
            ref Vp9Common cm,
            int miRow,
            int miCol,
            BlockSize bsize,
            int bwl,
            int bhl)
        {
            bool less8x8 = bsize < BlockSize.Block8x8;
            int bw = 1 << (bwl - 1);
            int bh = 1 << (bhl - 1);
            int xMis = Math.Min(bw, cm.MiCols - miCol);
            int yMis = Math.Min(bh, cm.MiRows - miRow);
            ref Reader r = ref twd.BitReader;
            ref MacroBlockD xd = ref twd.Xd;

            ref ModeInfo mi = ref SetOffsets(ref cm, ref xd, bsize, miRow, miCol, bw, bh, xMis, yMis, bwl, bhl);

            if (bsize >= BlockSize.Block8x8 && (cm.SubsamplingX != 0 || cm.SubsamplingY != 0))
            {
                BlockSize uvSubsize = Luts.SsSizeLookup[(int)bsize][cm.SubsamplingX][cm.SubsamplingY];
                if (uvSubsize == BlockSize.BlockInvalid)
                {
                    xd.ErrorInfo.Value.InternalError(CodecErr.CorruptFrame, "Invalid block size.");
                }
            }

            DecodeMv.ReadModeInfo(ref twd, ref cm, miRow, miCol, xMis, yMis);

            if (mi.Skip != 0)
            {
                xd.DecResetSkipContext();
            }

            if (!mi.IsInterBlock())
            {
                Span<MacroBlockDPlane> planeSpan = xd.Plane.AsSpan();
                
                for (int plane = 0; plane < Constants.MaxMbPlane; ++plane)
                {
                    ref MacroBlockDPlane pd = ref planeSpan[plane];
                    TxSize txSize = plane != 0 ? mi.GetUvTxSize(ref pd) : mi.TxSize;
                    int num4x4W = pd.N4W;
                    int num4x4H = pd.N4H;
                    int step = 1 << (int)txSize;
                    int row, col;
                    int maxBlocksWide =
                        num4x4W + (xd.MbToRightEdge >= 0 ? 0 : xd.MbToRightEdge >> (5 + pd.SubsamplingX));
                    int maxBlocksHigh =
                        num4x4H + (xd.MbToBottomEdge >= 0 ? 0 : xd.MbToBottomEdge >> (5 + pd.SubsamplingY));

                    xd.MaxBlocksWide = (uint)(xd.MbToRightEdge >= 0 ? 0 : maxBlocksWide);
                    xd.MaxBlocksHigh = (uint)(xd.MbToBottomEdge >= 0 ? 0 : maxBlocksHigh);

                    for (row = 0; row < maxBlocksHigh; row += step)
                    {
                        for (col = 0; col < maxBlocksWide; col += step)
                        {
                            PredictAndReconstructIntraBlock(ref twd, ref mi, plane, row, col, txSize);
                        }
                    }
                }
            }
            else
            {
                // Prediction
                DecBuildInterPredictorsSb(ref cm, ref xd, miRow, miCol);

                // Reconstruction
                if (mi.Skip == 0)
                {
                    Span<MacroBlockDPlane> planeSpan = xd.Plane.AsSpan();
                    
                    int eobtotal = 0;

                    for (int plane = 0; plane < Constants.MaxMbPlane; ++plane)
                    {
                        ref MacroBlockDPlane pd = ref planeSpan[plane];
                        TxSize txSize = plane != 0 ? mi.GetUvTxSize(ref pd) : mi.TxSize;
                        int num4x4W = pd.N4W;
                        int num4x4H = pd.N4H;
                        int step = 1 << (int)txSize;
                        int row, col;
                        int maxBlocksWide =
                            num4x4W + (xd.MbToRightEdge >= 0 ? 0 : xd.MbToRightEdge >> (5 + pd.SubsamplingX));
                        int maxBlocksHigh = num4x4H +
                                            (xd.MbToBottomEdge >= 0 ? 0 : xd.MbToBottomEdge >> (5 + pd.SubsamplingY));

                        xd.MaxBlocksWide = (uint)(xd.MbToRightEdge >= 0 ? 0 : maxBlocksWide);
                        xd.MaxBlocksHigh = (uint)(xd.MbToBottomEdge >= 0 ? 0 : maxBlocksHigh);

                        for (row = 0; row < maxBlocksHigh; row += step)
                        {
                            for (col = 0; col < maxBlocksWide; col += step)
                            {
                                eobtotal += ReconstructInterBlock(ref twd, ref mi, plane, row, col, txSize);
                            }
                        }
                    }

                    if (!less8x8 && eobtotal == 0)
                    {
                        mi.Skip = 1; // Skip loopfilter
                    }
                }
            }

            xd.Corrupted |= r.HasError();

            if (cm.Lf.FilterLevel != 0)
            {
                LoopFilter.BuildMask(ref cm, ref mi, miRow, miCol, bw, bh);
            }
        }

        private static void DecUpdatePartitionContext(
            ref TileWorkerData twd,
            int miRow,
            int miCol,
            BlockSize subsize,
            int bw)
        {
            Span<sbyte> aboveCtx = twd.Xd.AboveSegContext.Slice(miCol).AsSpan();
            Span<sbyte> leftCtx = MemoryMarshal.CreateSpan(ref twd.Xd.LeftSegContext[miRow & Constants.MiMask],
                8 - (miRow & Constants.MiMask));

            // Update the partition context at the end notes. Set partition bits
            // of block sizes larger than the current one to be one, and partition
            // bits of smaller block sizes to be zero.
            aboveCtx.Slice(0, bw).Fill(Luts.PartitionContextLookup[(int)subsize].Above);
            leftCtx.Slice(0, bw).Fill(Luts.PartitionContextLookup[(int)subsize].Left);
        }

        private static PartitionType ReadPartition(
            ref TileWorkerData twd,
            int miRow,
            int miCol,
            int hasRows,
            int hasCols,
            int bsl)
        {
            int ctx = twd.DecPartitionPlaneContext(miRow, miCol, bsl);
            ReadOnlySpan<byte> probs = MemoryMarshal.CreateReadOnlySpan(ref twd.Xd.PartitionProbs[ctx][0], 3);
            PartitionType p;
            ref Reader r = ref twd.BitReader;

            if (hasRows != 0 && hasCols != 0)
            {
                p = (PartitionType)r.ReadTree(Luts.PartitionTree, probs);
            }
            else if (hasRows == 0 && hasCols != 0)
            {
                p = r.Read(probs[1]) != 0 ? PartitionType.PartitionSplit : PartitionType.PartitionHorz;
            }
            else if (hasRows != 0 && hasCols == 0)
            {
                p = r.Read(probs[2]) != 0 ? PartitionType.PartitionSplit : PartitionType.PartitionVert;
            }
            else
            {
                p = PartitionType.PartitionSplit;
            }

            if (!twd.Xd.Counts.IsNull)
            {
                ++twd.Xd.Counts.Value.Partition[ctx][(int)p];
            }

            return p;
        }

        private static void DecodePartition(
            ref TileWorkerData twd,
            ref Vp9Common cm,
            int miRow,
            int miCol,
            BlockSize bsize,
            int n4x4L2)
        {
            int n8x8L2 = n4x4L2 - 1;
            int num8x8Wh = 1 << n8x8L2;
            int hbs = num8x8Wh >> 1;
            PartitionType partition;
            BlockSize subsize;
            bool hasRows = miRow + hbs < cm.MiRows;
            bool hasCols = miCol + hbs < cm.MiCols;
            ref MacroBlockD xd = ref twd.Xd;

            if (miRow >= cm.MiRows || miCol >= cm.MiCols)
            {
                return;
            }

            partition = ReadPartition(ref twd, miRow, miCol, hasRows ? 1 : 0, hasCols ? 1 : 0, n8x8L2);
            subsize = Luts.SubsizeLookup[(int)partition][(int)bsize];
            if (hbs == 0)
            {
                // Calculate bmode block dimensions (log 2)
                xd.BmodeBlocksWl = (byte)(1 >> ((partition & PartitionType.PartitionVert) != 0 ? 1 : 0));
                xd.BmodeBlocksHl = (byte)(1 >> ((partition & PartitionType.PartitionHorz) != 0 ? 1 : 0));
                DecodeBlock(ref twd, ref cm, miRow, miCol, subsize, 1, 1);
            }
            else
            {
                switch (partition)
                {
                    case PartitionType.PartitionNone:
                        DecodeBlock(ref twd, ref cm, miRow, miCol, subsize, n4x4L2, n4x4L2);
                        break;
                    case PartitionType.PartitionHorz:
                        DecodeBlock(ref twd, ref cm, miRow, miCol, subsize, n4x4L2, n8x8L2);
                        if (hasRows)
                        {
                            DecodeBlock(ref twd, ref cm, miRow + hbs, miCol, subsize, n4x4L2, n8x8L2);
                        }

                        break;
                    case PartitionType.PartitionVert:
                        DecodeBlock(ref twd, ref cm, miRow, miCol, subsize, n8x8L2, n4x4L2);
                        if (hasCols)
                        {
                            DecodeBlock(ref twd, ref cm, miRow, miCol + hbs, subsize, n8x8L2, n4x4L2);
                        }

                        break;
                    case PartitionType.PartitionSplit:
                        DecodePartition(ref twd, ref cm, miRow, miCol, subsize, n8x8L2);
                        DecodePartition(ref twd, ref cm, miRow, miCol + hbs, subsize, n8x8L2);
                        DecodePartition(ref twd, ref cm, miRow + hbs, miCol, subsize, n8x8L2);
                        DecodePartition(ref twd, ref cm, miRow + hbs, miCol + hbs, subsize, n8x8L2);
                        break;
                    default:
                        Debug.Assert(false, "Invalid partition type");
                        break;
                }
            }

            // Update partition context
            if (bsize >= BlockSize.Block8x8 &&
                (bsize == BlockSize.Block8x8 || partition != PartitionType.PartitionSplit))
            {
                DecUpdatePartitionContext(ref twd, miRow, miCol, subsize, num8x8Wh);
            }
        }

        private static void SetupTokenDecoder(
            ArrayPtr<byte> data,
            int readSize,
            ref InternalErrorInfo errorInfo,
            ref Reader r)
        {
            // Validate the calculated partition length. If the buffer described by the
            // partition can't be fully read then throw an error.
            if (!ReadIsValid(data, readSize))
            {
                errorInfo.InternalError(CodecErr.CorruptFrame, "Truncated packet or corrupt tile length");
            }

            if (r.Init(data, readSize))
            {
                errorInfo.InternalError(CodecErr.MemError, "Failed to allocate bool decoder 1");
            }
        }

        private static void ReadCoefProbsCommon(ReadOnlySpan<Array2<Array6<Array6<Array3<byte>>>>> coefProbs1,
            ref Reader r, int txSize)
        {
            if (r.ReadBit() != 0)
            {
                for (int i = 0; i < Constants.PlaneTypes; ++i)
                {
                    Span<Array6<Array6<Array3<byte>>>> coefProbs2 = coefProbs1[i].AsSpan(); 
                    
                    for (int j = 0; j < Entropy.RefTypes; ++j)
                    {
                        Span<Array6<Array3<byte>>> coefProbs3 = coefProbs2[j].AsSpan(); 
                        
                        for (int k = 0; k < Entropy.CoefBands; ++k)
                        {
                            Span<Array3<byte>> coefProbs4 = coefProbs3[k].AsSpan();
                            
                            for (int l = 0; l < Entropy.BAND_COEFF_CONTEXTS(k); ++l)
                            {
                                Span<byte> coefProbs5 = coefProbs4[l].AsSpan();
                                
                                for (int m = 0; m < Entropy.UnconstrainedNodes; ++m)
                                {
                                    r.DiffUpdateProb(ref coefProbs5[m]);
                                }
                            }
                        }
                    }
                }
            }
        }

        private static void ReadCoefProbs(ref Vp9EntropyProbs fc, TxMode txMode, ref Reader r)
        {
            Span<Array2<Array2<Array6<Array6<Array3<byte>>>>>> coefProbsSpan = fc.CoefProbs.AsSpan();
            
            int maxTxSize = (int)Luts.TxModeToBiggestTxSize[(int)txMode];
            
            for (int txSize = (int)TxSize.Tx4x4; txSize <= maxTxSize; ++txSize)
            {
                ReadCoefProbsCommon(coefProbsSpan[txSize].AsSpan(), ref r, txSize);
            }
        }

        private static void SetupLoopfilter(ref Types.LoopFilter lf, ref ReadBitBuffer rb)
        {
            lf.FilterLevel = rb.ReadLiteral(6);
            lf.SharpnessLevel = rb.ReadLiteral(3);

            // Read in loop filter deltas applied at the MB level based on mode or ref
            // frame.
            lf.ModeRefDeltaUpdate = false;

            lf.ModeRefDeltaEnabled = rb.ReadBit() != 0;
            if (lf.ModeRefDeltaEnabled)
            {
                lf.ModeRefDeltaUpdate = rb.ReadBit() != 0;
                if (lf.ModeRefDeltaUpdate)
                {
                    Span<sbyte> refDeltasSpan = lf.RefDeltas.AsSpan();
                    Span<sbyte> modeDeltasSpan = lf.ModeDeltas.AsSpan();
                    
                    for (int i = 0; i < LoopFilter.MaxRefLfDeltas; i++)
                    {
                        if (rb.ReadBit() != 0)
                        {
                            refDeltasSpan[i] = (sbyte)rb.ReadSignedLiteral(6);
                        }
                    }

                    for (int i = 0; i < LoopFilter.MaxModeLfDeltas; i++)
                    {
                        if (rb.ReadBit() != 0)
                        {
                            modeDeltasSpan[i] = (sbyte)rb.ReadSignedLiteral(6);
                        }
                    }
                }
            }
        }

        private static void SetupQuantization(ref Vp9Common cm, ref MacroBlockD xd, ref ReadBitBuffer rb)
        {
            cm.BaseQindex = rb.ReadLiteral(QuantCommon.QindexBits);
            cm.YDcDeltaQ = rb.ReadDeltaQ();
            cm.UvDcDeltaQ = rb.ReadDeltaQ();
            cm.UvAcDeltaQ = rb.ReadDeltaQ();
            cm.DequantBitDepth = cm.BitDepth;
            xd.Lossless = cm.BaseQindex == 0 && cm.YDcDeltaQ == 0 && cm.UvDcDeltaQ == 0 && cm.UvAcDeltaQ == 0;

            xd.Bd = (int)cm.BitDepth;
        }

        private static readonly byte[] LiteralToFilter =
        [
            Constants.EightTapSmooth, Constants.EightTap, Constants.EightTapSharp, Constants.Bilinear
        ];

        private static byte ReadInterpFilter(ref ReadBitBuffer rb)
        {
            return rb.ReadBit() != 0
                ? (byte)Constants.Switchable
                : LiteralToFilter[rb.ReadLiteral(2)];
        }

        private static void SetupRenderSize(ref Vp9Common cm, ref ReadBitBuffer rb)
        {
            cm.RenderWidth = cm.Width;
            cm.RenderHeight = cm.Height;
            if (rb.ReadBit() != 0)
            {
                rb.ReadFrameSize(out cm.RenderWidth, out cm.RenderHeight);
            }
        }

        private static void SetupFrameSize(MemoryAllocator allocator, ref Vp9Common cm, ref ReadBitBuffer rb)
        {
            int width = 0, height = 0;
            ref BufferPool pool = ref cm.BufferPool.Value;
            rb.ReadFrameSize(out width, out height);
            cm.ResizeContextBuffers(allocator, width, height);
            SetupRenderSize(ref cm, ref rb);

            Span<RefCntBuffer> frameBuffsSpan = pool.FrameBufs.AsSpan();

            if (cm.GetFrameNewBuffer().ReallocFrameBuffer(
                    allocator,
                    cm.Width,
                    cm.Height,
                    cm.SubsamplingX,
                    cm.SubsamplingY,
                    cm.UseHighBitDepth,
                    Surface.DecBorderInPixels,
                    cm.ByteAlignment,
                    new Ptr<VpxCodecFrameBuffer>(ref frameBuffsSpan[cm.NewFbIdx].RawFrameBuffer),
                    FrameBuffers.GetFrameBuffer,
                    pool.CbPriv) != 0)
            {
                cm.Error.InternalError(CodecErr.MemError, "Failed to allocate frame buffer");
            }

            frameBuffsSpan[cm.NewFbIdx].Released = 0;
            frameBuffsSpan[cm.NewFbIdx].Buf.SubsamplingX = cm.SubsamplingX;
            frameBuffsSpan[cm.NewFbIdx].Buf.SubsamplingY = cm.SubsamplingY;
            frameBuffsSpan[cm.NewFbIdx].Buf.BitDepth = (uint)cm.BitDepth;
            frameBuffsSpan[cm.NewFbIdx].Buf.ColorSpace = cm.ColorSpace;
            frameBuffsSpan[cm.NewFbIdx].Buf.ColorRange = cm.ColorRange;
            frameBuffsSpan[cm.NewFbIdx].Buf.RenderWidth = cm.RenderWidth;
            frameBuffsSpan[cm.NewFbIdx].Buf.RenderHeight = cm.RenderHeight;
        }

        private static bool ValidRefFrameImgFmt(
            BitDepth refBitDepth,
            int refXss, int refYss,
            BitDepth thisBitDepth,
            int thisXss,
            int thisYss)
        {
            return refBitDepth == thisBitDepth && refXss == thisXss && refYss == thisYss;
        }

        private static void SetupFrameSizeWithRefs(MemoryAllocator allocator, ref Vp9Common cm,
            ref ReadBitBuffer rb)
        {
            int width = 0, height = 0;
            bool found = false;

            bool hasValidRefFrame = false;
            ref BufferPool pool = ref cm.BufferPool.Value;
            Span<RefBuffer> frameRefsSpan = cm.FrameRefs.AsSpan();
            
            for (int i = 0; i < Constants.RefsPerFrame; ++i)
            {
                if (rb.ReadBit() != 0)
                {
                    if (frameRefsSpan[i].Idx != RefBuffer.InvalidIdx)
                    {
                        ref Surface buf = ref frameRefsSpan[i].Buf;
                        width = buf.YCropWidth;
                        height = buf.YCropHeight;
                        found = true;
                        break;
                    }

                    cm.Error.InternalError(CodecErr.CorruptFrame, "Failed to decode frame size");
                }
            }

            if (!found)
            {
                rb.ReadFrameSize(out width, out height);
            }

            if (width <= 0 || height <= 0)
            {
                cm.Error.InternalError(CodecErr.CorruptFrame, "Invalid frame size");
            }

            // Check to make sure at least one of frames that this frame references
            // has valid dimensions.
            for (int i = 0; i < Constants.RefsPerFrame; ++i)
            {
                ref RefBuffer refFrame = ref frameRefsSpan[i];
                hasValidRefFrame |=
                    refFrame.Idx != RefBuffer.InvalidIdx &&
                    ScaleFactors.ValidRefFrameSize(refFrame.Buf.YCropWidth, refFrame.Buf.YCropHeight, width,
                        height);
            }

            if (!hasValidRefFrame)
            {
                cm.Error.InternalError(CodecErr.CorruptFrame, "Referenced frame has invalid size");
            }

            for (int i = 0; i < Constants.RefsPerFrame; ++i)
            {
                ref RefBuffer refFrame = ref frameRefsSpan[i];
                if (refFrame.Idx == RefBuffer.InvalidIdx ||
                    !ValidRefFrameImgFmt(
                        (BitDepth)refFrame.Buf.BitDepth,
                        refFrame.Buf.SubsamplingX,
                        refFrame.Buf.SubsamplingY,
                        cm.BitDepth,
                        cm.SubsamplingX,
                        cm.SubsamplingY))
                {
                    cm.Error.InternalError(CodecErr.CorruptFrame,
                        "Referenced frame has incompatible color format");
                }
            }

            cm.ResizeContextBuffers(allocator, width, height);
            SetupRenderSize(ref cm, ref rb);
            
            Span<RefCntBuffer> frameBuffsSpan = pool.FrameBufs.AsSpan();

            if (cm.GetFrameNewBuffer().ReallocFrameBuffer(
                    allocator,
                    cm.Width,
                    cm.Height,
                    cm.SubsamplingX,
                    cm.SubsamplingY,
                    cm.UseHighBitDepth,
                    Surface.DecBorderInPixels,
                    cm.ByteAlignment,
                    new Ptr<VpxCodecFrameBuffer>(ref frameBuffsSpan[cm.NewFbIdx].RawFrameBuffer),
                    FrameBuffers.GetFrameBuffer,
                    pool.CbPriv) != 0)
            {
                cm.Error.InternalError(CodecErr.MemError, "Failed to allocate frame buffer");
            }

            frameBuffsSpan[cm.NewFbIdx].Released = 0;
            frameBuffsSpan[cm.NewFbIdx].Buf.SubsamplingX = cm.SubsamplingX;
            frameBuffsSpan[cm.NewFbIdx].Buf.SubsamplingY = cm.SubsamplingY;
            frameBuffsSpan[cm.NewFbIdx].Buf.BitDepth = (uint)cm.BitDepth;
            frameBuffsSpan[cm.NewFbIdx].Buf.ColorSpace = cm.ColorSpace;
            frameBuffsSpan[cm.NewFbIdx].Buf.ColorRange = cm.ColorRange;
            frameBuffsSpan[cm.NewFbIdx].Buf.RenderWidth = cm.RenderWidth;
            frameBuffsSpan[cm.NewFbIdx].Buf.RenderHeight = cm.RenderHeight;
        }

        // Reads the next tile returning its size and adjusting '*data' accordingly
        // based on 'isLast'.
        private static void GetTileBuffer(
            bool isLast,
            ref InternalErrorInfo errorInfo,
            ref ArrayPtr<byte> data,
            ref TileBuffer buf)
        {
            int size;

            if (!isLast)
            {
                if (!ReadIsValid(data, 4))
                {
                    errorInfo.InternalError(CodecErr.CorruptFrame, "Truncated packet or corrupt tile length");
                }

                size = BinaryPrimitives.ReadInt32BigEndian(data.AsSpan());
                data = data.Slice(4);

                if (size > data.Length)
                {
                    errorInfo.InternalError(CodecErr.CorruptFrame, "Truncated packet or corrupt tile size");
                }
            }
            else
            {
                size = data.Length;
            }

            buf.Data = data;
            buf.Size = size;

            data = data.Slice(size);
        }

        private static void GetTileBuffers(ref Vp9Common cm, ArrayPtr<byte> data, int tileCols,
            Span<TileBuffer> tileBuffers)
        {
            for (int c = 0; c < tileCols; ++c)
            {
                bool isLast = c == tileCols - 1;
                ref TileBuffer buf = ref tileBuffers[c];
                buf.Col = c;
                GetTileBuffer(isLast, ref cm.Error, ref data, ref buf);
            }
        }

        private static void GetTileBuffers(
            ref Vp9Common cm,
            ArrayPtr<byte> data,
            int tileCols,
            int tileRows,
            Span<Array64<TileBuffer>> tileBuffers1)
        {
            for (int r = 0; r < tileRows; ++r)
            {
                Span<TileBuffer> tileBuffers2 = tileBuffers1[r].AsSpan();
                
                for (int c = 0; c < tileCols; ++c)
                {
                    bool isLast = r == tileRows - 1 && c == tileCols - 1;
                    ref TileBuffer buf = ref tileBuffers2[c];
                    GetTileBuffer(isLast, ref cm.Error, ref data, ref buf);
                }
            }
        }

        public static unsafe ArrayPtr<byte> DecodeTiles(ref Vp9Common cm, ArrayPtr<byte> data)
        {
            int alignedCols = TileInfo.MiColsAlignedToSb(cm.MiCols);
            int tileCols = 1 << cm.Log2TileCols;
            int tileRows = 1 << cm.Log2TileRows;
            Array4<Array64<TileBuffer>> tileBuffers = new();
            int tileRow, tileCol;
            int miRow, miCol;

            Debug.Assert(tileRows <= 4);
            Debug.Assert(tileCols <= 1 << 6);

            // Note: this memset assumes above_context[0], [1] and [2]
            // are allocated as part of the same buffer.
            MemoryUtil.Fill(cm.AboveContext.ToPointer(), (sbyte)0, Constants.MaxMbPlane * 2 * alignedCols);
            MemoryUtil.Fill(cm.AboveSegContext.ToPointer(), (sbyte)0, alignedCols);

            LoopFilter.ResetLfm(ref cm);
            
            Span<Array64<TileBuffer>> tileBuffers1 = tileBuffers.AsSpan();
            Span<TileWorkerData> tileWorkerDataSpan = cm.TileWorkerData.AsSpan();

            GetTileBuffers(ref cm, data, tileCols, tileRows, tileBuffers1);
            // Load all tile information into tile_data.
            for (tileRow = 0; tileRow < tileRows; ++tileRow)
            {
                Span<TileBuffer> tileBuffers2 = tileBuffers1[tileRow].AsSpan();
                
                for (tileCol = 0; tileCol < tileCols; ++tileCol)
                {
                    ref TileBuffer buf = ref tileBuffers2[tileCol];
                    ref TileWorkerData tileData = ref tileWorkerDataSpan[(tileCols * tileRow) + tileCol];
                    tileData.Xd = cm.Mb;
                    tileData.Xd.Corrupted = false;
                    tileData.Xd.Counts = cm.Counts;
                    tileData.Dqcoeff = new Array32<Array32<int>>();
                    tileData.Xd.Tile.Init(ref cm, tileRow, tileCol);
                    SetupTokenDecoder(buf.Data, buf.Size, ref cm.Error, ref tileData.BitReader);
                    cm.InitMacroBlockD(ref tileData.Xd, new ArrayPtr<int>(ref tileData.Dqcoeff[0][0], 32 * 32));
                }
            }

            for (tileRow = 0; tileRow < tileRows; ++tileRow)
            {
                TileInfo tile = new();
                tile.SetRow(ref cm, tileRow);
                for (miRow = tile.MiRowStart; miRow < tile.MiRowEnd; miRow += Constants.MiBlockSize)
                {
                    for (tileCol = 0; tileCol < tileCols; ++tileCol)
                    {
                        int col = tileCol;
                        ref TileWorkerData tileData = ref tileWorkerDataSpan[(tileCols * tileRow) + col];
                        tile.SetCol(ref cm, col);
                        tileData.Xd.LeftContext = new Array3<Array16<sbyte>>();
                        tileData.Xd.LeftSegContext = new Array8<sbyte>();
                        for (miCol = tile.MiColStart; miCol < tile.MiColEnd; miCol += Constants.MiBlockSize)
                        {
                            DecodePartition(ref tileData, ref cm, miRow, miCol, BlockSize.Block64x64, 4);
                        }

                        cm.Mb.Corrupted |= tileData.Xd.Corrupted;
                        if (cm.Mb.Corrupted)
                        {
                            cm.Error.InternalError(CodecErr.CorruptFrame, "Failed to decode tile data");
                        }
                    }
                }
            }

            // Get last tile data.
            return tileWorkerDataSpan[(tileCols * tileRows) - 1].BitReader.FindEnd();
        }

        private static bool DecodeTileCol(ref TileWorkerData tileData, ref Vp9Common cm,
            Span<TileBuffer> tileBuffers)
        {
            ref TileInfo tile = ref tileData.Xd.Tile;
            int finalCol = (1 << cm.Log2TileCols) - 1;
            ArrayPtr<byte> bitReaderEnd = ArrayPtr<byte>.Null;

            int n = tileData.BufStart;

            tileData.Xd.Corrupted = false;

            do
            {
                ref TileBuffer buf = ref tileBuffers[n];

                Debug.Assert(cm.Log2TileRows == 0);
                tileData.Dqcoeff = new Array32<Array32<int>>();
                tile.Init(ref cm, 0, buf.Col);
                SetupTokenDecoder(buf.Data, buf.Size, ref tileData.ErrorInfo, ref tileData.BitReader);
                cm.InitMacroBlockD(ref tileData.Xd, new ArrayPtr<int>(ref tileData.Dqcoeff[0][0], 32 * 32));
                tileData.Xd.ErrorInfo = new Ptr<InternalErrorInfo>(ref tileData.ErrorInfo);

                for (int miRow = tile.MiRowStart; miRow < tile.MiRowEnd; miRow += Constants.MiBlockSize)
                {
                    tileData.Xd.LeftContext = new Array3<Array16<sbyte>>();
                    tileData.Xd.LeftSegContext = new Array8<sbyte>();
                    for (int miCol = tile.MiColStart; miCol < tile.MiColEnd; miCol += Constants.MiBlockSize)
                    {
                        DecodePartition(ref tileData, ref cm, miRow, miCol, BlockSize.Block64x64, 4);
                    }
                }

                if (buf.Col == finalCol)
                {
                    bitReaderEnd = tileData.BitReader.FindEnd();
                }
            } while (!tileData.Xd.Corrupted && ++n <= tileData.BufEnd);

            tileData.DataEnd = bitReaderEnd;
            return !tileData.Xd.Corrupted;
        }

        public static ArrayPtr<byte> DecodeTilesMt(ref Vp9Common cm, ArrayPtr<byte> data, int maxThreads)
        {
            ArrayPtr<byte> bitReaderEnd = ArrayPtr<byte>.Null;

            int tileCols = 1 << cm.Log2TileCols;
            int tileRows = 1 << cm.Log2TileRows;
            int totalTiles = tileCols * tileRows;
            int numWorkers = Math.Min(maxThreads, tileCols);
            int n;

            Debug.Assert(tileCols <= 1 << 6);
            Debug.Assert(tileRows == 1);

            LoopFilter.ResetLfm(ref cm);

            cm.AboveContext.AsSpan().Clear();
            cm.AboveSegContext.AsSpan().Clear();
            
            Span<TileWorkerData> tileWorkerDataSpan = cm.TileWorkerData.AsSpan();

            for (n = 0; n < numWorkers; ++n)
            {
                ref TileWorkerData tileData = ref tileWorkerDataSpan[n + totalTiles];

                tileData.Xd = cm.Mb;
                tileData.Xd.Counts = new Ptr<Vp9BackwardUpdates>(ref tileData.Counts);
                tileData.Counts = new Vp9BackwardUpdates();
            }

            Array64<TileBuffer> tileBuffers = new();
            Span<TileBuffer> tileBuffersSpan = tileBuffers.AsSpan();

            GetTileBuffers(ref cm, data, tileCols, tileBuffersSpan);

            tileBuffersSpan[..tileCols].Sort(CompareTileBuffers);

            if (numWorkers == tileCols)
            {
                TileBuffer largest = tileBuffersSpan[0];
                tileBuffersSpan[1..].CopyTo(tileBuffersSpan[..^1]);
                tileBuffersSpan[tileCols - 1] = largest;
            }
            else
            {
                int start = 0, end = tileCols - 2;
                TileBuffer tmp;

                // Interleave the tiles to distribute the load between threads, assuming a
                // larger tile implies it is more difficult to decode.
                while (start < end)
                {
                    tmp = tileBuffersSpan[start];
                    tileBuffersSpan[start] = tileBuffersSpan[end];
                    tileBuffersSpan[end] = tmp;
                    start += 2;
                    end -= 2;
                }
            }

            int baseVal = tileCols / numWorkers;
            int remain = tileCols % numWorkers;
            int bufStart = 0;

            for (n = 0; n < numWorkers; ++n)
            {
                int count = baseVal + ((remain + n) / numWorkers);
                ref TileWorkerData tileData = ref tileWorkerDataSpan[n + totalTiles];

                tileData.BufStart = bufStart;
                tileData.BufEnd = bufStart + count - 1;
                tileData.DataEnd = data.Slice(data.Length);
                bufStart += count;
            }

            Ptr<Vp9Common> cmPtr = new(ref cm);

            Parallel.For(0, numWorkers, n =>
            {
                ref TileWorkerData tileData = ref cmPtr.Value.TileWorkerData[n + totalTiles];

                if (!DecodeTileCol(ref tileData, ref cmPtr.Value, tileBuffers.AsSpan()))
                {
                    cmPtr.Value.Mb.Corrupted = true;
                }
            });

            for (; n > 0; --n)
            {
                if (bitReaderEnd.IsNull)
                {
                    ref TileWorkerData tileData = ref tileWorkerDataSpan[n - 1 + totalTiles];
                    bitReaderEnd = tileData.DataEnd;
                }
            }

            for (n = 0; n < numWorkers; ++n)
            {
                ref TileWorkerData tileData = ref tileWorkerDataSpan[n + totalTiles];
                AccumulateFrameCounts(ref cm.Counts.Value, ref tileData.Counts);
            }

            Debug.Assert(!bitReaderEnd.IsNull || cm.Mb.Corrupted);
            return bitReaderEnd;
        }

        private static int CompareTileBuffers(TileBuffer bufA, TileBuffer bufB)
        {
            return (bufA.Size < bufB.Size ? 1 : 0) - (bufA.Size > bufB.Size ? 1 : 0);
        }

        private static void AccumulateFrameCounts(ref Vp9BackwardUpdates accum, ref Vp9BackwardUpdates counts)
        {
            Span<uint> a = MemoryMarshal.Cast<Vp9BackwardUpdates, uint>(MemoryMarshal.CreateSpan(ref accum, 1));
            Span<uint> c = MemoryMarshal.Cast<Vp9BackwardUpdates, uint>(MemoryMarshal.CreateSpan(ref counts, 1));

            for (int i = 0; i < a.Length; i++)
            {
                a[i] += c[i];
            }
        }

        private static void ErrorHandler(Ptr<Vp9Common> data)
        {
            ref Vp9Common cm = ref data.Value;
            cm.Error.InternalError(CodecErr.CorruptFrame, "Truncated packet");
        }

        private static void FlushAllFbOnKey(ref Vp9Common cm)
        {
            if (cm.FrameType == FrameType.KeyFrame && cm.CurrentVideoFrame > 0)
            {
                Span<RefCntBuffer> frameBuffs = cm.BufferPool.Value.FrameBufs.AsSpan();
                ref BufferPool pool = ref cm.BufferPool.Value;

                for (int i = 0; i < Constants.FrameBuffers; ++i)
                {
                    if (i == cm.NewFbIdx)
                    {
                        continue;
                    }

                    frameBuffs[i].RefCount = 0;
                    if (frameBuffs[i].Released == 0)
                    {
                        FrameBuffers.ReleaseFrameBuffer(pool.CbPriv, ref frameBuffs[i].RawFrameBuffer);
                        frameBuffs[i].Released = 1;
                    }
                }
            }
        }

        private const int SyncCode0 = 0x49;
        private const int SyncCode1 = 0x83;
        private const int SyncCode2 = 0x42;

        private const int FrameMarker = 0x2;

        private static bool ReadSyncCode(ref ReadBitBuffer rb)
        {
            return rb.ReadLiteral(8) == SyncCode0 &&
                   rb.ReadLiteral(8) == SyncCode1 &&
                   rb.ReadLiteral(8) == SyncCode2;
        }

        private static void RefCntFb(Span<RefCntBuffer> bufs, ref int idx, int newIdx)
        {
            int refIndex = idx;

            if (refIndex >= 0 && bufs[refIndex].RefCount > 0)
            {
                bufs[refIndex].RefCount--;
            }

            idx = newIdx;

            bufs[newIdx].RefCount++;
        }

        private static ulong ReadUncompressedHeader(MemoryAllocator allocator, ref Vp9Decoder pbi,
            ref ReadBitBuffer rb)
        {
            ref Vp9Common cm = ref pbi.Common;
            ref BufferPool pool = ref cm.BufferPool.Value;
            Span<RefCntBuffer> frameBuffs = pool.FrameBufs.AsSpan();
            int mask, refIndex = 0;
            ulong sz;

            cm.LastFrameType = cm.FrameType;
            cm.LastIntraOnly = cm.IntraOnly;

            Span<int> refFrameSpan = cm.RefFrameMap.AsSpan();

            if (rb.ReadLiteral(2) != FrameMarker)
            {
                cm.Error.InternalError(CodecErr.UnsupBitstream, "Invalid frame marker");
            }

            cm.Profile = rb.ReadProfile();
            if (cm.Profile >= BitstreamProfile.MaxProfiles)
            {
                cm.Error.InternalError(CodecErr.UnsupBitstream, "Unsupported bitstream profile");
            }

            cm.ShowExistingFrame = rb.ReadBit();
            if (cm.ShowExistingFrame != 0)
            {
                // Show an existing frame directly.
                int frameToShow = refFrameSpan[rb.ReadLiteral(3)];
                if (frameToShow < 0 || frameBuffs[frameToShow].RefCount < 1)
                {
                    cm.Error.InternalError(CodecErr.UnsupBitstream,
                        $"Buffer {frameToShow} does not contain a decoded frame");
                }

                RefCntFb(frameBuffs, ref cm.NewFbIdx, frameToShow);
                pbi.RefreshFrameFlags = 0;
                cm.Lf.FilterLevel = 0;
                cm.ShowFrame = 1;

                return 0;
            }

            cm.FrameType = (FrameType)rb.ReadBit();
            cm.ShowFrame = rb.ReadBit();
            cm.ErrorResilientMode = rb.ReadBit();

            if (cm.FrameType == FrameType.KeyFrame)
            {
                if (!ReadSyncCode(ref rb))
                {
                    cm.Error.InternalError(CodecErr.UnsupBitstream, "Invalid frame sync code");
                }

                cm.ReadBitdepthColorspaceSampling(ref rb);
                pbi.RefreshFrameFlags = (1 << Constants.RefFrames) - 1;

                Span<RefBuffer> frameRefsSpan = cm.FrameRefs.AsSpan();

                for (int i = 0; i < Constants.RefsPerFrame; ++i)
                {
                    frameRefsSpan[i].Idx = RefBuffer.InvalidIdx;
                    frameRefsSpan[i].Buf = default;
                }

                SetupFrameSize(allocator, ref cm, ref rb);
                if (pbi.NeedResync != 0)
                {
                    refFrameSpan.Fill(-1);
                    FlushAllFbOnKey(ref cm);
                    pbi.NeedResync = 0;
                }
            }
            else
            {
                cm.IntraOnly = (cm.ShowFrame != 0 ? 0 : rb.ReadBit()) != 0;

                cm.ResetFrameContext = cm.ErrorResilientMode != 0 ? 0 : rb.ReadLiteral(2);

                if (cm.IntraOnly)
                {
                    if (!ReadSyncCode(ref rb))
                    {
                        cm.Error.InternalError(CodecErr.UnsupBitstream, "Invalid frame sync code");
                    }

                    if (cm.Profile > BitstreamProfile.Profile0)
                    {
                        cm.ReadBitdepthColorspaceSampling(ref rb);
                    }
                    else
                    {
                        // NOTE: The intra-only frame header does not include the specification
                        // of either the color format or color sub-sampling in profile 0. VP9
                        // specifies that the default color format should be YUV 4:2:0 in this
                        // case (normative).
                        cm.ColorSpace = VpxColorSpace.Bt601;
                        cm.ColorRange = VpxColorRange.Studio;
                        cm.SubsamplingY = cm.SubsamplingX = 1;
                        cm.BitDepth = BitDepth.Bits8;
                        cm.UseHighBitDepth = false;
                    }

                    pbi.RefreshFrameFlags = rb.ReadLiteral(Constants.RefFrames);
                    SetupFrameSize(allocator, ref cm, ref rb);
                    if (pbi.NeedResync != 0)
                    {
                        refFrameSpan.Fill(-1);
                        pbi.NeedResync = 0;
                    }
                }
                else if (pbi.NeedResync != 1)
                {
                    Span<RefBuffer> frameRefsSpan = cm.FrameRefs.AsSpan();
                    Span<sbyte> refFrameSignBiasSpan = cm.RefFrameSignBias.AsSpan();
                    
                    /* Skip if need resync */
                    pbi.RefreshFrameFlags = rb.ReadLiteral(Constants.RefFrames);
                    for (int i = 0; i < Constants.RefsPerFrame; ++i)
                    {
                        int refr = rb.ReadLiteral(Constants.RefFramesLog2);
                        int idx = refFrameSpan[refr];
                        ref RefBuffer refFrame = ref frameRefsSpan[i];
                        refFrame.Idx = idx;
                        refFrame.Buf = frameBuffs[idx].Buf;
                        refFrameSignBiasSpan[Constants.LastFrame + i] = (sbyte)rb.ReadBit();
                    }

                    SetupFrameSizeWithRefs(allocator, ref cm, ref rb);

                    cm.AllowHighPrecisionMv = rb.ReadBit() != 0;
                    cm.InterpFilter = ReadInterpFilter(ref rb);

                    for (int i = 0; i < Constants.RefsPerFrame; ++i)
                    {
                        ref RefBuffer refBuf = ref frameRefsSpan[i];
                        refBuf.Sf.SetupScaleFactorsForFrame(
                            refBuf.Buf.YCropWidth,
                            refBuf.Buf.YCropHeight,
                            cm.Width,
                            cm.Height);
                    }
                }
            }

            cm.GetFrameNewBuffer().BitDepth = (uint)cm.BitDepth;
            cm.GetFrameNewBuffer().ColorSpace = cm.ColorSpace;
            cm.GetFrameNewBuffer().ColorRange = cm.ColorRange;
            cm.GetFrameNewBuffer().RenderWidth = cm.RenderWidth;
            cm.GetFrameNewBuffer().RenderHeight = cm.RenderHeight;

            if (pbi.NeedResync != 0)
            {
                cm.Error.InternalError(CodecErr.CorruptFrame,
                    "Keyframe / intra-only frame required to reset decoder state");
            }

            if (cm.ErrorResilientMode == 0)
            {
                cm.RefreshFrameContext = rb.ReadBit();
                cm.FrameParallelDecodingMode = rb.ReadBit();
                if (cm.FrameParallelDecodingMode == 0)
                {
                    cm.Counts.Value = new Vp9BackwardUpdates();
                }
            }
            else
            {
                cm.RefreshFrameContext = 0;
                cm.FrameParallelDecodingMode = 1;
            }

            // This flag will be overridden by the call to SetupPastIndependence
            // below, forcing the use of context 0 for those frame types.
            cm.FrameContextIdx = (uint)rb.ReadLiteral(Constants.FrameContextsLog2);

            Span<int> nextRefFrameMapSpan = cm.NextRefFrameMap.AsSpan();

            // Generate next_ref_frame_map.
            for (mask = pbi.RefreshFrameFlags; mask != 0; mask >>= 1)
            {
                if ((mask & 1) != 0)
                {
                    nextRefFrameMapSpan[refIndex] = cm.NewFbIdx;
                    ++frameBuffs[cm.NewFbIdx].RefCount;
                }
                else
                {
                    nextRefFrameMapSpan[refIndex] = cm.RefFrameMap[refIndex];
                }

                // Current thread holds the reference frame.
                if (cm.RefFrameMap[refIndex] >= 0)
                {
                    ++frameBuffs[cm.RefFrameMap[refIndex]].RefCount;
                }

                ++refIndex;
            }

            for (; refIndex < Constants.RefFrames; ++refIndex)
            {
                nextRefFrameMapSpan[refIndex] = refFrameSpan[refIndex];
                // Current thread holds the reference frame.
                if (refFrameSpan[refIndex] >= 0)
                {
                    ++frameBuffs[refFrameSpan[refIndex]].RefCount;
                }
            }

            pbi.HoldRefBuf = 1;

            if (cm.FrameIsIntraOnly() || cm.ErrorResilientMode != 0)
            {
                EntropyMode.SetupPastIndependence(ref cm);
            }

            SetupLoopfilter(ref cm.Lf, ref rb);
            SetupQuantization(ref cm, ref cm.Mb, ref rb);
            cm.Seg.SetupSegmentation(ref cm.Fc.Value, ref rb);
            cm.SetupSegmentationDequant();

            cm.SetupTileInfo(ref rb);
            sz = (ulong)rb.ReadLiteral(16);

            if (sz == 0)
            {
                cm.Error.InternalError(CodecErr.CorruptFrame, "Invalid header size");
            }

            return sz;
        }

        private static bool ReadCompressedHeader(ref Vp9Decoder pbi, ArrayPtr<byte> data, ulong partitionSize)
        {
            ref Vp9Common cm = ref pbi.Common;
            ref MacroBlockD xd = ref cm.Mb;
            ref Vp9EntropyProbs fc = ref cm.Fc.Value;
            Reader r = new();

            if (r.Init(data, (int)partitionSize))
            {
                cm.Error.InternalError(CodecErr.MemError, "Failed to allocate bool decoder 0");
            }

            cm.TxMode = xd.Lossless ? TxMode.Only4x4 : r.ReadTxMode();
            if (cm.TxMode == TxMode.TxModeSelect)
            {
                ReadTxModeProbs(ref fc, ref r);
            }

            ReadCoefProbs(ref fc, cm.TxMode, ref r);

            Span<byte> skipProbSpan = fc.SkipProb.AsSpan();

            for (int k = 0; k < Constants.SkipContexts; ++k)
            {
                r.DiffUpdateProb(ref skipProbSpan[k]);
            }

            if (!cm.FrameIsIntraOnly())
            {
                ReadInterModeProbs(ref fc, ref r);

                if (cm.InterpFilter == Constants.Switchable)
                {
                    ReadSwitchableInterpProbs(ref fc, ref r);
                }
                
                Span<byte> intraInterProbSpan = fc.IntraInterProb.AsSpan();

                for (int i = 0; i < Constants.IntraInterContexts; i++)
                {
                    r.DiffUpdateProb(ref intraInterProbSpan[i]);
                }

                cm.ReferenceMode = cm.ReadFrameReferenceMode(ref r);
                if (cm.ReferenceMode != ReferenceMode.Single)
                {
                    cm.SetupCompoundReferenceMode();
                }

                cm.ReadFrameReferenceModeProbs(ref r);
                
                Span<Array9<byte>> yModeProbSpan1 = fc.YModeProb.AsSpan();

                for (int j = 0; j < EntropyMode.BlockSizeGroups; j++)
                {
                    Span<byte> yModeProbSpan2 = yModeProbSpan1[j].AsSpan();
                    
                    for (int i = 0; i < Constants.IntraModes - 1; ++i)
                    {
                        r.DiffUpdateProb(ref yModeProbSpan2[i]);
                    }
                }
                
                Span<Array3<byte>> partitionProbSpan1 = fc.PartitionProb.AsSpan();

                for (int j = 0; j < Constants.PartitionContexts; ++j)
                {
                    Span<byte> partitionProbSpan2 = partitionProbSpan1[j].AsSpan();
                    
                    for (int i = 0; i < Constants.PartitionTypes - 1; ++i)
                    {
                        r.DiffUpdateProb(ref partitionProbSpan2[i]);
                    }
                }

                ReadMvProbs(ref fc, cm.AllowHighPrecisionMv, ref r);
            }

            return r.HasError();
        }

        private static ref ReadBitBuffer InitReadBitBuffer(ref ReadBitBuffer rb, ReadOnlySpan<byte> data)
        {
            rb.BitOffset = 0;
            rb.BitBuffer = data;
            return ref rb;
        }

        public static unsafe void Decode(MemoryAllocator allocator,
            ref Vp9Decoder pbi,
            ArrayPtr<byte> data,
            out ArrayPtr<byte> pDataEnd,
            bool multithreaded = true)
        {
            ref Vp9Common cm = ref pbi.Common;
            ref MacroBlockD xd = ref cm.Mb;
            ReadBitBuffer rb = new();
            int contextUpdated = 0;
            Span<byte> clearData = stackalloc byte[80];
            ulong firstPartitionSize =
                ReadUncompressedHeader(allocator, ref pbi, ref InitReadBitBuffer(ref rb, data.AsSpan()));
            int tileRows = 1 << cm.Log2TileRows;
            int tileCols = 1 << cm.Log2TileCols;
            ref Surface newFb = ref cm.GetFrameNewBuffer();
            xd.CurBuf = newFb;

            if (firstPartitionSize == 0)
            {
                // showing a frame directly
                pDataEnd = data.Slice(cm.Profile <= BitstreamProfile.Profile2 ? 1 : 2);
                return;
            }

            data = data.Slice((int)rb.BytesRead());
            if (!ReadIsValid(data, (int)firstPartitionSize))
            {
                cm.Error.InternalError(CodecErr.CorruptFrame, "Truncated packet or corrupt header length");
            }

            cm.UsePrevFrameMvs =
                cm.ErrorResilientMode == 0 &&
                cm.Width == cm.LastWidth &&
                cm.Height == cm.LastHeight &&
                !cm.LastIntraOnly &&
                cm.LastShowFrame != 0 &&
                cm.LastFrameType != FrameType.KeyFrame;

            xd.SetupBlockPlanes(cm.SubsamplingX, cm.SubsamplingY);

            Span<Vp9EntropyProbs> frameContextsSpan = cm.FrameContexts.AsSpan();

            cm.Fc = new Ptr<Vp9EntropyProbs>(ref frameContextsSpan[(int)cm.FrameContextIdx]);

            xd.Corrupted = false;
            newFb.Corrupted = ReadCompressedHeader(ref pbi, data, firstPartitionSize) ? 1 : 0;
            if (newFb.Corrupted != 0)
            {
                cm.Error.InternalError(CodecErr.CorruptFrame, "Decode failed. Frame data header is corrupted.");
            }

            if (cm.Lf.FilterLevel != 0 && cm.SkipLoopFilter == 0)
            {
                LoopFilter.LoopFilterFrameInit(ref cm, cm.Lf.FilterLevel);
            }

            int threadCount = multithreaded ? Math.Max(1, Environment.ProcessorCount / 2) : 0;

            if (cm.TileWorkerData.IsNull || tileCols * tileRows != cm.TotalTiles)
            {
                int numTileWorkers = (tileCols * tileRows) + threadCount;
                if (!cm.TileWorkerData.IsNull)
                {
                    allocator.Free(cm.TileWorkerData);
                }

                cm.CheckMemError( ref cm.TileWorkerData, allocator.Allocate<TileWorkerData>(numTileWorkers));
                cm.TotalTiles = tileRows * tileCols;
            }

            if (multithreaded)
            {
                pDataEnd = DecodeTilesMt(ref pbi.Common, data.Slice((int)firstPartitionSize), threadCount);

                LoopFilter.LoopFilterFrameMt(
                    ref cm.Mb.CurBuf,
                    ref cm,
                    ref cm.Mb,
                    cm.Lf.FilterLevel,
                    false,
                    false,
                    threadCount);
            }
            else
            {
                pDataEnd = DecodeTiles(ref pbi.Common, data.Slice((int)firstPartitionSize));

                LoopFilter.LoopFilterFrame(ref cm.Mb.CurBuf, ref cm, ref cm.Mb, cm.Lf.FilterLevel, false, false);
            }

            if (!xd.Corrupted)
            {
                if (cm.ErrorResilientMode == 0 && cm.FrameParallelDecodingMode == 0)
                {
                    cm.AdaptCoefProbs();

                    if (!cm.FrameIsIntraOnly())
                    {
                        cm.AdaptModeProbs();
                        cm.AdaptMvProbs(cm.AllowHighPrecisionMv);
                    }
                }
            }
            else
            {
                cm.Error.InternalError(CodecErr.CorruptFrame, "Decode failed. Frame data is corrupted.");
            }

            // Non frame parallel update frame context here.
            if (cm.RefreshFrameContext != 0 && contextUpdated == 0)
            {
                frameContextsSpan[(int)cm.FrameContextIdx] = cm.Fc.Value;
            }
        }
    }
}
