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
+65 -24
+65 -24
View File
@@ -16,9 +16,8 @@
* 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.
*
* All values below are validated only for the single-slice, I-frame, CQP case
* currently implemented. Inter prediction, references and rate control are not
* emitted.
* Validated for I and P frames in CQP mode, single reference, no motion
* estimation. B frames, multiple references and rate control are not emitted.
*/
#include <string.h>
@@ -30,6 +29,16 @@
#include "util/vl_zscan_data.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
genX(hasvk_mfc_emit_static_state)(struct anv_cmd_buffer *cmd_buffer,
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]] =
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) {
anv_batch_emit(&cmd_buffer->batch, GENX(MFX_QM_STATE), qm) {
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]] =
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;
@@ -226,6 +254,12 @@ genX(hasvk_mfc_emit_static_state)(struct anv_cmd_buffer *cmd_buffer,
for (unsigned q = 0; q < 32; q++)
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) {
anv_batch_emit(&cmd_buffer->batch, GENX(MFX_FQM_STATE), fqm) {
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++)
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
@@ -364,14 +404,17 @@ genX(hasvk_mfc_emit_slice)(struct anv_cmd_buffer *cmd_buffer,
* InlineData[6] (DW9): IntraStruct = per-MB IntraPredAvailFlags plus
* ChromaIntraPredMode = DC in bits [1:0].
*
* P-frame path: every MB is P_Skip. Per PRM §2.3.1.1, DW3 bit13 IntraMbFlag=0
* (inter), bits12:8 MbType5Bits=1 (BP_L0_16x16 — the only P type convertible
* to skip, PRM MbType table), bit2 SkipMbFlag=1 (forces P_Skip: HW uses the
* predicted MV and forces CBP to 0), bits22:20 MvFormat=0 (no MV data). No
* residual and no indirect MV buffer are needed; the reconstructed MB equals
* the motion-compensated (predicted-MV) reference block. For static content
* relative to the reference this is exact. DW7-9 carry the inter sub-mb shape
* (0 = 16x16) and reference indices L0/L1 (0 for the single reference). */
* P-frame path: every MB is a coded BP_L0_16x16 inter MB with a zero motion
* vector, so PAK transforms and codes the residual against the co-located
* reference block. Per PRM §2.3.1.1, DW3 bit13 IntraMbFlag=0 (inter),
* bits12:8 MbType5Bits=1 (BP_L0_16x16), bits22:20 MvFormat=100 (8MV, reading
* the zeroed 32-byte block from the indirect MV buffer; MvFormat=0 hangs the
* MFC). Unlike Intra16x16 there is no luma DC block, but chroma keeps its 2x2
* DC one. DW7-9 carry the inter sub-mb shape (0 = 16x16) and 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++) {
uint32_t x = 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;
if (is_p) {
/* P_Skip: SkipMbFlag forces the skip (predicted MV = 0 for the
* uniform all-skip case, CBP forced to 0), so the reconstructed MB
* equals the co-located reference block. Exact for content identical
* 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. */
/* HASVK_ENC_SKIP_ONLY falls back to the older all-skip path, which
* carries no residual at all: useful for isolating whether a P
* frame problem is in the prediction or in the residual. */
pak.IndirectPAKMVDataLength = 32; /* 8MV * 4 bytes */
if (anv_video_enc_skip_only()) {
pak.InlineData[0] = (1u << 8) /* MbType5Bits = 1: BP_L0_16x16 */
| (4u << 20) /* MvFormat = 100 (8MV) */
| (1u << 2); /* SkipMbFlag: force P_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[5] = 0; /* ref index L0 = 0 */
pak.InlineData[6] = 0; /* ref index L1 = 0 */