Cycle 8 Phase 3 closed: H.264 deblock NEON = 92 Medge/s
M1: 10000/10000 bit-exact (after orientation fix: ff_h264_v_loop_ filter is "vertical filtering of horizontal edges", not "vertical edge"; 16 columns process the edge horizontally with 8 rows of vertical context). M3: 91.947 Medge/s per core. Per-edge 10.9 ns. 11x worst-case 1080p30 floor, 30x realistic floor. Filter triggers on 25 % of edges (random alpha/beta/tc0 covers both gating paths). Cycle 8 Phase 9 lesson: H.264/FFmpeg "v_loop_filter" naming uses filter DIRECTION (vertical) not edge orientation. Edge is horizontal; filter operates vertically across it. Distinct from cycle 6's column-major-block lesson but related discovery pattern. Encoded for future cycles. R8 prediction revised: 0.09-0.14 ORANGE (down from Phase 1's 0.3-0.8 estimate). H.264 deblock is 2x faster on NEON than VP9 LPF wd=4 (cycle 2) but H.264 deblock has more per-edge branches that hurt QPU more. Worth building anyway: - ORANGE in cycle 1's "M4 may rescue" band - Mixed-kernel deployment helper value (Issue 003) matters more than isolation R - 25%-trigger rate gives 4x effective contribution multiplier on QPU side - tests/h264_deblock_ref.c (column-walking C ref per row segment) - tests/bench_neon_h264deblock.c (M1 + M3 bench) - CMakeLists.txt: cycle 8 NEON bench wiring + h264dsp_neon.S - docs/k8_h264deblock_phase3.md (closure) Next: Phase 4 plan QPU shader, Phase 5 Sonnet review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -120,6 +120,21 @@ add_executable(bench_neon_h264idct8
|
||||
)
|
||||
target_compile_options(bench_neon_h264idct8 PRIVATE -O3 -march=armv8-a+simd)
|
||||
|
||||
# Cycle 8 — H.264 luma vertical deblock NEON M3 baseline bench.
|
||||
set(FFASM_H264DSP_SOURCES
|
||||
${FFSNAP}/libavcodec/aarch64/h264dsp_neon.S
|
||||
)
|
||||
set_source_files_properties(${FFASM_H264DSP_SOURCES} PROPERTIES
|
||||
COMPILE_OPTIONS "${FFASM_FLAGS}"
|
||||
LANGUAGE ASM)
|
||||
|
||||
add_executable(bench_neon_h264deblock
|
||||
tests/bench_neon_h264deblock.c
|
||||
tests/h264_deblock_ref.c
|
||||
${FFASM_H264DSP_SOURCES}
|
||||
)
|
||||
target_compile_options(bench_neon_h264deblock PRIVATE -O3 -march=armv8-a+simd)
|
||||
|
||||
add_executable(bench_neon_idct
|
||||
tests/bench_neon_idct.c
|
||||
tests/vp9_idct8_ref.c
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
cycle: 8
|
||||
phase: 3
|
||||
status: closed 2026-05-18 — M1 PASS, M3₈ = 91.95 Medge/s
|
||||
date_opened: 2026-05-18
|
||||
date_closed: 2026-05-18
|
||||
parent: k8_h264deblock_phase1.md
|
||||
host: hertz
|
||||
---
|
||||
|
||||
# Cycle 8, Phase 3 — H.264 luma deblock NEON baseline
|
||||
|
||||
## M1 + M3
|
||||
|
||||
```
|
||||
=== M1₈ bit-exact (10000 random edges) ===
|
||||
M1₈ correctness: 10000 / 10000 edges bit-exact (100.0000%)
|
||||
filter triggered on 2507/10000 edges (25.07%)
|
||||
|
||||
=== M3₈ NEON throughput ===
|
||||
total edges: 20 443 136
|
||||
elapsed (kernel)=0.222 s
|
||||
throughput = 91.947 Medge/s
|
||||
per-edge = 10.9 ns
|
||||
H.264 1080p30 worst-case floor: 11.49x margin
|
||||
H.264 1080p30 realistic floor: 30.65x margin
|
||||
```
|
||||
|
||||
Filter triggers 25 % of the time — realistic gating: random
|
||||
alpha/beta/tc0 cover both filter-applies and skip cases.
|
||||
|
||||
## Key Phase 9 lesson — H.264 v_loop_filter is VERTICAL filtering of HORIZONTAL edges
|
||||
|
||||
The FFmpeg naming convention "v_loop_filter_luma" / "h_loop_filter_luma"
|
||||
refers to the **filter direction**, not the edge orientation:
|
||||
|
||||
- `v_loop_filter_luma` — filter applied VERTICALLY across a
|
||||
HORIZONTAL edge (16-col wide edge between row -1 and row 0).
|
||||
pix points to row 0, column 0 of the bottom block.
|
||||
- `h_loop_filter_luma` — filter applied HORIZONTALLY across a
|
||||
VERTICAL edge (16-row tall edge between col -1 and col 0).
|
||||
|
||||
This is the H.264 spec convention but it tripped up the cycle 8
|
||||
first C-ref draft (which assumed v_loop_filter operated on a
|
||||
vertical edge with row-wise filtering). Trace showed only ±1 pixel
|
||||
differences which initially looked like a rounding issue but was
|
||||
actually a layout misinterpretation:
|
||||
- The 16 "columns" in the NEON's vector lanes correspond to image
|
||||
COLUMNS spanning the edge horizontally.
|
||||
- The 8 "rows" (p3..p0 / q0..q3 context) span the edge vertically.
|
||||
|
||||
Cycle 6 had a similar lesson with column-major-block; cycle 8 has
|
||||
this related-but-distinct edge-orientation lesson. Encoded for
|
||||
future cycles.
|
||||
|
||||
## R₈ prediction (revised from Phase 1)
|
||||
|
||||
Phase 1 predicted R₈ = 0.3-0.8 ORANGE/YELLOW based on VP9 LPF
|
||||
analog. With M3₈ = 92 Medge/s captured (vs cycle 2's 48
|
||||
Medge/s), the picture refines:
|
||||
|
||||
- H.264 deblock per-edge 10.9 ns vs cycle 2's 20 ns — **H.264 is
|
||||
~2× faster on NEON per edge**
|
||||
- Cycle 2 QPU was 19.6 Medge/s = R = 0.41 GREEN
|
||||
- H.264 deblock is MORE complex per edge (alpha/beta gating, tc0
|
||||
array, ap/aq side conditions, conditional p1/q1 writes) → QPU
|
||||
work per edge likely 1.5-2× heavier than cycle 2's QPU
|
||||
- Expected QPU M2 = 8-13 Medge/s
|
||||
- **Predicted R₈ = 0.09-0.14 → ORANGE (lower than predicted)**
|
||||
|
||||
Still likely worth building the QPU shader because:
|
||||
- ORANGE is in the "M4 may still rescue" band (per cycle 1
|
||||
calibration where R=0.92 turned into +7.2% M4)
|
||||
- For real deployment, mixed-kernel (Issue 003) helper value
|
||||
matters more than isolation R
|
||||
- Even at modest QPU contribution, the 25 %-of-edges-trigger
|
||||
reality means QPU only needs to handle the 25 % that actually
|
||||
filter; that's a 4× effective contribution multiplier
|
||||
|
||||
## Cycle comparison
|
||||
|
||||
| | Cycle 2 LPF wd=4 | Cycle 8 H.264 deblock |
|
||||
|---|---|---|
|
||||
| Codec | VP9 | H.264 |
|
||||
| Edge size | 8 rows, 4-tap | 8 rows, 4-tap (similar) |
|
||||
| NEON M3 | 48.285 Medge/s | **91.947 Medge/s** (1.9× faster) |
|
||||
| Per-edge ns | 20.7 | **10.9** |
|
||||
| Filter triggering rate | ~30 % (cycle 2 bench) | 25 % |
|
||||
| Cycle 2 verdict | GREEN (M4 +6.9 %) | TBD (predicted ORANGE) |
|
||||
|
||||
H.264 deblock's per-edge work is comparable to VP9 LPF but
|
||||
2× faster on NEON due to:
|
||||
- 16 columns processed in parallel (vs VP9 LPF 4-tap's 8 columns)
|
||||
- More efficient byte-vector ops in FFmpeg's NEON implementation
|
||||
- H.264 deblock doesn't have VP9's wd=4/8/16 variant overhead
|
||||
|
||||
## Acceptance for Phase 7
|
||||
|
||||
- ✓ M1 bit-exact (100.00 % on 10 000 random edges)
|
||||
- ✓ M3 captured (91.947 Medge/s)
|
||||
- ✓ 30fps@1080p floor exceeded by 11× worst-case
|
||||
- → Phase 4 plan QPU shader (next)
|
||||
|
||||
## Cycle 8 next phase
|
||||
|
||||
Phase 4: plan v3d_h264deblock.comp. Likely follows cycle 2 LPF
|
||||
shader template (no barrier, edge per lane decomposition,
|
||||
uint8 dst SSBO). Differences:
|
||||
- 16 columns per edge (not 8)
|
||||
- alpha/beta gating with multiple short-circuit conditions
|
||||
- tc0 per 4-col segment
|
||||
- ap/aq side conditions affecting p1/q1 writes
|
||||
- More compute per pixel than cycle 2
|
||||
|
||||
Then Phase 5 Sonnet review (non-skippable), Phase 6 implement,
|
||||
Phase 7 measure.
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Cycle 8 Phase 3 — NEON M3 baseline for H.264 luma vertical
|
||||
* deblock (non-intra, bS<4).
|
||||
*
|
||||
* M1 against the standalone C reference, M3 throughput.
|
||||
*
|
||||
* License: BSD-2-Clause; links FFmpeg LGPL-2.1+ snapshot.
|
||||
*/
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <getopt.h>
|
||||
|
||||
extern void daedalus_h264_v_loop_filter_luma_ref(
|
||||
uint8_t *pix, ptrdiff_t stride,
|
||||
int alpha, int beta, int8_t tc0[4]);
|
||||
|
||||
extern void ff_h264_v_loop_filter_luma_neon(
|
||||
uint8_t *pix, ptrdiff_t stride,
|
||||
int alpha, int beta, int8_t *tc0);
|
||||
|
||||
/* Edge layout: 8 rows × 16 cols (rows -4..+3 around edge). The
|
||||
* edge is between rows -1 and 0 (= a HORIZONTAL edge filtered
|
||||
* VERTICALLY per H.264 v_loop_filter convention).
|
||||
*
|
||||
* Tile: 16 rows × 16 cols. Edge at row 4 (rows 0..3 above + edge
|
||||
* + rows 5..7 below; rows 8..15 are halo). pix points to tile +
|
||||
* EDGE_ROW*stride. */
|
||||
#define TILE_STRIDE 16
|
||||
#define TILE_ROWS 16
|
||||
#define TILE_BYTES (TILE_ROWS * TILE_STRIDE)
|
||||
#define EDGE_ROW 4
|
||||
|
||||
static uint64_t xs_state;
|
||||
static inline uint64_t xs(void) {
|
||||
uint64_t x = xs_state;
|
||||
x ^= x << 13; x ^= x >> 7; x ^= x << 17;
|
||||
return xs_state = x;
|
||||
}
|
||||
|
||||
/* Generate a tile with a horizontal edge at row EDGE_ROW (between
|
||||
* rows 3 and 4). Top side (rows 0..3) clusters around side_a_base,
|
||||
* bottom (rows 4..7) around side_b_base. Other rows are halo. */
|
||||
static void gen_tile(uint8_t *tile)
|
||||
{
|
||||
int side_a_base = (int)(xs() % 200) + 20;
|
||||
int side_b_base = (int)(xs() % 200) + 20;
|
||||
int noise = (int)(xs() % 30) + 1;
|
||||
for (int r = 0; r < TILE_ROWS; r++) {
|
||||
for (int c = 0; c < TILE_STRIDE; c++) {
|
||||
int v;
|
||||
if (r >= EDGE_ROW - 4 && r < EDGE_ROW + 4) {
|
||||
/* edge region rows EDGE_ROW-4..EDGE_ROW+3 */
|
||||
int local = r - (EDGE_ROW - 4);
|
||||
int base = local < 4 ? side_a_base : side_b_base;
|
||||
int n = ((int)(xs() % (2 * noise + 1))) - noise;
|
||||
v = base + n;
|
||||
} else {
|
||||
v = (int)(xs() & 0xff); /* halo */
|
||||
}
|
||||
tile[r * TILE_STRIDE + c] = (uint8_t)(v < 0 ? 0 : v > 255 ? 255 : v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void gen_thresholds(int *alpha, int *beta, int8_t tc0[4])
|
||||
{
|
||||
/* Realistic H.264 alpha/beta ranges: typical 0..30 in spec
|
||||
* tables for QP 30..40. Allow up to 64 to stress alpha/beta
|
||||
* gating. */
|
||||
*alpha = (int)(xs() % 64) + 1;
|
||||
*beta = (int)(xs() % 16) + 1;
|
||||
/* tc0 from spec table: -1 means "no filter for this segment",
|
||||
* 0..6 typical non-zero values. */
|
||||
for (int s = 0; s < 4; s++) {
|
||||
int r = (int)(xs() % 8);
|
||||
tc0[s] = (int8_t)(r == 0 ? -1 : (r - 1));
|
||||
}
|
||||
}
|
||||
|
||||
static double now_seconds(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
|
||||
return ts.tv_sec + ts.tv_nsec * 1e-9;
|
||||
}
|
||||
|
||||
static int correctness_check(uint64_t seed, int n)
|
||||
{
|
||||
xs_state = seed ? seed : 0xdeb1ec500dULL;
|
||||
int mismatches = 0, prints = 0;
|
||||
int filtered_count = 0;
|
||||
|
||||
uint8_t tile_a[TILE_BYTES], tile_b[TILE_BYTES], tile_saved[TILE_BYTES];
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
gen_tile(tile_a);
|
||||
memcpy(tile_b, tile_a, TILE_BYTES);
|
||||
memcpy(tile_saved, tile_a, TILE_BYTES);
|
||||
|
||||
int alpha, beta;
|
||||
int8_t tc0[4];
|
||||
gen_thresholds(&alpha, &beta, tc0);
|
||||
|
||||
uint8_t *pix_a = tile_a + EDGE_ROW * TILE_STRIDE;
|
||||
uint8_t *pix_b = tile_b + EDGE_ROW * TILE_STRIDE;
|
||||
|
||||
daedalus_h264_v_loop_filter_luma_ref(pix_a, TILE_STRIDE, alpha, beta, tc0);
|
||||
ff_h264_v_loop_filter_luma_neon(pix_b, TILE_STRIDE, alpha, beta, tc0);
|
||||
|
||||
/* Check the edge region rows ±2 (the only rows deblock can modify). */
|
||||
int diff = 0;
|
||||
for (int r = EDGE_ROW - 2; r < EDGE_ROW + 2; r++) {
|
||||
for (int c = 0; c < TILE_STRIDE; c++) {
|
||||
if (tile_a[r*TILE_STRIDE + c] != tile_b[r*TILE_STRIDE + c]) diff++;
|
||||
}
|
||||
}
|
||||
/* Count whether filter actually triggered for any row. */
|
||||
int triggered = (memcmp(tile_a, tile_saved, TILE_BYTES) != 0);
|
||||
if (triggered) filtered_count++;
|
||||
|
||||
if (diff) {
|
||||
if (prints < 3) {
|
||||
fprintf(stderr, "MISMATCH edge %d (%d/64 modifiable pixels differ), alpha=%d beta=%d, tc0=[%d,%d,%d,%d]:\n",
|
||||
i, diff, alpha, beta, tc0[0], tc0[1], tc0[2], tc0[3]);
|
||||
fprintf(stderr, " input tile (cols 0..15):");
|
||||
for (int r = 0; r < TILE_ROWS; r++) {
|
||||
fprintf(stderr, "\n r%2d ", r);
|
||||
for (int c = 0; c < TILE_STRIDE; c++)
|
||||
fprintf(stderr, "%3u ", tile_saved[r*TILE_STRIDE + c]);
|
||||
}
|
||||
fprintf(stderr, "\n ref out (edge rows 2..5, all cols):");
|
||||
for (int r = EDGE_ROW - 2; r < EDGE_ROW + 2; r++) {
|
||||
fprintf(stderr, "\n r%2d ", r);
|
||||
for (int c = 0; c < TILE_STRIDE; c++)
|
||||
fprintf(stderr, "%3u ", tile_a[r*TILE_STRIDE + c]);
|
||||
}
|
||||
fprintf(stderr, "\n neon out (edge rows 2..5, all cols):");
|
||||
for (int r = EDGE_ROW - 2; r < EDGE_ROW + 2; r++) {
|
||||
fprintf(stderr, "\n r%2d ", r);
|
||||
for (int c = 0; c < TILE_STRIDE; c++)
|
||||
fprintf(stderr, "%3u ", tile_b[r*TILE_STRIDE + c]);
|
||||
}
|
||||
fprintf(stderr, "\n");
|
||||
prints++;
|
||||
}
|
||||
mismatches++;
|
||||
}
|
||||
}
|
||||
|
||||
printf("M1₈ correctness: %d / %d edges bit-exact (%.4f%%)\n",
|
||||
n - mismatches, n, 100.0 * (n - mismatches) / n);
|
||||
printf(" filter triggered on %d/%d edges (%.2f%%)\n",
|
||||
filtered_count, n, 100.0 * filtered_count / n);
|
||||
return mismatches;
|
||||
}
|
||||
|
||||
static void throughput_neon(uint64_t seed, int n_edges, double duration_s)
|
||||
{
|
||||
xs_state = seed ? seed : 0xdeb1ec500dULL;
|
||||
uint8_t *master = malloc((size_t) n_edges * TILE_BYTES);
|
||||
uint8_t *work = malloc((size_t) n_edges * TILE_BYTES);
|
||||
int *alphas = malloc(n_edges * sizeof(int));
|
||||
int *betas = malloc(n_edges * sizeof(int));
|
||||
int8_t (*tc0s)[4] = malloc(n_edges * 4);
|
||||
if (!master || !work || !alphas || !betas || !tc0s) {
|
||||
fprintf(stderr, "alloc fail\n"); exit(1);
|
||||
}
|
||||
for (int i = 0; i < n_edges; i++) {
|
||||
gen_tile(master + i * TILE_BYTES);
|
||||
gen_thresholds(&alphas[i], &betas[i], tc0s[i]);
|
||||
}
|
||||
|
||||
memcpy(work, master, (size_t) n_edges * TILE_BYTES);
|
||||
for (int i = 0; i < n_edges; i++)
|
||||
ff_h264_v_loop_filter_luma_neon(work + i * TILE_BYTES + EDGE_ROW * TILE_STRIDE,
|
||||
TILE_STRIDE, alphas[i], betas[i], tc0s[i]);
|
||||
|
||||
double t0 = now_seconds();
|
||||
double t_end = t0 + duration_s;
|
||||
uint64_t done = 0;
|
||||
while (now_seconds() < t_end) {
|
||||
memcpy(work, master, (size_t) n_edges * TILE_BYTES);
|
||||
for (int i = 0; i < n_edges; i++)
|
||||
ff_h264_v_loop_filter_luma_neon(work + i * TILE_BYTES + EDGE_ROW * TILE_STRIDE,
|
||||
TILE_STRIDE, alphas[i], betas[i], tc0s[i]);
|
||||
done += n_edges;
|
||||
}
|
||||
double elapsed = now_seconds() - t0;
|
||||
|
||||
int iters = (int)(done / n_edges);
|
||||
double s0 = now_seconds();
|
||||
for (int i = 0; i < iters; i++)
|
||||
memcpy(work, master, (size_t) n_edges * TILE_BYTES);
|
||||
double s1 = now_seconds();
|
||||
|
||||
double kernel_seconds = elapsed - (s1 - s0);
|
||||
double medges = done / kernel_seconds / 1e6;
|
||||
|
||||
printf("M3₈ NEON throughput:\n");
|
||||
printf(" edges/batch: %d\n", n_edges);
|
||||
printf(" batches done: %d\n", iters);
|
||||
printf(" total edges: %llu\n", (unsigned long long) done);
|
||||
printf(" elapsed (kernel)=%.6f s\n", kernel_seconds);
|
||||
printf(" throughput = %.3f Medge/s\n", medges);
|
||||
printf(" per-edge = %.1f ns\n", kernel_seconds / done * 1e9);
|
||||
/* 1080p H.264 worst-case: ~8 Medge/s (luma v+h). Realistic: 2-4. */
|
||||
printf(" H.264 1080p30 worst-case floor: %.2fx margin (8.0 Medge/s req'd)\n", medges / 8.0);
|
||||
printf(" H.264 1080p30 realistic floor: %.2fx margin (3.0 Medge/s req'd)\n", medges / 3.0);
|
||||
|
||||
free(master); free(work); free(alphas); free(betas); free(tc0s);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
int n_edges = 65536;
|
||||
double duration = 5.0;
|
||||
uint64_t seed = 0;
|
||||
int do_correctness = 1;
|
||||
|
||||
static struct option opts[] = {
|
||||
{"edges", required_argument, 0, 'e'},
|
||||
{"duration", required_argument, 0, 'd'},
|
||||
{"seed", required_argument, 0, 's'},
|
||||
{"no-correctness", no_argument, 0, 'C'},
|
||||
{0,0,0,0}
|
||||
};
|
||||
for (int c; (c = getopt_long(argc, argv, "e:d:s:C", opts, 0)) != -1;) {
|
||||
switch (c) {
|
||||
case 'e': n_edges = atoi(optarg); break;
|
||||
case 'd': duration = atof(optarg); break;
|
||||
case 's': seed = strtoull(optarg, 0, 0); break;
|
||||
case 'C': do_correctness = 0; break;
|
||||
default: return 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (do_correctness) {
|
||||
printf("=== M1₈ bit-exact (10000 random edges) ===\n");
|
||||
int mis = correctness_check(seed, 10000);
|
||||
if (mis != 0) {
|
||||
fprintf(stderr, "M1 gate FAILED — refusing to measure throughput.\n");
|
||||
return 1;
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
printf("=== M3₈ NEON throughput ===\n");
|
||||
throughput_neon(seed, n_edges, duration);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Standalone bit-exact C reference for H.264 luma "vertical"
|
||||
* loop filter (v_loop_filter_luma): applies filter VERTICALLY
|
||||
* across a HORIZONTAL edge. The edge spans the 16-column
|
||||
* macroblock width, between rows -1 and 0.
|
||||
*
|
||||
* Mirrors FFmpeg `ff_h264_v_loop_filter_luma_neon` in
|
||||
* external/ffmpeg-snapshot/libavcodec/aarch64/h264dsp_neon.S
|
||||
* line 111. Operates on a 8-row × 16-col region:
|
||||
* pix[r*stride + c] for r in -4..+3, c in 0..15
|
||||
* With pix pointing to row 0, col 0 of the bottom block.
|
||||
*
|
||||
* 16 columns divided into 4 segments of 4 cols; each segment
|
||||
* has its own tc0 strength (tc0[0..3]).
|
||||
*
|
||||
* Note: FFmpeg's "v_loop_filter" naming uses the FILTER
|
||||
* DIRECTION (vertical = across the edge from above), not the
|
||||
* edge orientation (horizontal). H.264 spec calls this the
|
||||
* "horizontal edge" filter.
|
||||
*
|
||||
* Signature:
|
||||
* void(uint8_t *pix, ptrdiff_t stride,
|
||||
* int alpha, int beta, int8_t tc0[4]);
|
||||
*
|
||||
* License: LGPL-2.1-or-later (matches FFmpeg upstream).
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
static inline int clip_u8(int v) { return v < 0 ? 0 : v > 255 ? 255 : v; }
|
||||
static inline int clip3(int v, int lo, int hi) {
|
||||
return v < lo ? lo : v > hi ? hi : v;
|
||||
}
|
||||
static inline int abs_i(int x) { return x < 0 ? -x : x; }
|
||||
|
||||
/* Apply luma deblock to one COLUMN at the horizontal edge.
|
||||
* p0..p3 are pixels above the edge (pix[-stride..-4*stride]),
|
||||
* q0..q3 below (pix[0..+3*stride]).
|
||||
* tc0_s is the segment's tc0 value (already known >= 0).
|
||||
*
|
||||
* Writes back to pix[-2*stride], pix[-1*stride], pix[0], pix[+stride]
|
||||
* (= p1, p0, q0, q1).
|
||||
*/
|
||||
static void h264_deblock_luma_col(uint8_t *pix, ptrdiff_t stride,
|
||||
int alpha, int beta, int tc0_s)
|
||||
{
|
||||
int p3 = pix[-4*stride], p2 = pix[-3*stride], p1 = pix[-2*stride], p0 = pix[-1*stride];
|
||||
int q0 = pix[ 0*stride], q1 = pix[ 1*stride], q2 = pix[ 2*stride], q3 = pix[ 3*stride];
|
||||
(void) p3; (void) q3; /* not used in bS<4 path */
|
||||
|
||||
/* Edge pre-conditions. */
|
||||
if (abs_i(p0 - q0) >= alpha) return;
|
||||
if (abs_i(p1 - p0) >= beta) return;
|
||||
if (abs_i(q1 - q0) >= beta) return;
|
||||
|
||||
/* Side conditions. */
|
||||
int ap = abs_i(p2 - p0);
|
||||
int aq = abs_i(q2 - q0);
|
||||
int ap_lt_beta = (ap < beta);
|
||||
int aq_lt_beta = (aq < beta);
|
||||
|
||||
/* Combined filter strength. */
|
||||
int tc = tc0_s + ap_lt_beta + aq_lt_beta;
|
||||
|
||||
/* p0 / q0 update. */
|
||||
int delta = clip3(((q0 - p0) * 4 + (p1 - q1) + 4) >> 3, -tc, tc);
|
||||
int p0p = clip_u8(p0 + delta);
|
||||
int q0p = clip_u8(q0 - delta);
|
||||
|
||||
/* p1 update (only if ap<beta). */
|
||||
int p1p = p1;
|
||||
if (ap_lt_beta) {
|
||||
int delta_p1 = clip3((p2 + ((p0 + q0 + 1) >> 1) - 2*p1) >> 1, -tc0_s, tc0_s);
|
||||
p1p = p1 + delta_p1;
|
||||
}
|
||||
/* q1 update (only if aq<beta). */
|
||||
int q1p = q1;
|
||||
if (aq_lt_beta) {
|
||||
int delta_q1 = clip3((q2 + ((p0 + q0 + 1) >> 1) - 2*q1) >> 1, -tc0_s, tc0_s);
|
||||
q1p = q1 + delta_q1;
|
||||
}
|
||||
|
||||
pix[-2*stride] = (uint8_t) p1p;
|
||||
pix[-1*stride] = (uint8_t) p0p;
|
||||
pix[ 0*stride] = (uint8_t) q0p;
|
||||
pix[ 1*stride] = (uint8_t) q1p;
|
||||
}
|
||||
|
||||
void daedalus_h264_v_loop_filter_luma_ref(
|
||||
uint8_t *pix, ptrdiff_t stride,
|
||||
int alpha, int beta, int8_t tc0[4])
|
||||
{
|
||||
/* H.264 deblock "outer" precondition: alpha == 0 OR beta == 0
|
||||
* skips filtering. Also if ALL tc0[*] == -1, skip
|
||||
* (h264_loop_filter_start macro check). */
|
||||
if (alpha == 0 || beta == 0) return;
|
||||
if (tc0[0] < 0 && tc0[1] < 0 && tc0[2] < 0 && tc0[3] < 0) return;
|
||||
|
||||
/* 16 columns divided into 4 segments of 4 columns each. */
|
||||
for (int s = 0; s < 4; s++) {
|
||||
int tc0_s = tc0[s];
|
||||
if (tc0_s < 0) continue; /* bS = 0 segment → skip */
|
||||
for (int c = 0; c < 4; c++) {
|
||||
int col = s * 4 + c;
|
||||
h264_deblock_luma_col(pix + col, stride, alpha, beta, tc0_s);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user