Skip to content

RISC-V support: misaligned-access fixes + scalar fast paths (fixes #28) - #29

Open
youkorr wants to merge 4 commits into
tvlabs:masterfrom
youkorr:riscv-esp32p4
Open

RISC-V support: misaligned-access fixes + scalar fast paths (fixes #28)#29
youkorr wants to merge 4 commits into
tvlabs:masterfrom
youkorr:riscv-esp32p4

Conversation

@youkorr

@youkorr youkorr commented Jul 2, 2026

Copy link
Copy Markdown

RISC-V support: misaligned-access fixes + scalar fast paths (fixes #28)

Summary

This series makes edge264 work — and perform — on SIMD-less RISC-V
targets (developed and validated on the ESP32-P4, RV32IMAFC @ 400 MHz,
decoding live RTSP High Profile streams from surveillance cameras).

It contains three commits:

  1. riscv: fix misaligned-trap crashes and harden the slice loop
    fixes the crash reported in ESP32‑P4 port: crash in add_idct8x8 when decoding High Profile streams #28. On RISC-V a misaligned wide access
    traps (x86/ARM tolerate it, and qemu-user emulates it, which is why
    it doesn't reproduce there). The aligned wide accesses of the High
    Profile 8x8 kernels are routed through fixed-size __builtin_memcpy
    wrappers under #ifdef __riscv — identical values, trap-free
    lowering, zero impact on other ISAs. Also adds a hard bound on the
    slice macroblock loop so a clobbered pic_width_in_mbs can no longer
    hang the decoder (watchdog reset on embedded targets).

  2. riscv: scalar deblocking pipeline — with the vector code
    scalarized by the compiler, the deblocking transposes dominate whole-
    frame cost (62-75% of decode time in a gprof profile). Adds an
    in-place scalar pixel pipeline (exact 8.7.2.3/8.7.2.4 formulas, used
    on __riscv only) plus two cheap bS=0 skip fast paths.

  3. riscv: scalar unweighted motion compensation — same treatment
    for the second hotspot: scalar 6-tap/bilinear kernels for the 15
    fractional positions, full-pel unweighted prediction as plain row
    copies. Weighted/bipred keep the vector path.

Correctness

Every commit is bit-exact against the SSE build, verified with a
decode-and-hash harness (x86 SIMD build as reference, riscv64 build run
under qemu-user) on 12 streams covering: IDR/P, CABAC and CAVLC,
B-frames, weighted prediction (weightp=2), QP 15 and 40, 8x8 transform
on/off, multi-slice frames, and non-zero deblock alpha/beta offsets.

Performance (640x360 High/CABAC, qemu-riscv64, same machine)

workload before after speedup
IDR frame 102 ms 20 ms 5.0x
P frame, static scene 11.7 ms 3.1 ms 3.8x
P frame, moving scene 29.7 ms 7.2 ms 4.1x

On the real target (ESP32-P4 @ 400 MHz, PSRAM frame buffers, live Tapo
camera): first IDR went from ~13 s to ~180 ms, P-frames from ~200 ms to
25-40 ms — real-time decoding of a 15 fps High Profile stream on a
microcontroller.

Non-RISC-V targets are unaffected: the scalar kernels and the memcpy
load wrappers are compiled only under #ifdef __riscv; the two bS=0
deblock skip paths are active everywhere but are pure wins.

youkorr added 4 commits July 2, 2026 17:04
Fixes the ESP32-P4 (RV32IMAFC) crash reported in tvlabs#28: on RISC-V a
misaligned wide load/store TRAPS, unlike x86/ARM which tolerate it and
unlike qemu-user which emulates it transparently. The High Profile 8x8
kernels (add_idct8x8, weighted DC in headers, intra) access the sample
plane with aligned wide accesses while the plane is not guaranteed
16-aligned on this target, faulting after the first IDR.

- edge264_internal.h: on __riscv, route loada32/64/128 (and the x2/x4
  gathers) through fixed-size __builtin_memcpy wrappers. Values are
  identical; the compiler lowers them to trap-free narrow accesses.
  Other ISAs are unaffected (the wrappers are #ifdef __riscv).
- edge264_residual.c, edge264_intra.c, edge264_headers.c: convert the
  remaining raw wide plane accesses to the loada/storea helpers so the
  RISC-V wrappers actually cover them.
- edge264_slice.c: add a hard bound on the macroblock loop. If ctx->t
  is clobbered by a malformed live stream, pic_width_in_mbs can grow so
  the natural end-of-frame check never fires and the loop never returns
  (watchdog reset on embedded targets). The bound is captured while the
  context is still valid, with an absolute 65536-MB ceiling as a last
  resort; legitimate frames are never truncated.

Validated bit-exact against the SSE build on 12 streams (IDR/P,
CABAC+CAVLC, B-frames, weighted prediction, QP 15-40, multi-slice,
deblock offsets, 8x8 transform on/off) with a qemu-riscv64 harness.
On an ISA without a vector unit (RISC-V without V, e.g. ESP32-P4) the
compiler scalarizes the 128-bit deblocking code; the 16x16/8x8 byte
transposes explode into thousands of byte moves per macroblock and
dominate whole-frame cost: profiling the scalarized build shows
deblock_mb at 62-75% of total decode time.

- Add a scalar pixel pipeline (used on __riscv only): each edge is
  filtered in place with the exact ITU-T H.264 8.7.2.3/8.7.2.4 formulas,
  no transposes, with per-4-pixel-group (tC0<0) and per-pixel
  (filterSamplesFlag) early exits. The vector path is untouched and
  still used on SIMD targets.
- Add two skip fast paths in deblock_mb, active on all targets: when
  every tC0 is -1 and no neighbour is intra, the pixel stage is a pure
  identity and is skipped; and a ~40-op scalar pre-test detects the
  all-bS=0 case for P macroblocks with uniform motion and no residuals
  (static surveillance scenes) before the vector strength computation.

Measured on qemu-riscv64 (640x360 High/CABAC): IDR 102 -> 20 ms, static
P-frames 11.7 -> 3.1 ms, moving P 29.7 -> 8.2 ms. On an ESP32-P4 @
400 MHz the first IDR of a live RTSP camera went from ~13 s to ~180 ms.
Bit-exact against the SSE build on the 12-stream suite.
The scalarized bilinear/6-tap interpolators are the second hotspot on
SIMD-less RISC-V (up to 40% of P-frame time as soon as the scene moves,
e.g. PTZ pans). Add scalar kernels implementing H.264 8.4.2.2.1/2
exactly, used on __riscv for unweighted single prediction only:

- luma: 6-tap half-pel H/V, center j via unclipped intermediates
  (+512)>>10, quarter-pel averages for all 15 fractional positions
- chroma: bilinear (8-xF)(8-yF) with pure-H/pure-V shortcuts, matching
  the interleaved Cb/Cr row layout of the vector kernel
- full-pel unweighted prediction reduces to plain row copies (all
  targets): the dominant case on static surveillance streams (P_Skip)

Weighted and bidirectional prediction keep the vector path (its
weighting stage blends into dst). Measured on qemu-riscv64: moving
scene P-frames 23.4 -> 7.2 ms. Bit-exact against the SSE build on the
12-stream suite, including the weightp and B-frame streams which
exercise the vector fallback.
Requested in tvlabs#28: a short clip to guard the misaligned-access crash fix
(commit 1), and a test file to guard the scalar deblock/motion fast
paths (commits 2-3) against the planned Q4-2026 multithreading rework.

- crash-repro-8x8-riscv.264: minimal High/CABAC/8x8-transform clip
  exercising the code path that used to trap on RISC-V.
- riscv-scalar-fastpaths.264 + .golden: static + panning content
  exercising both fast paths, with per-frame FNV-1a hashes recorded
  from an unmodified x86 SIMD build as backend-independent ground
  truth.
- riscv_regression_check.c: ~150-line, dependency-free (edge264.h +
  libc only) harness to decode a clip and diff per-frame hashes against
  a golden file, exiting non-zero with the first mismatching frame.

See tests/README-riscv-regression.md for usage.
@youkorr

youkorr commented Jul 7, 2026

Copy link
Copy Markdown
Author

Hi Thibault,

Pushed a 4th commit addressing both of your requests:

Short clip for the crash fixtests/crash-repro-8x8-riscv.264 (16 KB, QCIF). High profile, CABAC, 8x8 transform enabled, so it exercises the exact code path (add_idct8x8 and friends) that used to trap on misaligned RISC-V accesses.

Test file for the scalar fast-path fixtests/riscv-scalar-fastpaths.264 + .golden. A static scene (exercises the deblock bS=0 skip and the full-pel motion-compensation fast path) followed by a synthetic pan (exercises the scalar 6-tap/bilinear fractional MC kernels). The .golden file is one FNV-1a hash per output frame, recorded from an unmodified x86 SIMD build — backend-independent ground truth, not "the RISC-V answer".

To make it easy to run against any build (your default SIMD build, the scalar CLANG backend, a real RISC-V cross build, whatever), I included a small (~150-line) standalone harness, tests/riscv_regression_check.c — only depends on edge264.h + libc:

cc -O2 -I. -o riscv_regression_check tests/riscv_regression_check.c src/edge264.c -lpthread -lm
./riscv_regression_check tests/riscv-scalar-fastpaths.264 0 tests/riscv-scalar-fastpaths.golden

Exits non-zero with the exact frame index and both hashes on any mismatch. I validated at n_threads=0, which is the exact mode the RISC-V scalar fast paths run in on the ESP32-P4 target. Should give you a concrete, cheap guard for these code paths ahead of the multithreading rework.

I didn't touch edge264_check.c itself — from what I could tell, main() runs a fixed list of named tests rather than scanning tests/ automatically, so these new files just sit there inert unless someone explicitly builds/runs the harness. Happy to wire it in differently if you'd prefer it integrated into make check instead.

Also opened #30 for an unrelated crash I hit while building this — not part of this PR.

Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ESP32‑P4 port: crash in add_idct8x8 when decoding High Profile streams

1 participant