encodeFrame method

EncodedVp8Frame encodeFrame({
  1. required Uint8List srcY,
  2. required Uint8List srcU,
  3. required Uint8List srcV,
  4. required int srcYStride,
  5. required int srcUvStride,
  6. bool? forceKey,
  7. int? qiOverride,
})

Implementation

EncodedVp8Frame encodeFrame({
  required Uint8List srcY,
  required Uint8List srcU,
  required Uint8List srcV,
  required int srcYStride,
  required int srcUvStride,
  bool? forceKey,
  int? qiOverride,
}) {
  if (qiOverride != null && (qiOverride < 0 || qiOverride > 127)) {
    throw ArgumentError('qiOverride must be in [0,127] (got $qiOverride)');
  }
  final sw = Stopwatch()..start();
  final hardKey = forceKey == true ||
      _prevLast == null ||
      _framesSinceKey >= keyframeInterval;
  var isKey = hardKey || _pendingKeyRequest;

  final rc = rateController;

  // Phase 22 — escalate to keyframe when adaptive triggers fire.
  // Done BEFORE the drop check so a recovery key isn't itself dropped.
  if (!isKey) {
    if (rc != null &&
        forceKeyAfterConsecutiveDrops > 0 &&
        rc.consecutiveDrops >= forceKeyAfterConsecutiveDrops) {
      isKey = true;
    } else if (sceneCutMeanAbsDiff != null && _prevLast != null) {
      final m = _meanAbsDiffY(srcY, srcYStride, _prevLast!);
      if (m > sceneCutMeanAbsDiff!) {
        isKey = true;
      }
    }
  }

  // Phase 34 — minKeyframeInterval veto: defer soft KF triggers
  // (request / scene-cut / drop-recovery) when the previous KF is
  // too recent. Hard triggers (forceKey, missing reference, hitting
  // the keyframeInterval cap) always win. A pending request that is
  // vetoed here remains pending and fires on the first eligible
  // frame.
  if (isKey &&
      !hardKey &&
      minKeyframeInterval > 0 &&
      _framesSinceKey < minKeyframeInterval) {
    isKey = false;
  } else {
    _pendingKeyRequest = false;
  }

  // Rate-controller frame drop: only for inter frames; preserves all
  // reference buffers so the next encoded frame still motion-searches
  // against what the decoder last received.
  if (!isKey && rc != null && rc.shouldDrop(isKey: false)) {
    rc.noteDrop();
    _framesSinceKey += 1;
    _lastStats = null;
    _cum._noteDrop();
    onFrameDropped?.call();
    return EncodedVp8Frame(
      bytes: Uint8List(0),
      isKey: false,
      reconY: Uint8List(0),
      reconU: Uint8List(0),
      reconV: Uint8List(0),
      yStride: 0,
      uvStride: 0,
      isDropped: true,
    );
  }

  final pickedQi = qiOverride ?? (rc != null ? rc.pickQi(isKey: isKey) : qi);
  final frameQi =
      pickedQi < minQi ? minQi : (pickedQi > maxQi ? maxQi : pickedQi);
  final frameSadPerBit16 =
      useMvRateCost ? computeRdConsts(frameQi).sadPerBit16 : 0;
  final frameFilterLevel = autoLoopFilter
      ? deriveLoopFilterLevel(frameQi,
          min: autoLoopFilterMin, max: autoLoopFilterMax)
      : filterLevel;

  if (isKey) {
    // A keyframe always re-seeds GOLDEN and ALTREF, so any pending
    // refresh requests are satisfied implicitly.
    _pendingGoldenRequest = false;
    _pendingAltrefRequest = false;
    final segPair = _buildSegmentation(width, height, srcY, srcYStride);
    final enc = encodeKeyframe(
      width: width,
      height: height,
      qi: frameQi,
      srcY: srcY,
      srcU: srcU,
      srcV: srcV,
      srcYStride: srcYStride,
      srcUvStride: srcUvStride,
      filterLevel: frameFilterLevel,
      sharpness: sharpness,
      filterType: filterType,
      mbNoCoeffSkip: mbNoCoeffSkip,
      probSkipFalse: probSkipFalse,
      log2NumTokenPartitions: log2NumTokenPartitions,
      segmentation: segPair?.spec,
      perMbSegmentIds: segPair?.ids,
      rdSadPerBit16: useRdIntraMode ? frameSadPerBit16 : 0,
      kfYModeProbs: useRdIntraMode ? kfYModeProb : null,
      kfUvModeProbs: useRdIntraMode ? kfUvModeProb : null,
    );
    final fresh = _refFromEnc(enc);
    _prevLast = fresh;
    _prevGolden = cloneRefFrame(fresh);
    _prevAltref = cloneRefFrame(fresh);
    _framesSinceKey = 1;
    rc?.update(frameBytes: enc.bytes.length, isKey: true);
    final mbCountKf = (width >> 4) * (height >> 4);
    final statsKf = Vp8FrameStats(
      frameIndex: _cum._framesEncoded,
      isKeyframe: true,
      qi: frameQi,
      filterLevel: frameFilterLevel,
      totalMbCount: mbCountKf,
      intraMbCount: mbCountKf,
      zeroMvMbCount: 0,
      nearestMvMbCount: 0,
      nearMvMbCount: 0,
      newMvMbCount: 0,
      newMvLastMbCount: 0,
      newMvGoldenMbCount: 0,
      newMvAltrefMbCount: 0,
      goldenRefMbCount: 0,
      altrefRefMbCount: 0,
      denoisedMbCount: 0,
      staticShortCircuitMbCount: 0,
      bytesEmitted: enc.bytes.length,
      meanAbsMvQuarterPel: 0.0,
      maxAbsMvQuarterPel: 0,
      encodeDurationMicros: sw.elapsedMicroseconds,
      psnrY: _psnrY(srcY, srcYStride, enc.reconY, enc.yStride, width, height),
    );
    _lastStats = statsKf;
    _cum._accumulate(statsKf, isKey: true);
    onFrameEncoded?.call(statsKf);
    return EncodedVp8Frame(
      bytes: enc.bytes,
      isKey: true,
      reconY: enc.reconY,
      reconU: enc.reconU,
      reconV: enc.reconV,
      yStride: enc.yStride,
      uvStride: enc.uvStride,
      stats: statsKf,
    );
  }

  final prevLast = _prevLast!;
  final prevGolden = _prevGolden!;
  final prevAltref = _prevAltref!;
  final mbCols = width >> 4;
  final mbRows = height >> 4;

  // Optional temporal denoise on Y against the previous reconstruction.
  // The result replaces `srcY` for both motion search and the encode.
  Uint8List effSrcY = srcY;
  int effSrcYStride = srcYStride;
  final dn = denoiser;
  if (dn != null) {
    effSrcY = dn.denoiseY(
      width: width,
      height: height,
      srcY: srcY,
      srcYStride: srcYStride,
      prevRef: prevLast,
    );
    effSrcYStride = srcYStride;
  }

  final clamped = searchRange.clamp(0, 16);
  final signBias = <bool>[false, false, false, false];
  final miList = List<ModeInfo>.generate(mbCols * mbRows, (_) => ModeInfo());
  final nearMvs = List<Mv>.generate(4, (_) => Mv(0, 0));
  final cnt = Int32List(4);
  final predBuf = Uint8List(256);
  final specs = <InterMbSpec>[];

  // Build the set of MB indices forced to intra-refresh this frame.
  final mbCount = mbCols * mbRows;
  final cirCount = cyclicRefreshPercent == 0
      ? 0
      : ((cyclicRefreshPercent * mbCount + 50) ~/ 100).clamp(0, mbCount);
  final cirSet = cirCount == 0 ? null : <int>{};
  if (cirSet != null) {
    for (var k = 0; k < cirCount; k++) {
      cirSet.add((_cirCursor + k) % mbCount);
    }
    _cirCursor = (_cirCursor + cirCount) % mbCount;
  }

  var staticShortCircuitMbCount = 0;
  for (var r = 0; r < mbRows; r++) {
    for (var c = 0; c < mbCols; c++) {
      final mbIdx = r * mbCols + c;
      final mi = miList[mbIdx];

      // Cyclic intra refresh: force this MB to be coded as intra.
      if (cirSet != null && cirSet.contains(mbIdx)) {
        mi.refFrame = refIntra;
        mi.yMode = dcPred;
        mi.uvMode = dcPred;
        mi.mv.row = 0;
        mi.mv.col = 0;
        specs.add(const InterIntraMbSpec(yMode: dcPred, uvMode: dcPred));
        continue;
      }

      final zeroSad = sad16x16At(
        srcY: effSrcY,
        srcYStride: effSrcYStride,
        ref: prevLast,
        mbRow: r,
        mbCol: c,
      );

      // Static-MB short-circuit: when the LAST-ZEROMV SAD is at or
      // below the configured threshold, lock the MB to LAST-ZEROMV
      // and skip every other candidate (NEAREST/NEAR/NEW/GOLDEN).
      if (staticMbSadThreshold > 0 && zeroSad <= staticMbSadThreshold) {
        mi.refFrame = refLast;
        mi.yMode = zeroMv;
        mi.mv.row = 0;
        mi.mv.col = 0;
        specs.add(const InterZeroMvMbSpec(refFrame: refLast));
        staticShortCircuitMbCount += 1;
        continue;
      }

      InterMbSpec pick;
      var bestSad = zeroSad;
      Mv bestMv = Mv(0, 0);
      int bestMode = 0; // 0=ZERO, 1=NEAREST, 2=NEAR, 3=NEW

      // P68: mv-ref probability context for the optional Lagrangian
      // per-mode bias. Defaulted to vp8_mode_contexts row 0
      // (cnt = [0,0,0,0]); recomputed below if findNearMvs runs.
      var mvRefProbs = const <int>[7, 1, 1, 143];
      int rdBias(int modeToken, [Mv? mv]) {
        if (!useRdInterMode || frameSadPerBit16 <= 0) return 0;
        var rate = mvRefModeBitCost(modeToken, mvRefProbs);
        if (modeToken == newMv) rate += mvBitCost(mv!, defaultMvContext);
        return (frameSadPerBit16 * rate) >> 8;
      }

      // Predictor candidates (NEAREST/NEAR), if enabled.
      Mv nearestCand = Mv(0, 0);
      Mv nearCand = Mv(0, 0);
      var haveNearest = false;
      var haveNear = false;
      if (predictorMotionSearch) {
        final above = r > 0 ? miList[(r - 1) * mbCols + c] : offFrameMi;
        final left = c > 0 ? miList[r * mbCols + (c - 1)] : offFrameMi;
        final aboveLeft = (r > 0 && c > 0)
            ? miList[(r - 1) * mbCols + (c - 1)]
            : offFrameMi;
        findNearMvs(
          above: above,
          left: left,
          aboveLeft: aboveLeft,
          refFrame: refLast,
          signBias: signBias,
          nearMvs: nearMvs,
          cnt: cnt,
        );
        // Swap+tiebreak (mirrors writer's NEAREST/NEAR path).
        if (cnt[3] > 0 && nearMvs[3].asInt == nearMvs[1].asInt) {
          cnt[1] += 1;
        }
        if (cnt[2] > cnt[1]) {
          final tc = cnt[1];
          cnt[1] = cnt[2];
          cnt[2] = tc;
          final tr = nearMvs[1].row;
          final tco = nearMvs[1].col;
          nearMvs[1].row = nearMvs[2].row;
          nearMvs[1].col = nearMvs[2].col;
          nearMvs[2].row = tr;
          nearMvs[2].col = tco;
        }
        if (useRdInterMode) {
          mvRefProbs = mvRefProbsFromCnt(cnt);
        }
        // ZEROMV baseline bias is added now that mvRefProbs is final.
        bestSad += rdBias(zeroMv);
        final mbToLeftEdge = -((c * 16) << 3) - mvMarginEdge;
        final mbToRightEdge = (((mbCols - 1 - c) * 16) << 3) + mvMarginEdge;
        final mbToTopEdge = -((r * 16) << 3) - mvMarginEdge;
        final mbToBottomEdge = (((mbRows - 1 - r) * 16) << 3) + mvMarginEdge;

        // NEAREST candidate = nearMvs[1] after clamp.
        final nm1 = Mv(nearMvs[1].row, nearMvs[1].col);
        clampMv2(
            nm1, mbToLeftEdge, mbToRightEdge, mbToTopEdge, mbToBottomEdge);
        if (nm1.row != 0 || nm1.col != 0) {
          nearestCand = nm1;
          haveNearest = true;
          final sad = sad16x16Subpel(
            srcY: effSrcY,
            srcYStride: effSrcYStride,
            ref: prevLast,
            mbRow: r,
            mbCol: c,
            mv: nm1,
            predBuf: predBuf,
          );
          if (sad + (useRdInterMode ? rdBias(nearestMv) : nearestSadBias) <
              bestSad) {
            bestSad =
                sad + (useRdInterMode ? rdBias(nearestMv) : nearestSadBias);
            bestMv = nm1;
            bestMode = 1;
          }
        }
        // NEAR candidate = nearMvs[2] after clamp.
        final nm2 = Mv(nearMvs[2].row, nearMvs[2].col);
        clampMv2(
            nm2, mbToLeftEdge, mbToRightEdge, mbToTopEdge, mbToBottomEdge);
        if (nm2.row != 0 || nm2.col != 0) {
          nearCand = nm2;
          haveNear = true;
          final sad = sad16x16Subpel(
            srcY: effSrcY,
            srcYStride: effSrcYStride,
            ref: prevLast,
            mbRow: r,
            mbCol: c,
            mv: nm2,
            predBuf: predBuf,
          );
          if (sad + (useRdInterMode ? rdBias(nearMv) : nearSadBias) <
              bestSad) {
            bestSad = sad + (useRdInterMode ? rdBias(nearMv) : nearSadBias);
            bestMv = nm2;
            bestMode = 2;
          }
        }
      }

      // NEW candidate: integer search (seeded at nearest predictor if
      // we have one) + sub-pel refinement.
      if (clamped > 0) {
        // If predictorMotionSearch ran above, bestSad already includes
        // the ZEROMV RD bias; if not, add it here so NEW competes on
        // the same footing.
        if (!predictorMotionSearch) bestSad += rdBias(zeroMv);
        final MotionSearchResult integer;
        if (useDiamondMotionSearch) {
          integer = diamondIntegerMotionSearch(
            srcY: effSrcY,
            srcYStride: effSrcYStride,
            ref: prevLast,
            mbRow: r,
            mbCol: c,
            searchRange: clamped,
            mvRateBias: mvRateBias,
            sadPerBit: frameSadPerBit16,
            seedMv: predictorMotionSearch && haveNearest ? nearestCand : null,
          );
        } else if (predictorMotionSearch && haveNearest) {
          integer = naiveIntegerMotionSearchSeeded(
            srcY: effSrcY,
            srcYStride: effSrcYStride,
            ref: prevLast,
            mbRow: r,
            mbCol: c,
            centerMv: nearestCand,
            searchRange: clamped,
            mvRateBias: mvRateBias,
            sadPerBit: frameSadPerBit16,
          );
        } else {
          integer = naiveIntegerMotionSearch(
            srcY: effSrcY,
            srcYStride: effSrcYStride,
            ref: prevLast,
            mbRow: r,
            mbCol: c,
            searchRange: clamped,
            mvRateBias: mvRateBias,
            sadPerBit: frameSadPerBit16,
          );
        }
        final refined = subPelRefine
            ? subPelRefineSearch(
                srcY: effSrcY,
                srcYStride: effSrcYStride,
                ref: prevLast,
                mbRow: r,
                mbCol: c,
                integerMv: integer.mv,
                sadPerBit: frameSadPerBit16,
              )
            : integer;
        if (refined.mv.row != 0 || refined.mv.col != 0) {
          final newBias = useRdInterMode
              ? rdBias(newMv, refined.mv)
              : (zeroMvSadBias > newMvSadBias ? zeroMvSadBias : newMvSadBias);
          if (refined.sad + newBias < bestSad) {
            bestSad = refined.sad + newBias;
            bestMv = refined.mv;
            bestMode = 3;
          }
        }
      }

      switch (bestMode) {
        case 1:
          pick = InterNearestMvMbSpec(refFrame: refLast);
          mi.refFrame = refLast;
          mi.yMode = nearestMv;
          mi.mv.row = nearestCand.row;
          mi.mv.col = nearestCand.col;
          break;
        case 2:
          pick = InterNearMvMbSpec(refFrame: refLast);
          mi.refFrame = refLast;
          mi.yMode = nearMv;
          mi.mv.row = nearCand.row;
          mi.mv.col = nearCand.col;
          break;
        case 3:
          pick = InterNewMvMbSpec(mv: bestMv, refFrame: refLast);
          mi.refFrame = refLast;
          mi.yMode = newMv;
          mi.mv.row = bestMv.row;
          mi.mv.col = bestMv.col;
          break;
        default:
          pick = const InterZeroMvMbSpec(refFrame: refLast);
          mi.refFrame = refLast;
          mi.yMode = zeroMv;
          mi.mv.row = 0;
          mi.mv.col = 0;
      }

      // P69: cross-ref RD bias helper. When useRdInterMode is on we
      // re-base bestSad so it represents the LAST pick's *full*
      // signalling rate (mode tree + refFrameBitCost(refLast)) and
      // each cross-ref candidate is compared with its own full rate
      // (refFrameBitCost(refX) + mvRefModeBitCost + maybe mvBitCost).
      // Cross-ref mvRefProbs default to vp8_mode_contexts row 0
      // because the per-ref cnt vector is not recomputed.
      const crossProbLast = 128, crossProbGf = 128;
      const crossMvRefProbs = <int>[7, 1, 1, 143];
      int crossRefRdBias(int refFrame, int modeToken, [Mv? mv]) {
        if (!useRdInterMode || frameSadPerBit16 <= 0) return 0;
        var rate = refFrameBitCost(refFrame, crossProbLast, crossProbGf) +
            mvRefModeBitCost(modeToken, crossMvRefProbs);
        if (modeToken == newMv) rate += mvBitCost(mv!, defaultMvContext);
        return (frameSadPerBit16 * rate) >> 8;
      }

      if (useRdInterMode && frameSadPerBit16 > 0) {
        bestSad += (frameSadPerBit16 *
                refFrameBitCost(refLast, crossProbLast, crossProbGf)) >>
            8;
      }

      // Optional: NEWMV search against the GOLDEN reference. If a
      // valid (non-zero) refined MV beats the current best by
      // [goldenNewMvSadBias], switch the MB to NEWMV-from-GOLDEN.
      if (tryGoldenNewMv && clamped > 0) {
        final integerG = useDiamondMotionSearch
            ? diamondIntegerMotionSearch(
                srcY: effSrcY,
                srcYStride: effSrcYStride,
                ref: prevGolden,
                mbRow: r,
                mbCol: c,
                searchRange: clamped,
                mvRateBias: mvRateBias,
                sadPerBit: frameSadPerBit16,
              )
            : naiveIntegerMotionSearch(
                srcY: effSrcY,
                srcYStride: effSrcYStride,
                ref: prevGolden,
                mbRow: r,
                mbCol: c,
                searchRange: clamped,
                mvRateBias: mvRateBias,
                sadPerBit: frameSadPerBit16,
              );
        final refinedG = subPelRefine
            ? subPelRefineSearch(
                srcY: effSrcY,
                srcYStride: effSrcYStride,
                ref: prevGolden,
                mbRow: r,
                mbCol: c,
                integerMv: integerG.mv,
                sadPerBit: frameSadPerBit16,
              )
            : integerG;
        if (refinedG.mv.row != 0 || refinedG.mv.col != 0) {
          final gBias = useRdInterMode
              ? crossRefRdBias(refGolden, newMv, refinedG.mv)
              : goldenNewMvSadBias;
          if (refinedG.sad + gBias < bestSad) {
            bestSad = refinedG.sad + gBias;
            pick = InterNewMvMbSpec(mv: refinedG.mv, refFrame: refGolden);
            mi.refFrame = refGolden;
            mi.yMode = newMv;
            mi.mv.row = refinedG.mv.row;
            mi.mv.col = refinedG.mv.col;
          }
        }
      }
      // Optional: also try ZEROMV against the GOLDEN reference. If it
      // beats the LAST-relative best by [goldenSadBias], switch the
      // MB's ref to GOLDEN (still ZEROMV, no MV bits spent).
      if (tryGoldenZeroMv) {
        final goldenZeroSad = sad16x16At(
          srcY: effSrcY,
          srcYStride: effSrcYStride,
          ref: prevGolden,
          mbRow: r,
          mbCol: c,
        );
        if (goldenZeroSad +
                (useRdInterMode
                    ? crossRefRdBias(refGolden, zeroMv)
                    : goldenSadBias) <
            bestSad) {
          pick = const InterZeroMvMbSpec(refFrame: refGolden);
          mi.refFrame = refGolden;
          mi.yMode = zeroMv;
          mi.mv.row = 0;
          mi.mv.col = 0;
          bestSad = goldenZeroSad +
              (useRdInterMode ? crossRefRdBias(refGolden, zeroMv) : 0);
        }
      }
      // Symmetric ALTREF ZEROMV eval. Compared against the current
      // best (which may already be GOLDEN); the winner of the three
      // refs takes the slot.
      if (tryAltrefZeroMv) {
        final altrefZeroSad = sad16x16At(
          srcY: effSrcY,
          srcYStride: effSrcYStride,
          ref: prevAltref,
          mbRow: r,
          mbCol: c,
        );
        if (altrefZeroSad +
                (useRdInterMode
                    ? crossRefRdBias(refAltref, zeroMv)
                    : altrefSadBias) <
            bestSad) {
          pick = const InterZeroMvMbSpec(refFrame: refAltref);
          mi.refFrame = refAltref;
          mi.yMode = zeroMv;
          mi.mv.row = 0;
          mi.mv.col = 0;
          bestSad = altrefZeroSad +
              (useRdInterMode ? crossRefRdBias(refAltref, zeroMv) : 0);
        }
      }
      // Symmetric ALTREF NEWMV search. Mirrors the GOLDEN NEWMV
      // block above.
      if (tryAltrefNewMv && clamped > 0) {
        final integerA = useDiamondMotionSearch
            ? diamondIntegerMotionSearch(
                srcY: effSrcY,
                srcYStride: effSrcYStride,
                ref: prevAltref,
                mbRow: r,
                mbCol: c,
                searchRange: clamped,
                mvRateBias: mvRateBias,
                sadPerBit: frameSadPerBit16,
              )
            : naiveIntegerMotionSearch(
                srcY: effSrcY,
                srcYStride: effSrcYStride,
                ref: prevAltref,
                mbRow: r,
                mbCol: c,
                searchRange: clamped,
                mvRateBias: mvRateBias,
                sadPerBit: frameSadPerBit16,
              );
        final refinedA = subPelRefine
            ? subPelRefineSearch(
                srcY: effSrcY,
                srcYStride: effSrcYStride,
                ref: prevAltref,
                mbRow: r,
                mbCol: c,
                integerMv: integerA.mv,
                sadPerBit: frameSadPerBit16,
              )
            : integerA;
        if (refinedA.mv.row != 0 || refinedA.mv.col != 0) {
          final aBias = useRdInterMode
              ? crossRefRdBias(refAltref, newMv, refinedA.mv)
              : altrefNewMvSadBias;
          if (refinedA.sad + aBias < bestSad) {
            bestSad = refinedA.sad + aBias;
            pick = InterNewMvMbSpec(mv: refinedA.mv, refFrame: refAltref);
            mi.refFrame = refAltref;
            mi.yMode = newMv;
            mi.mv.row = refinedA.mv.row;
            mi.mv.col = refinedA.mv.col;
          }
        }
      }
      specs.add(pick);
    }
  }

  final segPair = _buildSegmentation(width, height, effSrcY, effSrcYStride);
  // Decide whether to refresh GOLDEN this inter. With a non-zero
  // [goldenRefreshPeriod] we refresh exactly every Nth inter since
  // the last keyframe, ignoring [refreshGoldenOnInter]; otherwise we
  // honour the static flag.
  final interIndex = _framesSinceKey + 1;
  final periodicGoldenRefresh = goldenRefreshPeriod > 0
      ? (interIndex % goldenRefreshPeriod == 0)
      : refreshGoldenOnInter;
  final refreshGoldenThisFrame =
      _pendingGoldenRequest || periodicGoldenRefresh;
  // Symmetric ALTREF refresh decision.
  final periodicAltrefRefresh = altrefRefreshPeriod > 0
      ? (interIndex % altrefRefreshPeriod == 0)
      : refreshAltrefOnInter;
  final refreshAltrefThisFrame =
      _pendingAltrefRequest || periodicAltrefRefresh;
  // Consume the external request — the next inter is now guaranteed
  // to carry a fresh golden, so subsequent inters revert to the
  // configured periodic/static schedule.
  _pendingGoldenRequest = false;
  _pendingAltrefRequest = false;
  final enc = encodeInterFrame(
    width: width,
    height: height,
    qi: frameQi,
    srcY: effSrcY,
    srcU: srcU,
    srcV: srcV,
    srcYStride: effSrcYStride,
    srcUvStride: srcUvStride,
    mbs: specs,
    prevRef: prevLast,
    prevGolden: prevGolden,
    prevAltref: prevAltref,
    filterLevel: frameFilterLevel,
    sharpness: sharpness,
    filterType: filterType,
    mbNoCoeffSkip: mbNoCoeffSkip,
    probSkipFalse: probSkipFalse,
    refreshLast: true,
    refreshGolden: refreshGoldenThisFrame,
    refreshAltref: refreshAltrefThisFrame,
    log2NumTokenPartitions: log2NumTokenPartitions,
    segmentation: segPair?.spec,
    perMbSegmentIds: segPair?.ids,
  );

  final fresh = _refFromEnc(enc);
  _prevLast = fresh;
  if (refreshGoldenThisFrame) {
    _prevGolden = cloneRefFrame(fresh);
  }
  if (refreshAltrefThisFrame) {
    _prevAltref = cloneRefFrame(fresh);
  }
  _framesSinceKey += 1;
  rc?.update(frameBytes: enc.bytes.length, isKey: false);

  // Stats: walk specs/miList once to tally MB-mode + ref counts.
  var intraMbCount = 0;
  var zeroMvMbCount = 0;
  var nearestMvMbCount = 0;
  var nearMvMbCount = 0;
  var newMvMbCount = 0;
  var newMvLastMbCount = 0;
  var newMvGoldenMbCount = 0;
  var newMvAltrefMbCount = 0;
  var goldenRefMbCount = 0;
  var altrefRefMbCount = 0;
  var mvAbsSum = 0;
  var mvAbsMax = 0;
  var mvCount = 0;
  for (var i = 0; i < specs.length; i++) {
    final m = miList[i];
    if (m.refFrame == refIntra) {
      intraMbCount += 1;
      continue;
    }
    if (m.refFrame == refGolden) goldenRefMbCount += 1;
    if (m.refFrame == refAltref) altrefRefMbCount += 1;
    final ar = m.mv.row >= 0 ? m.mv.row : -m.mv.row;
    final ac = m.mv.col >= 0 ? m.mv.col : -m.mv.col;
    final amax = ar > ac ? ar : ac;
    mvAbsSum += amax;
    if (amax > mvAbsMax) mvAbsMax = amax;
    mvCount += 1;
    switch (m.yMode) {
      case nearestMv:
        nearestMvMbCount += 1;
        break;
      case nearMv:
        nearMvMbCount += 1;
        break;
      case newMv:
        newMvMbCount += 1;
        if (m.refFrame == refGolden) {
          newMvGoldenMbCount += 1;
        } else if (m.refFrame == refAltref) {
          newMvAltrefMbCount += 1;
        } else {
          newMvLastMbCount += 1;
        }
        break;
      default:
        zeroMvMbCount += 1;
    }
  }
  final stats = Vp8FrameStats(
    frameIndex: _cum._framesEncoded,
    isKeyframe: false,
    qi: frameQi,
    filterLevel: frameFilterLevel,
    totalMbCount: mbCount,
    intraMbCount: intraMbCount,
    zeroMvMbCount: zeroMvMbCount,
    nearestMvMbCount: nearestMvMbCount,
    nearMvMbCount: nearMvMbCount,
    newMvMbCount: newMvMbCount,
    newMvLastMbCount: newMvLastMbCount,
    newMvGoldenMbCount: newMvGoldenMbCount,
    newMvAltrefMbCount: newMvAltrefMbCount,
    goldenRefMbCount: goldenRefMbCount,
    altrefRefMbCount: altrefRefMbCount,
    denoisedMbCount: dn?.lastDenoisedMbCount ?? 0,
    staticShortCircuitMbCount: staticShortCircuitMbCount,
    bytesEmitted: enc.bytes.length,
    meanAbsMvQuarterPel: mvCount == 0 ? 0.0 : mvAbsSum / mvCount,
    maxAbsMvQuarterPel: mvAbsMax,
    encodeDurationMicros: sw.elapsedMicroseconds,
    psnrY: _psnrY(srcY, srcYStride, enc.reconY, enc.yStride, width, height),
  );
  _lastStats = stats;
  _cum._accumulate(stats, isKey: false);
  onFrameEncoded?.call(stats);
  return EncodedVp8Frame(
    bytes: enc.bytes,
    isKey: false,
    reconY: enc.reconY,
    reconU: enc.reconU,
    reconV: enc.reconV,
    yStride: enc.yStride,
    uvStride: enc.uvStride,
    stats: stats,
  );
}