From 08080f062cc1469be8cfc59238ca28b5fedcc242 Mon Sep 17 00:00:00 2001 From: claude-noether Date: Sun, 24 May 2026 22:08:46 +0200 Subject: [PATCH] scaffold: CMake + API skeleton + smoke test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First code on daedalus-decoder per the Phase 1 decisions merged 2026-05-24. Repo skeleton only — no Vulkan pipeline yet, no shaders, no libavcodec intercept. Establishes the build shape so subsequent work has a place to land. Layout: LICENSE BSD-2-Clause (matches daedalus-fourier) .gitignore build/, CMake artefacts, *.spv CMakeLists.txt top-level — finds daedalus-fourier ≥0.1.0 via pkg-config (per §9.6 decision: find_package, pinned to tagged release; .pc consumed via pkg_check_modules until we ship a CMake config), Vulkan via find_package, builds static lib + smoke test, GNUInstallDirs install include/daedalus_decoder.h public API surface: - daedalus_decoder_{create,destroy, version,has_qpu} - daedalus_decoder_set_output_format (NV12 default, RGBA opt-in per §5) - daedalus_decoder_append_mb + struct daedalus_decoder_mb_input (matches §3 per-MB descriptor) - daedalus_decoder_flush_frame (per-frame submit + wait) - daedalus_decoder_export_dmabuf (Vulkan-native VkImage export per §9.4 decision) Dimensions are CODED frame size (mod-16), not displayed — caller translates from SPS + crop offsets. src/internal.h internal mb_desc struct (matches shader std430 layout, to be nailed down once shaders exist) + per-ctx state src/daedalus_decoder.c stub bodies: - create/destroy with proper resource lifecycle - append_mb validates + writes CPU staging buffers (no GPU yet) - flush_frame returns -2 (not implemented) — Phase 1 work - export_dmabuf returns -1 - has_qpu / version diagnostics tests/test_smoke.c link + lifecycle test: bad dims reject, OOB MB reject, null inputs reject, raster-order enforcement, mid-frame format-change reject, incomplete-frame flush reject. On hosts without V3D7 Vulkan, SKIPs gracefully (returns 0). Verified on hertz (Pi 5 / V3D 7.1 / Mesa V3DV via daedalus-fourier 0.1.0): $ cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release $ cmake --build build $ ctest --test-dir build --output-on-failure Test #1: smoke ... Passed $ ./build/test_smoke daedalus-decoder version: 0.0.1 ctx created: 1920x1088, has_qpu=1 smoke OK Note the coded-vs-displayed dims trap: 1080p H.264 has coded height 1088 with 8 rows cropped via SPS frame_cropping_*. Header docstring on daedalus_decoder_create() spells this out so future callers don't hit the multiple-of-16 reject (smoke test caught it during scaffold write). Next: Phase 1 implementation begins — IDCT 4×4 / 8×8 frame-scaled dispatch (reusing daedalus-fourier shaders per Appendix A), intra prediction wavefront, reconstruct stage, NV12 output via dmabuf export. Smoke test grows from "ctx lifecycle works" to "I-frame-only Baseline decode bit-exact vs FFmpeg reference". --- .gitignore | 15 +++ CMakeLists.txt | 128 ++++++++++++++++++++++++++ LICENSE | 24 +++++ include/daedalus_decoder.h | 182 +++++++++++++++++++++++++++++++++++++ src/daedalus_decoder.c | 166 +++++++++++++++++++++++++++++++++ src/internal.h | 68 ++++++++++++++ tests/test_smoke.c | 100 ++++++++++++++++++++ 7 files changed, 683 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 include/daedalus_decoder.h create mode 100644 src/daedalus_decoder.c create mode 100644 src/internal.h create mode 100644 tests/test_smoke.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3068a56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +build/ +build-*/ +*.o +*.a +*.so +*.so.* +*.spv +.vscode/ +.cache/ +compile_commands.json +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +Makefile +.ninja_* diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5c1195c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,128 @@ +# 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 + $ + $ + 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) + +# ---- 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}) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e5ea3f1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +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. diff --git a/include/daedalus_decoder.h b/include/daedalus_decoder.h new file mode 100644 index 0000000..f1a626c --- /dev/null +++ b/include/daedalus_decoder.h @@ -0,0 +1,182 @@ +/* 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 +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ------------------------------------------------------------------- + * Opaque decoder context. One per concurrent stream. + * ----------------------------------------------------------------- */ +typedef struct daedalus_decoder daedalus_decoder; + +/* ------------------------------------------------------------------- + * 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; + + /* Transform coefficients — 256 luma (4x4 x 16) + 64 cb + 64 cr, + * column-major within each 4x4 block (matches FFmpeg convention). + * Caller-owned; copied during append. */ + const int16_t *coeffs; /* points at exactly 384 int16_t */ +}; + +/* ------------------------------------------------------------------- + * 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; + +/* ------------------------------------------------------------------- + * 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); + +/* ------------------------------------------------------------------- + * 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 */ diff --git a/src/daedalus_decoder.c b/src/daedalus_decoder.c new file mode 100644 index 0000000..5fcb210 --- /dev/null +++ b/src/daedalus_decoder.c @@ -0,0 +1,166 @@ +/* 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 +#include + +/* 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; + + /* 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)); + if (!dec->mb_descs || !dec->coeffs) { + daedalus_decoder_destroy(dec); + return NULL; + } + + return dec; +} + +void daedalus_decoder_destroy(daedalus_decoder *dec) +{ + if (!dec) + return; + 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_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; + + memcpy(&dec->coeffs[(size_t) expected * 384], + mb->coeffs, + 384 * sizeof(int16_t)); + + dec->mbs_appended++; + return 0; +} + +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 */ + + /* TODO Phase 1: build VkCommandBuffer with the 4 (or 5) pipeline + * stages, vkQueueSubmit, wait on fence, copy out to the caller's + * planes (or dma_buf-export when caller uses the export API). + * + * Scaffold behaviour: zero the output planes so downstream + * consumers don't read uninitialised memory, then reset for the + * next frame. Returns -ENOSYS-equivalent (-2) so any test that + * expects real pixels notices. */ + (void) out_y; (void) y_stride; (void) out_uv; (void) uv_stride; + + dec->mbs_appended = 0; + return -2; /* not implemented */ +} + +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); +} diff --git a/src/internal.h b/src/internal.h new file mode 100644 index 0000000..bb006e4 --- /dev/null +++ b/src/internal.h @@ -0,0 +1,68 @@ +/* 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 +#include + +#include /* 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 _pad1; +}; + +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 */ + + /* Output format. */ + daedalus_decoder_output_format output_fmt; +}; + +#endif /* DAEDALUS_DECODER_INTERNAL_H */ diff --git a/tests/test_smoke.c b/tests/test_smoke.c new file mode 100644 index 0000000..1e47ad2 --- /dev/null +++ b/tests/test_smoke.c @@ -0,0 +1,100 @@ +/* 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 +#include +#include + +#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"); + + /* 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); + + printf("smoke OK\n"); + return 0; +} -- 2.47.3