hasvk: Emit the inter quantizer matrices and code P residuals on gen7.

MFX_QM_STATE and MFX_FQM_STATE hold separate register sets for the intra
and the inter scaling lists, but only the intra ones were programmed.  The
hardware therefore quantized every inter residual with whatever those
registers happened to hold, so a coded P macroblock came back corrupted
while I frames were correct.  That is why the P path shipped as skip-only.

With both sets programmed, P macroblocks can carry a real residual: encode
them as coded BP_L0_16x16 with a zero motion vector instead of forcing
P_Skip.  A P frame now tracks changing content rather than repeating its
reference.

Measured on a 176x144 clip at QP 26, P frame luma PSNR against the true
source: 12.81 dB before, 43.87 dB after (the I frame scores 44.53 dB), at
2062 bytes versus the I frame's 4474.  With an unchanged source the P frame
costs 67 bytes, down from 4862.

dEQP-VK.video.encode.h264.* goes from 12 to 14 passes with no new failures.
There is still no motion estimation, so a moving block costs a full residual
rather than a motion vector.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
elwrclandClaude Opus 5 committed 2026-08-21 06:43:57 +03:00
1 parent 4cd710a261
commit 30cbb40eb0
1 file changed
+70 -29
+70 -29
View File
@@ -16,9 +16,8 @@
* by decode, so its fields are matched against the VA-API reference encoder * by decode, so its fields are matched against the VA-API reference encoder
* (gen6_mfc_avc_slice_state, intel-vaapi-driver) rather than an in-tree user. * (gen6_mfc_avc_slice_state, intel-vaapi-driver) rather than an in-tree user.
* *
* All values below are validated only for the single-slice, I-frame, CQP case * Validated for I and P frames in CQP mode, single reference, no motion
* currently implemented. Inter prediction, references and rate control are not * estimation. B frames, multiple references and rate control are not emitted.
* emitted.
*/ */
#include <string.h> #include <string.h>
@@ -30,6 +29,16 @@
#include "util/vl_zscan_data.h" #include "util/vl_zscan_data.h"
#include "anv_video_h264_bitstream.h" #include "anv_video_h264_bitstream.h"
/* Debug switch: code P macroblocks as pure skips, carrying no residual. */
static bool
anv_video_enc_skip_only(void)
{
static int enabled = -1;
if (enabled < 0)
enabled = getenv("HASVK_ENC_SKIP_ONLY") != NULL;
return enabled;
}
void void
genX(hasvk_mfc_emit_static_state)(struct anv_cmd_buffer *cmd_buffer, genX(hasvk_mfc_emit_static_state)(struct anv_cmd_buffer *cmd_buffer,
const struct anv_image *source_image, const struct anv_image *source_image,
@@ -205,6 +214,18 @@ genX(hasvk_mfc_emit_static_state)(struct anv_cmd_buffer *cmd_buffer,
qm.ForwardQuantizerMatrix[m * 16 + vl_zscan_normal_16[q]] = qm.ForwardQuantizerMatrix[m * 16 + vl_zscan_normal_16[q]] =
scaling_lists.ScalingList4x4[m][q]; scaling_lists.ScalingList4x4[m][q];
} }
/* Inter matrices are a separate set of registers: ScalingList4x4[3..5] are
* the inter Y/Cb/Cr lists, [0..2] the intra ones. Without this the hardware
* quantizes every inter residual with whatever those registers happen to
* hold, which leaves I frames correct and P residuals corrupted. */
anv_batch_emit(&cmd_buffer->batch, GENX(MFX_QM_STATE), qm) {
qm.DWordLength = 16;
qm.AVC = AVC_4x4_Inter_MATRIX;
for (unsigned m = 0; m < 3; m++)
for (unsigned q = 0; q < 16; q++)
qm.ForwardQuantizerMatrix[m * 16 + vl_zscan_normal_16[q]] =
scaling_lists.ScalingList4x4[3 + m][q];
}
if (pps->flags.transform_8x8_mode_flag) { if (pps->flags.transform_8x8_mode_flag) {
anv_batch_emit(&cmd_buffer->batch, GENX(MFX_QM_STATE), qm) { anv_batch_emit(&cmd_buffer->batch, GENX(MFX_QM_STATE), qm) {
qm.DWordLength = 16; qm.DWordLength = 16;
@@ -213,6 +234,13 @@ genX(hasvk_mfc_emit_static_state)(struct anv_cmd_buffer *cmd_buffer,
qm.ForwardQuantizerMatrix[vl_zscan_normal[q]] = qm.ForwardQuantizerMatrix[vl_zscan_normal[q]] =
scaling_lists.ScalingList8x8[0][q]; scaling_lists.ScalingList8x8[0][q];
} }
anv_batch_emit(&cmd_buffer->batch, GENX(MFX_QM_STATE), qm) {
qm.DWordLength = 16;
qm.AVC = AVC_8x8_Inter_MATRIX;
for (unsigned q = 0; q < 64; q++)
qm.ForwardQuantizerMatrix[vl_zscan_normal[q]] =
scaling_lists.ScalingList8x8[1][q];
}
} }
/* MFX_FQM_STATE (forward quantizer matrix) does not exist on Gen7.5/Gen8; /* MFX_FQM_STATE (forward quantizer matrix) does not exist on Gen7.5/Gen8;
@@ -226,6 +254,12 @@ genX(hasvk_mfc_emit_static_state)(struct anv_cmd_buffer *cmd_buffer,
for (unsigned q = 0; q < 32; q++) for (unsigned q = 0; q < 32; q++)
fqm.ForwardQuantizerMatrix[q] = 0x10001000; fqm.ForwardQuantizerMatrix[q] = 0x10001000;
} }
anv_batch_emit(&cmd_buffer->batch, GENX(MFX_FQM_STATE), fqm) {
fqm.DWordLength = 32;
fqm.AVC = AVC_4x4_Inter_MATRIX;
for (unsigned q = 0; q < 32; q++)
fqm.ForwardQuantizerMatrix[q] = 0x10001000;
}
if (pps->flags.transform_8x8_mode_flag) { if (pps->flags.transform_8x8_mode_flag) {
anv_batch_emit(&cmd_buffer->batch, GENX(MFX_FQM_STATE), fqm) { anv_batch_emit(&cmd_buffer->batch, GENX(MFX_FQM_STATE), fqm) {
fqm.DWordLength = 32; fqm.DWordLength = 32;
@@ -233,6 +267,12 @@ genX(hasvk_mfc_emit_static_state)(struct anv_cmd_buffer *cmd_buffer,
for (unsigned q = 0; q < 32; q++) for (unsigned q = 0; q < 32; q++)
fqm.ForwardQuantizerMatrix[q] = 0x10001000; fqm.ForwardQuantizerMatrix[q] = 0x10001000;
} }
anv_batch_emit(&cmd_buffer->batch, GENX(MFX_FQM_STATE), fqm) {
fqm.DWordLength = 32;
fqm.AVC = AVC_8x8_Inter_MATRIX;
for (unsigned q = 0; q < 32; q++)
fqm.ForwardQuantizerMatrix[q] = 0x10001000;
}
} }
/* Direct MV buffers. I-frames scan no reference direct MVs, so only the /* Direct MV buffers. I-frames scan no reference direct MVs, so only the
@@ -364,14 +404,17 @@ genX(hasvk_mfc_emit_slice)(struct anv_cmd_buffer *cmd_buffer,
* InlineData[6] (DW9): IntraStruct = per-MB IntraPredAvailFlags plus * InlineData[6] (DW9): IntraStruct = per-MB IntraPredAvailFlags plus
* ChromaIntraPredMode = DC in bits [1:0]. * ChromaIntraPredMode = DC in bits [1:0].
* *
* P-frame path: every MB is P_Skip. Per PRM §2.3.1.1, DW3 bit13 IntraMbFlag=0 * P-frame path: every MB is a coded BP_L0_16x16 inter MB with a zero motion
* (inter), bits12:8 MbType5Bits=1 (BP_L0_16x16 — the only P type convertible * vector, so PAK transforms and codes the residual against the co-located
* to skip, PRM MbType table), bit2 SkipMbFlag=1 (forces P_Skip: HW uses the * reference block. Per PRM §2.3.1.1, DW3 bit13 IntraMbFlag=0 (inter),
* predicted MV and forces CBP to 0), bits22:20 MvFormat=0 (no MV data). No * bits12:8 MbType5Bits=1 (BP_L0_16x16), bits22:20 MvFormat=100 (8MV, reading
* residual and no indirect MV buffer are needed; the reconstructed MB equals * the zeroed 32-byte block from the indirect MV buffer; MvFormat=0 hangs the
* the motion-compensated (predicted-MV) reference block. For static content * MFC). Unlike Intra16x16 there is no luma DC block, but chroma keeps its 2x2
* relative to the reference this is exact. DW7-9 carry the inter sub-mb shape * DC one. DW7-9 carry the inter sub-mb shape (0 = 16x16) and reference
* (0 = 16x16) and reference indices L0/L1 (0 for the single reference). */ * indices L0/L1 (0 for the single reference).
*
* There is no motion estimation yet: a moving block still costs a full
* residual rather than a motion vector. */
for (uint32_t mb = slice_start_mb; mb < slice_end_mb; mb++) { for (uint32_t mb = slice_start_mb; mb < slice_end_mb; mb++) {
uint32_t x = mb % width_in_mbs; uint32_t x = mb % width_in_mbs;
uint32_t y = mb / width_in_mbs; uint32_t y = mb / width_in_mbs;
@@ -384,25 +427,23 @@ genX(hasvk_mfc_emit_slice)(struct anv_cmd_buffer *cmd_buffer,
pak.InlineData[3] = ((uint32_t)last_mb << 26) | qp; pak.InlineData[3] = ((uint32_t)last_mb << 26) | qp;
if (is_p) { if (is_p) {
/* P_Skip: SkipMbFlag forces the skip (predicted MV = 0 for the /* HASVK_ENC_SKIP_ONLY falls back to the older all-skip path, which
* uniform all-skip case, CBP forced to 0), so the reconstructed MB * carries no residual at all: useful for isolating whether a P
* equals the co-located reference block. Exact for content identical * frame problem is in the prediction or in the residual. */
* to the reference. An inter MB requires a valid MvFormat (100/110);
* MvFormat=0 hangs the MFC, so 8MV (100) reads a zeroed 32-byte block
* from the dedicated indirect MV buffer.
*
* NOTE: coded inter (SkipMbFlag=0, CBP set) for changed content was
* implemented and works at QP0 (bit-exact residual) but shows per-MB
* blocky artefacts at higher QP — an encoder/decoder inter MV-
* predictor (mvp) mismatch, analogous to the intra edge-DC corner.
* Left as future work (needs per-MB mvp/neighbour handling); skip is
* the validated path. */
pak.IndirectPAKMVDataLength = 32; /* 8MV * 4 bytes */ pak.IndirectPAKMVDataLength = 32; /* 8MV * 4 bytes */
pak.InlineData[0] = (1u << 8) /* MbType5Bits = 1: BP_L0_16x16 */ if (anv_video_enc_skip_only()) {
| (4u << 20) /* MvFormat = 100 (8MV) */ pak.InlineData[0] = (1u << 8) /* MbType5Bits = 1: BP_L0_16x16 */
| (1u << 2); /* SkipMbFlag: force P_Skip */ | (4u << 20) /* MvFormat = 100 (8MV) */
pak.InlineData[1] = (y << 8) | x; /* Cbp4x4Y = 0 for skip */ | (1u << 2); /* SkipMbFlag: force P_Skip */
pak.InlineData[2] = 0; /* Cbp4x4V/U = 0 for skip */ pak.InlineData[1] = (y << 8) | x; /* Cbp4x4Y = 0 for skip */
pak.InlineData[2] = 0; /* Cbp4x4V/U = 0 for skip */
} else {
pak.InlineData[0] = (1u << 8) /* MbType5Bits = 1: BP_L0_16x16 */
| (4u << 20) /* MvFormat = 100 (8MV) */
| (3u << 18); /* CbpDcU/V: chroma DC coded */
pak.InlineData[1] = 0xFFFF0000 | (y << 8) | x; /* all luma 4x4 */
pak.InlineData[2] = 0x000F000F; /* all chroma AC */
}
pak.InlineData[4] = 0; /* inter sub-mb shape = 16x16 */ pak.InlineData[4] = 0; /* inter sub-mb shape = 16x16 */
pak.InlineData[5] = 0; /* ref index L0 = 0 */ pak.InlineData[5] = 0; /* ref index L0 = 0 */
pak.InlineData[6] = 0; /* ref index L1 = 0 */ pak.InlineData[6] = 0; /* ref index L1 = 0 */