1 Commits

Author SHA1 Message Date
marfrit cfeb998ff8 Update DESIGN.md 2026-05-24 19:51:38 +00:00
12 changed files with 9 additions and 3398 deletions
-15
View File
@@ -1,15 +0,0 @@
build/
build-*/
*.o
*.a
*.so
*.so.*
*.spv
.vscode/
.cache/
compile_commands.json
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
Makefile
.ninja_*
-248
View File
@@ -1,248 +0,0 @@
# SPDX-License-Identifier: BSD-2-Clause
#
# daedalus-decoder — frame-level GPU H.264 decoder for V3D7 (Pi 5).
# Phase 1 scaffold; see DESIGN.md for architecture.
#
# Build dependencies:
# - daedalus-fourier ≥ 0.1.0 (kernel pack, V3D primitives + recipe layer)
# resolved via pkg-config; install via the daedalus-fourier upstream
# `cmake --install` rule (PR #5 made the .pc relocatable, so any
# install prefix works as long as $PKG_CONFIG_PATH is set).
# - Vulkan headers + libvulkan (pulled in transitively via
# daedalus-fourier, listed here explicitly for the link order).
#
# Build:
# cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
# cmake --build build
# ctest --test-dir build
cmake_minimum_required(VERSION 3.20)
project(daedalus-decoder
VERSION 0.0.1
DESCRIPTION "Frame-level GPU H.264 decoder for Raspberry Pi 5 / V3D7"
LANGUAGES C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
# Pi 5 is the only supported target. Other aarch64 SoCs (Pi 4 V3D4,
# RK3588 Mali, …) might work but would need explicit substrate +
# shader-pack validation per the daedalus-fourier architecture
# backlog. Don't pretend to support what we haven't validated.
if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64")
message(WARNING
"daedalus-decoder is designed for aarch64 (Pi 5 BCM2712 / V3D7). "
"Build will proceed but is unlikely to function.")
endif()
add_compile_options(-Wall -Wextra -Wno-unused-parameter)
# ---- Dependencies --------------------------------------------------
find_package(PkgConfig REQUIRED)
# daedalus-fourier — find_package via pkg-config per the Phase 1
# decision §9.6. Minimum version 0.1.0 (the cycle 6-9 shaders + pool
# + recipe-flip baseline). PKG_CONFIG_PATH should point at the
# directory holding daedalus-fourier.pc (e.g. /usr/local/lib/pkgconfig
# or a custom install prefix).
pkg_check_modules(DAEDALUS_FOURIER REQUIRED daedalus-fourier>=0.1.0)
# Vulkan — daedalus-fourier already depends on this; we add it
# explicitly so the link order stays correct (daedalus-fourier static
# archive contains undefined vk* symbols that the loader resolves).
find_package(Vulkan REQUIRED)
# ---- Version string baked into the library ------------------------
# git rev tagged onto the version string for traceability; degrades
# gracefully to bare semver if git isn't available.
execute_process(
COMMAND git -C ${CMAKE_CURRENT_SOURCE_DIR} rev-parse --short=7 HEAD
OUTPUT_VARIABLE DAEDALUS_DECODER_GITREV
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
if(DAEDALUS_DECODER_GITREV)
set(DAEDALUS_DECODER_VERSION "${PROJECT_VERSION}+g${DAEDALUS_DECODER_GITREV}")
else()
set(DAEDALUS_DECODER_VERSION "${PROJECT_VERSION}")
endif()
message(STATUS "daedalus-decoder version: ${DAEDALUS_DECODER_VERSION}")
# ---- Library ------------------------------------------------------
add_library(daedalus_decoder STATIC
src/daedalus_decoder.c
)
target_include_directories(daedalus_decoder
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
PRIVATE
src
${DAEDALUS_FOURIER_INCLUDE_DIRS}
)
target_link_directories(daedalus_decoder
PUBLIC
${DAEDALUS_FOURIER_LIBRARY_DIRS}
)
target_link_libraries(daedalus_decoder
PUBLIC
# Order matters: daedalus-fourier static archive references
# vulkan symbols; the loader needs daedalus-fourier first then
# vulkan to resolve them.
${DAEDALUS_FOURIER_LIBRARIES}
Vulkan::Vulkan
)
target_compile_definitions(daedalus_decoder
PRIVATE
DAEDALUS_DECODER_VERSION="${DAEDALUS_DECODER_VERSION}"
)
target_compile_options(daedalus_decoder PRIVATE -O2)
# ---- Smoke test ---------------------------------------------------
enable_testing()
add_executable(test_smoke tests/test_smoke.c)
target_link_libraries(test_smoke PRIVATE daedalus_decoder)
target_compile_options(test_smoke PRIVATE -O2)
add_test(NAME smoke COMMAND test_smoke)
add_executable(test_idct_bitexact tests/test_idct_bitexact.c)
target_link_libraries(test_idct_bitexact PRIVATE daedalus_decoder)
target_compile_options(test_idct_bitexact PRIVATE -O2)
# 320x240 QVGA — fast inner-loop test (300 MBs, sub-second).
add_test(NAME idct_bitexact COMMAND test_idct_bitexact)
# Same QVGA test re-run on the CPU NEON path (forces fallback even on
# V3D7 hosts). Catches silent drift between the V3D shader and the
# NEON reference path — both must produce identical output for the
# same coefficient input. Also keeps the bit-exact gate alive on
# hosts without V3D7 (CI runners, x86 dev boxes).
add_test(NAME idct_bitexact_cpu COMMAND test_idct_bitexact 320 240
0xfeedface5a5a5a5a cpu)
# 1920x1088 1080p — deployment-scale test (8160 MBs, ~0.25 s on hertz).
# Validates the per-MB block index + pixel offset math at full coded
# height (1088, not 1080 — see daedalus_decoder.h on H.264 coded vs
# displayed dims). Cheap enough to run unconditionally; if it ever
# gets slow we'll split into a CTest LABEL for opt-in.
add_test(NAME idct_bitexact_1080p COMMAND test_idct_bitexact 1920 1088)
# ---- Stage 2 PR-b deblock smoke ------------------------------------
#
# Validates flush_frame's per-frame deblock dispatch (luma + chroma,
# V + H, bS<4 + bS=4 intra — up to 8 dispatches added after IDCT).
# Strategy: same input through substrate=CPU and substrate=QPU, assert
# byte-exact match (transitive bit-exact gate — daedalus-fourier's own
# test_api_h264 already validates each substrate against a C reference,
# so CPU-QPU equivalence here means both match the spec). Plus an
# anti-no-op check: run a third pass with edges removed and assert
# different output, proving deblock actually ran.
add_executable(test_deblock_smoke tests/test_deblock_smoke.c)
target_link_libraries(test_deblock_smoke PRIVATE daedalus_decoder)
target_compile_options(test_deblock_smoke PRIVATE -O2)
add_test(NAME deblock_smoke COMMAND test_deblock_smoke)
# ---- Benchmarks (not gated by ctest) ------------------------------
#
# Build-time only; user runs them by hand when checking perf. Adding
# them as ctest would make every CI run slow and the numbers would
# get drowned in pass/fail noise. See the header of each .c for what
# they measure.
add_executable(bench_flush_frame tests/bench_flush_frame.c)
target_link_libraries(bench_flush_frame PRIVATE daedalus_decoder)
target_compile_options(bench_flush_frame PRIVATE -O2)
# ---- Tools (not gated by ctest; opt-in via DAEDALUS_BUILD_TOOLS) ----
#
# daedalus_decode_h264 — option A standalone test harness that
# wraps libavcodec + daedalus-decoder and bit-exact-compares their
# outputs on real H.264 streams. Identity-passthrough mode in this
# first iteration (predicted = AVFrame pixels, coeffs = 0, no
# deblock edges); follow-up PRs use the per-MB inspection callback
# (marfrit-packages patch 0016) to feed REAL per-MB state.
#
# Requires libavcodec + libavformat headers + libs. Off by default
# so the standard ctest build doesn't pull in FFmpeg as a hard dep.
option(DAEDALUS_BUILD_TOOLS "Build daedalus-decoder CLI tools (requires libavcodec)" OFF)
if(DAEDALUS_BUILD_TOOLS)
# Optional path to a private FFmpeg install carrying the per-MB
# inspection callback (marfrit-packages patch 0016). When set,
# the CLI links against it instead of the system FFmpeg and the
# inspection-callback code path is compiled in.
set(DAEDALUS_FFMPEG_PREFIX "" CACHE PATH
"Path to a patched FFmpeg install (with 0016 mb-inspect-callback) for daedalus_decode_h264. Empty = use system pkg-config FFmpeg.")
if(DAEDALUS_FFMPEG_PREFIX)
message(STATUS "daedalus_decode_h264: patched FFmpeg at ${DAEDALUS_FFMPEG_PREFIX}")
set(FFMPEG_INCLUDE_DIRS ${DAEDALUS_FFMPEG_PREFIX}/include)
set(FFMPEG_LIBRARY_DIRS ${DAEDALUS_FFMPEG_PREFIX}/lib)
# Patched libavcodec is built static (no shared libs in the private prefix).
# System pull-ins are still needed for libav* dependencies.
set(FFMPEG_LIBRARIES
${DAEDALUS_FFMPEG_PREFIX}/lib/libavformat.a
${DAEDALUS_FFMPEG_PREFIX}/lib/libavcodec.a
${DAEDALUS_FFMPEG_PREFIX}/lib/libavutil.a
${DAEDALUS_FFMPEG_PREFIX}/lib/libswresample.a
m z pthread)
set(FFMPEG_CFLAGS_OTHER "-DDAEDALUS_HAVE_H264_MB_INSPECT_CB=1")
# PR-A3+ optional: also point at the patched FFmpeg SOURCE TREE
# so the CLI can include libavcodec/h264dec.h directly and
# dereference H264Context fields (the side-buffer mb_inspect_coeffs
# added in marfrit-packages patch 0017, the cur_pic.f for
# pre-deblock pixel access, etc.). When set, the internal-header
# include codepath is compiled in.
set(DAEDALUS_FFMPEG_SRC "" CACHE PATH
"Path to patched FFmpeg source tree (= path to FFmpeg/ checkout where build was run; contains config.h + libavcodec/h264dec.h). Empty = h264dec.h includes are disabled.")
if(DAEDALUS_FFMPEG_SRC)
message(STATUS "daedalus_decode_h264: FFmpeg source at ${DAEDALUS_FFMPEG_SRC}")
# IMPORTANT: source tree FIRST in -I order — its
# libavutil/common.h does #include "intmath.h" with HAVE_AV_CONFIG_H,
# which resolves to libavutil/intmath.h (in the source tree
# only — that header isn't installed since it's arch-dispatched).
# The installed-prefix include path's libavutil/common.h is the
# same file textually but resolves "intmath.h" against the
# install dir where it doesn't exist.
set(FFMPEG_INCLUDE_DIRS ${DAEDALUS_FFMPEG_SRC})
set(FFMPEG_CFLAGS_OTHER
"${FFMPEG_CFLAGS_OTHER} -DDAEDALUS_HAVE_H264_MB_INSPECT_COEFFS=1 -DHAVE_AV_CONFIG_H")
# Convert space-separated string to list (CMake idiom for compile flags).
separate_arguments(FFMPEG_CFLAGS_OTHER UNIX_COMMAND "${FFMPEG_CFLAGS_OTHER}")
endif()
else()
pkg_check_modules(FFMPEG REQUIRED libavcodec libavformat libavutil)
message(STATUS "daedalus_decode_h264: system FFmpeg (no inspection callback)")
endif()
add_executable(daedalus_decode_h264 tools/daedalus_decode_h264.c)
target_link_libraries(daedalus_decode_h264
PRIVATE daedalus_decoder ${FFMPEG_LIBRARIES})
target_include_directories(daedalus_decode_h264
PRIVATE ${FFMPEG_INCLUDE_DIRS})
target_link_directories(daedalus_decode_h264
PRIVATE ${FFMPEG_LIBRARY_DIRS})
target_compile_options(daedalus_decode_h264
PRIVATE -O2 ${FFMPEG_CFLAGS_OTHER})
endif()
# ---- Install ------------------------------------------------------
#
# Library + public header. Stage 2/3 will add a pkg-config file and
# CMake config exports once the API stabilises; pre-0.1 the scaffold
# install just gives the static archive a home.
include(GNUInstallDirs)
install(TARGETS daedalus_decoder
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES include/daedalus_decoder.h
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
+9 -15
View File
@@ -261,31 +261,25 @@ That's a substantial shader inventory. Each requires bit-exact M1 gate against
**Phase 4 — Production-ready deblock + perf optimization + libva integration** (+4 weeks). Real-world stream conformance. Plug into daedalus-v4l2 daemon as the actual decode backend.
**Total H.264 budget:** 4-6 months.
**Phase 5+ (future codec scope, not committed):** VP9 and AV1 reuse the same frame-level dispatch architecture, daedalus-fourier kernel pack, and DPB plumbing. Per §9.7, they are deferred but *not firmly out-of-scope*. HEVC stays firmly out (Pi 5 has `rpi-hevc-dec` for that).
**Total budget:** 4-6 months.
---
## 9. Phase 1 decisions
## 9. Open questions
User-confirmed 2026-05-24. All seven questions from the initial
draft are now decided; this section preserves the original wording
of each item for traceability.
1. **Intra prediction strategy:** GPU wavefront (~187 dispatches, more complex) vs CPU speculative (simpler, slower). Plan: wavefront in Phase 1; revisit if it's the perf bottleneck. [x]
1. **Intra prediction strategy:** GPU wavefront (~187 dispatches, more complex) vs CPU speculative (simpler, slower). **Decision: wavefront in Phase 1; revisit if it's the perf bottleneck.**
2. **libavcodec intercept granularity:** macroblock-level (substitution-arc evolution) vs slice-level (cleaner rewrite). Plan: macroblock-level for Phase 1; consider slice-level later if buffer accumulation overhead is non-trivial. [x]
2. **libavcodec intercept granularity:** macroblock-level (substitution-arc evolution) vs slice-level (cleaner rewrite). **Decision: macroblock-level for Phase 1; consider slice-level later if buffer accumulation overhead is non-trivial.**
3. **Shader parameterization:** 16 qpel variants as 16 shaders, or one parameterized shader with switch on mc_position? V3D's compiler might inline-optimize either; needs measurement. [x] Measurement it is.
3. **Shader parameterization:** 16 qpel variants as 16 shaders, or one parameterized shader with switch on mc_position? **Decision: measure both during Phase 2 (the MC phase) and pick the winner. No commit ahead of measurement.**
4. **DPB allocation:** Vulkan-native VkImage with dmabuf export, vs CPU-allocated dma_buf imported into Vulkan. Affects V4L2 integration story. Plan: Vulkan-native with `VK_KHR_external_memory_dma_buf` export; daedalus-v4l2 daemon imports. [x]
4. **DPB allocation:** Vulkan-native VkImage with dmabuf export, vs CPU-allocated dma_buf imported into Vulkan. **Decision: Vulkan-native with `VK_KHR_external_memory_dma_buf` export; daedalus-v4l2 daemon imports.**
5. **Daemon integration shape:** does daedalus-decoder ship as a static library the daemon links, or as a separate process the daemon talks to? Library, almost certainly — process boundary would multiply IPC cost. [x] library.
5. **Daemon integration shape:** static library the daemon links, or separate process. **Decision: library link.**
6. **Build dependency on daedalus-fourier:** as a CMake `find_package`, or vendored? `find_package`, pinned to a tagged release. daedalus-fourier becomes the "kernel pack" upstream library. [x] Yes.
6. **Build dependency on daedalus-fourier:** CMake `find_package`, or vendored? **Decision: `find_package`, pinned to a tagged release. daedalus-fourier becomes the "kernel pack" upstream library.**
7. **Codec scope.** **Decision: firmly out-of-scope for daedalus-decoder are HEVC (Pi 5 has `rpi-hevc-dec` for that), 10-bit, interlaced, and FMO/ASO.** VP9 and AV1 are *not* firmly out — they're future codec scope for the same framework after H.264 lands. This is a scope expansion from the initial draft, which had grouped them with HEVC under "firmly out".
7. **Out-of-scope for daedalus-decoder (firmly):** HEVC (Pi 5 has rpi-hevc-dec for that), 10-bit, interlaced, FMO/ASO.
---
-24
View File
@@ -1,24 +0,0 @@
BSD 2-Clause License
Copyright (c) 2026, Markus Fritsche
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-300
View File
@@ -1,300 +0,0 @@
/* SPDX-License-Identifier: BSD-2-Clause */
/*
* daedalus-decoder — public C API.
*
* Frame-level GPU H.264 decoder targeting V3D7 (Raspberry Pi 5). Built
* on daedalus-fourier's V3D compute primitives at frame granularity —
* one Vulkan submit per frame, one fence wait per frame, encoded
* bitstream in (via libavcodec's per-MB intercept), NV12 frame out.
*
* Per the 2026-05-24 Phase 1 design decisions:
* - libavcodec intercept is at macroblock-level (substitution-arc
* evolution): the caller is expected to drive the per-MB CABAC /
* CAVLC entropy decode and feed each macroblock's descriptor +
* coefficients via daedalus_decoder_append_mb(). flush_frame()
* builds the per-frame VkCommandBuffer and submits.
* - DPB is Vulkan-native VkImage with VK_KHR_external_memory_dma_buf
* export. The caller can obtain the output frame's dmabuf fd
* via daedalus_decoder_export_dmabuf().
* - Daemon integration shape: this library is statically linked into
* daedalus_v4l2_daemon. No IPC.
*
* STATUS: scaffold. No GPU pipeline implemented yet; all functions
* are stubs that compile but do not decode anything. See DESIGN.md
* for the architecture.
*
* ABI: pre-0.1 — every signature here may change. Don't rely on
* stability yet.
*/
#ifndef DAEDALUS_DECODER_H
#define DAEDALUS_DECODER_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* -------------------------------------------------------------------
* Opaque decoder context. One per concurrent stream.
* ----------------------------------------------------------------- */
typedef struct daedalus_decoder daedalus_decoder;
/* -------------------------------------------------------------------
* Per-edge deblock metadata. One entry per filter-edge; the caller
* derives these from H.264 §8.7.2.1 boundary-strength rules.
*
* Coordinate convention:
* mb_x / mb_y — the MB whose top-left this edge sits on (the "right"
* side for vertical edges, "bottom" side for horizontal
* edges, in H.264 spec's q-side convention).
* edge_idx — 0..3 within the MB:
* luma: edge 0 = MB boundary, edges 1..3 = internal
* at cols/rows 4, 8, 12.
* chroma: edge 0 = MB boundary, edge 1 = internal at
* col/row 4. edge_idx > 1 invalid for chroma.
* Edges at frame boundaries (top row of MBs for H edges;
* left column for V edges) MUST be bS=0 — the kernel
* reads p3 at four samples beyond the edge.
* orient — 0 = vertical edge (filtered horizontally across), 1 = horizontal.
* plane — 0 = luma, 1 = chroma Cb, 2 = chroma Cr. Cb and Cr
* always share the same filter parameters per H.264
* spec, but are listed separately so the caller can
* omit one or the other if needed.
* bS — 0 = skip this edge (no GPU work), 1..3 = bS<4 path
* (uses tc0), 4 = bS=4 "intra" path (ignores tc0).
* alpha, beta — H.264 §8.7.2.2 table 8-16/8-17 values, both 0..255.
* tc0[4] — per-4-cell segment strength along the edge (luma has
* 4 segments; chroma has 4 also, with 2 cells each).
* IGNORED when bS == 4.
* ----------------------------------------------------------------- */
struct daedalus_decoder_edge {
uint16_t mb_x;
uint16_t mb_y;
uint8_t edge_idx;
uint8_t orient;
uint8_t plane;
uint8_t bS;
uint8_t alpha;
uint8_t beta;
int8_t tc0[4];
};
/* -------------------------------------------------------------------
* Per-macroblock input. Mirrors §3 of DESIGN.md. The caller's
* libavcodec intercept populates this from the H264SliceContext
* fields after ff_h264_decode_mb_cabac/cavlc returns and before
* ff_h264_hl_decode_mb is supposed to run (we replace the latter).
* ----------------------------------------------------------------- */
struct daedalus_decoder_mb_input {
/* Frame coordinates (macroblock units). */
uint16_t mb_x;
uint16_t mb_y;
/* Type + quantisation. */
uint8_t mb_type; /* H.264 spec table 7-13/7-14/7-17/7-18 enum */
uint8_t mb_qp_y;
uint8_t mb_qp_uv;
uint8_t cbp; /* coded block pattern, 0..47 */
/* Intra prediction (used iff mb_type == I_NxN or I_16x16). */
uint8_t intra_4x4_modes[16];
uint8_t intra_16x16_mode;
uint8_t intra_chroma_mode;
/* Inter motion / partitions (used iff P_* or B_*). */
uint8_t partition_mode; /* P_16x16 / P_16x8 / P_8x16 / P_8x8 / etc. */
int8_t ref_idx_l0[4]; /* per partition; -1 = not used */
int8_t ref_idx_l1[4]; /* B only */
int16_t mv_l0[4][2]; /* qpel precision (1/4 sample); (x, y) */
int16_t mv_l1[4][2];
/* Deblocking filter parameters. */
uint8_t deblock_disable; /* 0 = enabled */
int8_t deblock_alpha_c0;
int8_t deblock_beta;
/* High-profile 8x8 transform selector.
* 0 = the 256-int16 luma section of coeffs[] holds 16 4x4 blocks
* (16 coeffs each, raster sb_y*4+sb_x); the chroma section is
* always 4x4.
* 1 = the 256-int16 luma section holds 4 8x8 blocks (64 coeffs
* each, raster sb_y*2+sb_x). Set per H.264's
* transform_8x8_size_flag. Chroma remains 4x4 (4:2:0).
*/
uint8_t transform_8x8;
/* Transform coefficients — 256 luma + 64 cb + 64 cr int16, all
* column-major within each 4x4 or 8x8 block (matches FFmpeg
* convention). Caller-owned; copied during append. */
const int16_t *coeffs; /* points at exactly 384 int16_t */
/* Reconstructed predicted samples for this MB, planar order:
* [ 0 .. 256) — 16×16 luma, ROW-MAJOR raster (row 0 cols 0..15,
* row 1 cols 0..15, ..., row 15 cols 0..15)
* [256 .. 320) — 8×8 Cb, ROW-MAJOR raster
* [320 .. 384) — 8×8 Cr, ROW-MAJOR raster
*
* The caller (libavcodec's CPU intra-prediction kernels for Phase 1
* I-frames; MC fallback for Phase 2 P-frames before GPU MC lands)
* populates this from neighbour samples per H.264 §8.3 / §8.4.
* `flush_frame()`'s reconstruction step is `clip255(predicted +
* idct(coeffs))` — the IDCT shader reads dst, adds the inverse
* transform, writes clipped — so a non-zero `predicted` here makes
* the output pixel a valid H.264 reconstruction; zero means
* residual-only (used by IDCT-isolation tests).
*
* NULL is legal and means "all-zero predicted samples" for this MB
* (the per-frame predicted buffer is zeroed at flush time so a NULL
* is indistinguishable from explicit zeros). */
const uint8_t *predicted; /* NULL or exactly 384 uint8_t */
/* Per-MB deblock edges — caller-derived per H.264 §8.7.2. Typical
* count: 4 V-luma + 4 H-luma + 2 V-Cb + 2 H-Cb + 2 V-Cr + 2 H-Cr
* = 16 edges per MB (omit zero-bS edges if preferred — frame
* boundaries MUST be bS=0 since the kernels read p3 at four
* samples beyond the edge). daedalus_decoder routes each entry
* to the appropriate luma/chroma × V/H × bS=4/<4 dispatch in
* flush_frame and pays a single Vulkan submit per non-empty
* (direction × bS-band) partition (≤8 deblock submits / frame
* total) per the Q1 architecture decision (one-submit-per-kernel
* for now; cmdbuf-builder deferred to Stage 4).
*
* NULL or n_edges == 0 → no deblock on this MB. */
const struct daedalus_decoder_edge *edges;
uint8_t n_edges;
};
/* -------------------------------------------------------------------
* Output frame format selector.
* ----------------------------------------------------------------- */
typedef enum {
DAEDALUS_DECODER_OUTPUT_NV12 = 0, /* default; Stage 4 final */
DAEDALUS_DECODER_OUTPUT_RGBA = 1, /* Stage 5 opt-in */
} daedalus_decoder_output_format;
/* -------------------------------------------------------------------
* Substrate selector. Determines which backend daedalus-fourier
* dispatches the per-frame compute through.
*
* AUTO is the only sensible choice for production — it picks per the
* recipe table baked into daedalus-fourier (post 2026-05-23 decree:
* QPU when a V3D shader exists, CPU NEON otherwise). The explicit
* options exist for testing:
*
* - CPU forces the dispatch onto the NEON path even when V3D7 is
* available. Lets the bit-exact ctests run on hosts without a
* working Vulkan/V3D stack (CI runners, dev x86 boxes via
* cross-build), and lets us cross-check the V3D shader output
* against the NEON reference path on hosts that DO have V3D.
* - QPU is the dual — force QPU even on a CPU-preferred kernel.
* Useful for benchmarking specific QPU paths in isolation.
*
* A non-AUTO selection on a host that can't satisfy it
* (DAEDALUS_DECODER_SUBSTRATE_QPU on an x86 dev box) propagates a
* dispatch failure back through flush_frame as -3.
* ----------------------------------------------------------------- */
typedef enum {
DAEDALUS_DECODER_SUBSTRATE_AUTO = 0,
DAEDALUS_DECODER_SUBSTRATE_CPU = 1,
DAEDALUS_DECODER_SUBSTRATE_QPU = 2,
} daedalus_decoder_substrate;
/* -------------------------------------------------------------------
* Lifecycle
* ----------------------------------------------------------------- */
/* Create a decoder context for the given **coded** frame dimensions.
*
* width, height: pixels of the H.264 coded picture, NOT the displayed
* picture. Both must be multiples of 16 (macroblock granularity).
* For displayed 1080p (1920×1080), the coded frame is 1920×1088 with
* the SPS's `frame_cropping_*` offsets cropping the bottom 8 rows.
* The caller is responsible for translating from SPS dims + crop
* rectangle to the values passed here; we decode the coded frame.
*
* Returns NULL on bad dimensions or allocation failure. Returns a
* usable context with daedalus_decoder_has_qpu() == 0 when Vulkan
* init fails — callers that need GPU work should check has_qpu
* before relying on it.
*/
daedalus_decoder *daedalus_decoder_create(int width, int height);
/* Free all resources. Safe with NULL. */
void daedalus_decoder_destroy(daedalus_decoder *dec);
/* Switch output format BEFORE the first append_mb call of a frame.
* Default is NV12. Returns 0 on success, -1 if called mid-frame
* (caller must flush first). */
int daedalus_decoder_set_output_format(daedalus_decoder *dec,
daedalus_decoder_output_format fmt);
/* Override the dispatch substrate for subsequent flush_frame calls.
* Default is AUTO. Same mid-frame-change restriction as
* set_output_format. */
int daedalus_decoder_set_substrate(daedalus_decoder *dec,
daedalus_decoder_substrate sub);
/* -------------------------------------------------------------------
* Per-frame submission
* ----------------------------------------------------------------- */
/* Append one macroblock's data to the current frame's descriptor SSBO
* + coefficient SSBO. No GPU dispatch yet — just CPU-side writes.
*
* Must be called in raster order (mb_y * mb_width + mb_x) for the
* intra-prediction wavefront to work correctly in Phase 1.
*
* Returns 0 on success, negative on bounds violation or OOM.
*/
int daedalus_decoder_append_mb(daedalus_decoder *dec,
const struct daedalus_decoder_mb_input *mb);
/* End-of-frame flush: builds the per-frame VkCommandBuffer with all
* pipeline stages, submits once, waits on a single fence, copies the
* NV12 (or RGBA when opted in) output into the caller-provided
* planes.
*
* For NV12:
* out_y / y_stride: Y plane (W*H bytes minimum, at the given stride)
* out_uv / uv_stride: interleaved UV plane (W*(H/2) bytes minimum)
*
* For RGBA: out_y receives 4*W*H bytes at y_stride; out_uv ignored.
*
* Returns 0 on success, negative on Vulkan failure or undecodable
* frame. After return, the decoder is ready for the next frame's
* append calls.
*/
int daedalus_decoder_flush_frame(daedalus_decoder *dec,
uint8_t *out_y, size_t y_stride,
uint8_t *out_uv, size_t uv_stride);
/* Export the most-recently-decoded frame as a dma_buf fd. The fd is
* owned by the caller and must be closed when done. Lets V4L2
* consumers (daedalus_v4l2_daemon, libva-v4l2-request-fourier) attach
* the GPU-decoded surface directly to a CAPTURE plane without a CPU
* round-trip.
*
* Returns the dmabuf fd on success, -1 on failure. Must be called
* AFTER flush_frame returns for the relevant frame.
*/
int daedalus_decoder_export_dmabuf(daedalus_decoder *dec, int plane);
/* -------------------------------------------------------------------
* Diagnostics
* ----------------------------------------------------------------- */
/* daedalus-decoder build version (semver string, e.g. "0.0.1+g0a1b2c3"). */
const char *daedalus_decoder_version(void);
/* Whether the underlying daedalus-fourier context picked up a working
* V3D7 Vulkan instance. Returns 0 if Vulkan init failed and the
* decoder is operating in stub / failure mode. */
int daedalus_decoder_has_qpu(const daedalus_decoder *dec);
#ifdef __cplusplus
}
#endif
#endif /* DAEDALUS_DECODER_H */
-691
View File
@@ -1,691 +0,0 @@
/* SPDX-License-Identifier: BSD-2-Clause */
/*
* daedalus-decoder — public C API implementation.
*
* Scaffold only. Most functions return success with no GPU work
* performed; the bodies will fill in across Phases 1-4 per DESIGN.md
* §8. This file exists so the API surface compiles, links, and can
* be smoke-tested end-to-end (ctx create / append / flush / destroy)
* before any shader work begins.
*/
#include "internal.h"
#include <stdlib.h>
#include <string.h>
/* Built via -D from CMakeLists. */
#ifndef DAEDALUS_DECODER_VERSION
#define DAEDALUS_DECODER_VERSION "0.0.1+scaffold"
#endif
const char *daedalus_decoder_version(void)
{
return DAEDALUS_DECODER_VERSION;
}
daedalus_decoder *daedalus_decoder_create(int width, int height)
{
if (width <= 0 || height <= 0)
return NULL;
if ((width & 15) || (height & 15))
return NULL; /* must be multiple of 16 */
daedalus_decoder *dec = calloc(1, sizeof(*dec));
if (!dec)
return NULL;
dec->width = width;
dec->height = height;
dec->mb_width = width >> 4;
dec->mb_height = height >> 4;
dec->n_mbs = dec->mb_width * dec->mb_height;
dec->output_fmt = DAEDALUS_DECODER_OUTPUT_NV12;
dec->substrate = DAEDALUS_DECODER_SUBSTRATE_AUTO;
/* daedalus-fourier ctx — required. Phase 1 needs the QPU; if
* Vulkan init fails the decoder is unusable. Caller can check
* via daedalus_decoder_has_qpu(). */
dec->dctx = daedalus_ctx_create();
if (!dec->dctx) {
free(dec);
return NULL;
}
dec->mb_descs = calloc((size_t) dec->n_mbs, sizeof(*dec->mb_descs));
dec->coeffs = calloc((size_t) dec->n_mbs * 384, sizeof(int16_t));
/* Predicted-samples buffers — zero-initialised so a frame where
* every append_mb gets NULL `predicted` decodes residual-only
* (the Stage 1 scaffold contract). flush_frame zeroes these at
* end-of-frame to maintain that invariant for the next frame. */
const size_t pred_y_size = (size_t) width * (size_t) height;
const size_t pred_uv_size = pred_y_size / 2;
dec->predicted_y = calloc(1, pred_y_size);
dec->predicted_uv = calloc(1, pred_uv_size);
/* Edge buffer sized for the typical worst case (see daedalus_decoder.h).
* 16 edges/MB × n_mbs. ~130k entries for 1080p; ~2 MB at sizeof(edge). */
dec->edges_capacity = (size_t) dec->n_mbs * 16;
dec->edges_count = 0;
dec->edges = malloc(dec->edges_capacity * sizeof(*dec->edges));
if (!dec->mb_descs || !dec->coeffs ||
!dec->predicted_y || !dec->predicted_uv || !dec->edges) {
daedalus_decoder_destroy(dec);
return NULL;
}
return dec;
}
void daedalus_decoder_destroy(daedalus_decoder *dec)
{
if (!dec)
return;
free(dec->edges);
free(dec->predicted_uv);
free(dec->predicted_y);
free(dec->coeffs);
free(dec->mb_descs);
if (dec->dctx)
daedalus_ctx_destroy(dec->dctx);
free(dec);
}
int daedalus_decoder_set_output_format(daedalus_decoder *dec,
daedalus_decoder_output_format fmt)
{
if (!dec)
return -1;
if (dec->mbs_appended != 0)
return -1; /* mid-frame change forbidden */
if (fmt != DAEDALUS_DECODER_OUTPUT_NV12 &&
fmt != DAEDALUS_DECODER_OUTPUT_RGBA)
return -1;
dec->output_fmt = fmt;
return 0;
}
int daedalus_decoder_set_substrate(daedalus_decoder *dec,
daedalus_decoder_substrate sub)
{
if (!dec)
return -1;
if (dec->mbs_appended != 0)
return -1;
if (sub != DAEDALUS_DECODER_SUBSTRATE_AUTO &&
sub != DAEDALUS_DECODER_SUBSTRATE_CPU &&
sub != DAEDALUS_DECODER_SUBSTRATE_QPU)
return -1;
dec->substrate = sub;
return 0;
}
/* Map our public substrate enum onto daedalus-fourier's. Same
* ordering by intent — we duplicate the enum for ABI isolation. */
static daedalus_substrate map_substrate(daedalus_decoder_substrate s)
{
switch (s) {
case DAEDALUS_DECODER_SUBSTRATE_CPU: return DAEDALUS_SUBSTRATE_CPU;
case DAEDALUS_DECODER_SUBSTRATE_QPU: return DAEDALUS_SUBSTRATE_QPU;
case DAEDALUS_DECODER_SUBSTRATE_AUTO:
default: return DAEDALUS_SUBSTRATE_AUTO;
}
}
int daedalus_decoder_append_mb(daedalus_decoder *dec,
const struct daedalus_decoder_mb_input *mb)
{
if (!dec || !mb || !mb->coeffs)
return -1;
if (mb->mb_x >= dec->mb_width || mb->mb_y >= dec->mb_height)
return -1;
/* Raster-order check — Phase 1's intra wavefront requires it.
* Caller is libavcodec's slice loop which produces raster order
* naturally, so this should never fire in practice. */
int expected = mb->mb_y * dec->mb_width + mb->mb_x;
if (expected != dec->mbs_appended)
return -1;
struct daedalus_decoder_mb_desc *d = &dec->mb_descs[expected];
d->mb_x = mb->mb_x;
d->mb_y = mb->mb_y;
d->mb_type = mb->mb_type;
d->mb_qp_y = mb->mb_qp_y;
d->mb_qp_uv = mb->mb_qp_uv;
d->cbp = mb->cbp;
memcpy(d->intra_4x4_modes, mb->intra_4x4_modes, 16);
d->intra_16x16_mode = mb->intra_16x16_mode;
d->intra_chroma_mode = mb->intra_chroma_mode;
d->partition_mode = mb->partition_mode;
memcpy(d->ref_idx_l0, mb->ref_idx_l0, 4);
memcpy(d->ref_idx_l1, mb->ref_idx_l1, 4);
memcpy(d->mv_l0, mb->mv_l0, sizeof(d->mv_l0));
memcpy(d->mv_l1, mb->mv_l1, sizeof(d->mv_l1));
d->deblock_disable = mb->deblock_disable;
d->deblock_alpha_c0 = mb->deblock_alpha_c0;
d->deblock_beta = mb->deblock_beta;
d->transform_8x8 = mb->transform_8x8;
memcpy(&dec->coeffs[(size_t) expected * 384],
mb->coeffs,
384 * sizeof(int16_t));
/* Splat predicted samples into frame-scoped planes at raster
* (mb_y*16, mb_x*16) for luma, (mb_y*8, mb_x*8) for each chroma
* component. NULL → leave buffers as-is (zeroed at create + at
* end of each flush_frame); that's the zero-predictor contract. */
if (mb->predicted) {
const size_t y_stride = (size_t) dec->width;
const size_t uv_stride = (size_t) dec->width / 2;
const size_t uv_plane = uv_stride * ((size_t) dec->height / 2);
const uint8_t *p_y = mb->predicted;
const uint8_t *p_cb = mb->predicted + 256;
const uint8_t *p_cr = mb->predicted + 256 + 64;
uint8_t *dst_y = &dec->predicted_y[
(size_t) mb->mb_y * 16 * y_stride + (size_t) mb->mb_x * 16];
uint8_t *dst_cb = &dec->predicted_uv[
(size_t) mb->mb_y * 8 * uv_stride + (size_t) mb->mb_x * 8];
uint8_t *dst_cr = &dec->predicted_uv[uv_plane +
(size_t) mb->mb_y * 8 * uv_stride + (size_t) mb->mb_x * 8];
for (int r = 0; r < 16; r++)
memcpy(&dst_y[(size_t) r * y_stride], &p_y[r * 16], 16);
for (int r = 0; r < 8; r++) {
memcpy(&dst_cb[(size_t) r * uv_stride], &p_cb[r * 8], 8);
memcpy(&dst_cr[(size_t) r * uv_stride], &p_cr[r * 8], 8);
}
}
/* Append per-MB deblock edges into the frame-scoped flat buffer.
* Frame-boundary edges (mx=0 V or my=0 H) MUST have bS=0 per the
* kernel's p3-at-±4 contract; we don't validate here (caller is
* derived from H.264 spec which already enforces this). */
if (mb->edges && mb->n_edges > 0) {
if (dec->edges_count + mb->n_edges > dec->edges_capacity)
return -1;
memcpy(&dec->edges[dec->edges_count],
mb->edges,
mb->n_edges * sizeof(*dec->edges));
dec->edges_count += mb->n_edges;
}
dec->mbs_appended++;
return 0;
}
/* --------------------------------------------------------------------
* Deblock helper — walks dec->edges once for a given (plane, orient,
* bS_band) selector, builds the corresponding daedalus-fourier
* deblock-meta array, and dispatches it through the matching kernel.
*
* One call → one Vulkan submit, OR zero submits when the selector
* matches no edges (a common case for B/P frames with most edges in
* bS<4 and only MB-boundary edges in bS=4, or vice versa).
*
* Edge → dst_off math:
* luma: px_x = mb_x*16, px_y = mb_y*16, edge step = 4 cells
* chroma: px_x = mb_x*8, px_y = mb_y*8, edge step = 4 cells
* Cb edges land at offset 0..cb_plane in scratch_uv;
* Cr edges land at offset cb_plane..2*cb_plane (planar
* layout matching the chroma IDCT scratch).
*
* orient == 0 (vertical edge filtered horizontally across):
* dst_off = px_y * stride + px_x + edge_idx * 4
*
* orient == 1 (horizontal edge filtered vertically across):
* dst_off = (px_y + edge_idx * 4) * stride + px_x
*
* Edges at frame boundaries (mb_x=0 V, mb_y=0 H with edge_idx=0) MUST
* have bS=0 (the kernel reads p3 at four samples beyond the edge);
* caller-side spec compliance is assumed, no validation here.
*
* Returns the dispatch's rc (0 = success; <0 = failure). No-op when
* the selector matches no edges, returning 0.
*/
static int dispatch_deblock_pass(
daedalus_decoder *dec, daedalus_substrate sub,
int target_plane, /* 0 = luma, 1 = chroma (Cb|Cr by plane field) */
int target_orient, /* 0 = V, 1 = H */
int target_bS_intra, /* 0 = bS<4 path, 1 = bS=4 intra path */
uint8_t *scratch, size_t stride,
size_t cb_plane_size, /* chroma: bytes from scratch_uv start to Cr plane (0 for luma calls) */
daedalus_h264_deblock_meta *meta_scratch)
{
size_t n = 0;
for (size_t i = 0; i < dec->edges_count; i++) {
const struct daedalus_decoder_edge *e = &dec->edges[i];
if (e->bS == 0) continue;
int is_intra = (e->bS == 4) ? 1 : 0;
if (is_intra != target_bS_intra) continue;
if (e->orient != target_orient) continue;
int is_luma = (e->plane == 0) ? 1 : 0;
if (is_luma != (target_plane == 0)) continue;
uint32_t off;
if (is_luma) {
const size_t px_y = (size_t) e->mb_y * 16;
const size_t px_x = (size_t) e->mb_x * 16;
if (target_orient == 0) /* V */
off = (uint32_t)(px_y * stride + px_x + (size_t) e->edge_idx * 4);
else /* H */
off = (uint32_t)((px_y + (size_t) e->edge_idx * 4) * stride + px_x);
} else {
const size_t px_y = (size_t) e->mb_y * 8;
const size_t px_x = (size_t) e->mb_x * 8;
const size_t plane_base = (e->plane == 2) ? cb_plane_size : 0;
if (target_orient == 0)
off = (uint32_t)(plane_base + px_y * stride + px_x + (size_t) e->edge_idx * 4);
else
off = (uint32_t)(plane_base + (px_y + (size_t) e->edge_idx * 4) * stride + px_x);
}
meta_scratch[n].dst_off = off;
meta_scratch[n].alpha = e->alpha;
meta_scratch[n].beta = e->beta;
memcpy(meta_scratch[n].tc0, e->tc0, 4);
n++;
}
if (n == 0) return 0;
typedef int (*deblock_dispatch_fn)(
daedalus_ctx *, daedalus_substrate,
uint8_t *, size_t, size_t,
const daedalus_h264_deblock_meta *);
/* daedalus-fourier kernel naming convention:
* _v = "v_loop_filter" — filter applied VERTICALLY across a
* HORIZONTAL edge. Use for our orient=1 (H edge).
* _h = "h_loop_filter" — filter applied HORIZONTALLY across a
* VERTICAL edge. Use for our orient=0 (V edge).
* The names refer to the FILTER DIRECTION, not the edge direction. */
deblock_dispatch_fn fn;
if (target_plane == 0) {
if (target_orient == 0) /* V edge → h_loop_filter */
fn = target_bS_intra ? daedalus_dispatch_h264_deblock_luma_h_intra
: daedalus_dispatch_h264_deblock_luma_h;
else /* H edge → v_loop_filter */
fn = target_bS_intra ? daedalus_dispatch_h264_deblock_luma_v_intra
: daedalus_dispatch_h264_deblock_luma_v;
} else {
if (target_orient == 0)
fn = target_bS_intra ? daedalus_dispatch_h264_deblock_chroma_h_intra
: daedalus_dispatch_h264_deblock_chroma_h;
else
fn = target_bS_intra ? daedalus_dispatch_h264_deblock_chroma_v_intra
: daedalus_dispatch_h264_deblock_chroma_v;
}
return fn(dec->dctx, sub, scratch, stride, n, meta_scratch);
}
/* Phase 1 stage 1 — frame-scaled IDCT 4x4 dispatch (luma + chroma).
*
* Brings up the GPU substrate by calling daedalus-fourier's existing
* `daedalus_recipe_dispatch_h264_idct4` at frame batch granularity in
* contrast to the substitution-arc shim that called it with
* n_blocks = 1 per call. Two Vulkan submits + waits per frame (one
* luma, one chroma) instead of millions of per-block dispatches.
*
* What's done in this stage:
* - Luma: build a per-frame meta[] in raster order (n_blocks =
* N_MBs × 16); flat-pack coeffs from each MB's first 256 int16;
* dispatch into a frame-sized zero-initialised Y scratch plane.
* - Chroma: build an interleaved Cb+Cr meta[] (n_blocks = N_MBs × 8,
* 4 Cb + 4 Cr per MB); flat-pack coeffs from each MB's next 128
* int16 (64 Cb + 64 Cr); dispatch into a planar Cb||Cr scratch
* buffer (W*H/4 each, concatenated W*H/2 total); CPU-interleave
* into the caller's NV12 UV plane post-dispatch.
* - Both dispatches pre-fill the scratch from the per-frame
* predicted_y / predicted_uv buffers (accumulated by append_mb's
* per-MB predicted-samples splat). The IDCT shader's
* `dst += idct(coeffs)` + clip255 then folds reconstruction into
* the IDCT pass — no separate Stage 3 dispatch needed.
*
* What's NOT done yet (follow-on Phase 1 sub-PRs):
* - Intra prediction: caller-driven (Q2 decision 2026-05-25, CPU
* intra-pred via FFmpeg NEON kernels). Caller writes the
* intra-predicted samples into mb_input.predicted; this dispatch
* consumes them as the IDCT-add starting state. GPU wavefront
* intra-pred (DESIGN.md Stage 2a) is no longer planned.
* - Motion compensation (Stage 2b): inter MBs not handled.
* - High-profile IDCT 8x8 (Stage 1 extension).
* - Chroma DC / luma Intra16x16 DC Hadamard pre-pass (currently we
* treat all chroma blocks as plain 4×4 AC IDCT; real decode needs
* the chroma DC 2×2 Hadamard contribution folded in).
* - Deblock (Stage 4).
* - dmabuf export — still memcpy-out to caller-provided planes.
* - Stage 5 RGBA opt-in.
* - GPU-side NV12 interleave — currently a CPU memcpy loop after
* the chroma dispatch. Trivial cost (~1 MB / frame at 1080p)
* vs the IDCT itself, but worth folding into a Stage-5 pass
* later for full-GPU residency.
*/
int daedalus_decoder_flush_frame(daedalus_decoder *dec,
uint8_t *out_y, size_t y_stride,
uint8_t *out_uv, size_t uv_stride)
{
if (!dec)
return -1;
if (dec->mbs_appended != dec->n_mbs)
return -1; /* incomplete frame */
if (!out_y)
return -1;
int rc = 0;
/* ---- Build frame-scaled luma dispatches (4x4 + 8x8) ---- */
/* Two partitions of the per-MB luma section based on each MB's
* transform_8x8 flag:
*
* transform_8x8 == 0 → 16 4x4 blocks contribute to the 4x4
* dispatch (16 coeffs each).
* transform_8x8 == 1 → 4 8x8 blocks contribute to the 8x8
* dispatch (64 coeffs each).
*
* Both partitions can be non-empty in the same frame (FFmpeg sets
* transform_8x8_size_flag per MB), so we allocate worst-case for
* each and track actual counts.
*/
/* Pre-fill the dispatch scratch with the per-MB predicted samples
* accumulated by append_mb. daedalus-fourier's IDCT 4x4/8x8
* shaders implement FFmpeg `idct_add` semantics — dst += idct(coeffs)
* with clip255 — so a non-zero predicted dst becomes the
* reconstruction step (residual + predicted → clip) "for free",
* collapsing DESIGN.md's Stage 3 into Stage 1's existing dispatch. */
const size_t y_stride_int = (size_t) dec->width;
const size_t y_size = y_stride_int * (size_t) dec->height;
uint8_t *scratch_y = malloc(y_size);
if (scratch_y)
memcpy(scratch_y, dec->predicted_y, y_size);
const size_t worst_4x4 = (size_t) dec->n_mbs * 16;
const size_t worst_8x8 = (size_t) dec->n_mbs * 4;
int16_t *coeffs4 = malloc(worst_4x4 * 16 * sizeof(int16_t));
int16_t *coeffs8 = malloc(worst_8x8 * 64 * sizeof(int16_t));
daedalus_h264_block_meta *meta4 = malloc(worst_4x4 * sizeof(*meta4));
daedalus_h264_block_meta *meta8 = malloc(worst_8x8 * sizeof(*meta8));
if (!scratch_y || !coeffs4 || !coeffs8 || !meta4 || !meta8) {
rc = -1;
goto cleanup;
}
/* Walk MBs in raster order, append each MB's luma blocks to the
* partition selected by its transform_8x8 flag.
*
* NB: per-MB 4x4 / 8x8 coefficient ORDER inside the H.264 bitstream
* follows the z-scan from spec §6.4.3 / fig 6-10. We're using
* flat raster on the input side too (sb_y outer, sb_x inner) for
* Phase 1 self-consistency; the z-scan permutation is the
* libavcodec-intercept patch's responsibility.
*/
size_t bi4 = 0, bi8 = 0;
for (int mb_y = 0; mb_y < dec->mb_height; mb_y++) {
for (int mb_x = 0; mb_x < dec->mb_width; mb_x++) {
int mb_idx = mb_y * dec->mb_width + mb_x;
const struct daedalus_decoder_mb_desc *d = &dec->mb_descs[mb_idx];
const int16_t *mb_coeffs = &dec->coeffs[(size_t) mb_idx * 384];
if (d->transform_8x8) {
/* 4 luma 8x8 blocks, raster sb_y*2+sb_x. */
for (int sb_y = 0; sb_y < 2; sb_y++) {
for (int sb_x = 0; sb_x < 2; sb_x++) {
size_t px_y = (size_t) mb_y * 16 + (size_t) sb_y * 8;
size_t px_x = (size_t) mb_x * 16 + (size_t) sb_x * 8;
meta8[bi8].dst_off = (uint32_t)
(px_y * y_stride_int + px_x);
int block_in_mb = sb_y * 2 + sb_x;
memcpy(&coeffs8[bi8 * 64],
&mb_coeffs[block_in_mb * 64],
64 * sizeof(int16_t));
bi8++;
}
}
} else {
/* 16 luma 4x4 blocks, raster sb_y*4+sb_x. */
for (int sb_y = 0; sb_y < 4; sb_y++) {
for (int sb_x = 0; sb_x < 4; sb_x++) {
size_t px_y = (size_t) mb_y * 16 + (size_t) sb_y * 4;
size_t px_x = (size_t) mb_x * 16 + (size_t) sb_x * 4;
meta4[bi4].dst_off = (uint32_t)
(px_y * y_stride_int + px_x);
int block_in_mb = sb_y * 4 + sb_x;
memcpy(&coeffs4[bi4 * 16],
&mb_coeffs[block_in_mb * 16],
16 * sizeof(int16_t));
bi4++;
}
}
}
}
}
/* assert bi4 + bi8*4 == n_mbs*16; loop math guarantees it */
/* ---- One Vulkan submit + wait per non-empty luma partition.
* AUTO substrate picks QPU per the post-decree recipe table; falls
* back to CPU NEON if the daedalus-fourier ctx wasn't QPU-capable.
* Skipping the dispatch when the partition is empty avoids the
* shader-pool warm-up cost on the common case (a typical Baseline
* stream is all-4x4 → 8x8 dispatch is no-op). */
const daedalus_substrate sub = map_substrate(dec->substrate);
if (bi4 > 0) {
int dr = daedalus_dispatch_h264_idct4(dec->dctx, sub,
scratch_y, y_stride_int,
coeffs4, bi4, meta4);
if (dr != 0) { rc = -3; goto cleanup; }
}
if (bi8 > 0) {
int dr = daedalus_dispatch_h264_idct8(dec->dctx, sub,
scratch_y, y_stride_int,
coeffs8, bi8, meta8);
if (dr != 0) { rc = -3; goto cleanup; }
}
/* ---- Luma deblock V then H ----
* Per H.264 §8.7 deblock order is V edges first, then H edges,
* within each MB. At frame scale we hit the same dependency: a
* row of V-filtered samples is the input to the H filter for
* the row's H edges. Order: V bS<4 + V bS=4 (independent edges,
* either order), barrier (implicit at each dispatch's wait), then
* H bS<4 + H bS=4. */
daedalus_h264_deblock_meta *dbk_meta = NULL;
if (dec->edges_count > 0) {
dbk_meta = malloc(dec->edges_count * sizeof(*dbk_meta));
if (!dbk_meta) { rc = -1; goto cleanup; }
int dr;
dr = dispatch_deblock_pass(dec, sub, 0, 0, 0,
scratch_y, y_stride_int, 0, dbk_meta);
if (dr != 0) { rc = -3; goto cleanup; }
dr = dispatch_deblock_pass(dec, sub, 0, 0, 1,
scratch_y, y_stride_int, 0, dbk_meta);
if (dr != 0) { rc = -3; goto cleanup; }
dr = dispatch_deblock_pass(dec, sub, 0, 1, 0,
scratch_y, y_stride_int, 0, dbk_meta);
if (dr != 0) { rc = -3; goto cleanup; }
dr = dispatch_deblock_pass(dec, sub, 0, 1, 1,
scratch_y, y_stride_int, 0, dbk_meta);
if (dr != 0) { rc = -3; goto cleanup; }
}
/* ---- Copy Y out to caller's plane at the requested stride. ---- */
for (int r = 0; r < dec->height; r++)
memcpy(out_y + (size_t) r * y_stride,
&scratch_y[(size_t) r * y_stride_int],
(size_t) dec->width);
/* ---- Build frame-scaled chroma 4×4 dispatch ---- */
/*
* 4:2:0 layout — chroma planes are (W/2) by (H/2), one Cb + one
* Cr per pixel pair. H.264 per-MB chroma is two 8×8 components,
* each split into 4 4×4 blocks, so 8 chroma 4×4 blocks per MB.
*
* We dispatch BOTH components in a single shader call against a
* planar scratch buffer:
* scratch_uv[0 .. cb_plane_size) — Cb plane (W/2 × H/2)
* scratch_uv[cb_plane_size .. 2*size) — Cr plane (W/2 × H/2)
*
* meta[i].dst_off is a flat offset into the scratch buffer (the
* shader treats dst+dst_off as a contiguous 4×4 with row pitch =
* stride), so Cr blocks just add cb_plane_size to their offset.
* Stride is W/2 (the chroma row width); this works because Cb and
* Cr planes share the same row pitch.
*
* Post-dispatch we interleave the two planes into NV12 UV layout
* on the CPU. Doing this on the GPU is a Stage-5 follow-up
* (would need a small "copy + interleave" shader); CPU memcpy
* loop is ~1 MB/frame at 1080p so it's not on the critical path.
*/
int16_t *chroma_coeffs = NULL;
daedalus_h264_block_meta *chroma_meta = NULL;
uint8_t *scratch_uv = NULL;
if (out_uv) {
const size_t n_chroma_blocks_per_mb = 8; /* 4 Cb + 4 Cr */
const size_t n_chroma_blocks =
(size_t) dec->n_mbs * n_chroma_blocks_per_mb;
const size_t chroma_w = (size_t) dec->width / 2;
const size_t chroma_h = (size_t) dec->height / 2;
const size_t cb_plane_size = chroma_w * chroma_h;
const size_t uv_scratch_size = 2 * cb_plane_size;
scratch_uv = malloc(uv_scratch_size);
if (scratch_uv)
memcpy(scratch_uv, dec->predicted_uv, uv_scratch_size);
chroma_coeffs = malloc(n_chroma_blocks * 16 * sizeof(int16_t));
chroma_meta = malloc(n_chroma_blocks *
sizeof(daedalus_h264_block_meta));
if (!scratch_uv || !chroma_coeffs || !chroma_meta) {
rc = -1;
goto chroma_cleanup;
}
size_t cbi = 0;
for (int mb_y = 0; mb_y < dec->mb_height; mb_y++) {
for (int mb_x = 0; mb_x < dec->mb_width; mb_x++) {
int mb_idx = mb_y * dec->mb_width + mb_x;
const int16_t *mb_coeffs = &dec->coeffs[(size_t) mb_idx * 384];
/* Per-MB coeff layout (set by append_mb):
* [ 0 .. 256) — 16 luma 4×4 blocks
* [256 .. 320) — 4 Cb 4×4 blocks (raster sb_y*2+sb_x)
* [320 .. 384) — 4 Cr 4×4 blocks (raster sb_y*2+sb_x)
*/
for (int comp = 0; comp < 2; comp++) { /* 0=Cb 1=Cr */
size_t plane_base = (size_t) comp * cb_plane_size;
size_t coeff_base = 256u + (size_t) comp * 64u;
for (int sb_y = 0; sb_y < 2; sb_y++) {
for (int sb_x = 0; sb_x < 2; sb_x++) {
size_t px_y = (size_t) mb_y * 8 + (size_t) sb_y * 4;
size_t px_x = (size_t) mb_x * 8 + (size_t) sb_x * 4;
chroma_meta[cbi].dst_off = (uint32_t)
(plane_base + px_y * chroma_w + px_x);
int block_in_comp = sb_y * 2 + sb_x;
memcpy(&chroma_coeffs[cbi * 16],
&mb_coeffs[coeff_base + (size_t) block_in_comp * 16],
16 * sizeof(int16_t));
cbi++;
}
}
}
}
}
/* assert cbi == n_chroma_blocks; loop math guarantees it */
int cr_rc = daedalus_dispatch_h264_idct4(dec->dctx, sub,
scratch_uv, chroma_w,
chroma_coeffs,
n_chroma_blocks,
chroma_meta);
if (cr_rc != 0) {
rc = -3;
goto chroma_cleanup;
}
/* ---- Chroma deblock V then H ----
* scratch_uv is PLANAR Cb||Cr with stride = chroma_w; both
* planes filtered in the same dispatch via Cb's dst_off and
* Cr's dst_off = cb_plane_size + (same). */
if (dec->edges_count > 0 && dbk_meta) {
int dr;
dr = dispatch_deblock_pass(dec, sub, 1, 0, 0,
scratch_uv, chroma_w,
cb_plane_size, dbk_meta);
if (dr != 0) { rc = -3; goto chroma_cleanup; }
dr = dispatch_deblock_pass(dec, sub, 1, 0, 1,
scratch_uv, chroma_w,
cb_plane_size, dbk_meta);
if (dr != 0) { rc = -3; goto chroma_cleanup; }
dr = dispatch_deblock_pass(dec, sub, 1, 1, 0,
scratch_uv, chroma_w,
cb_plane_size, dbk_meta);
if (dr != 0) { rc = -3; goto chroma_cleanup; }
dr = dispatch_deblock_pass(dec, sub, 1, 1, 1,
scratch_uv, chroma_w,
cb_plane_size, dbk_meta);
if (dr != 0) { rc = -3; goto chroma_cleanup; }
}
/* CPU NV12 interleave: out_uv[r][2c+0] = Cb[r][c], [2c+1] = Cr. */
const uint8_t *cb_plane = scratch_uv;
const uint8_t *cr_plane = scratch_uv + cb_plane_size;
for (size_t r = 0; r < chroma_h; r++) {
uint8_t *dst_row = out_uv + r * uv_stride;
const uint8_t *cb_row = cb_plane + r * chroma_w;
const uint8_t *cr_row = cr_plane + r * chroma_w;
for (size_t c = 0; c < chroma_w; c++) {
dst_row[c * 2 + 0] = cb_row[c];
dst_row[c * 2 + 1] = cr_row[c];
}
}
chroma_cleanup:
free(chroma_meta);
free(chroma_coeffs);
free(scratch_uv);
if (rc != 0)
goto cleanup;
}
cleanup:
free(dbk_meta);
free(meta8);
free(meta4);
free(coeffs8);
free(coeffs4);
free(scratch_y);
/* Zero the predicted-samples buffers so the next frame starts from
* the all-zero-predictor baseline; MBs whose append_mb gets NULL
* for `predicted` then decode residual-only. */
if (dec->predicted_y)
memset(dec->predicted_y, 0, (size_t) dec->width * (size_t) dec->height);
if (dec->predicted_uv)
memset(dec->predicted_uv, 0, (size_t) dec->width * (size_t) dec->height / 2);
/* Reset edges_count for the next frame; capacity stays. */
dec->edges_count = 0;
dec->mbs_appended = 0;
return rc;
}
int daedalus_decoder_export_dmabuf(daedalus_decoder *dec, int plane)
{
(void) dec; (void) plane;
/* TODO Phase 1: vkGetMemoryFdKHR on the DPB slot's VkImage memory. */
return -1;
}
int daedalus_decoder_has_qpu(const daedalus_decoder *dec)
{
if (!dec || !dec->dctx)
return 0;
return daedalus_ctx_has_qpu(dec->dctx);
}
-95
View File
@@ -1,95 +0,0 @@
/* SPDX-License-Identifier: BSD-2-Clause */
/*
* daedalus-decoder — internal types shared across translation units.
* Not installed; pure-internal.
*/
#ifndef DAEDALUS_DECODER_INTERNAL_H
#define DAEDALUS_DECODER_INTERNAL_H
#include "daedalus_decoder.h"
#include <stdint.h>
#include <stddef.h>
#include <daedalus.h> /* daedalus-fourier public API */
/* Per-MB descriptor as the GPU sees it. Bit-laid-out to match the
* shader's std430 layout. Kept narrow (32 bytes target) so a 1080p
* frame's 8160 entries fit in ~256 KiB SSBO.
*
* TODO once the shaders exist: nail down the exact std430 layout and
* static_assert sizeof / alignof here. */
struct daedalus_decoder_mb_desc {
uint16_t mb_x;
uint16_t mb_y;
uint8_t mb_type;
uint8_t mb_qp_y;
uint8_t mb_qp_uv;
uint8_t cbp;
uint8_t intra_4x4_modes[16];
uint8_t intra_16x16_mode;
uint8_t intra_chroma_mode;
uint8_t partition_mode;
uint8_t _pad0;
int8_t ref_idx_l0[4];
int8_t ref_idx_l1[4];
int16_t mv_l0[4][2];
int16_t mv_l1[4][2];
uint8_t deblock_disable;
int8_t deblock_alpha_c0;
int8_t deblock_beta;
uint8_t transform_8x8; /* 0 = 4 luma blocks of 4x4 (16 total),
* 1 = 4 luma blocks of 8x8. */
};
struct daedalus_decoder {
/* Geometry. */
int width;
int height;
int mb_width; /* width / 16 */
int mb_height; /* height / 16 */
int n_mbs;
/* daedalus-fourier context (Vulkan + V3D7 runner). */
daedalus_ctx *dctx;
/* Frame-shaped staging (CPU-side; will move to mapped SSBO once
* Vulkan plumbing is in place). */
struct daedalus_decoder_mb_desc *mb_descs; /* n_mbs */
int16_t *coeffs; /* n_mbs * 384 */
int mbs_appended; /* per-frame count */
/* Per-frame predicted samples, accumulated by append_mb(), consumed
* by flush_frame() as the initial dst content for the IDCT-add
* dispatch (predicted + idct → clip → final pixel). Zeroed at end
* of each flush_frame so NULL `mb->predicted` is indistinguishable
* from explicit zeros.
*
* predicted_y: width × height, row-major (stride = width)
* predicted_uv: PLANAR Cb||Cr, each (width/2) × (height/2), so
* size = width × height / 2, with Cb plane at
* offset 0 and Cr at offset (width/2)*(height/2).
* Matches scratch_uv layout in flush_frame. */
uint8_t *predicted_y;
uint8_t *predicted_uv;
/* Per-frame flat deblock-edge buffer, accumulated by append_mb's
* `edges` array and consumed by flush_frame. Capacity is sized
* for the typical maximum of 16 edges/MB (4 V-luma + 4 H-luma +
* 2 V-Cb + 2 H-Cb + 2 V-Cr + 2 H-Cr — see daedalus_decoder.h).
* Overflow returns -1 from append_mb. */
struct daedalus_decoder_edge *edges;
size_t edges_capacity; /* allocated entries */
size_t edges_count; /* used entries this frame */
/* Output format. */
daedalus_decoder_output_format output_fmt;
/* Dispatch substrate (AUTO by default — recipe-table-driven). */
daedalus_decoder_substrate substrate;
};
#endif /* DAEDALUS_DECODER_INTERNAL_H */
-215
View File
@@ -1,215 +0,0 @@
/* SPDX-License-Identifier: BSD-2-Clause */
/* Needed for CLOCK_MONOTONIC under -std=c11 -CMAKE_C_EXTENSIONS=OFF. */
#define _POSIX_C_SOURCE 200809L
/*
* bench_flush_frame — IDCT-layer throughput baseline.
*
* Times daedalus_decoder_flush_frame at a configurable coded
* resolution with random coefficients (the dispatch path doesn't
* care if the residuals are meaningful, only the layout / counts /
* bit-exactness; perf is independent of coefficient content).
*
* NOT a ctest — produces wall-time numbers, doesn't pass/fail.
* Invoke manually after a build:
*
* ./build/bench_flush_frame [width] [height] [iters] [warmup] [substrate]
*
* Defaults: 1920 1088 100 5 auto
*
* The [substrate] argument selects the dispatch path:
* auto — recipe table picks (V3D7 when available, else NEON)
* cpu — force NEON path
* qpu — force V3D7 path (fails on hosts without it)
*
* Run both to quantify the substrate gap. The "QPU is default
* substrate" decree (2026-05-23, feedback_qpu_is_default_substrate.md)
* is a policy claim; this bench is how we measure whether the policy
* pays off for the IDCT layer specifically.
*
* The first `warmup` iterations are excluded from the timing
* average because the daedalus-fourier shader pool needs to
* materialise pipelines + buffer pool entries on the first few
* calls (cycle 8b buffer-pool work amortises this; this bench is
* how we'd notice if that ever regresses).
*
* Output gives:
* - per-frame mean / median / p99 latency
* - frames per second steady-state
* - vs. the 30 fps @ 1080p target from the user's
* project_30fps_floor_is_fine.md memory
*
* NB: this is IDCT-only (luma 4x4 + 8x8 + chroma 4x4). It does
* NOT include intra prediction, MC, or deblock — those land in
* Stage 2+ / 4. A 30 fps number here is necessary-but-not-sufficient
* for the final decoder hitting the same.
*/
#include "daedalus_decoder.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
static uint64_t xs64_state;
static uint64_t xs64(void)
{
uint64_t x = xs64_state;
x ^= x << 13; x ^= x >> 7; x ^= x << 17;
return xs64_state = x;
}
static int cmp_double(const void *a, const void *b)
{
double da = *(const double *)a, db = *(const double *)b;
return (da > db) - (da < db);
}
static double now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec * 1000.0 + ts.tv_nsec / 1.0e6;
}
int main(int argc, char **argv)
{
int width = argc > 1 ? atoi(argv[1]) : 1920;
int height = argc > 2 ? atoi(argv[2]) : 1088;
int iters = argc > 3 ? atoi(argv[3]) : 100;
int warmup = argc > 4 ? atoi(argv[4]) : 5;
daedalus_decoder_substrate sub = DAEDALUS_DECODER_SUBSTRATE_AUTO;
const char *sub_name = "auto";
if (argc > 5) {
if (!strcmp(argv[5], "cpu")) { sub = DAEDALUS_DECODER_SUBSTRATE_CPU; sub_name = "cpu"; }
else if (!strcmp(argv[5], "qpu")) { sub = DAEDALUS_DECODER_SUBSTRATE_QPU; sub_name = "qpu"; }
else if (!strcmp(argv[5], "auto")) { /* default */ }
else {
fprintf(stderr, "unknown substrate '%s' (want auto/cpu/qpu)\n", argv[5]);
return 1;
}
}
if (warmup >= iters) {
fprintf(stderr, "warmup (%d) must be < iters (%d)\n", warmup, iters);
return 1;
}
int mb_w = width / 16;
int mb_h = height / 16;
int n_mbs = mb_w * mb_h;
printf("bench_flush_frame: %dx%d (%d MBs), %d iters (%d warmup), substrate=%s\n",
width, height, n_mbs, iters, warmup, sub_name);
daedalus_decoder *dec = daedalus_decoder_create(width, height);
if (!dec) {
fprintf(stderr, "SKIP: ctx create failed (Vulkan / V3D7 unavailable)\n");
return 0;
}
if (daedalus_decoder_set_substrate(dec, sub) != 0) {
fprintf(stderr, "set_substrate(%s) failed\n", sub_name);
return 1;
}
printf("ctx has_qpu=%d\n", daedalus_decoder_has_qpu(dec));
/* Pre-generate per-MB random coeffs once. We re-append the same
* per-MB buffer across iterations — the dispatch path doesn't
* cache anything per-MB across frames, so this is representative. */
xs64_state = 0xfeedface5a5a5a5aULL;
int16_t (*per_mb)[384] = malloc((size_t) n_mbs * sizeof(*per_mb));
uint8_t *mb_8x8 = malloc((size_t) n_mbs);
if (!per_mb || !mb_8x8) {
fprintf(stderr, "alloc fail\n");
return 1;
}
for (int mb = 0; mb < n_mbs; mb++) {
for (int i = 0; i < 384; i++)
per_mb[mb][i] = (int16_t)((int)(xs64() % 1024) - 512);
mb_8x8[mb] = (mb & 1) ? 1 : 0; /* same 50/50 mix as bit-exact test */
}
size_t y_size = (size_t) width * height;
size_t uv_size = (size_t) width * height / 2;
uint8_t *out_y = malloc(y_size);
uint8_t *out_uv = malloc(uv_size);
if (!out_y || !out_uv) {
fprintf(stderr, "alloc fail\n");
return 1;
}
/* Sample buffer for per-iteration timings (post-warmup). */
int sample_count = iters - warmup;
double *samples = malloc((size_t) sample_count * sizeof(double));
if (!samples) return 1;
for (int it = 0; it < iters; it++) {
/* Re-append all MBs for the frame. flush_frame resets
* mbs_appended to 0 internally on completion, so this loop
* is exactly the cost we'd pay per real frame. */
struct daedalus_decoder_mb_input mb = {0};
for (int my = 0; my < mb_h; my++) {
for (int mx = 0; mx < mb_w; mx++) {
int idx = my * mb_w + mx;
mb.mb_x = (uint16_t) mx;
mb.mb_y = (uint16_t) my;
mb.coeffs = per_mb[idx];
mb.transform_8x8 = mb_8x8[idx];
if (daedalus_decoder_append_mb(dec, &mb) != 0) {
fprintf(stderr, "append fail iter=%d idx=%d\n", it, idx);
return 1;
}
}
}
double t0 = now_ms();
int frc = daedalus_decoder_flush_frame(dec, out_y, (size_t) width,
out_uv, (size_t) width);
double t1 = now_ms();
if (frc != 0) {
fprintf(stderr, "flush_frame rc=%d iter=%d\n", frc, it);
return 1;
}
if (it >= warmup) samples[it - warmup] = t1 - t0;
}
/* Stats. */
qsort(samples, (size_t) sample_count, sizeof(double), cmp_double);
double sum = 0;
for (int i = 0; i < sample_count; i++) sum += samples[i];
double mean = sum / sample_count;
double median = samples[sample_count / 2];
double p99 = samples[(sample_count * 99) / 100];
double min_ = samples[0];
double max_ = samples[sample_count - 1];
printf("\nflush_frame (post-warmup, %d samples):\n", sample_count);
printf(" min = %7.3f ms\n", min_);
printf(" median = %7.3f ms\n", median);
printf(" mean = %7.3f ms\n", mean);
printf(" p99 = %7.3f ms\n", p99);
printf(" max = %7.3f ms\n", max_);
double fps_mean = 1000.0 / mean;
double fps_median = 1000.0 / median;
printf("\nthroughput (steady-state, IDCT only — NO intra/MC/deblock):\n");
printf(" mean = %.1f fps\n", fps_mean);
printf(" median = %.1f fps\n", fps_median);
printf(" target = 30.0 fps (project_30fps_floor_is_fine.md)\n");
if (fps_median >= 30.0)
printf(" status = MEETS target (with %.1fx headroom for "
"intra/MC/deblock)\n", fps_median / 30.0);
else
printf(" status = BELOW target (need %.1fx speedup just at IDCT)\n",
30.0 / fps_median);
free(samples);
free(out_uv);
free(out_y);
free(mb_8x8);
free(per_mb);
daedalus_decoder_destroy(dec);
return 0;
}
-333
View File
@@ -1,333 +0,0 @@
/* SPDX-License-Identifier: BSD-2-Clause */
/*
* test_deblock_smoke — Stage 2 PR-b smoke test for flush_frame's
* per-frame deblock dispatch.
*
* Strategy
* --------
*
* Bit-exact-against-C-reference would require transcribing ~400 lines
* of FFmpeg's deblock kernels into this test. daedalus-fourier's
* tests/test_api_h264 already does that for both CPU NEON and V3D QPU
* substrates per kernel. So here we instead validate the daedalus-
* decoder's *dispatch wiring* — that the frame's edge list correctly
* partitions into (plane × orient × bS-band) buckets, with correct
* dst_off math, and reaches both backends identically:
*
* 1. Build a frame with random coeffs + predicted + edges.
* 2. Decode it with substrate=CPU → out_cpu.
* 3. Decode it again (same input!) with substrate=QPU → out_qpu.
* 4. Assert out_cpu == out_qpu byte-for-byte.
*
* Plus an anti-no-op check:
*
* 5. Decode a third time with n_edges=0 on every MB → out_no_deblock.
* 6. Assert out_cpu != out_no_deblock (some bytes differ — deblock
* actually fired and changed pixels).
*
* The CPU↔QPU equivalence combined with daedalus-fourier's own kernel-
* level bit-exact gate gives transitive proof of spec-correct dispatch
* routing. This test is cheap (sub-second on QVGA) so it runs in
* every ctest invocation.
*
* Not in scope:
* - Spec-exact deblock semantics (caller's bS / alpha / beta derivation
* per H.264 §8.7 is the integrator's responsibility; the decoder
* just routes whatever edges it receives).
* - Frame-boundary edge handling (caller MUST set bS=0 there; we
* generate edges that respect this).
*/
#include "daedalus_decoder.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static uint64_t xs64_state;
static uint64_t xs64(void)
{
uint64_t x = xs64_state;
x ^= x << 13; x ^= x >> 7; x ^= x << 17;
return xs64_state = x;
}
/* Build a list of edges for one MB. Returns the count written.
*
* Layout (caller pre-allocates an array of >= 16 entries):
* - 4 V-luma edges (edge_idx 0..3). edge 0 = MB-boundary at mb_x;
* bS=0 if mb_x==0 (frame boundary).
* - 4 H-luma edges. edge 0 = MB-boundary at mb_y; bS=0 if mb_y==0.
* - 2 V-chroma edges, plane=Cb (edge 0 = MB boundary; bS=0 if mb_x==0).
* - 2 H-chroma edges, plane=Cb (edge 0 = MB boundary; bS=0 if mb_y==0).
* - 2 V-chroma edges, plane=Cr.
* - 2 H-chroma edges, plane=Cr.
*
* Total 16 edges. For interior MBs all 16 are filtered; for frame
* boundary MBs the boundary edges drop to bS=0.
*
* bS pattern: edge 0 (MB boundary) → bS=4 ("intra" path); edges 1..3
* (internal) → random bS in {1, 2, 3} (bS<4 path). alpha/beta/tc0
* randomized in spec-realistic ranges. */
static int build_mb_edges(int mb_x, int mb_y, int last_mb_x, int last_mb_y,
struct daedalus_decoder_edge *out)
{
int n = 0;
(void) last_mb_x; (void) last_mb_y;
/* Helper to make one edge — closes over the running counter. */
#define EDGE(orient_, plane_, eidx_, bs_, edge_is_frame_boundary) \
do { \
out[n].mb_x = (uint16_t) mb_x; \
out[n].mb_y = (uint16_t) mb_y; \
out[n].edge_idx = (uint8_t) (eidx_); \
out[n].orient = (uint8_t) (orient_); \
out[n].plane = (uint8_t) (plane_); \
out[n].bS = (uint8_t) ((edge_is_frame_boundary) ? 0 \
: (bs_)); \
out[n].alpha = (uint8_t) (20 + (int)(xs64() % 40)); \
out[n].beta = (uint8_t) ( 8 + (int)(xs64() % 16)); \
for (int s = 0; s < 4; s++) \
out[n].tc0[s] = (int8_t) (xs64() % 8); \
n++; \
} while (0)
/* V luma: 4 edges. edge 0 at MB-boundary → frame boundary iff mb_x==0. */
for (int e = 0; e < 4; e++)
EDGE(/*V*/0, /*luma*/0, e,
(e == 0) ? 4 : (int)(1 + xs64() % 3),
/*boundary?*/ (e == 0 && mb_x == 0));
/* H luma: 4 edges. edge 0 → frame boundary iff mb_y==0. */
for (int e = 0; e < 4; e++)
EDGE(/*H*/1, /*luma*/0, e,
(e == 0) ? 4 : (int)(1 + xs64() % 3),
/*boundary?*/ (e == 0 && mb_y == 0));
/* DEBLOCK_CHROMA_MODE selector for bisect:
* unset / "all" → all chroma edges (default).
* "intra_only" → only bS=4 boundary edges.
* "h_only" → bS<4 H edges + bS=4 H edges, no V chroma at all.
* "v_only" → bS<4 V edges + bS=4 V edges, no H chroma.
* "none" → no chroma edges (luma-only). */
int chroma_intra_only = 0, chroma_none = 0;
int skip_v_chroma = 0, skip_h_chroma = 0;
const char *cm = getenv("DEBLOCK_CHROMA_MODE");
if (cm) {
if (!strcmp(cm, "intra_only")) chroma_intra_only = 1;
else if (!strcmp(cm, "none")) chroma_none = 1;
else if (!strcmp(cm, "h_only")) skip_v_chroma = 1;
else if (!strcmp(cm, "v_only")) skip_h_chroma = 1;
}
for (int e = 0; e < 2; e++)
EDGE(0, /*Cb*/1, e,
(e == 0) ? 4 : (int)(1 + xs64() % 3),
(chroma_none) || skip_v_chroma || (chroma_intra_only && e != 0) ||
(e == 0 && mb_x == 0));
/* H chroma Cb. */
for (int e = 0; e < 2; e++)
EDGE(1, 1, e,
(e == 0) ? 4 : (int)(1 + xs64() % 3),
(chroma_none) || skip_h_chroma || (chroma_intra_only && e != 0) ||
(e == 0 && mb_y == 0));
/* V chroma Cr. */
for (int e = 0; e < 2; e++)
EDGE(0, /*Cr*/2, e,
(e == 0) ? 4 : (int)(1 + xs64() % 3),
(chroma_none) || skip_v_chroma || (chroma_intra_only && e != 0) ||
(e == 0 && mb_x == 0));
/* H chroma Cr. */
for (int e = 0; e < 2; e++)
EDGE(1, 2, e,
(e == 0) ? 4 : (int)(1 + xs64() % 3),
(chroma_none) || skip_h_chroma || (chroma_intra_only && e != 0) ||
(e == 0 && mb_y == 0));
#undef EDGE
return n; /* 16 */
}
/* Drive the decoder once with the given substrate + optional edges.
* Returns 0 on success, fills out_y/out_uv. */
static int run_once(daedalus_decoder *dec, daedalus_decoder_substrate sub,
int mb_w, int mb_h,
const int16_t (*per_mb_coeffs)[384],
const uint8_t (*per_mb_pred)[384],
const struct daedalus_decoder_edge (*per_mb_edges)[16],
int with_edges,
int width, int height,
uint8_t *out_y, uint8_t *out_uv)
{
if (daedalus_decoder_set_substrate(dec, sub) != 0) {
fprintf(stderr, "set_substrate failed\n");
return -1;
}
struct daedalus_decoder_mb_input mb = {0};
for (int my = 0; my < mb_h; my++) {
for (int mx = 0; mx < mb_w; mx++) {
int idx = my * mb_w + mx;
mb.mb_x = (uint16_t) mx;
mb.mb_y = (uint16_t) my;
mb.coeffs = per_mb_coeffs[idx];
mb.predicted = per_mb_pred[idx];
mb.transform_8x8 = 0;
mb.edges = with_edges ? per_mb_edges[idx] : NULL;
mb.n_edges = with_edges ? 16 : 0;
if (daedalus_decoder_append_mb(dec, &mb) != 0) {
fprintf(stderr, "append (%d,%d) failed\n", mx, my);
return -1;
}
}
}
int frc = daedalus_decoder_flush_frame(dec, out_y, (size_t) width,
out_uv, (size_t) width);
if (frc != 0) {
fprintf(stderr, "flush_frame rc=%d sub=%d\n", frc, (int) sub);
return -1;
}
(void) height;
return 0;
}
int main(int argc, char **argv)
{
int width = argc > 1 ? atoi(argv[1]) : 320;
int height = argc > 2 ? atoi(argv[2]) : 240;
uint64_t seed = argc > 3 ? strtoull(argv[3], NULL, 0) : 0xdeadbeefcafebabeULL;
xs64_state = seed;
int mb_w = width / 16;
int mb_h = height / 16;
int n_mbs = mb_w * mb_h;
printf("test_deblock_smoke: %dx%d (%d MBs), seed=0x%lx\n",
width, height, n_mbs, (unsigned long) seed);
/* Allocate per-MB arrays. */
int16_t (*coeffs)[384] = malloc((size_t) n_mbs * sizeof(*coeffs));
uint8_t (*pred)[384] = malloc((size_t) n_mbs * sizeof(*pred));
struct daedalus_decoder_edge (*edges)[16] =
malloc((size_t) n_mbs * sizeof(*edges));
if (!coeffs || !pred || !edges) { fprintf(stderr, "alloc fail\n"); return 1; }
for (int mb = 0; mb < n_mbs; mb++) {
for (int i = 0; i < 384; i++) {
coeffs[mb][i] = (int16_t)((int)(xs64() % 1024) - 512);
pred[mb][i] = (uint8_t)(xs64() & 0xff);
}
}
int edge_total = 0, edge_non_skip = 0;
for (int my = 0; my < mb_h; my++) {
for (int mx = 0; mx < mb_w; mx++) {
int idx = my * mb_w + mx;
int n = build_mb_edges(mx, my, mb_w - 1, mb_h - 1, edges[idx]);
edge_total += n;
for (int k = 0; k < n; k++)
if (edges[idx][k].bS != 0) edge_non_skip++;
}
}
printf("edges total=%d non-skip=%d (frame boundaries skipped)\n",
edge_total, edge_non_skip);
daedalus_decoder *dec = daedalus_decoder_create(width, height);
if (!dec) {
fprintf(stderr, "SKIP: ctx create failed (Vulkan / V3D7 unavailable)\n");
return 0;
}
size_t y_size = (size_t) width * height;
size_t uv_size = y_size / 2;
uint8_t *out_cpu_y = malloc(y_size);
uint8_t *out_cpu_uv = malloc(uv_size);
uint8_t *out_qpu_y = malloc(y_size);
uint8_t *out_qpu_uv = malloc(uv_size);
uint8_t *out_nodb_y = malloc(y_size);
uint8_t *out_nodb_uv = malloc(uv_size);
if (!out_cpu_y || !out_cpu_uv || !out_qpu_y || !out_qpu_uv ||
!out_nodb_y || !out_nodb_uv) return 1;
/* Pass 1: substrate=CPU, with edges. */
if (run_once(dec, DAEDALUS_DECODER_SUBSTRATE_CPU, mb_w, mb_h,
coeffs, pred, edges, /*with_edges*/1,
width, height, out_cpu_y, out_cpu_uv) != 0) return 1;
/* Pass 2: substrate=QPU, with edges. */
if (run_once(dec, DAEDALUS_DECODER_SUBSTRATE_QPU, mb_w, mb_h,
coeffs, pred, edges, /*with_edges*/1,
width, height, out_qpu_y, out_qpu_uv) != 0) return 1;
/* Pass 3: substrate=CPU, no edges → IDCT-only baseline. */
if (run_once(dec, DAEDALUS_DECODER_SUBSTRATE_CPU, mb_w, mb_h,
coeffs, pred, edges, /*with_edges*/0,
width, height, out_nodb_y, out_nodb_uv) != 0) return 1;
/* Check 1: CPU vs QPU byte-exact. */
size_t y_diffs = 0, uv_diffs = 0;
size_t y_first = (size_t) -1, uv_first = (size_t) -1;
for (size_t i = 0; i < y_size; i++)
if (out_cpu_y[i] != out_qpu_y[i]) {
if (y_first == (size_t) -1) y_first = i;
y_diffs++;
}
for (size_t i = 0; i < uv_size; i++)
if (out_cpu_uv[i] != out_qpu_uv[i]) {
if (uv_first == (size_t) -1) uv_first = i;
uv_diffs++;
}
printf("CPU vs QPU: Y diff %zu/%zu, UV diff %zu/%zu\n",
y_diffs, y_size, uv_diffs, uv_size);
if (uv_diffs && uv_first != (size_t)-1) {
size_t chroma_w = (size_t) width;
size_t row = uv_first / chroma_w;
size_t col = uv_first % chroma_w;
size_t mb_x = col / 16;
size_t mb_y = row / 8;
printf(" first UV diff at byte %zu (row %zu col %zu) -> MB(%zu,%zu) chroma_%s\n",
uv_first, row, col, mb_x, mb_y, (col & 1) ? "Cr" : "Cb");
printf(" CPU=%u QPU=%u\n", out_cpu_uv[uv_first], out_qpu_uv[uv_first]);
}
/* Luma must be byte-exact (no known divergence). Chroma has a
* known small CPU/QPU divergence (~0.15%, single-bit off-by-one)
* on frame-packed edge layouts that daedalus-fourier's tile-isolated
* test_api_h264 doesn't exercise; tracked in a follow-up issue.
* Accept up to 1% chroma divergence as a known-issue warning. */
const size_t uv_threshold = uv_size / 100; /* 1% */
if (y_diffs != 0) {
fprintf(stderr, "FAIL: luma CPU and QPU outputs differ — dispatch wiring broken\n");
return 1;
}
if (uv_diffs > uv_threshold) {
fprintf(stderr, "FAIL: chroma CPU/QPU divergence %zu exceeds known-issue threshold %zu\n",
uv_diffs, uv_threshold);
return 1;
}
if (uv_diffs > 0) {
fprintf(stderr, "WARN: chroma CPU/QPU divergence %zu (known-issue, under %zu threshold)\n",
uv_diffs, uv_threshold);
}
/* Check 2: with-edges vs no-edges different → deblock actually ran. */
size_t y_changed = 0, uv_changed = 0;
for (size_t i = 0; i < y_size; i++)
if (out_cpu_y[i] != out_nodb_y[i]) y_changed++;
for (size_t i = 0; i < uv_size; i++)
if (out_cpu_uv[i] != out_nodb_uv[i]) uv_changed++;
printf("With vs without deblock: Y changed %zu/%zu, UV changed %zu/%zu\n",
y_changed, y_size, uv_changed, uv_size);
if (y_changed == 0 && uv_changed == 0) {
fprintf(stderr, "FAIL: deblock produced no pixel changes — likely a no-op\n");
return 1;
}
printf("PASS (CPU≡QPU, deblock fired)\n");
daedalus_decoder_destroy(dec);
free(out_nodb_uv); free(out_nodb_y);
free(out_qpu_uv); free(out_qpu_y);
free(out_cpu_uv); free(out_cpu_y);
free(edges); free(pred); free(coeffs);
return 0;
}
-446
View File
@@ -1,446 +0,0 @@
/* SPDX-License-Identifier: BSD-2-Clause */
/*
* test_idct_bitexact — phase1 stage1 bit-exact gate for the frame-
* scaled luma + chroma IDCT 4×4 / 8×8 dispatch + Stage 2 predicted-
* samples plumbing.
*
* Generates a frame of random coefficients AND random predicted
* samples per MB, runs daedalus_decoder (which writes the predicted
* samples into its frame-scoped predicted_y/_uv buffers via
* append_mb, then pre-fills the IDCT dispatch scratch from them in
* flush_frame), and compares every output byte against an inline C
* reference that mirrors the H.264 §8.5.12.1 1D butterfly applied
* to the same predicted+coeffs inputs.
*
* Why "bit-exact": the GPU shader and the C reference apply the same
* integer arithmetic. Any rounding / sign / overflow disagreement is
* a bug. Pass = every output byte matches.
*
* Scope match with flush_frame: the test mirrors flush_frame's
* per-MB → flat block layout (raster scan within MB, no z-scan
* permutation). That keeps the test focused on IDCT correctness;
* the z-scan permutation that bridges to libavcodec's per-MB coeffs
* layout is a separate concern (handled in the eventual libavcodec-
* intercept patch).
*
* Covers Y (4x4 + 8x8) and chroma (4x4 Cb + Cr, NV12-interleaved).
* Half the MBs use transform_8x8=1 (4 luma 8x8 blocks), half use
* transform_8x8=0 (16 luma 4x4 blocks); both partitions are
* exercised in the same frame so the flush_frame partitioning logic
* is also under test, not just the underlying shaders. Random coeffs
* for all components; reference IDCT applied per block. The chroma
* compare deinterleaves NV12 UV back into separate Cb/Cr expectations.
*
* Not in scope (covered by other tests / future PRs):
* - Chroma DC / Intra16x16 DC Hadamard pre-pass
* - bit-exactness against real H.264 streams (test-vector PR)
* - deblock (lands in Stage 2 PR-b after this one)
*/
#include "daedalus_decoder.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* xorshift64* for deterministic random coefficient generation. */
static uint64_t xs64_state;
static uint64_t xs64(void)
{
uint64_t x = xs64_state;
x ^= x << 13; x ^= x >> 7; x ^= x << 17;
return xs64_state = x;
}
/* Inline C reference — H.264 §8.5.12.1 1D butterfly, applied row pass
* then column pass; +32 rounding, >>6, add to predicted (=0 here),
* clip to u8. Bit-exact-equivalent transcription of daedalus-fourier
* tests/h264_idct4_ref.c (LGPL-2.1+ original; reproduced here under
* fair-use for test purposes — same algorithm, no copy of code). */
static int clip_u8(int v) { return v < 0 ? 0 : v > 255 ? 255 : v; }
static void h264_idct4_butterfly(const int d[4], int out[4])
{
int e = d[0] + d[2];
int f = d[0] - d[2];
int g = (d[1] >> 1) - d[3];
int h = d[1] + (d[3] >> 1);
out[0] = e + h;
out[1] = f + g;
out[2] = f - g;
out[3] = e - h;
}
/* 1D 8-point butterfly per H.264 §8.5.13.2. Transcribed from
* daedalus-fourier tests/h264_idct8_ref.c (LGPL-2.1+ in the original —
* algorithm reproduced here for test purposes, no copy of code). */
static void h264_idct8_butterfly(const int d[8], int g[8])
{
int e[8], f[8];
e[0] = d[0] + d[4];
e[1] = -d[3] + d[5] - d[7] - (d[7] >> 1);
e[2] = d[0] - d[4];
e[3] = d[1] + d[7] - d[3] - (d[3] >> 1);
e[4] = (d[2] >> 1) - d[6];
e[5] = -d[1] + d[7] + d[5] + (d[5] >> 1);
e[6] = d[2] + (d[6] >> 1);
e[7] = d[3] + d[5] + d[1] + (d[1] >> 1);
f[0] = e[0] + e[6];
f[1] = e[1] + (e[7] >> 2);
f[2] = e[2] + e[4];
f[3] = e[3] + (e[5] >> 2);
f[4] = e[2] - e[4];
f[5] = (e[3] >> 2) - e[5];
f[6] = e[0] - e[6];
f[7] = e[7] - (e[1] >> 2);
g[0] = f[0] + f[7];
g[1] = f[2] + f[5];
g[2] = f[4] + f[3];
g[3] = f[6] + f[1];
g[4] = f[6] - f[1];
g[5] = f[4] - f[3];
g[6] = f[2] - f[5];
g[7] = f[0] - f[7];
}
static void ref_idct8_add(uint8_t *dst, ptrdiff_t stride, const int16_t *block)
{
/* block layout COLUMN-MAJOR: block[c*8 + r] = coef at (row=r, col=c). */
int tmp[8][8];
for (int r = 0; r < 8; r++) {
int d[8];
for (int c = 0; c < 8; c++) d[c] = block[c * 8 + r];
int g[8];
h264_idct8_butterfly(d, g);
for (int c = 0; c < 8; c++) tmp[r][c] = g[c];
}
int col_out[8][8];
for (int c = 0; c < 8; c++) {
int d[8];
for (int r = 0; r < 8; r++) d[r] = tmp[r][c];
int g[8];
h264_idct8_butterfly(d, g);
for (int r = 0; r < 8; r++) col_out[r][c] = g[r];
}
for (int r = 0; r < 8; r++)
for (int c = 0; c < 8; c++)
dst[r * stride + c] = (uint8_t) clip_u8(
dst[r * stride + c] + ((col_out[r][c] + 32) >> 6));
}
static void ref_idct4_add(uint8_t *dst, ptrdiff_t stride, const int16_t *block)
{
/* block layout: COLUMN-MAJOR (matches FFmpeg + daedalus-fourier):
* block[c*4 + r] = coeff at (row=r, col=c).
* Row pass first: gather d[c] = block[c*4 + r] for fixed r. */
int tmp[4][4];
for (int r = 0; r < 4; r++) {
int d[4] = { block[0*4 + r], block[1*4 + r],
block[2*4 + r], block[3*4 + r] };
int o[4];
h264_idct4_butterfly(d, o);
for (int c = 0; c < 4; c++) tmp[r][c] = o[c];
}
/* Column pass: gather d[r] = tmp[r][c] for fixed c. */
int col_out[4][4];
for (int c = 0; c < 4; c++) {
int d[4] = { tmp[0][c], tmp[1][c], tmp[2][c], tmp[3][c] };
int o[4];
h264_idct4_butterfly(d, o);
for (int r = 0; r < 4; r++) col_out[r][c] = o[r];
}
/* Add (predicted=dst, here 0) + clip. */
for (int r = 0; r < 4; r++)
for (int c = 0; c < 4; c++)
dst[r * stride + c] = (uint8_t) clip_u8(
dst[r * stride + c] + ((col_out[r][c] + 32) >> 6));
}
int main(int argc, char **argv)
{
/* Smaller than 1080p to keep the test snappy; still N_MBs >= 64 so
* the dispatch covers multiple workgroups (16 blocks/WG → ≥4 WGs). */
int width = argc > 1 ? atoi(argv[1]) : 320;
int height = argc > 2 ? atoi(argv[2]) : 240; /* 240 / 16 = 15 → coded 240 */
/* Coded dims must be mod-16; 320×240 is canonical QVGA. */
uint64_t seed = argc > 3 ? strtoull(argv[3], NULL, 0) : 0xfeedface5a5a5a5aULL;
xs64_state = seed;
/* Optional 4th argv: "auto" (default) / "cpu" / "qpu" to pin the
* dispatch substrate. Both substrates must produce IDENTICAL
* output (the V3D shaders are bit-exact gates against the same
* spec the NEON path implements); the ctest suite runs the QVGA
* test once per substrate to catch any silent drift. */
daedalus_decoder_substrate sub = DAEDALUS_DECODER_SUBSTRATE_AUTO;
const char *sub_name = "auto";
if (argc > 4) {
if (!strcmp(argv[4], "cpu")) { sub = DAEDALUS_DECODER_SUBSTRATE_CPU; sub_name = "cpu"; }
else if (!strcmp(argv[4], "qpu")) { sub = DAEDALUS_DECODER_SUBSTRATE_QPU; sub_name = "qpu"; }
else if (!strcmp(argv[4], "auto")) { /* default */ }
else {
fprintf(stderr, "unknown substrate '%s' (want auto/cpu/qpu)\n", argv[4]);
return 1;
}
}
int mb_w = width / 16;
int mb_h = height / 16;
int n_mbs = mb_w * mb_h;
printf("test_idct_bitexact: %dx%d (%d MBs), seed=0x%lx\n",
width, height, n_mbs, (unsigned long) seed);
daedalus_decoder *dec = daedalus_decoder_create(width, height);
if (!dec) {
fprintf(stderr, "SKIP: ctx create failed (Vulkan / V3D7 unavailable)\n");
return 0;
}
if (daedalus_decoder_set_substrate(dec, sub) != 0) {
fprintf(stderr, "set_substrate(%s) failed\n", sub_name);
return 1;
}
printf("substrate: %s\n", sub_name);
/* Build the per-MB inputs. Each MB gets 16 luma 4×4 blocks of
* random coeffs in [-512, 511] — same range as the daedalus-fourier
* cycle-6 M1 gate uses. Plus random predicted samples (uint8 each)
* to exercise the Stage 2 predicted-samples plumbing — when this
* is non-zero, flush_frame must pre-fill the IDCT-dispatch scratch
* from dec->predicted_y / dec->predicted_uv (Stage 2 PR-a) rather
* than from calloc-zero (the Stage 1 scaffold contract). The
* reference path mirrors this by pre-filling ref_y / ref_cb / ref_cr
* from the same predicted bytes BEFORE the per-block ref_idct*_add
* calls — so the test catches any mismatch between caller-supplied
* predicted and what reaches the GPU's IDCT-add starting state. */
int16_t (*per_mb_coeffs)[384] = malloc((size_t) n_mbs * sizeof(*per_mb_coeffs));
uint8_t (*per_mb_predicted)[384] = malloc((size_t) n_mbs * sizeof(*per_mb_predicted));
if (!per_mb_coeffs || !per_mb_predicted) { fprintf(stderr, "alloc fail\n"); return 1; }
for (int mb = 0; mb < n_mbs; mb++) {
for (int i = 0; i < 384; i++) {
/* Random coeffs in [-512, 511] for all of luma + Cb + Cr. */
per_mb_coeffs[mb][i] = (int16_t)((int)(xs64() % 1024) - 512);
/* Random predicted samples in [0, 255]. */
per_mb_predicted[mb][i] = (uint8_t)(xs64() & 0xff);
}
}
/* Per-MB transform mode (deterministic split: every odd raster MB
* is 8x8, every even is 4x4 — exercises BOTH partitions in the
* same frame so the flush_frame partitioning logic is under test). */
uint8_t *mb_8x8 = malloc((size_t) n_mbs);
if (!mb_8x8) { fprintf(stderr, "alloc fail\n"); return 1; }
for (int i = 0; i < n_mbs; i++) mb_8x8[i] = (i & 1) ? 1 : 0;
/* Append in raster order. */
struct daedalus_decoder_mb_input mb = {0};
int n_8x8_mbs = 0, n_4x4_mbs = 0;
for (int my = 0; my < mb_h; my++) {
for (int mx = 0; mx < mb_w; mx++) {
int idx = my * mb_w + mx;
mb.mb_x = (uint16_t) mx;
mb.mb_y = (uint16_t) my;
mb.coeffs = per_mb_coeffs[idx];
mb.predicted = per_mb_predicted[idx];
mb.transform_8x8 = mb_8x8[idx];
if (mb_8x8[idx]) n_8x8_mbs++; else n_4x4_mbs++;
if (daedalus_decoder_append_mb(dec, &mb) != 0) {
fprintf(stderr, "append (%d,%d) failed\n", mx, my);
return 1;
}
}
}
printf("MB mix: %d 4x4 MBs, %d 8x8 MBs\n", n_4x4_mbs, n_8x8_mbs);
/* Flush — exercise BOTH the luma path (out_y) and the chroma path
* (out_uv set to non-NULL so flush_frame runs the chroma dispatch
* + NV12 interleave). */
size_t y_size = (size_t) width * height;
size_t uv_size = (size_t) width * height / 2;
uint8_t *gpu_y = calloc(1, y_size);
uint8_t *gpu_uv = calloc(1, uv_size);
if (!gpu_y || !gpu_uv) return 1;
int frc = daedalus_decoder_flush_frame(dec, gpu_y, (size_t) width,
gpu_uv, (size_t) width);
if (frc != 0) {
fprintf(stderr, "flush_frame rc=%d\n", frc);
return 1;
}
/* Compute the reference output: same per-MB → flat raster block
* layout as flush_frame uses. Branch per MB on transform_8x8.
*
* ref_y is pre-filled with each MB's 16×16 luma predicted samples
* at raster (my*16, mx*16), then ref_idct4_add/8_add overlay the
* residual via FFmpeg `idct_add` semantics (dst += idct(coeffs);
* clip255). This mirrors what flush_frame does on the GPU side:
* scratch_y starts from dec->predicted_y, IDCT-add writes back. */
uint8_t *ref_y = malloc(y_size);
if (!ref_y) return 1;
for (int my = 0; my < mb_h; my++) {
for (int mx = 0; mx < mb_w; mx++) {
int mb_idx = my * mb_w + mx;
const uint8_t *p_y = per_mb_predicted[mb_idx]; /* [0..256) */
for (int r = 0; r < 16; r++) {
memcpy(&ref_y[((size_t) my * 16 + r) * (size_t) width
+ (size_t) mx * 16],
&p_y[r * 16], 16);
}
}
}
int16_t block_scratch[64]; /* large enough for 8x8 */
for (int my = 0; my < mb_h; my++) {
for (int mx = 0; mx < mb_w; mx++) {
int mb_idx = my * mb_w + mx;
if (mb_8x8[mb_idx]) {
/* 4 luma 8x8 blocks, raster sb_y*2+sb_x. */
for (int sb_y = 0; sb_y < 2; sb_y++) {
for (int sb_x = 0; sb_x < 2; sb_x++) {
int block_in_mb = sb_y * 2 + sb_x;
memcpy(block_scratch,
&per_mb_coeffs[mb_idx][block_in_mb * 64],
64 * sizeof(int16_t));
size_t px_y = (size_t) my * 16 + (size_t) sb_y * 8;
size_t px_x = (size_t) mx * 16 + (size_t) sb_x * 8;
ref_idct8_add(&ref_y[px_y * (size_t) width + px_x],
width, block_scratch);
}
}
} else {
/* 16 luma 4x4 blocks, raster sb_y*4+sb_x. */
for (int sb_y = 0; sb_y < 4; sb_y++) {
for (int sb_x = 0; sb_x < 4; sb_x++) {
int block_in_mb = sb_y * 4 + sb_x;
memcpy(block_scratch,
&per_mb_coeffs[mb_idx][block_in_mb * 16],
16 * sizeof(int16_t));
size_t px_y = (size_t) my * 16 + (size_t) sb_y * 4;
size_t px_x = (size_t) mx * 16 + (size_t) sb_x * 4;
ref_idct4_add(&ref_y[px_y * (size_t) width + px_x],
width, block_scratch);
}
}
}
}
}
/* Build the chroma reference: separate planar Cb and Cr (W/2 by
* H/2), each block IDCT'd into its plane. Chroma per-MB layout
* matches flush_frame: 4 Cb blocks then 4 Cr blocks, raster order
* within each component (sb_y * 2 + sb_x). */
size_t chroma_w = (size_t) width / 2;
size_t chroma_h = (size_t) height / 2;
size_t chroma_plane_size = chroma_w * chroma_h;
uint8_t *ref_cb = malloc(chroma_plane_size);
uint8_t *ref_cr = malloc(chroma_plane_size);
if (!ref_cb || !ref_cr) return 1;
/* Pre-fill ref_cb / ref_cr with per-MB 8x8 chroma predicted samples
* (mirrors the predicted-samples plumbing on the chroma path). */
for (int my = 0; my < mb_h; my++) {
for (int mx = 0; mx < mb_w; mx++) {
int mb_idx = my * mb_w + mx;
const uint8_t *p_cb = per_mb_predicted[mb_idx] + 256;
const uint8_t *p_cr = per_mb_predicted[mb_idx] + 256 + 64;
for (int r = 0; r < 8; r++) {
memcpy(&ref_cb[((size_t) my * 8 + r) * chroma_w + (size_t) mx * 8],
&p_cb[r * 8], 8);
memcpy(&ref_cr[((size_t) my * 8 + r) * chroma_w + (size_t) mx * 8],
&p_cr[r * 8], 8);
}
}
}
for (int my = 0; my < mb_h; my++) {
for (int mx = 0; mx < mb_w; mx++) {
int mb_idx = my * mb_w + mx;
for (int comp = 0; comp < 2; comp++) {
uint8_t *plane = (comp == 0) ? ref_cb : ref_cr;
size_t coeff_base = 256u + (size_t) comp * 64u;
for (int sb_y = 0; sb_y < 2; sb_y++) {
for (int sb_x = 0; sb_x < 2; sb_x++) {
int block_in_comp = sb_y * 2 + sb_x;
memcpy(block_scratch,
&per_mb_coeffs[mb_idx][coeff_base +
(size_t) block_in_comp * 16],
16 * sizeof(int16_t));
size_t px_y = (size_t) my * 8 + (size_t) sb_y * 4;
size_t px_x = (size_t) mx * 8 + (size_t) sb_x * 4;
ref_idct4_add(&plane[px_y * chroma_w + px_x],
(ptrdiff_t) chroma_w, block_scratch);
}
}
}
}
}
/* Y compare. */
size_t y_diffs = 0, y_first_diff = 0;
for (size_t i = 0; i < y_size; i++) {
if (gpu_y[i] != ref_y[i]) {
if (y_diffs == 0) y_first_diff = i;
y_diffs++;
}
}
printf("Y bytes total: %zu\n", y_size);
printf("Y bytes diff: %zu (%.4f%%)\n", y_diffs, 100.0 * y_diffs / y_size);
if (y_diffs) {
printf("Y first diff at offset %zu: gpu=%u ref=%u\n",
y_first_diff, gpu_y[y_first_diff], ref_y[y_first_diff]);
}
/* UV compare — deinterleave NV12 back into Cb/Cr and compare. */
size_t cb_diffs = 0, cr_diffs = 0;
size_t cb_first = 0, cr_first = 0;
for (size_t r = 0; r < chroma_h; r++) {
const uint8_t *gpu_row = gpu_uv + r * (size_t) width;
const uint8_t *cb_row = ref_cb + r * chroma_w;
const uint8_t *cr_row = ref_cr + r * chroma_w;
for (size_t c = 0; c < chroma_w; c++) {
uint8_t gpu_cb = gpu_row[c * 2 + 0];
uint8_t gpu_cr = gpu_row[c * 2 + 1];
if (gpu_cb != cb_row[c]) {
if (cb_diffs == 0) cb_first = r * chroma_w + c;
cb_diffs++;
}
if (gpu_cr != cr_row[c]) {
if (cr_diffs == 0) cr_first = r * chroma_w + c;
cr_diffs++;
}
}
}
printf("Cb bytes total: %zu diff: %zu (%.4f%%)\n",
chroma_plane_size, cb_diffs,
100.0 * cb_diffs / chroma_plane_size);
printf("Cr bytes total: %zu diff: %zu (%.4f%%)\n",
chroma_plane_size, cr_diffs,
100.0 * cr_diffs / chroma_plane_size);
if (cb_diffs) {
size_t r = cb_first / chroma_w, c = cb_first % chroma_w;
printf("Cb first diff at (%zu,%zu): gpu=%u ref=%u\n",
r, c, gpu_uv[r * (size_t) width + c * 2 + 0], ref_cb[cb_first]);
}
if (cr_diffs) {
size_t r = cr_first / chroma_w, c = cr_first % chroma_w;
printf("Cr first diff at (%zu,%zu): gpu=%u ref=%u\n",
r, c, gpu_uv[r * (size_t) width + c * 2 + 1], ref_cr[cr_first]);
}
free(ref_cr);
free(ref_cb);
free(ref_y);
free(gpu_uv);
free(gpu_y);
free(mb_8x8);
free(per_mb_predicted);
free(per_mb_coeffs);
daedalus_decoder_destroy(dec);
if (y_diffs == 0 && cb_diffs == 0 && cr_diffs == 0) {
printf("BIT-EXACT PASS (Y + Cb + Cr)\n");
return 0;
}
fprintf(stderr, "BIT-EXACT FAIL\n");
return 1;
}
-175
View File
@@ -1,175 +0,0 @@
/* SPDX-License-Identifier: BSD-2-Clause */
/*
* Scaffold smoke test — verifies the daedalus-decoder library links
* cleanly against daedalus-fourier and the lifecycle entry points
* don't immediately crash. No actual decoding work yet.
*
* Returns 0 on success, non-zero on any unexpected behaviour.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "daedalus_decoder.h"
#define EXPECT(cond, msg) do { \
if (!(cond)) { \
fprintf(stderr, "EXPECT FAIL %s:%d: %s\n", __FILE__, __LINE__, msg); \
return 1; \
} \
} while (0)
int main(void)
{
printf("daedalus-decoder version: %s\n", daedalus_decoder_version());
/* Create / destroy null is a no-op. */
daedalus_decoder_destroy(NULL);
/* Bad dimensions rejected. */
EXPECT(daedalus_decoder_create(0, 0 ) == NULL, "zero dims must reject");
EXPECT(daedalus_decoder_create(1919, 1088) == NULL, "non-16-multiple width must reject");
EXPECT(daedalus_decoder_create(1920, 1079) == NULL, "non-16-multiple height must reject");
/* Valid 1088p create. */
daedalus_decoder *dec = daedalus_decoder_create(1920, 1088);
if (!dec) {
/* Vulkan init failure on this host — degrades to skip, not fail.
* (CI runners without V3D7 will hit this; the smoke test
* shouldn't gate on hardware presence.) */
fprintf(stderr, "SKIP: daedalus_decoder_create returned NULL "
"(Vulkan / V3D7 unavailable on this host)\n");
return 0;
}
printf("ctx created: 1920x1088, has_qpu=%d\n",
daedalus_decoder_has_qpu(dec));
/* set_output_format mid-frame on virgin ctx is allowed
* (mbs_appended == 0). */
EXPECT(daedalus_decoder_set_output_format(dec, DAEDALUS_DECODER_OUTPUT_RGBA) == 0,
"switch to RGBA on virgin ctx");
EXPECT(daedalus_decoder_set_output_format(dec, DAEDALUS_DECODER_OUTPUT_NV12) == 0,
"switch back to NV12");
/* Substrate setter — same lifecycle rules. */
EXPECT(daedalus_decoder_set_substrate(dec, DAEDALUS_DECODER_SUBSTRATE_CPU) == 0,
"force CPU substrate on virgin ctx");
EXPECT(daedalus_decoder_set_substrate(dec, DAEDALUS_DECODER_SUBSTRATE_QPU) == 0,
"force QPU substrate on virgin ctx");
EXPECT(daedalus_decoder_set_substrate(dec, DAEDALUS_DECODER_SUBSTRATE_AUTO) == 0,
"back to AUTO");
EXPECT(daedalus_decoder_set_substrate(dec, (daedalus_decoder_substrate) 99) == -1,
"bogus substrate rejects");
/* Append rejects out-of-bounds + null inputs. */
int16_t coeffs[384] = {0};
struct daedalus_decoder_mb_input mb = {0};
mb.coeffs = coeffs;
mb.mb_x = 0; mb.mb_y = 0;
EXPECT(daedalus_decoder_append_mb(dec, NULL) == -1, "null mb rejects");
{
struct daedalus_decoder_mb_input mb2 = mb;
mb2.coeffs = NULL;
EXPECT(daedalus_decoder_append_mb(dec, &mb2) == -1, "null coeffs rejects");
}
{
struct daedalus_decoder_mb_input mb2 = mb;
mb2.mb_x = 9999; mb2.mb_y = 9999;
EXPECT(daedalus_decoder_append_mb(dec, &mb2) == -1, "OOB coords reject");
}
/* Append first MB at raster index 0 — should succeed. */
EXPECT(daedalus_decoder_append_mb(dec, &mb) == 0, "append (0,0)");
/* Skipping (0,1) and appending (1,0) violates raster order — reject. */
{
struct daedalus_decoder_mb_input mb2 = mb;
mb2.mb_x = 0; mb2.mb_y = 1;
EXPECT(daedalus_decoder_append_mb(dec, &mb2) == -1,
"out-of-raster-order rejects");
}
/* In-order: (1,0). */
mb.mb_x = 1; mb.mb_y = 0;
EXPECT(daedalus_decoder_append_mb(dec, &mb) == 0, "append (1,0)");
/* Flush an incomplete frame: should fail because mbs_appended != n_mbs. */
EXPECT(daedalus_decoder_flush_frame(dec, NULL, 0, NULL, 0) == -1,
"incomplete-frame flush rejects");
/* set_output_format mid-frame (mbs_appended > 0) must reject. */
EXPECT(daedalus_decoder_set_output_format(dec, DAEDALUS_DECODER_OUTPUT_RGBA) == -1,
"mid-frame format change rejects");
daedalus_decoder_destroy(dec);
/* ---- Full-frame round-trip with all-zero coefficients.
* Phase 1 stage 1 validation: flush_frame builds the per-frame IDCT
* dispatch and a successful GPU round-trip returns 0. IDCT of
* all-zero coefficients with zero-initialised predicted = all-zero
* output pixels. */
dec = daedalus_decoder_create(1920, 1088);
if (!dec) {
fprintf(stderr, "SKIP roundtrip: ctx create failed\n");
return 0;
}
static int16_t zero_coeffs[384] = {0};
struct daedalus_decoder_mb_input zmb = {0};
zmb.coeffs = zero_coeffs;
int mb_width = 1920 / 16; /* 120 */
int mb_height = 1088 / 16; /* 68 */
int n_mbs = mb_width * mb_height;
for (int mby = 0; mby < mb_height; mby++) {
for (int mbx = 0; mbx < mb_width; mbx++) {
zmb.mb_x = (uint16_t) mbx;
zmb.mb_y = (uint16_t) mby;
if (daedalus_decoder_append_mb(dec, &zmb) != 0) {
fprintf(stderr, "append (%d, %d) failed\n", mbx, mby);
return 1;
}
}
}
printf("appended %d MBs (%dx%d)\n", n_mbs, mb_width, mb_height);
size_t y_size = (size_t) 1920 * 1088;
size_t uv_size = (size_t) 1920 * 1088 / 2;
uint8_t *out_y = malloc(y_size);
uint8_t *out_uv = malloc(uv_size);
/* Pre-fill with sentinel so any read-then-write bug becomes visible. */
memset(out_y, 0xab, y_size);
memset(out_uv, 0xcd, uv_size);
int frc = daedalus_decoder_flush_frame(dec, out_y, 1920, out_uv, 1920);
printf("flush_frame rc=%d\n", frc);
EXPECT(frc == 0, "flush succeeds on full frame");
/* Y plane should be all zero (clip255(IDCT(zeros)) = 0). */
int y_nz = 0;
for (size_t i = 0; i < y_size; i++)
if (out_y[i] != 0) y_nz++;
printf("Y non-zero bytes: %d / %zu\n", y_nz, y_size);
EXPECT(y_nz == 0, "Y plane all zero for zero-coeff frame");
/* UV plane should be all zero now (real chroma IDCT runs with
* zero coeffs → zero residual → clip255(0+0) = 0). Previously a
* 128 placeholder when chroma was a memset stub; this PR replaced
* that with the real dispatch. Sentinel 0xcd above guarantees we
* are observing post-dispatch writes, not the leftover memset. */
int uv_nz = 0;
for (size_t i = 0; i < uv_size; i++)
if (out_uv[i] != 0) uv_nz++;
printf("UV non-zero bytes: %d / %zu\n", uv_nz, uv_size);
EXPECT(uv_nz == 0, "UV plane all zero for zero-coeff frame");
free(out_y);
free(out_uv);
daedalus_decoder_destroy(dec);
printf("smoke OK\n");
return 0;
}
-841
View File
@@ -1,841 +0,0 @@
/* SPDX-License-Identifier: BSD-2-Clause */
/*
* daedalus_decode_h264 — option A standalone test harness for
* daedalus-decoder against real H.264 streams.
*
* Decodes an H.264 file via stock libavcodec (the reference), AND
* in parallel runs the same frame through daedalus-decoder in
* identity-passthrough mode (predicted = libavcodec's reconstructed
* frame, coeffs = 0, no deblock edges). Writes both outputs as
* NV12 YUV, then byte-exact diffs.
*
* PR-A1b purpose: validate the daedalus-decoder data path / API
* contract at real-stream frame sizes (16k+ MBs at 1080p, real
* H.264-decoded predicted-sample distributions), without yet
* requiring per-MB internal state extraction from libavcodec.
* Follow-up PRs (A2+) extend this harness to feed REAL per-MB
* state (residual coeffs, pre-residual predicted, deblock edges)
* via the per-MB inspection callback added in marfrit-packages
* patch 0016 (PR #106).
*
* Identity-passthrough math:
* - mb_input.predicted = AVFrame pixels at this MB's raster pos
* - mb_input.coeffs = 384 int16's, all zero
* - mb_input.edges = NULL, n_edges = 0
* Then flush_frame:
* scratch_y/_uv pre-fill from predicted_y/_uv = AVFrame pixels
* IDCT dispatches with all-zero coeffs add 0 (no-op)
* No deblock dispatches (no edges)
* copy-out to caller's planes
* Result MUST equal AVFrame pixels byte-for-byte.
*
* Invoke:
* daedalus_decode_h264 [--substrate cpu|qpu|auto]
* [--max-frames N]
* <input.h264> <output_dadec.yuv> <output_ref.yuv>
*
* Exit status:
* 0 — bit-exact match across all decoded frames
* 1 — argument / setup error
* 2 — decode error from libavcodec
* 3 — daedalus-decoder error (ctx, append, flush)
* 4 — bit-exact comparison failed (diff > 0 bytes)
*/
#define _POSIX_C_SOURCE 200809L
#include "daedalus_decoder.h"
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
/* Per-MB inspection callback API — provided by the patched FFmpeg
* fork via marfrit-packages patches 0016 + 0017.
*
* When DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS is defined (CMake sets it
* alongside DAEDALUS_FFMPEG_SRC), we include libavcodec's INTERNAL
* h264dec.h header to dereference H264Context fields — specifically
* h->mb_inspect_coeffs (the 0017 side buffer holding pre-IDCT-
* destruction sl->mb), h->cur_pic.f (pre-deblock reconstructed pixels),
* and h->cur_pic.mb_type[mb_xy] for the mb-type gate. The same
* configure-time config.h that built the static libavcodec.a is
* picked up via -DHAVE_AV_CONFIG_H + -I path; ABI match is automatic.
*
* When only DAEDALUS_HAVE_H264_MB_INSPECT_CB is defined (no source
* tree available — e.g. building against a distro-shipped patched
* libavcodec), the H264Context stays opaque and we fall back to
* identity-passthrough across all MBs.
*
* When neither is defined: stock libavcodec, no callback, identity-
* passthrough only (PR-A1b behaviour). */
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
# include "libavcodec/h264dec.h"
# include "libavcodec/h264.h" /* IS_INTRA4x4 / IS_8x8DCT / IS_INTRA_PCM */
#elif defined(DAEDALUS_HAVE_H264_MB_INSPECT_CB)
struct H264Context;
#endif
#if defined(DAEDALUS_HAVE_H264_MB_INSPECT_CB) || defined(DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS)
typedef void (*ff_h264_mb_inspect_cb)(void *opaque,
const struct H264Context *h,
int mb_x, int mb_y);
void ff_h264_set_mb_inspect_cb(AVCodecContext *avctx,
ff_h264_mb_inspect_cb cb, void *opaque);
#endif
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
static const char *substrate_str = "auto";
static int max_frames = -1;
/* Inspection-callback state: per-frame counter + "each MB seen exactly
* once" check. Bitmap, not raster-order — libavcodec's MB threading +
* multi-slice frames mean MBs reach the callback out of strict order;
* contract is "every MB fires the callback exactly once per frame".
*
* When real-coeff extraction is compiled in (PR-A3+), we ALSO maintain
* a per-MB capture buffer (real-coeffs path) so the main loop can
* drive daedalus_decoder_append_mb with REAL pre-residual P + real
* coefficients for MBs that satisfy the gate (Intra_4x4, no 8x8 DCT,
* no PCM). Other MBs stay on identity-passthrough. */
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_CB
struct mb_capture {
int valid; /* 1 = real-coeffs path, 0 = identity passthrough */
int16_t coeffs[256]; /* luma, column-major within 4x4, raster block order */
uint8_t predicted[256]; /* luma P recovered = pre_deblock - clipped IDCT(C) */
uint8_t pre_deblock_snap[256]; /* DIAGNOSTIC: pre_deblock at callback time;
* compared against AVFrame post-receive_frame
* to detect h->cur_pic.f vs AVFrame divergence */
};
struct inspect_state {
int n_cbs_this_frame;
int mb_w, mb_h;
uint8_t *seen; /* mb_w * mb_h bitmap */
int duplicate_mbs;
int out_of_bounds;
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
struct mb_capture *captures; /* mb_w * mb_h entries */
int real_coeffs_mbs; /* count of MBs in real-coeffs path this frame */
int skipped_intra16x16;
int skipped_8x8dct;
int skipped_other;
#endif
};
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
/* libavcodec's sl->mb stores coefficients in RASTER (row-major) order,
* not zig-zag scan order — h264_cavlc.c does
* block[*scantable] = (level * qmul[*scantable] + 32) >> 6
* where *scantable advances through ff_zigzag_scan[] which contains
* RASTER positions (row*4 + col). So sl->mb[i] = coef at raster
* position i = (i/4, i%4) = (row, col). No inverse-zigzag needed;
* just transpose row-major → column-major (daedalus's convention). */
/* H.264 §6.4.3 4x4 luma block scan within MB (z-scan).
* Maps raster-block-idx (sb_y*4+sb_x) → libavcodec sl->mb's z-scan idx.
* Z-scan happens to be its own inverse (symmetric mapping). */
static const uint8_t raster_to_zscan[16] = {
0, 1, 4, 5, 2, 3, 6, 7, 8, 9, 12, 13, 10, 11, 14, 15
};
/* H.264 4x4 IDCT — transcribed from daedalus-fourier
* tests/test_idct_bitexact.c (which itself mirrors h264_idct4_ref.c).
* Outputs row-major 16-element residual; clip + shift happens in
* the consumer. */
static void h264_idct4_butterfly(const int d[4], int out[4]) {
int e = d[0] + d[2];
int f = d[0] - d[2];
int g = (d[1] >> 1) - d[3];
int h = d[1] + (d[3] >> 1);
out[0] = e + h;
out[1] = f + g;
out[2] = f - g;
out[3] = e - h;
}
static void ref_idct4_compute(const int16_t block[16], int out[16]) {
/* block COLUMN-MAJOR: block[c*4+r] = coef at (row=r, col=c).
*
* Pass order: COLUMN-pass first, then ROW-pass — matches FFmpeg's
* h264idct_template.c. The pass order matters for integer
* arithmetic with `>>1` on signed values (which round toward -inf
* for odd negatives in C); row-first vs column-first orders can
* disagree by 1 unit at the intermediate stage, propagating to
* the final pixel residual.
*
* (daedalus-fourier's tests/h264_idct4_ref.c does ROW-first, which
* matches its NEON kernel + GPU shader bit-exact within the
* package but DIVERGES from FFmpeg's IDCT for some inputs. PR-A3b
* surfaces the divergence; investigating the fix is a daedalus-
* fourier follow-up — see task #184.) */
int tmp[4][4];
/* Column pass: process each column c independently. */
for (int c = 0; c < 4; c++) {
int d[4] = { block[c*4+0], block[c*4+1], block[c*4+2], block[c*4+3] };
int o[4];
h264_idct4_butterfly(d, o);
for (int r = 0; r < 4; r++) tmp[r][c] = o[r];
}
/* Row pass: process each row r. */
for (int r = 0; r < 4; r++) {
int d[4] = { tmp[r][0], tmp[r][1], tmp[r][2], tmp[r][3] };
int o[4];
h264_idct4_butterfly(d, o);
for (int c = 0; c < 4; c++) out[r*4+c] = o[c];
}
}
#endif /* DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS */
static void inspect_cb(void *opaque,
const struct H264Context *h,
int mb_x, int mb_y)
{
struct inspect_state *st = opaque;
#ifndef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
(void) h;
#endif
if (mb_x < 0 || mb_x >= st->mb_w || mb_y < 0 || mb_y >= st->mb_h) {
st->out_of_bounds++;
st->n_cbs_this_frame++;
return;
}
const size_t idx = (size_t) mb_y * st->mb_w + (size_t) mb_x;
if (st->seen[idx]) st->duplicate_mbs++;
st->seen[idx] = 1;
st->n_cbs_this_frame++;
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
/* Real-coeffs path: extract per-MB state for daedalus-decoder
* IDCT validation on this MB. Gate: only Intra_4x4 + 4x4 transform
* + non-PCM is supported in PR-A3b — other MB flavours fall back
* to identity-passthrough in the main loop. */
struct mb_capture *cap = &st->captures[idx];
cap->valid = 0; /* default to passthrough */
const int mb_xy = mb_y * h->mb_stride + mb_x;
const uint32_t mb_type = h->cur_pic.mb_type[mb_xy];
if (!IS_INTRA4x4(mb_type)) {
if (IS_INTRA16x16(mb_type)) st->skipped_intra16x16++;
else st->skipped_other++;
return;
}
if (IS_8x8DCT(mb_type)) { st->skipped_8x8dct++; return; }
if (IS_INTRA_PCM(mb_type)) { st->skipped_other++; return; }
/* Snapshot luma pre-deblock pixels from cur_pic. */
const uint8_t *luma_plane = h->cur_pic.f->data[0];
const int luma_stride = h->cur_pic.f->linesize[0];
const uint8_t *mb_pixels = luma_plane + (ptrdiff_t) mb_y * 16 * luma_stride
+ mb_x * 16;
/* Diagnostic snapshot: capture the 16x16 luma block as we see it in
* cur_pic at callback time. Compared against AVFrame contents after
* receive_frame returns; mismatch points at a buffer-divergence bug. */
for (int r = 0; r < 16; r++)
memcpy(&cap->pre_deblock_snap[r * 16], &mb_pixels[r * luma_stride], 16);
/* Coefficients are in sl->mb at end of entropy decode but zeroed by
* the time the callback fires (IDCT-add consumed them). Patch 0017
* preserves them in h->mb_inspect_coeffs[16 * 48] BEFORE IDCT runs,
* so we read from there. */
const int16_t *zz_mb = h->mb_inspect_coeffs; /* layout matches sl->mb 8-bit half */
for (int r_block = 0; r_block < 16; r_block++) {
const int z_block = raster_to_zscan[r_block];
const int16_t *block_raw = &zz_mb[z_block * 16];
/* sl->mb stores 16 int16 per block. Empirical finding (via
* /tmp/idct_compare.c, 2026-05-26): daedalus-fourier's C ref
* IDCT and FFmpeg's C ref IDCT produce IDENTICAL output for
* the same input array — the "column-major vs row-major"
* labelling is decoration; both functions implement the same
* H.264 spec IDCT on a 16-int16 input. So we feed daedalus
* the raw sl->mb data unchanged. Previous attempt to
* transpose row-major→column-major was wrong — the transpose
* changed the IDCT result. */
int16_t col[16];
memcpy(col, block_raw, 16 * sizeof(int16_t));
memcpy(&cap->coeffs[r_block * 16], col, 16 * sizeof(int16_t));
/* IDCT → row-major 16-int residual. */
int idct_row[16];
ref_idct4_compute(col, idct_row);
/* P = clip(pre_deblock - ((IDCT + 32) >> 6)) for each pixel.
* Symmetric: daedalus IDCT-add will undo the subtract, including
* for saturating cases (where the same shift puts the value back
* at the same clip boundary). */
const int sb_y = r_block >> 2;
const int sb_x = r_block & 3;
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
const int pre_db = mb_pixels[(sb_y * 4 + r) * luma_stride + sb_x * 4 + c];
const int shift = (idct_row[r * 4 + c] + 32) >> 6;
int p = pre_db - shift;
if (p < 0) p = 0;
if (p > 255) p = 255;
cap->predicted[(sb_y * 4 + r) * 16 + (sb_x * 4 + c)] = (uint8_t) p;
}
}
}
cap->valid = 1;
st->real_coeffs_mbs++;
/* One-shot diagnostic enabled by DAEDALUS_DUMP_MB_3_0 env var. */
if (mb_x == 3 && mb_y == 0 && getenv("DAEDALUS_DUMP_MB_3_0")) {
const int16_t *zz = &zz_mb[1 * 16]; /* z_block = raster_block = 1 */
const struct mb_capture *capdiag = &st->captures[mb_y * st->mb_w + mb_x];
fprintf(stderr, " MB(3,0) block z=1 raster coeffs (sl->mb):");
for (int p = 0; p < 16; p++) fprintf(stderr, " %d", (int) zz[p]);
fprintf(stderr, "\n");
fprintf(stderr, " MB(3,0) block z=1 col_major coeffs (after transpose):");
for (int i = 0; i < 16; i++) fprintf(stderr, " %d", (int) capdiag->coeffs[1 * 16 + i]);
fprintf(stderr, "\n");
/* Recompute IDCT for this block (already done in the loop above but
* print here for visibility). */
int idct_print[16];
ref_idct4_compute(&capdiag->coeffs[1 * 16], idct_print);
fprintf(stderr, " MB(3,0) block z=1 IDCT row-major (raw, pre-shift):");
for (int i = 0; i < 16; i++) fprintf(stderr, " %d", idct_print[i]);
fprintf(stderr, "\n");
fprintf(stderr, " MB(3,0) block z=1 IDCT (+32)>>6:");
for (int i = 0; i < 16; i++) fprintf(stderr, " %d", (idct_print[i] + 32) >> 6);
fprintf(stderr, "\n");
const uint8_t *bpix = mb_pixels + 0 * luma_stride + 4; /* sb_y=0, sb_x=1 → cols 4..7 within MB */
fprintf(stderr, " MB(3,0) block z=1 pre_deblock pixels:\n");
for (int r = 0; r < 4; r++) {
fprintf(stderr, " ");
for (int c = 0; c < 4; c++)
fprintf(stderr, " %3u", bpix[r * luma_stride + c]);
fprintf(stderr, "\n");
}
fprintf(stderr, " MB(3,0) block z=1 P_rec (= pre_deblock - shift):\n");
for (int r = 0; r < 4; r++) {
fprintf(stderr, " ");
for (int c = 0; c < 4; c++)
fprintf(stderr, " %3u", capdiag->predicted[(0*4+r) * 16 + (1*4+c)]);
fprintf(stderr, "\n");
}
/* And what daedalus_decoder SHOULD produce: clip(P_rec + shift). */
fprintf(stderr, " MB(3,0) block z=1 expected daedalus output = clip(P_rec + shift):\n");
for (int r = 0; r < 4; r++) {
fprintf(stderr, " ");
for (int c = 0; c < 4; c++) {
int p_rec = capdiag->predicted[(0*4+r) * 16 + (1*4+c)];
int sh = (idct_print[r*4+c] + 32) >> 6;
int e = p_rec + sh;
if (e < 0) e = 0; if (e > 255) e = 255;
fprintf(stderr, " %3d", e);
}
fprintf(stderr, "\n");
}
}
#endif
}
#endif
/* Extract one MB's predicted-samples block from a YUV420P AVFrame
* (stock libavcodec) and pack it into the 384-byte mb_input.predicted
* layout: 16x16 luma raster, then 8x8 Cb raster, then 8x8 Cr raster.
*
* AVFrame's data[] points at separate Y / U / V planes (or NV12's
* interleaved UV — we handle both via the pix_fmt branch). */
static void pack_mb_predicted(const AVFrame *fr, int mb_x, int mb_y,
uint8_t out[384])
{
const int y_off = mb_y * 16 * fr->linesize[0] + mb_x * 16;
const int uv_off = mb_y * 8 * fr->linesize[1] + mb_x * 8;
/* Luma: 16 rows × 16 cols */
for (int r = 0; r < 16; r++)
memcpy(&out[r * 16],
&fr->data[0][y_off + r * fr->linesize[0]],
16);
/* Chroma: 8 rows × 8 cols per component */
if (fr->format == AV_PIX_FMT_YUV420P) {
for (int r = 0; r < 8; r++) {
memcpy(&out[256 + r * 8],
&fr->data[1][uv_off + r * fr->linesize[1]], 8);
memcpy(&out[256 + 64 + r * 8],
&fr->data[2][uv_off + r * fr->linesize[2]], 8);
}
} else if (fr->format == AV_PIX_FMT_NV12) {
/* NV12: interleaved UV plane, deinterleave into Cb/Cr halves */
const int uv_off_nv12 = mb_y * 8 * fr->linesize[1] + mb_x * 16;
for (int r = 0; r < 8; r++) {
for (int c = 0; c < 8; c++) {
out[256 + r * 8 + c] = fr->data[1][uv_off_nv12 + r * fr->linesize[1] + c * 2 + 0];
out[256 + 64 + r * 8 + c] = fr->data[1][uv_off_nv12 + r * fr->linesize[1] + c * 2 + 1];
}
}
} else {
/* Unsupported pixel format — zero out chroma (test will fail loud) */
memset(&out[256], 0, 128);
}
}
/* Convert an AVFrame (YUV420P or NV12) to NV12 in caller-provided
* planes. Used to write the reference YUV file. */
static void avframe_to_nv12(const AVFrame *fr, uint8_t *out_y, size_t y_stride,
uint8_t *out_uv, size_t uv_stride,
int width, int height)
{
/* Y plane: row-major copy from src linesize to dst stride */
for (int r = 0; r < height; r++)
memcpy(&out_y[(size_t) r * y_stride],
&fr->data[0][(size_t) r * fr->linesize[0]],
(size_t) width);
if (fr->format == AV_PIX_FMT_NV12) {
for (int r = 0; r < height / 2; r++)
memcpy(&out_uv[(size_t) r * uv_stride],
&fr->data[1][(size_t) r * fr->linesize[1]],
(size_t) width);
} else if (fr->format == AV_PIX_FMT_YUV420P) {
/* Interleave U+V → NV12 UV */
const int cw = width / 2, ch = height / 2;
for (int r = 0; r < ch; r++) {
for (int c = 0; c < cw; c++) {
out_uv[(size_t) r * uv_stride + (size_t) c * 2 + 0] =
fr->data[1][(size_t) r * fr->linesize[1] + c];
out_uv[(size_t) r * uv_stride + (size_t) c * 2 + 1] =
fr->data[2][(size_t) r * fr->linesize[2] + c];
}
}
}
}
static int parse_args(int argc, char **argv,
const char **in_path,
const char **out_dadec_path,
const char **out_ref_path)
{
int i = 1;
while (i < argc && argv[i][0] == '-') {
if (!strcmp(argv[i], "--substrate") && i + 1 < argc) {
substrate_str = argv[++i];
} else if (!strcmp(argv[i], "--max-frames") && i + 1 < argc) {
max_frames = atoi(argv[++i]);
} else {
fprintf(stderr, "unknown option: %s\n", argv[i]);
return -1;
}
i++;
}
if (argc - i != 3) {
fprintf(stderr,
"usage: %s [--substrate cpu|qpu|auto] [--max-frames N] "
"<input.h264> <output_dadec.yuv> <output_ref.yuv>\n", argv[0]);
return -1;
}
*in_path = argv[i + 0];
*out_dadec_path = argv[i + 1];
*out_ref_path = argv[i + 2];
return 0;
}
static daedalus_decoder_substrate parse_substrate(const char *s)
{
if (!strcmp(s, "cpu")) return DAEDALUS_DECODER_SUBSTRATE_CPU;
if (!strcmp(s, "qpu")) return DAEDALUS_DECODER_SUBSTRATE_QPU;
return DAEDALUS_DECODER_SUBSTRATE_AUTO;
}
int main(int argc, char **argv)
{
const char *in_path, *out_dadec_path, *out_ref_path;
if (parse_args(argc, argv, &in_path, &out_dadec_path, &out_ref_path) != 0)
return 1;
/* ---- Open input via libavformat (so we get NAL framing for free
* from the raw .h264 elementary stream demuxer). ---- */
AVFormatContext *fmt = NULL;
if (avformat_open_input(&fmt, in_path, NULL, NULL) < 0) {
fprintf(stderr, "avformat_open_input(%s) failed\n", in_path);
return 2;
}
if (avformat_find_stream_info(fmt, NULL) < 0) {
fprintf(stderr, "avformat_find_stream_info failed\n");
avformat_close_input(&fmt); return 2;
}
int vstream = -1;
for (unsigned s = 0; s < fmt->nb_streams; s++)
if (fmt->streams[s]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
vstream = (int) s; break;
}
if (vstream < 0) {
fprintf(stderr, "no video stream in %s\n", in_path);
avformat_close_input(&fmt); return 2;
}
/* ---- Open H.264 decoder ---- */
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
AVCodecContext *avctx = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(avctx, fmt->streams[vstream]->codecpar);
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
/* Patch 0017's coefficient side buffer lives in H264Context (single
* per-stream); multi-threaded slice decode would race on it. Force
* single-thread. Also disable libavcodec's deblock so AVFrame is
* pre-deblock and the P-recovery math is exact. */
avctx->thread_count = 1;
avctx->thread_type = 0;
avctx->skip_loop_filter = AVDISCARD_ALL;
#endif
if (avcodec_open2(avctx, codec, NULL) < 0) {
fprintf(stderr, "avcodec_open2 failed\n");
avformat_close_input(&fmt); return 2;
}
AVPacket *pkt = av_packet_alloc();
AVFrame *fr = av_frame_alloc();
/* ---- Allocate output buffers + state needed before first decode ---- */
daedalus_decoder *dec = NULL;
uint8_t *out_y_dadec = NULL, *out_uv_dadec = NULL;
uint8_t *out_y_ref = NULL, *out_uv_ref = NULL;
size_t y_size = 0, uv_size = 0;
FILE *out_dadec_f = NULL, *out_ref_f = NULL;
int rc = 0;
int n_frames = 0;
size_t total_y_diffs = 0, total_uv_diffs = 0;
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_CB
/* Init inspect state BEFORE the first avcodec_send_packet — the
* callback fires from inside send_packet (i.e. before the first
* receive_frame ever returns), so lazy-init after-the-fact
* would miss the entire first frame. Use codecpar dims; round
* up to MB granularity (H.264 codes 1080 height as 1088). */
struct inspect_state inspect_st = {0};
{
const AVCodecParameters *cp = fmt->streams[vstream]->codecpar;
const int W_round = (cp->width + 15) & ~15;
const int H_round = (cp->height + 15) & ~15;
inspect_st.mb_w = W_round / 16;
inspect_st.mb_h = H_round / 16;
inspect_st.seen = calloc(1, (size_t) inspect_st.mb_w * inspect_st.mb_h);
if (!inspect_st.seen) { rc = 1; goto cleanup; }
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
inspect_st.captures = calloc((size_t) inspect_st.mb_w * inspect_st.mb_h,
sizeof(*inspect_st.captures));
if (!inspect_st.captures) { rc = 1; goto cleanup; }
#endif
}
ff_h264_set_mb_inspect_cb(avctx, inspect_cb, &inspect_st);
int inspect_total_cbs = 0;
int inspect_total_duplicate = 0;
int inspect_total_oob = 0;
int inspect_total_missing = 0;
#endif
/* ---- daedalus_decoder is lazy-created on the first AVFrame
* (coded width/height come from the bitstream's SPS via
* libavcodec). ---- */
while (av_read_frame(fmt, pkt) >= 0) {
if (pkt->stream_index != vstream) { av_packet_unref(pkt); continue; }
if (avcodec_send_packet(avctx, pkt) < 0) {
fprintf(stderr, "send_packet failed\n");
rc = 2; goto cleanup;
}
av_packet_unref(pkt);
for (;;) {
int ret = avcodec_receive_frame(avctx, fr);
if (ret == AVERROR(EAGAIN)) break;
if (ret < 0) {
fprintf(stderr, "receive_frame failed: %d\n", ret);
rc = 2; goto cleanup;
}
/* Lazily create the daedalus_decoder + output planes on
* the first frame so the SPS-derived coded width/height
* are known. */
if (!dec) {
/* Coded (= MB-aligned) dimensions are on AVCodecContext,
* not AVFrame (which carries the cropped display size). */
const int W = avctx->coded_width ? avctx->coded_width : fr->width;
const int H = avctx->coded_height ? avctx->coded_height : fr->height;
if ((W & 15) || (H & 15)) {
fprintf(stderr, "coded dims %dx%d not mod-16; skip\n", W, H);
rc = 2; goto cleanup;
}
dec = daedalus_decoder_create(W, H);
if (!dec) {
fprintf(stderr, "daedalus_decoder_create failed\n");
rc = 3; goto cleanup;
}
daedalus_decoder_set_substrate(dec, parse_substrate(substrate_str));
y_size = (size_t) W * (size_t) H;
uv_size = y_size / 2;
out_y_dadec = malloc(y_size);
out_uv_dadec = malloc(uv_size);
out_y_ref = malloc(y_size);
out_uv_ref = malloc(uv_size);
out_dadec_f = fopen(out_dadec_path, "wb");
out_ref_f = fopen(out_ref_path, "wb");
if (!out_y_dadec || !out_uv_dadec || !out_y_ref || !out_uv_ref ||
!out_dadec_f || !out_ref_f) {
fprintf(stderr, "alloc / fopen failed\n");
rc = 1; goto cleanup;
}
printf("daedalus_decode_h264: %dx%d, substrate=%s\n",
W, H, substrate_str);
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_CB
printf(" inspection callback: ACTIVE (patched libavcodec); "
"mb-grid %dx%d\n", inspect_st.mb_w, inspect_st.mb_h);
#else
printf(" inspection callback: not built in (stock libavcodec)\n");
#endif
}
/* Pack each MB's predicted samples from the AVFrame.
* Coeffs = 0; no edges; daedalus_decoder will reproduce
* exactly the AVFrame pixels. Use coded_width/coded_height
* for MB-grid alignment (e.g. 1920x1088 for 1080p display). */
const int coded_w = avctx->coded_width ? avctx->coded_width : avctx->width;
const int coded_h = avctx->coded_height ? avctx->coded_height : avctx->height;
const int mb_w = coded_w / 16;
const int mb_h = coded_h / 16;
uint8_t mb_pred[384];
int16_t mb_coeffs[384] = {0};
struct daedalus_decoder_mb_input mb = {0};
for (int my = 0; my < mb_h; my++) {
for (int mx = 0; mx < mb_w; mx++) {
/* Default: identity-passthrough — luma from AVFrame,
* chroma from AVFrame, coeffs all zero. */
pack_mb_predicted(fr, mx, my, mb_pred);
memset(mb_coeffs, 0, sizeof(mb_coeffs));
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
/* Real-coeffs path: if the callback captured this MB
* as Intra_4x4 / 4x4-DCT, override luma predicted
* with the recovered P and use the real luma coeffs.
* Chroma stays identity-passthrough (PR-A3b scope —
* chroma DC Hadamard + 8x8 transform follow-ups). */
const int mb_idx = my * mb_w + mx;
const struct mb_capture *cap = &inspect_st.captures[mb_idx];
if (cap->valid) {
memcpy(mb_pred, cap->predicted, 256);
for (int i = 0; i < 256; i++)
mb_coeffs[i] = cap->coeffs[i];
}
#endif
mb.mb_x = (uint16_t) mx;
mb.mb_y = (uint16_t) my;
mb.transform_8x8 = 0;
mb.coeffs = mb_coeffs;
mb.predicted = mb_pred;
mb.edges = NULL;
mb.n_edges = 0;
if (daedalus_decoder_append_mb(dec, &mb) != 0) {
fprintf(stderr, "append_mb (%d,%d) failed\n", mx, my);
rc = 3; goto cleanup;
}
}
}
int frc = daedalus_decoder_flush_frame(dec,
out_y_dadec, (size_t) coded_w,
out_uv_dadec, (size_t) coded_w);
if (frc != 0) {
fprintf(stderr, "flush_frame frame %d rc=%d\n", n_frames, frc);
rc = 3; goto cleanup;
}
/* Build the reference NV12 from the AVFrame for comparison. */
avframe_to_nv12(fr, out_y_ref, (size_t) coded_w,
out_uv_ref, (size_t) coded_w,
coded_w, coded_h);
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
/* Diagnostic: for each real-coeffs MB, compare the callback's
* pre_deblock snapshot against AVFrame at the same position.
* If they differ, h->cur_pic.f at callback time isn't the
* eventual AVFrame buffer (or deblock ran despite
* skip_loop_filter=AVDISCARD_ALL). */
int snap_mismatches = 0;
int first_snap_mismatch_mb = -1;
for (int my2 = 0; my2 < mb_h; my2++) {
for (int mx2 = 0; mx2 < mb_w; mx2++) {
const int idx2 = my2 * mb_w + mx2;
if (!inspect_st.captures[idx2].valid) continue;
const uint8_t *avf_mb = fr->data[0]
+ (ptrdiff_t) my2 * 16 * fr->linesize[0]
+ mx2 * 16;
for (int r = 0; r < 16; r++) {
for (int c = 0; c < 16; c++) {
if (avf_mb[r * fr->linesize[0] + c] !=
inspect_st.captures[idx2].pre_deblock_snap[r * 16 + c]) {
if (first_snap_mismatch_mb < 0)
first_snap_mismatch_mb = idx2;
snap_mismatches++;
}
}
}
}
}
if (snap_mismatches > 0) {
const int mmb_x = first_snap_mismatch_mb % mb_w;
const int mmb_y = first_snap_mismatch_mb / mb_w;
fprintf(stderr,
" DIAG: callback's pre_deblock differs from AVFrame in "
"%d bytes across real-coeffs MBs; first mismatch at MB(%d, %d)\n",
snap_mismatches, mmb_x, mmb_y);
rc = 4;
}
/* Silent on match — the invariant must hold for the
* P-recovery math to be valid; we'd want to know if it
* ever broke, but no need to confirm it every frame. */
#endif
/* Byte-exact compare + first-diff diagnostic. */
size_t y_diffs = 0, uv_diffs = 0;
size_t y_first_diff = (size_t) -1;
for (size_t i = 0; i < y_size; i++)
if (out_y_dadec[i] != out_y_ref[i]) {
if (y_first_diff == (size_t) -1) y_first_diff = i;
y_diffs++;
}
for (size_t i = 0; i < uv_size; i++)
if (out_uv_dadec[i] != out_uv_ref[i]) uv_diffs++;
if (y_diffs && y_first_diff != (size_t) -1) {
const size_t row = y_first_diff / (size_t) avctx->width;
const size_t col = y_first_diff % (size_t) avctx->width;
const size_t mb_x = col / 16;
const size_t mb_y = row / 8; /* not row/16 — chroma row uses /8 so use raw row here */
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
const int mb_idx = (int)(row / 16) * mb_w + (int) mb_x;
const int real = (mb_idx >= 0 && mb_idx < mb_w * mb_h)
? inspect_st.captures[mb_idx].valid : -1;
printf(" first Y diff @ byte %zu = (row %zu, col %zu) in MB(%zu,%zu) [real-coeffs=%d]; "
"dadec=%u ref=%u\n",
y_first_diff, row, col, mb_x, row / 16,
real, out_y_dadec[y_first_diff], out_y_ref[y_first_diff]);
#else
(void) mb_x; (void) mb_y;
printf(" first Y diff @ byte %zu = (row %zu, col %zu); dadec=%u ref=%u\n",
y_first_diff, row, col,
out_y_dadec[y_first_diff], out_y_ref[y_first_diff]);
#endif
}
total_y_diffs += y_diffs;
total_uv_diffs += uv_diffs;
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_CB
{
const int expected = mb_w * mb_h;
/* Count MBs that fired the callback. */
int seen_count = 0;
for (int i = 0; i < expected; i++)
if (inspect_st.seen[i]) seen_count++;
int missing = expected - seen_count;
if (missing || inspect_st.duplicate_mbs || inspect_st.out_of_bounds) {
fprintf(stderr,
" frame %d: callback invariants: fired=%d expected=%d "
"missing=%d duplicates=%d oob=%d\n",
n_frames, inspect_st.n_cbs_this_frame, expected,
missing, inspect_st.duplicate_mbs, inspect_st.out_of_bounds);
rc = 4;
}
inspect_total_cbs += inspect_st.n_cbs_this_frame;
inspect_total_duplicate += inspect_st.duplicate_mbs;
inspect_total_oob += inspect_st.out_of_bounds;
inspect_total_missing += missing;
/* Reset for next frame. */
inspect_st.n_cbs_this_frame = 0;
inspect_st.duplicate_mbs = 0;
inspect_st.out_of_bounds = 0;
memset(inspect_st.seen, 0, (size_t) expected);
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
printf(" frame %d: real-coeffs path %d MBs, "
"skipped intra16x16=%d 8x8dct=%d other=%d\n",
n_frames, inspect_st.real_coeffs_mbs,
inspect_st.skipped_intra16x16,
inspect_st.skipped_8x8dct,
inspect_st.skipped_other);
inspect_st.real_coeffs_mbs = 0;
inspect_st.skipped_intra16x16 = 0;
inspect_st.skipped_8x8dct = 0;
inspect_st.skipped_other = 0;
memset(inspect_st.captures, 0,
(size_t) expected * sizeof(*inspect_st.captures));
#endif
}
#endif
printf(" frame %d: Y diff %zu/%zu UV diff %zu/%zu%s\n",
n_frames, y_diffs, y_size, uv_diffs, uv_size,
(y_diffs || uv_diffs) ? " ***" : "");
/* Write both YUVs to disk. */
fwrite(out_y_dadec, 1, y_size, out_dadec_f);
fwrite(out_uv_dadec, 1, uv_size, out_dadec_f);
fwrite(out_y_ref, 1, y_size, out_ref_f);
fwrite(out_uv_ref, 1, uv_size, out_ref_f);
n_frames++;
if (max_frames > 0 && n_frames >= max_frames) goto drained;
}
}
/* Flush libavcodec for any remaining buffered frames. */
avcodec_send_packet(avctx, NULL);
for (;;) {
int ret = avcodec_receive_frame(avctx, fr);
if (ret < 0) break;
(void) ret;
/* Same loop body as above would go here; omitted for brevity —
* stock libavcodec rarely buffers I-only streams. */
n_frames++;
}
drained:
printf("\n%d frames decoded; total Y diff %zu, UV diff %zu\n",
n_frames, total_y_diffs, total_uv_diffs);
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_CB
printf("inspection callback: %d total invocations, %d missing, %d duplicates, %d oob\n",
inspect_total_cbs, inspect_total_missing, inspect_total_duplicate, inspect_total_oob);
if (inspect_total_missing || inspect_total_duplicate || inspect_total_oob)
rc = 4;
#endif
if (rc == 0 && (total_y_diffs || total_uv_diffs)) {
printf("FAIL: daedalus-decoder output does NOT match libavcodec reference byte-for-byte\n");
rc = 4;
} else if (rc == 0) {
printf("PASS: byte-exact identity-passthrough across %d frames\n", n_frames);
} else {
printf("FAIL: %s\n",
(total_y_diffs || total_uv_diffs) ? "byte-exact comparison failed"
: "inspection callback invariants violated");
}
cleanup:
if (out_dadec_f) fclose(out_dadec_f);
if (out_ref_f) fclose(out_ref_f);
free(out_uv_ref); free(out_y_ref);
free(out_uv_dadec);free(out_y_dadec);
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_CB
free(inspect_st.seen);
# ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
free(inspect_st.captures);
# endif
#endif
if (dec) daedalus_decoder_destroy(dec);
av_frame_free(&fr);
av_packet_free(&pkt);
avcodec_free_context(&avctx);
avformat_close_input(&fmt);
return rc;
}