Files
LibretroAdmin ca3f5c4380 IPU: a coefficient split by a suspension resumes from ipu_cmd
When the bitstream runs dry between the run/level code of an AC
coefficient and its sign or escape bits, the block decoders suspend and
are called again. The second half needs only whether the code was an
escape and its level; both now travel in ipu_cmd.pos[5], which the
savestate carries, and the table entry is local to the call. A state
loaded into a fresh process resumes the coefficient from that.

pos[5] holds bit 1 to say the resume data is there, bit 0 for the
escape, and the level from bit 8 up. A state written by an older build
holds 1 there with nothing to go on, and that block ends at the
suspension.

tests/ipu/slice_hash.cpp decodes every fixture again with the input
running dry on every other quadword, so that hundreds of coefficients
are split across a suspension, and requires the same hash as the full
feed. It also resumes a split coefficient, and a state in the older
form, before anything has been decoded in the process.
2026-10-08 16:11:09 +02:00

677 lines
21 KiB
C++

/* The IPU slice decoder against ffmpeg, on real MPEG-2 streams.
*
* Everything below the slice header is the IPU's job: the macroblock
* address increment, the coded block pattern, the intra and non-intra VLC
* tables, the dequantiser, the scan order, DC prediction, and the inverse
* DCT. None of it had a test. The synthetic blocks the IDCT harness uses
* cannot reach it, because reaching it means producing a legal bitstream.
*
* So the fixtures are real streams, encoded by ffmpeg, and the reference
* pixels are ffmpeg's own decode of the same frame. A bug anywhere in that
* chain moves the output away from a decoder that shares no code with this
* one. The two do not agree bit for bit -- MPEG-2 does not specify one
* inverse DCT, and the two implementations round differently -- so the
* comparison is a bound, which is what IEEE 1180 asks of any IDCT pair.
*
* The hash on top of that is the bit-exact part: it pins what this tree
* produces, so a change meant to be free can be shown to be.
*
* The decoder is reached by including its translation unit, since the slice
* functions are static. Everything it links against that is not the decode
* path itself is stubbed below.
*
* ./slice_build.sh check against the pinned hashes
* ./slice_build.sh --print print hashes to re-pin
*/
#include <cstdio>
#include <cstring>
#include <cstdlib>
/* ------------------------------------------------------------------ */
/* Everything the decoder TU expects to find at link time. */
/* ------------------------------------------------------------------ */
#include "IPU/IPU_MultiISA.h"
#include "IPU/IPUdma.h"
#include "IPU/ipu_bitstream.h"
/* ipuRegs and ipu0ch are references into eeHw, so defining that array is
* what makes the register and channel state the decoder reads exist. */
PCSX2_ALIGN(__pagealignsize) u8 eeHw[PS2MEM_HARDWARE];
alignas(16) cpuRegisters cpuRegs;
alignas(16) decoder_t decoder;
alignas(16) tIPU_BP g_BP;
alignas(16) tIPU_cmd ipu_cmd;
IPUStatus IPUCoreStatus;
alignas(16) IPU_Fifo ipu_fifo;
int coded_block_pattern;
/* From IPU.cpp, which the harness does not link: the MPEG-2 non-linear
* quantiser scale table. */
alignas(16) const int non_linear_quantizer_scale[32] =
{
0, 1, 2, 3, 4, 5, 6, 7,
8, 10, 12, 14, 16, 18, 20, 22,
24, 28, 32, 36, 40, 44, 48, 52,
56, 64, 72, 80, 88, 96, 104, 112
};
alignas(16) u8 g_ipu_indx4[16 * 16 / 2];
rgb16_t g_ipu_vqclut[16];
u16 g_ipu_thresh[2];
void CPU_INT(EE_EventType, int) {}
void hwIntcIrq(int) {}
/* The IDEC and CSC paths reach EE memory. They are not on the route this
* drives, and at -O1 and above the optimiser drops them, but the harness
* should link without depending on that -- not least so it can be built at
* -O0, where a sanitizer reports most clearly. */
RETURNS_R128 vtlb_memRead128(u32) { return r128_zero(); }
void TAKES_R128 vtlb_memWrite128(u32, r128) {}
/* ---- the input FIFO is where the slice bits come from ---- */
static u8 g_srcbuf[1 << 16];
static const u8 *g_src;
static size_t g_src_len;
static size_t g_src_qwc; /* next quadword to hand over */
/* Set, the FIFO is empty on every other read: the decoder suspends
* wherever it next needs bits, and is called again. */
static int g_starve;
static unsigned g_starve_tick;
int IPU_Fifo_Input::read(void *value)
{
u8 *dst = (u8 *)value;
size_t off = g_src_qwc * 16;
if (g_starve && (g_starve_tick++ & 1))
return 0;
if (off >= g_src_len)
{
/* Out of stream. Report the stall rather than handing over
* zeroes: a decoder fed endless zeroes past the end can spin
* instead of yielding, and the yield is what the caller
* needs to see to stop. */
return 0;
}
memset(dst, 0, 16);
memcpy(dst, g_src + off,
(g_src_len - off) < 16 ? (g_src_len - off) : 16);
g_src_qwc++;
return 1;
}
/* The bit pointer reaches the FIFO through this, as IPU_Fifo.cpp provides
* it in the emulator. */
extern "C" int ipu_fifo_in_read(void *value)
{
return ipu_fifo.in.read(value);
}
/* ---- the output FIFO is not what this checks; mb8 is ---- */
int IPU_Fifo_Output::write(const u32 *, u32 size)
{
return size;
}
/* Negative testing: build with -DIPU_MUTANT against a copy of the decoder
* carrying a deliberate defect, to show this harness rejects it. Nothing
* in the tree defines it. */
#ifdef IPU_MUTANT
#include "IPU/mutant_dec.cpp"
#else
#include "IPU/IPU_MultiISA.cpp"
#endif
using namespace CURRENT_ISA;
#include "stream_fixtures.h"
/* ------------------------------------------------------------------ */
static uint64_t hash_init(void) { return 1469598103934665603ull; }
static void hash_bytes(uint64_t *h, const void *p, size_t n)
{
const unsigned char *b = (const unsigned char *)p;
for (size_t i = 0; i < n; i++)
*h = (*h ^ (uint64_t)b[i]) * 1099511628211ull;
}
static long compared;
static long st_n, st_sum, st_peak, st_interlaced;
static long interlace_checked, interlace_bad;
static long failures;
static int worst_err;
static long errhist[16];
static const char *worst_where = "";
static void note(int err, const char *where)
{
errhist[err < 15 ? err : 15]++;
st_n++; st_sum += err;
if (err > st_peak) st_peak = err;
if (err > worst_err)
{
worst_err = err;
worst_where = where;
}
}
/* ------------------------------------------------------------------ */
static u32 hread(u32 n)
{
const u32 v = ipu_bits_u32(g_BP.internal_qwc[0]._u8, g_BP.BP, n);
ipu_bp_advance(&g_BP, n);
return v;
}
static u32 hpeek(u32 n)
{
return ipu_bits_u32(g_BP.internal_qwc[0]._u8, g_BP.BP, n);
}
/* BDEC starts at the block data, so the macroblock header above it is the
* EE's job on hardware and the harness's here. In an I picture macroblocks
* cannot be skipped, so the address increment is always the single bit 1,
* and macroblock_type is 1 for intra or 01 for intra carrying a new
* quantiser scale.
*
* A frame picture that did not promise frame-only prediction carries a
* dct_type bit after macroblock_type, and that bit picks the interlaced
* block layout. Encoders turn alternate scan on together with it, so the
* alternate-scan fixture is also the one that exercises that layout. */
static int macroblock_header(const ipu_stream *st, int *quant_out,
int *dct_type_out)
{
*dct_type_out = 0;
if (hpeek(1) != 1)
return 0; /* an increment this does not handle */
hread(1);
if (hpeek(1) == 1)
hread(1); /* macroblock_type: intra */
else if (hpeek(2) == 1)
{
hread(2); /* macroblock_type: intra, quant follows */
{
const u32 qsc = hread(5);
*quant_out = st->q_scale_type
? non_linear_quantizer_scale[qsc]
: (int)(qsc << 1);
}
}
else
return 0;
if (st->picture_structure == 3 && !st->frame_pred_frame_dct)
*dct_type_out = (int)hread(1);
return 1;
}
/* Set up decoder the way ipuBDEC does, for one slice of one stream. */
static void setup(const ipu_stream *st, const ipu_slice *sl)
{
memset(&decoder, 0, sizeof(decoder));
memcpy(decoder.iq, st->intra_q, 64);
memcpy(decoder.niq, st->non_intra_q, 64);
decoder.coding_type = I_TYPE;
decoder.mpeg1 = 0;
decoder.q_scale_type = st->q_scale_type;
decoder.intra_vlc_format = st->intra_vlc_format;
decoder.scantype = st->alternate_scan;
decoder.intra_dc_precision = st->intra_dc_precision;
decoder.picture_structure = st->picture_structure;
decoder.frame_pred_frame_dct = st->frame_pred_frame_dct;
decoder.quantizer_scale = decoder.q_scale_type
? non_linear_quantizer_scale[sl->qsc]
: sl->qsc << 1;
/* Intra macroblocks, frame DCT, and reset the DC predictors once at
* the head of the slice -- which is what the slice header means. */
decoder.macroblock_modes = MACROBLOCK_INTRA;
decoder.dcr = 1;
memset(&ipu_cmd, 0, sizeof(ipu_cmd));
memset(&g_BP, 0, sizeof(g_BP));
/* Hand the decoder the stream from the quadword the slice's bits
* start in, and point BP at the bit within it. */
{
const size_t base = ((size_t)sl->bitpos / 8) & ~(size_t)15;
const size_t len = st->nbytes - base;
memset(g_srcbuf, 0, sizeof(g_srcbuf));
memcpy(g_srcbuf, st->bytes + base, len);
/* The decoder reads ahead past the last macroblock looking
* for the next start code. At the end of the picture there
* isn't one, so put a sequence_end there: without it the
* read-ahead walks zeroes to the end of the buffer. */
g_srcbuf[len + 0] = 0x00; g_srcbuf[len + 1] = 0x00;
g_srcbuf[len + 2] = 0x01; g_srcbuf[len + 3] = 0xb7;
g_src = g_srcbuf;
g_src_len = len + 64;
}
g_src_qwc = 0;
g_BP.FP = 0;
g_BP.BP = (u32)(sl->bitpos - (((sl->bitpos / 8) & ~15) * 8));
ipu_bp_fill_buffer(&g_BP, 32);
}
/* The fixtures again, with the input running dry every other quadword.
* Every suspension is a point a savestate can be taken at, and the
* decoder has to come back from each with nothing but what it keeps in
* ipu_cmd; so the macroblocks have to come out as they did fed in full.
* resumed counts the coefficients that went over a suspension between
* their two halves. */
static uint64_t starved_hash(long *resumed)
{
uint64_t h = hash_init();
g_starve_tick = 0;
*resumed = 0;
for (size_t si = 0; si < sizeof(ipu_streams) / sizeof(ipu_streams[0]); si++)
{
const ipu_stream *st = ipu_streams[si];
for (int s = 0; s < st->nslices; s++)
{
g_starve = 0;
setup(st, &st->slices[s]);
for (int mx = 0; mx < st->mbw; mx++)
{
int quant = decoder.quantizer_scale;
int dct_type = 0;
long spin = 0;
bool done = false;
/* The slice setup and the header are this harness's to
* read, fed in full; only the decoder is starved. */
if (!macroblock_header(st, &quant, &dct_type))
return 0;
decoder.quantizer_scale = quant;
decoder.macroblock_modes = MACROBLOCK_INTRA
| (dct_type ? DCT_TYPE_INTERLACED : 0);
memset(&ipu_cmd, 0, sizeof(ipu_cmd));
g_starve = 1;
while (!done)
{
ipu0ch.qwc = 0x4000;
ipuRegs.ctrl.OFC = 0;
done = mpeg2_slice();
if (!done && ipu_cmd.pos[5] >= 2)
(*resumed)++;
if (!done && ++spin > 100000)
{
g_starve = 0;
return 0;
}
}
g_starve = 0;
decoder.dcr = 0;
hash_bytes(&h, &decoder.mb8, sizeof(decoder.mb8));
}
}
}
return h;
}
/* A state loaded into a process that has decoded nothing, parked between
* the two halves of a coefficient: the second half is decoded from what
* ipu_cmd carries, and a state from before that carried it ends the
* block rather than reading a table entry no process has. */
static int resume_from_state(void)
{
static const u8 stream[64] = { 0x80, 0x00 };
int ok = 1;
memset(&decoder, 0, sizeof(decoder));
for (int i = 0; i < 64; i++)
decoder.iq[i] = 16;
decoder.quantizer_scale = 8;
memset(g_srcbuf, 0, sizeof(g_srcbuf));
memcpy(g_srcbuf, stream, sizeof(stream));
g_src = g_srcbuf; g_src_len = sizeof(stream); g_src_qwc = 0;
memset(&g_BP, 0, sizeof(g_BP));
ipu_bp_fill_buffer(&g_BP, 32);
memset(&ipu_cmd, 0, sizeof(ipu_cmd));
memset(decoder.DCTblock, 0, sizeof(decoder.DCTblock));
ipu_cmd.pos[4] = 4;
ipu_cmd.pos[5] = IPU_DCT_RESUME(0, 3);
get_intra_block();
/* level 3 at scan position 5, sign bit 1: -(3 * 8 * 16 >> 4) */
ok &= decoder.DCTblock[mpeg2_scan.norm[5]] == -24;
memset(&ipu_cmd, 0, sizeof(ipu_cmd));
ipu_cmd.pos[4] = 4;
ipu_cmd.pos[5] = IPU_DCT_LEGACY_RESUME;
ok &= get_intra_block() && ipu_cmd.pos[5] == 0 && ipu_cmd.pos[4] == 0;
return ok;
}
int main(int argc, char **argv)
{
const int print = (argc > 1 && strcmp(argv[1], "--print") == 0);
uint64_t h = hash_init();
/* Pinned from the tree as it stands. */
const uint64_t want_h = 0xfbccfec76a35fe0bull;
/* How far the IPU may sit from ffmpeg.
*
* The two do not use the same inverse DCT and the gap is not
* constant: it tracks how much high-frequency content a block
* carries, because the IPU's row pass stores its intermediate back
* to s16 and loses precision an unclamped one keeps. Measured on
* these fixtures: a flat frame quantised to DC only comes out
* bit-exact, a gradient sits at 0.27 mean, and detailed frames run
* 4 to 5 mean with peaks around 60.
*
* So the bound is per stream rather than per pixel, and it comes in
* two grades. The flat and smooth fixtures carry almost no
* high-frequency energy, so there the two decoders have to agree
* closely -- that is where the decode itself is shown to be right.
* The detailed fixtures only get a loose bound, enough to reject the
* shape of a real defect: the wrong scan order or a mis-stepped VLC
* table puts the mean in the tens and the peak past 200. Anything
* finer than that on detailed content is the hash's job, not this
* one's. */
const double tight_mean = 1.0, loose_mean = 12.0;
const long tight_peak = 8, loose_peak = 110;
/* Before anything is decoded, as in a process that has just loaded
* the state. */
const int resumed_ok = resume_from_state();
printf("tier: ");
#if defined(__AVX2__)
printf("avx2\n");
#elif defined(__SSE4_1__)
printf("sse4.1\n");
#elif defined(_M_ARM64) || defined(__aarch64__)
printf("aarch64 (NEON kernels)\n");
#else
printf("sse2\n");
#endif
/* The IPU0 channel has to look ready or the slice decoder parks. */
ipu0ch.chcr.STR = 1;
ipu0ch.qwc = 0x4000;
ipuRegs.ctrl.OFC = 0;
for (size_t si = 0; si < sizeof(ipu_streams) / sizeof(ipu_streams[0]); si++)
{
const ipu_stream *st = ipu_streams[si];
int mb_total = 0;
int stream_bad = 0;
for (int s = 0; s < st->nslices; s++)
{
const ipu_slice *sl = &st->slices[s];
setup(st, sl);
for (int mx = 0; mx < st->mbw; mx++)
{
const u8 *ref = st->ref + (size_t)(s * st->mbw + mx) * 384;
/* mpeg2_slice is a coroutine: it yields false
* whenever it wants more input or wants the
* output taken, keeping its place in
* ipu_cmd.pos. A macroblock is done when it
* finally returns true, so zero the resume
* state once and then call until it does. */
int spin = 0;
bool done = false;
int quant = decoder.quantizer_scale;
int dct_type = 0;
if (!macroblock_header(st, &quant, &dct_type))
{
printf(" %s: slice %d macroblock %d has a header "
"this harness does not parse\n",
st->name, s, mx);
failures++;
stream_bad = 1;
break;
}
decoder.quantizer_scale = quant;
decoder.macroblock_modes = MACROBLOCK_INTRA
| (dct_type ? DCT_TYPE_INTERLACED : 0);
if (dct_type) st_interlaced++;
tIPU_BP bp_before;
size_t qwc_before;
s16 pred_before[3];
int dcr_before = decoder.dcr;
memcpy(&bp_before, &g_BP, sizeof(g_BP));
qwc_before = g_src_qwc;
pred_before[0] = decoder.dc_dct_pred[0];
pred_before[1] = decoder.dc_dct_pred[1];
pred_before[2] = decoder.dc_dct_pred[2];
memset(&ipu_cmd, 0, sizeof(ipu_cmd));
while (!done)
{
ipu0ch.qwc = 0x4000;
ipuRegs.ctrl.OFC = 0;
done = mpeg2_slice();
if (!done && ++spin > 64)
break;
}
if (!done)
{
printf(" %s: slice %d macroblock %d did not "
"finish [pos0=%d pos1=%d]\n",
st->name, s, mx,
ipu_cmd.pos[0], ipu_cmd.pos[1]);
failures++;
stream_bad = 1;
break;
}
mb_total++;
/* Only the first macroblock of a slice resets the
* DC predictors; the rest carry them forward. */
decoder.dcr = 0;
hash_bytes(&h, &decoder.mb8, sizeof(decoder.mb8));
if (getenv("IPU_MAP") && s == 0 && mx == 0)
{
printf(" %s s0 mb0 luma |error| map:\n", st->name);
for (int y = 0; y < 16; y++)
{
printf(" ");
for (int x = 0; x < 16; x++)
{
int e = decoder.mb8.Y[y][x] - ref[y * 16 + x];
if (e < 0) e = -e;
printf("%4d", e);
}
printf("\n");
}
}
for (int y = 0; y < 16; y++)
for (int x = 0; x < 16; x++)
{
const int got = decoder.mb8.Y[y][x];
const int want = ref[y * 16 + x];
compared++;
note(got > want ? got - want : want - got, "luma");
}
/* The interlaced block layout is the one path here that
* no encoder would choose for this material, so it is
* reached by decoding the same bits a second time with
* it forced on. The two layouts write the same six
* blocks at different strides, so the results have to
* be related by the field interleave exactly -- rows
* 0..7 of the frame result land on the even rows and
* rows 8..15 on the odd ones. A wrong DCT_offset or
* DCT_stride breaks that relation. */
if (dct_type == 0)
{
macroblock_8 frame_mb = decoder.mb8;
tIPU_BP bp_after;
size_t qwc_after = g_src_qwc;
s16 pred_after[3];
memcpy(&bp_after, &g_BP, sizeof(g_BP));
pred_after[0] = decoder.dc_dct_pred[0];
pred_after[1] = decoder.dc_dct_pred[1];
pred_after[2] = decoder.dc_dct_pred[2];
memcpy(&g_BP, &bp_before, sizeof(g_BP));
g_src_qwc = qwc_before;
decoder.dc_dct_pred[0] = pred_before[0];
decoder.dc_dct_pred[1] = pred_before[1];
decoder.dc_dct_pred[2] = pred_before[2];
/* dcr is consumed by the pass that sees it, so the
* rewind has to put it back or the second pass
* skips the predictor reset at the head of a
* slice and decodes different DC values. */
decoder.dcr = dcr_before;
decoder.macroblock_modes =
MACROBLOCK_INTRA | DCT_TYPE_INTERLACED;
memset(&ipu_cmd, 0, sizeof(ipu_cmd));
spin = 0; done = false;
while (!done)
{
ipu0ch.qwc = 0x4000;
ipuRegs.ctrl.OFC = 0;
done = mpeg2_slice();
if (!done && ++spin > 64)
break;
}
if (done)
{
if (getenv("IPU_ILMAP") && si == 0 && s == 0 && mx == 0)
{
printf(" rows that match frame row k:\n");
for (int y = 0; y < 16; y++)
{
printf(" il row %2d ->", y);
for (int k = 0; k < 16; k++)
{
int same = 1;
for (int x = 0; x < 16; x++)
if (decoder.mb8.Y[y][x] != frame_mb.Y[k][x])
{ same = 0; break; }
if (same) printf(" %d", k);
}
printf("\n");
}
}
for (int y = 0; y < 16; y++)
for (int x = 0; x < 16; x++)
{
const int src = (y & 1) ? 8 + (y >> 1)
: (y >> 1);
interlace_checked++;
if (decoder.mb8.Y[y][x]
!= frame_mb.Y[src][x])
interlace_bad++;
}
}
else
interlace_bad++;
/* Put the decoder back exactly where the frame
* pass left it, so the next macroblock resumes
* from the right bit and the right predictors. */
memcpy(&g_BP, &bp_after, sizeof(g_BP));
g_src_qwc = qwc_after;
decoder.dc_dct_pred[0] = pred_after[0];
decoder.dc_dct_pred[1] = pred_after[1];
decoder.dc_dct_pred[2] = pred_after[2];
decoder.dcr = 0;
decoder.mb8 = frame_mb;
}
for (int y = 0; y < 8; y++)
for (int x = 0; x < 8; x++)
{
const int gcb = decoder.mb8.Cb[y][x];
const int gcr = decoder.mb8.Cr[y][x];
const int wcb = ref[256 + y * 8 + x];
const int wcr = ref[320 + y * 8 + x];
compared += 2;
note(gcb > wcb ? gcb - wcb : wcb - gcb, "Cb");
note(gcr > wcr ? gcr - wcr : wcr - gcr, "Cr");
}
}
if (stream_bad)
break;
}
{
const double mean = st_n ? (double)st_sum / (double)st_n : 0.0;
const double ml = st->tight ? tight_mean : loose_mean;
const long pl = st->tight ? tight_peak : loose_peak;
const int over = (mean > ml) || (st_peak > pl)
|| (mb_total != st->nslices * st->mbw);
printf(" %-9s %2d/%2d macroblocks mean |err| %6.3f peak %3ld %s%s\n",
st->name, mb_total, st->nslices * st->mbw,
mean, st_peak, st->tight ? "tight" : "loose",
over ? " OVER LIMIT" : "");
if (over)
failures++;
st_n = st_sum = st_peak = 0;
}
}
if (print)
{
printf(" { %016llx }\n", (unsigned long long)h);
return 0;
}
{
long resumed;
const uint64_t hs = starved_hash(&resumed);
printf(" starved input: %016llx, %ld coefficients resumed between halves%s\n",
(unsigned long long)hs, resumed,
(hs == h && resumed > 0) ? " ok" : " MISMATCH");
if (hs != h || resumed == 0)
failures++;
}
printf(" resume from a state: %s\n", resumed_ok ? "ok" : "FAILED");
if (!resumed_ok)
failures++;
printf(" slice %016llx%s\n", (unsigned long long)h,
want_h == 0 ? " (unpinned)" :
(h == want_h ? " ok" : " UNEXPECTED"));
printf(" worst deviation from ffmpeg: %d (%s)\n", worst_err, worst_where);
printf(" interlaced layout: %ld pixels checked against the field "
"interleave, %ld wrong\n", interlace_checked, interlace_bad);
if (interlace_bad)
failures++;
printf(" |error| histogram:");
for (int i = 0; i <= (worst_err < 15 ? worst_err : 15); i++)
printf(" %d:%ld", i, errhist[i]);
printf("\n");
{
const int bad = (failures != 0) || (want_h && h != want_h);
printf("%s: IPU slice decode, %ld pixel comparisons, %ld failures\n",
bad ? "FAIL" : "PASS", compared, failures);
return bad;
}
}