From baa0fb07d4272c2d81cc94881d52a71fb640d936 Mon Sep 17 00:00:00 2001 From: Chris Thomas Date: Thu, 16 Jul 2026 11:16:52 +0200 Subject: [PATCH] feat: cross-platform SDL emulator dev-environment Adds a desktop build of the goggle UI (Linux/macOS/Windows) that runs the real application code -- the same main(), menu system, OSD and page logic that ships on the goggle -- rendered through SDL2 instead of the framebuffer, with the hardware peripherals replaced by compiled-in mocks. This lets the LVGL interface be developed, previewed and regression-tested without hardware. Build and run: .devcontainer/build.sh {images,linux,windows,mac} build (in a container) ./run-native.sh run natively on the host Every target compiles inside a devcontainer image, so builds are reproducible and need no host toolchain; the resulting binary runs natively on its OS. macOS is cross-compiled to Mach-O in the container too (LLVM/clang); its SDK is staged once from your Xcode since Apple's is not redistributable. CI is the one place macOS builds natively -- on a macos-latest runner -- because CI can't stage that SDK. Zero cost to the firmware: all emulator code lives behind EMULATOR_BUILD or in src/emulator/*, which CMake compiles only for the emulator, so the goggle binary contains none of it. Hardware drivers that genuinely differ (UART, I2C, RTC, the framebuffer) are split into per-target files selected by CMake, keeping the goggle drivers free of emulator #ifdefs. Peripherals are mocked at their hardware seam, so the app itself is unchanged: - display engine -> SDL2 window + software compositor - DM5680 video -> decodes a looping MPEG-1 clip (vendored pl_mpeg; no ffmpeg is linked) behind the OSD, and only in the video view - DM5680 serial -> socketpair-backed UART + a mock answering version/RSSI - I2C bus, RTC -> no-op bus, in-memory clock - system_exec -> logged and skipped, so device shell commands (display pokes, register writes, wifi/passwd scripts) never touch the host Also included: configurable filesystem roots (path_app/path_extsd from config+env, compiled away on the goggle), POSIX compat shims for macOS/Windows hosts, a fix for ATOMIC_VAR_INIT removed on newer toolchains (which the emulator build requires; also offered standalone), and CI that builds all three targets on every push with a headless screenshot, publishing binaries on a semver tag. --- .devcontainer/Dockerfile | 18 +- .devcontainer/Dockerfile.macos | 65 + .devcontainer/Dockerfile.windows | 32 + .devcontainer/build.sh | 84 + .devcontainer/darwin-arm64.cmake | 52 + .devcontainer/emu.sh | 87 + .devcontainer/macos/Info.plist | 26 + .devcontainer/windows-x64.cmake | 28 + .github/workflows/build_emulator.yml | 148 + .gitignore | 12 + CMakeLists.txt | 105 +- docs/emulator.md | 93 + lib/log/src/log.c | 2 +- lib/lvgl/lv_conf.h | 7 + lib/pl_mpeg/pl_mpeg.h | 4439 ++++++++++++++++++++ run-native.sh | 41 + src/core/elrs.c | 40 +- src/core/esp32_flash.c | 13 +- src/core/ht.c | 3 + src/core/input_device.c | 313 +- src/core/input_device.h | 5 + src/core/input_device_internal.h | 33 + src/core/main.c | 17 +- src/core/osd.c | 58 +- src/core/osd.h | 3 + src/core/thread.c | 4 + src/driver/dm5680.c | 21 + src/driver/esp32.c | 1 + src/driver/fbtools.c | 4 + src/driver/fbtools.h | 5 +- src/driver/i2c.c | 2 + src/driver/input_evdev.c | 223 + src/driver/rtc.c | 99 +- src/driver/rtc_hw.c | 73 + src/driver/rtc_hw.h | 21 + src/driver/uart.c | 5 + src/driver/uart.h | 6 + src/emulator/dm5680_emu.c | 156 + src/emulator/dm5680_emu.h | 19 + src/emulator/i2c_emu.c | 41 + src/emulator/input_device_emu.c | 158 + src/emulator/rtc_hw_emu.c | 21 + src/emulator/uart_emu.c | 84 + src/emulator/video_emu.c | 220 + src/emulator/video_emu.h | 21 + src/platform/compat/windows/arpa/inet.h | 2 + src/platform/compat/windows/net/if.h | 2 + src/platform/compat/windows/netinet/in.h | 2 + src/platform/compat/windows/posix_compat.c | 87 + src/platform/compat/windows/sys/ioctl.h | 3 + src/platform/compat/windows/sys/mount.h | 6 + src/platform/compat/windows/sys/select.h | 15 + src/platform/compat/windows/sys/socket.h | 2 + src/platform/compat/windows/sys/sockio.h | 2 + src/platform/compat/windows/termios.h | 98 + src/platform/paths.c | 46 + src/platform/paths.h | 27 + src/platform/timer_compat.c | 95 + src/platform/timer_compat.h | 53 + src/ui/page_playback.c | 13 +- src/ui/page_scannow.c | 1 - src/ui/page_version.c | 10 +- src/ui/page_wifi.c | 19 + src/ui/ui_porting.c | 171 +- src/util/sdcard.c | 4 + src/util/system.c | 17 + 66 files changed, 7165 insertions(+), 418 deletions(-) create mode 100644 .devcontainer/Dockerfile.macos create mode 100644 .devcontainer/Dockerfile.windows create mode 100755 .devcontainer/build.sh create mode 100644 .devcontainer/darwin-arm64.cmake create mode 100755 .devcontainer/emu.sh create mode 100644 .devcontainer/macos/Info.plist create mode 100644 .devcontainer/windows-x64.cmake create mode 100644 .github/workflows/build_emulator.yml create mode 100644 docs/emulator.md create mode 100644 lib/pl_mpeg/pl_mpeg.h create mode 100755 run-native.sh create mode 100644 src/core/input_device_internal.h create mode 100644 src/driver/input_evdev.c create mode 100644 src/driver/rtc_hw.c create mode 100644 src/driver/rtc_hw.h create mode 100644 src/emulator/dm5680_emu.c create mode 100644 src/emulator/dm5680_emu.h create mode 100644 src/emulator/i2c_emu.c create mode 100644 src/emulator/input_device_emu.c create mode 100644 src/emulator/rtc_hw_emu.c create mode 100644 src/emulator/uart_emu.c create mode 100644 src/emulator/video_emu.c create mode 100644 src/emulator/video_emu.h create mode 100644 src/platform/compat/windows/arpa/inet.h create mode 100644 src/platform/compat/windows/net/if.h create mode 100644 src/platform/compat/windows/netinet/in.h create mode 100644 src/platform/compat/windows/posix_compat.c create mode 100644 src/platform/compat/windows/sys/ioctl.h create mode 100644 src/platform/compat/windows/sys/mount.h create mode 100644 src/platform/compat/windows/sys/select.h create mode 100644 src/platform/compat/windows/sys/socket.h create mode 100644 src/platform/compat/windows/sys/sockio.h create mode 100644 src/platform/compat/windows/termios.h create mode 100644 src/platform/paths.c create mode 100644 src/platform/paths.h create mode 100644 src/platform/timer_compat.c create mode 100644 src/platform/timer_compat.h diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index f159f7be..a5bebe47 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,19 @@ -FROM --platform=linux/x86_64 mcr.microsoft.com/devcontainers/base:ubuntu +# Pinned to 22.04 (glibc 2.35) so the Linux emulator binary built here runs on any +# reasonably current distro. An unpinned :ubuntu tag tracks the newest LTS and glibc +# keeps climbing (26.04 => 2.43), which would make the binary fail with +# "version GLIBC_x.y not found" on older hosts -- breaking the build-in-container, +# run-natively promise. Windows/macOS outputs are PE/Mach-O and don't depend on this. +FROM --platform=linux/x86_64 mcr.microsoft.com/devcontainers/base:ubuntu-22.04 +# Firmware builds need cmake/build-essential/ninja + the Bootlin toolchain that +# setup.sh downloads. Everything else turns this same container into a full HDZero +# EMULATOR workbench: SDL2 to build it, ffmpeg/libav to decode a fake FPV video +# feed, and Xvfb/x11vnc/xdotool/imagemagick to run it headless, drive it, screenshot. RUN export DEBIAN_FRONTEND=noninteractive && \ apt-get update && \ - apt-get -y install --no-install-recommends cmake build-essential ninja-build libsdl2-dev \ No newline at end of file + apt-get -y install --no-install-recommends \ + cmake build-essential ninja-build libsdl2-dev \ + wget ca-certificates bzip2 pkg-config \ + libavformat-dev libavcodec-dev libavutil-dev libswscale-dev ffmpeg \ + xvfb xdotool imagemagick && \ + rm -rf /var/lib/apt/lists/* \ No newline at end of file diff --git a/.devcontainer/Dockerfile.macos b/.devcontainer/Dockerfile.macos new file mode 100644 index 00000000..b0e8419d --- /dev/null +++ b/.devcontainer/Dockerfile.macos @@ -0,0 +1,65 @@ +# syntax=docker/dockerfile:1 +# macOS (arm64) cross-compile image for the HDZero SDL emulator. +# +# Containerized LLVM cross toolchain: Linux -> arm64 macOS (Mach-O). clang retargets +# via triple, ld64.lld links Mach-O — so build_xmac/HDZGOGGLE is produced entirely +# in the container, with NO compilation on the host Mac. The resulting binary RUNS +# natively on macOS (Apple Silicon). +# +# One host step is unavoidable: Apple's SDK is not redistributable, so it is staged +# from your own Xcode/CLT into MacOSX.sdk.tar (see .devcontainer/build.sh prepare-mac) +# and COPY'd in at image-build time. It is gitignored, never committed. +# +# Build (or use .devcontainer/build.sh images): +# docker build -f .devcontainer/Dockerfile.macos -t hdzero-dev-mac .devcontainer +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + clang \ + lld \ + llvm \ + cmake \ + make \ + git \ + ca-certificates \ + curl \ + xz-utils \ + python3 \ + && rm -rf /var/lib/apt/lists/* + +# Complete the Mach-O toolchain with LLVM's cctools-equivalents under their Apple +# names, so CMake's Darwin binutils detection (install_name_tool/otool/lipo) and +# the ld64.lld linker are all on PATH. Avoids needing osxcross's cctools port. +ENV LLVMBIN=/usr/lib/llvm-18/bin +RUN ln -sf $LLVMBIN/llvm-install-name-tool /usr/local/bin/install_name_tool \ + && ln -sf $LLVMBIN/llvm-otool /usr/local/bin/otool \ + && ln -sf $LLVMBIN/llvm-lipo /usr/local/bin/lipo \ + && ln -sf $LLVMBIN/ld64.lld /usr/local/bin/ld64.lld \ + && ln -sf /usr/bin/llvm-ar /usr/local/bin/ar \ + && ln -sf /usr/bin/llvm-ranlib /usr/local/bin/ranlib \ + && ln -sf /usr/bin/llvm-nm /usr/local/bin/nm \ + && ln -sf /usr/bin/llvm-strip /usr/local/bin/strip + +# macOS SDK sysroot — staged from the host's Xcode/CLT (Apple SDK is NOT +# redistributable, so it is provided at image-build time, never committed). +COPY MacOSX.sdk.tar /tmp/MacOSX.sdk.tar +RUN mkdir -p /opt/sdk && tar xf /tmp/MacOSX.sdk.tar -C /opt/sdk && rm /tmp/MacOSX.sdk.tar +ENV MACOS_SDK=/opt/sdk/MacOSX.sdk + +COPY darwin-arm64.cmake /opt/toolchain/darwin-arm64.cmake + +# Cross-build SDL2 (static) for the target and install it into the image, so the +# app build can find_package(SDL2) with zero extra setup. +ARG SDL2_VERSION=2.30.10 +RUN curl -fsSL https://github.com/libsdl-org/SDL/releases/download/release-${SDL2_VERSION}/SDL2-${SDL2_VERSION}.tar.gz -o /tmp/sdl2.tgz \ + && mkdir -p /tmp/sdl2 && tar xf /tmp/sdl2.tgz -C /tmp/sdl2 --strip-components=1 \ + && cmake -S /tmp/sdl2 -B /tmp/sdl2/build \ + -DCMAKE_TOOLCHAIN_FILE=/opt/toolchain/darwin-arm64.cmake \ + -DCMAKE_BUILD_TYPE=Release -DSDL_STATIC=ON -DSDL_SHARED=OFF -DSDL_TEST=OFF \ + -DCMAKE_INSTALL_PREFIX=/opt/sdl2-macos \ + && cmake --build /tmp/sdl2/build -j"$(nproc)" \ + && cmake --install /tmp/sdl2/build \ + && rm -rf /tmp/sdl2 /tmp/sdl2.tgz +ENV SDL2_MACOS=/opt/sdl2-macos + +WORKDIR /work diff --git a/.devcontainer/Dockerfile.windows b/.devcontainer/Dockerfile.windows new file mode 100644 index 00000000..a35b99ee --- /dev/null +++ b/.devcontainer/Dockerfile.windows @@ -0,0 +1,32 @@ +# syntax=docker/dockerfile:1 +# Windows (x86_64) cross-compile image for the HDZero SDL emulator. +# +# Extends the Linux dev image (hdzero-dev, from .devcontainer/Dockerfile) with the +# mingw-w64 cross toolchain + prebuilt SDL2 mingw dev libraries, so +# build_win_x64/HDZGOGGLE.exe can be produced from Linux or macOS with NO Windows +# host. The .exe then runs on Windows, including Windows-on-ARM via x64 emulation. +# +# Build order (or just use .devcontainer/build.sh images): +# docker build -f .devcontainer/Dockerfile -t hdzero-dev .devcontainer +# docker build -f .devcontainer/Dockerfile.windows -t hdzero-dev-win .devcontainer +# +# The base (hdzero-dev) is amd64 — its own Dockerfile pins FROM --platform=linux/x86_64. +# So on Apple Silicon this runs under emulation and cross-compiles the x86_64 Windows +# .exe; the "base image platform" build warning on arm64 hosts is expected and benign. +FROM hdzero-dev + +# mingw-w64 cross toolchain. The package provides both win32 and -posix threading +# variants; windows-x64.cmake selects -posix so winpthread (pthread_*) is available. +RUN export DEBIAN_FRONTEND=noninteractive && apt-get update -qq && \ + apt-get install -y -qq gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 && \ + rm -rf /var/lib/apt/lists/* + +# Prebuilt SDL2 mingw dev tree (headers + import libs + SDL2.dll) staged where +# windows-x64.cmake looks: SDL2_DIR = /opt/sdl2-mingw/x86_64-w64-mingw32/lib/cmake/SDL2. +ARG SDL2_VERSION=2.30.9 +RUN cd /tmp && \ + wget -q "https://github.com/libsdl-org/SDL/releases/download/release-${SDL2_VERSION}/SDL2-devel-${SDL2_VERSION}-mingw.tar.gz" && \ + tar xf "SDL2-devel-${SDL2_VERSION}-mingw.tar.gz" && \ + mkdir -p /opt/sdl2-mingw && \ + cp -r "SDL2-${SDL2_VERSION}/x86_64-w64-mingw32" /opt/sdl2-mingw/ && \ + rm -rf /tmp/SDL2* diff --git a/.devcontainer/build.sh b/.devcontainer/build.sh new file mode 100755 index 00000000..160d80ca --- /dev/null +++ b/.devcontainer/build.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# One reproducible entry point for every HDZero emulator build target. +# EVERY target is compiled INSIDE a devcontainer image; each RUNS natively on its OS. +# +# build.sh prepare-mac stage Apple's SDK from your Xcode into .devcontainer (one-time) +# build.sh images build all three images (hdzero-dev, hdzero-dev-win, hdzero-dev-mac) +# build.sh linux Linux emulator -> build_emu_linux/HDZGOGGLE (image: hdzero-dev) +# build.sh windows Windows emulator -> build_win_x64/HDZGOGGLE.exe (image: hdzero-dev-win, mingw) +# build.sh mac macOS emulator -> build_xmac/HDZGOGGLE (image: hdzero-dev-mac, LLVM cross) +# build.sh all images + linux + windows + mac +# +# Nothing is compiled on the host. The ONLY host-side step is prepare-mac, which +# TARs Apple's (non-redistributable) SDK out of your local Xcode so the mac image +# can COPY it in — SDK staging, not app compilation. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DC="$REPO_ROOT/.devcontainer" +DRUN() { docker run --rm -v "$REPO_ROOT:/src" -w /src "$@"; } + +# Make sure the target image is present AND current before running it. This goes +# through docker's layer cache, so it's near-instant when nothing changed, but it +# rebuilds when the Dockerfile or its base image changes. That matters: a plain +# "build only if missing" check would leave a *stale* image in place after e.g. the +# glibc-floor base pin changes, silently building a binary against the wrong glibc. +# (It also avoids the confusing "pull access denied" that a bare `docker run` gives +# when the image was never built locally, since we build rather than pull.) +ensure_image() { + local img="$1" dockerfile="$2" + echo ">> ensuring image '$img' is current (${dockerfile})" + docker build -f "$DC/$dockerfile" -t "$img" "$DC" +} + +prepare_mac() { + local sdk="/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk" + [ -d "$sdk" ] || sdk="$(xcrun --show-sdk-path 2>/dev/null || true)" + [ -n "$sdk" ] && [ -d "$sdk" ] || { echo "!! no macOS SDK found — install Xcode / run 'xcode-select --install'"; exit 1; } + echo ">> staging macOS SDK: $sdk" + tar -cf "$DC/MacOSX.sdk.tar" -C "$(dirname "$sdk")" "$(basename "$sdk")" + echo ">> wrote $DC/MacOSX.sdk.tar (gitignored)" +} + +images() { + docker build -f "$DC/Dockerfile" -t hdzero-dev "$DC" + docker build -f "$DC/Dockerfile.windows" -t hdzero-dev-win "$DC" + [ -f "$DC/MacOSX.sdk.tar" ] || prepare_mac + docker build -f "$DC/Dockerfile.macos" -t hdzero-dev-mac "$DC" +} + +linux() { + ensure_image hdzero-dev Dockerfile + DRUN hdzero-dev bash -lc ' + cmake -S . -B build_emu_linux -DEMULATOR_BUILD=ON -DHDZ_GOGGLE=ON -DCMAKE_BUILD_TYPE=Debug && + make -C build_emu_linux -j"$(nproc)"' +} + +windows() { + ensure_image hdzero-dev Dockerfile # base for the mingw image + ensure_image hdzero-dev-win Dockerfile.windows + DRUN hdzero-dev-win bash -lc ' + cmake -S . -B build_win_x64 -DEMULATOR_BUILD=ON -DHDZ_GOGGLE=ON -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_TOOLCHAIN_FILE=.devcontainer/windows-x64.cmake && + make -C build_win_x64 -j"$(nproc)"' +} + +mac() { + [ -f "$DC/MacOSX.sdk.tar" ] || prepare_mac + ensure_image hdzero-dev-mac Dockerfile.macos + DRUN hdzero-dev-mac bash -lc ' + cmake -S . -B build_xmac -DEMULATOR_BUILD=ON -DHDZ_GOGGLE=ON -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_TOOLCHAIN_FILE=.devcontainer/darwin-arm64.cmake && + make -C build_xmac -j"$(nproc)"' + echo ">> built build_xmac/HDZGOGGLE (arm64 Mach-O) — run natively with: BUILD_DIR=build_xmac ./run-native.sh" +} + +case "${1:-all}" in + prepare-mac) prepare_mac ;; + images) images ;; + linux) linux ;; + windows) windows ;; + mac) mac ;; + all) images; linux; windows; mac ;; + *) echo "usage: build.sh {prepare-mac|images|linux|windows|mac|all}"; exit 2 ;; +esac diff --git a/.devcontainer/darwin-arm64.cmake b/.devcontainer/darwin-arm64.cmake new file mode 100644 index 00000000..b3ff375e --- /dev/null +++ b/.devcontainer/darwin-arm64.cmake @@ -0,0 +1,52 @@ +# CMake cross-compile toolchain: Linux host -> arm64 macOS (Mach-O), using LLVM. +# clang retargets via triple; ld64.lld links Mach-O; SDK provides headers/.tbd stubs. +set(CMAKE_SYSTEM_NAME Darwin) +set(CMAKE_SYSTEM_PROCESSOR arm64) + +# macOS SDK sysroot (extracted into the image; overridable via env) +if(DEFINED ENV{MACOS_SDK}) + set(SDK "$ENV{MACOS_SDK}") +else() + set(SDK "/opt/sdk/MacOSX.sdk") +endif() + +# Minimum macOS deployment target (keep conservative for wide compatibility) +set(MACOS_MIN "12.0") +set(TARGET_TRIPLE "arm64-apple-macos${MACOS_MIN}") + +set(CMAKE_C_COMPILER clang) +set(CMAKE_CXX_COMPILER clang++) +set(CMAKE_ASM_COMPILER clang) +set(CMAKE_OBJC_COMPILER clang) +set(CMAKE_OBJCXX_COMPILER clang++) +set(CMAKE_C_COMPILER_TARGET "${TARGET_TRIPLE}") +set(CMAKE_CXX_COMPILER_TARGET "${TARGET_TRIPLE}") +set(CMAKE_ASM_COMPILER_TARGET "${TARGET_TRIPLE}") +set(CMAKE_OBJC_COMPILER_TARGET "${TARGET_TRIPLE}") +set(CMAKE_OBJCXX_COMPILER_TARGET "${TARGET_TRIPLE}") + +# Mach-O tooling from LLVM +set(CMAKE_AR llvm-ar) +set(CMAKE_RANLIB llvm-ranlib) +set(CMAKE_NM llvm-nm) +set(CMAKE_STRIP llvm-strip) + +set(CMAKE_OSX_SYSROOT "${SDK}") +set(CMAKE_OSX_ARCHITECTURES "arm64") +set(CMAKE_OSX_DEPLOYMENT_TARGET "${MACOS_MIN}") + +# Use LLVM's Mach-O linker (clang selects ld64.lld for Darwin targets with -fuse-ld=lld) +# clang derives -platform_version from the -macos triple automatically. +add_compile_options("-isysroot" "${SDK}") +add_link_options("-fuse-ld=lld" "-isysroot" "${SDK}") + +set(CMAKE_FIND_ROOT_PATH "${SDK}") +# Cross-built macOS deps (e.g. SDL2) live outside the SDK sysroot; add them so +# find_package/find_library (rooted by FIND_ROOT_PATH) can locate them. +if(DEFINED ENV{SDL2_MACOS}) + list(APPEND CMAKE_FIND_ROOT_PATH "$ENV{SDL2_MACOS}") +endif() +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/.devcontainer/emu.sh b/.devcontainer/emu.sh new file mode 100755 index 00000000..78fbd88a --- /dev/null +++ b/.devcontainer/emu.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# HDZero SDL emulator runner. Works both inside the VS Code devcontainer and under +# plain `docker run` — the repo root is derived from this script's own location, so +# it does not matter where the workspace is mounted (/workspaces/... or /src). +# +# emu.sh build configure + compile the emulator (build_emu_linux) +# emu.sh shot [name] [warmup] [keys] headless run -> screenshot emu-out/.png +# emu.sh all [name] [warmup] [keys] build, then shot +# +# The container's job is BUILD + HEADLESS SCREENSHOTS (great for CI / docs / PR +# previews). Interactive use belongs to the native app: ../run-native.sh (macOS/Linux). +# +# keys: space-separated xdotool keys ("Up Up space"); prefix a token with "hold:" +# for a long press (e.g. "hold:space" = wheel long-press, which enters the FPV view). +set -euo pipefail + +REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +BUILD_DIR="$REPO_ROOT/build_emu_linux" +OUT_DIR="${OUT_DIR:-$REPO_ROOT/emu-out}" +# All run-time logs go under the gitignored tmp/ tree (repo .gitignore: /tmp), +# so they never clutter the repo root or emu-out. +LOG_DIR="${LOG_DIR:-$REPO_ROOT/tmp/logs}" +DISPLAY_NUM=:99 +SCREEN=1920x1080x24 +mkdir -p "$OUT_DIR" "$LOG_DIR" + +do_build() { + cmake -S "$REPO_ROOT" -B "$BUILD_DIR" \ + -DEMULATOR_BUILD=ON -DCMAKE_BUILD_TYPE=Debug \ + -DHDZ_GOGGLE=ON -DHDZ_BOXPRO=OFF -DHDZ_GOGGLE2=OFF + make -C "$BUILD_DIR" -j"$(nproc)" +} + +# Stage the device runtime paths so the app finds its data where it expects on real +# hardware: +# /mnt/app -> built-in resources (OSD fonts, language) symlinked from the repo. +# Without this, load_fc_osd_font() can't find BTFL_*.bmp and the OSD +# (FC glyphs + every bmp element) renders blank. +# /mnt/extsd -> the SD card: a SAFE, gitignored copy at $HDZ_SDCARD (default +# repo/sdcard). The physical card is never exposed to the emulator. +# HDZ_MEDIA_DIR drives the playback page; HDZ_VIDEO (optional) is the fake FPV feed. +stage_runtime() { + : "${HDZ_SDCARD:=$REPO_ROOT/sdcard}" + # Point the app's configurable roots (paths_init) at the repo's built-in + # resources and the safe sandbox copy. No /mnt symlinks; the goggle build keeps + # its compiled-in /mnt/app + /mnt/extsd defaults. + export HDZ_APP_ROOT="${HDZ_APP_ROOT:-$REPO_ROOT/mkapp/app}" + export HDZ_EXTSD_ROOT="${HDZ_EXTSD_ROOT:-$HDZ_SDCARD}" + export HDZ_MEDIA_DIR="${HDZ_MEDIA_DIR:-$HDZ_SDCARD/DCIM/100HDZRO}" + export HDZ_VIDEO="${HDZ_VIDEO:-$HDZ_SDCARD/loop.mpg}" +} + +do_shot() { + local name="${1:-frame}" warmup="${2:-4}" keys="${3:-}" + export DISPLAY="$DISPLAY_NUM" + stage_runtime + Xvfb "$DISPLAY_NUM" -screen 0 "$SCREEN" >"$LOG_DIR/xvfb.log" 2>&1 & + local xvfb_pid=$! + sleep 1 + ( cd "$BUILD_DIR" && ./HDZGOGGLE ) >"$LOG_DIR/app.log" 2>&1 & + local app_pid=$! + sleep "$warmup" + if [ -n "$keys" ]; then + for k in $keys; do + case "$k" in + hold:*) kk="${k#hold:}"; xdotool keydown "$kk"; sleep 0.8; xdotool keyup "$kk" ;; + *) xdotool key "$k" ;; + esac + sleep 0.4 + done + sleep 1 + fi + import -window root -display "$DISPLAY_NUM" "$OUT_DIR/${name}.png" 2>"$LOG_DIR/import.log" \ + || { xwd -root -display "$DISPLAY_NUM" | convert xwd:- "$OUT_DIR/${name}.png"; } + kill "$app_pid" 2>/dev/null || true + kill "$xvfb_pid" 2>/dev/null || true + echo "wrote $OUT_DIR/${name}.png" + echo "--- app.log (tail) ---"; tail -n 15 "$LOG_DIR/app.log" || true +} + +cmd="${1:-shot}" +case "$cmd" in + build) do_build ;; + shot) shift || true; do_shot "$@" ;; + all) do_build; shift || true; do_shot "$@" ;; + *) echo "usage: emu.sh {build|shot|all} [name] [warmup] [keys]"; exit 2 ;; +esac diff --git a/.devcontainer/macos/Info.plist b/.devcontainer/macos/Info.plist new file mode 100644 index 00000000..1db56c08 --- /dev/null +++ b/.devcontainer/macos/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleName + HDZGOGGLE + CFBundleDisplayName + HDZero Goggle Emulator + CFBundleIdentifier + com.hdzero.goggle.emulator + CFBundleExecutable + HDZGOGGLE + CFBundlePackageType + APPL + CFBundleVersion + 1.0 + CFBundleShortVersionString + 1.0 + LSMinimumSystemVersion + 12.0 + NSHighResolutionCapable + + NSPrincipalClass + NSApplication + + diff --git a/.devcontainer/windows-x64.cmake b/.devcontainer/windows-x64.cmake new file mode 100644 index 00000000..785da9c6 --- /dev/null +++ b/.devcontainer/windows-x64.cmake @@ -0,0 +1,28 @@ +# Cross-compile the SDL emulator to Windows x86_64 (amd64) with mingw-w64, from +# the hdzero-dev-win image. Run/test the resulting .exe on Windows — or on a +# Windows-on-ARM VM via the built-in x64 emulation. +# +# The fake FPV video feed works here too: it decodes a looping MPEG-1 clip with +# pl_mpeg (single-header, portable), so there is no ffmpeg to link on any platform. +# +# cmake -S . -B build_win_x64 -DEMULATOR_BUILD=ON -DHDZ_GOGGLE=ON \ +# -DCMAKE_TOOLCHAIN_FILE=.devcontainer/windows-x64.cmake + +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR x86_64) + +set(TOOLCHAIN_PREFIX x86_64-w64-mingw32) +# The -posix variants use the POSIX threading model, which provides winpthread +# (pthread_*). The default (win32 model) lacks pthreads and fails to link. +set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc-posix) +set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++-posix) +set(CMAKE_RC_COMPILER ${TOOLCHAIN_PREFIX}-windres) + +# SDL2 mingw dev tree, staged into the image at /opt/sdl2-mingw. +set(SDL2_DIR /opt/sdl2-mingw/${TOOLCHAIN_PREFIX}/lib/cmake/SDL2) + +set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX} /opt/sdl2-mingw/${TOOLCHAIN_PREFIX}) +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/.github/workflows/build_emulator.yml b/.github/workflows/build_emulator.yml new file mode 100644 index 00000000..91ade4ea --- /dev/null +++ b/.github/workflows/build_emulator.yml @@ -0,0 +1,148 @@ +name: Build Emulator + +# Builds the SDL emulator for all three desktop targets. On every push / PR the +# three build jobs run as a regression gate (with a headless Linux screenshot). +# A GitHub Release with downloadable binaries is produced ONLY when a semver tag +# (v*) is pushed -- ordinary pushes never release. +# +# Linux + Windows build inside the devcontainer images (same Dockerfiles as local +# dev); macOS builds natively on a mac runner (its cross image needs Apple's +# non-redistributable SDK, which CI can't stage). The goggle firmware is covered +# by the existing build_and_release_* workflows. + +on: + push: + branches: ['**'] + tags: ['v*'] + pull_request: + +permissions: + contents: write # the release job needs to create releases + upload assets + +jobs: + linux: + name: Linux + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Build devcontainer image + run: docker build -f .devcontainer/Dockerfile -t hdzero-dev .devcontainer + + - name: Build Linux emulator + run: bash .devcontainer/build.sh linux + + # Regression gate for the "build in a container, run natively" promise: the + # binary's highest required GLIBC symbol must not exceed the base image's glibc. + # This is what caught us before -- an unpinned base drifted to glibc 2.43 and the + # binary stopped running on older hosts. glibc can't be installed side-by-side to + # test against, so we assert the symbol floor statically instead (fast + exact). + - name: Assert glibc floor (no ABI drift) + run: | + docker run --rm -v "$PWD:/src" -w /src hdzero-dev bash -c ' + floor=2.35 # .devcontainer/Dockerfile base (ubuntu-22.04); runs on this glibc and newer + need=$(objdump -T build_emu_linux/HDZGOGGLE | grep -oE "GLIBC_[0-9.]+" | sort -V | tail -1) + need=${need#GLIBC_} + echo "binary max GLIBC: ${need:-none} | floor: $floor" + if [ -n "$need" ] && [ "$(printf "%s\n%s\n" "$need" "$floor" | sort -V | tail -1)" != "$floor" ]; then + echo "::error::binary needs GLIBC_$need > floor $floor -- devcontainer base glibc drifted; pin .devcontainer/Dockerfile to an older base." + exit 1 + fi + ' + + - name: Headless screenshot (main menu) + run: | + docker run --rm -v "$PWD:/src" -w /src -e REPO_ROOT=/src hdzero-dev \ + bash .devcontainer/emu.sh shot menu 6 "" + + - name: Strip + stage binary + run: | + docker run --rm -v "$PWD:/src" -w /src hdzero-dev strip build_emu_linux/HDZGOGGLE + mkdir -p dist + cp build_emu_linux/HDZGOGGLE dist/hdzero-emulator-linux-x86_64 + + - uses: actions/upload-artifact@v4 + with: + name: emulator-linux + path: | + dist/hdzero-emulator-linux-x86_64 + emu-out/menu.png + + windows: + name: Windows + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Build devcontainer images + run: | + docker build -f .devcontainer/Dockerfile -t hdzero-dev .devcontainer + docker build -f .devcontainer/Dockerfile.windows -t hdzero-dev-win .devcontainer + + - name: Cross-compile Windows .exe + run: bash .devcontainer/build.sh windows + + - name: Strip + stage binary + run: | + docker run --rm -v "$PWD:/src" -w /src hdzero-dev-win x86_64-w64-mingw32-strip build_win_x64/HDZGOGGLE.exe + mkdir -p dist + cp build_win_x64/HDZGOGGLE.exe dist/hdzero-emulator-windows-x86_64.exe + + - uses: actions/upload-artifact@v4 + with: + name: emulator-windows + path: dist/hdzero-emulator-windows-x86_64.exe + + macos: + name: macOS + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Install SDL2 + run: brew install sdl2 + + - name: Build macOS emulator + run: | + cmake -S . -B build_emu -DEMULATOR_BUILD=ON -DHDZ_GOGGLE=ON -DCMAKE_BUILD_TYPE=Debug + make -C build_emu -j"$(sysctl -n hw.ncpu)" + + - name: Strip + stage binary + run: | + strip build_emu/HDZGOGGLE + mkdir -p dist + cp build_emu/HDZGOGGLE dist/hdzero-emulator-macos-arm64 + + - uses: actions/upload-artifact@v4 + with: + name: emulator-macos + path: dist/hdzero-emulator-macos-arm64 + + release: + name: Release + needs: [linux, windows, macos] + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Collect release binaries + run: | + mkdir -p release + find artifacts -type f -name 'hdzero-emulator-*' -exec cp {} release/ \; + echo "release assets:"; ls -la release + + - name: Publish release + uses: softprops/action-gh-release@v2 + with: + files: release/* + draft: true + generate_release_notes: true diff --git a/.gitignore b/.gitignore index 2d5d995e..77de1a44 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,15 @@ utilities/font/out # nix related result result-* +sdcard/ +emu-out/ +build_emu_linux/ + +# macOS SDK staged from local Xcode (Apple SDK not redistributable) + cross build dirs +.devcontainer/MacOSX.sdk.tar +build_xmac/ +build_win_x64/ +build_win_repro/ + +# runtime-written config (the app creates this in its working dir) +/setting.ini diff --git a/CMakeLists.txt b/CMakeLists.txt index 46b180db..6f0c6f4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,15 @@ project(HDZGOGGLE VERSION 1.1) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -set(COMMON_COMPILER_FLAGS "-Wno-unused-function -Wno-unused-variable -Wno-deprecated-declarations -ffunction-sections -fdata-sections -Wl,-gc-sections -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64") +set(COMMON_COMPILER_FLAGS "-Wno-unused-function -Wno-unused-variable -Wno-deprecated-declarations -ffunction-sections -fdata-sections -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64") +# Dead-strip unused sections: macOS ld64 uses -dead_strip, GNU ld uses -gc-sections. +if(APPLE) + # Also disable _FORTIFY_SOURCE: macOS clang's fortified __*_chk wrappers trap + # (SIGTRAP) on some firmware snprintf calls that are benign on Linux. + set(COMMON_COMPILER_FLAGS "${COMMON_COMPILER_FLAGS} -Wl,-dead_strip -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0") +else() + set(COMMON_COMPILER_FLAGS "${COMMON_COMPILER_FLAGS} -Wl,-gc-sections") +endif() option(EMULATOR_BUILD "emulate using SDL on host" OFF) if(NOT EMULATOR_BUILD) @@ -42,15 +50,32 @@ set(OS_APP_PATH ${CMAKE_CURRENT_BINARY_DIR}/mkapp/app/app) set(RECORD_APP_PATH ${CMAKE_CURRENT_BINARY_DIR}/mkapp/app/app/record) # common build settings -set(STANDARD_LIBRARIES - c - crypt - dl - m - pthread - rt - stdc++ -) +if(APPLE) + # On macOS, crypt/dl/rt live in libSystem; C++ runtime is libc++. + set(STANDARD_LIBRARIES + c + m + pthread + c++ + ) +elseif(WIN32) + # mingw (Windows): crypt/dl/rt don't exist; pthread is winpthread (posix model). + set(STANDARD_LIBRARIES + m + pthread + stdc++ + ) +else() + set(STANDARD_LIBRARIES + c + crypt + dl + m + pthread + rt + stdc++ + ) +endif() add_definitions( -DAWCHIP=AW_V5 @@ -88,6 +113,24 @@ file(GLOB SRC_FILES_UTIL "src/util/*.c" "src/util/*.h") file(GLOB SRC_FILES_PLAYER "src/player/*.c" "src/player/*.h") file(GLOB SRC_FILES_EMULATOR "src/emulator/*.c" "src/emulator/*.h") file(GLOB SRC_FILES_LANG "src/lang/*.c" "src/lang/*.h") +file(GLOB SRC_FILES_PLATFORM "src/platform/*.c" "src/platform/*.h") + +# Per-target driver swaps: the emulator provides its own implementations of certain +# hardware drivers (src/emulator/*), so drop the goggle versions to avoid duplicate +# symbols. Keeps the hardware drivers free of any #ifdef EMULATOR_BUILD. +# uart.c -> emulator/uart_emu.c (socketpair UART) +# i2c.c -> emulator/i2c_emu.c (no-op bus) +# rtc_hw.c -> emulator/rtc_hw_emu.c (in-memory clock; rtc.c shared) +# fbtools.c -> (none) (SDL renders instead; not referenced) +# input_evdev.c-> emulator/input_device_emu.c (SDL keyboard; core/input_device.c shared) +if(EMULATOR_BUILD) + list(REMOVE_ITEM SRC_FILES_DRIVER + "${CMAKE_CURRENT_SOURCE_DIR}/src/driver/uart.c" + "${CMAKE_CURRENT_SOURCE_DIR}/src/driver/i2c.c" + "${CMAKE_CURRENT_SOURCE_DIR}/src/driver/rtc_hw.c" + "${CMAKE_CURRENT_SOURCE_DIR}/src/driver/fbtools.c" + "${CMAKE_CURRENT_SOURCE_DIR}/src/driver/input_evdev.c") +endif() if(HDZ_GOGGLE) file(GLOB SRC_FILES_IMAGE "src/image/goggle/*.c" "src/image/goggle/*.h") @@ -106,6 +149,7 @@ set(SRC_FILES ${SRC_FILES_BMI} ${SRC_FILES_UTIL} ${SRC_FILES_LANG} + ${SRC_FILES_PLATFORM} ) if(EMULATOR_BUILD) @@ -138,9 +182,50 @@ target_link_libraries(${PROJECT_NAME} PRIVATE ) if(EMULATOR_BUILD) + # SDL2_INCLUDE_DIRS carries both /include and /include/SDL2, so + # the code's `#include ` resolves off Homebrew/MSYS too (on Linux the + # system /usr/include makes it implicit; the imported target only exposes the + # deep dir, hence the explicit dirs here). + target_include_directories(${PROJECT_NAME} PRIVATE ${SDL2_INCLUDE_DIRS}) target_link_libraries(${PROJECT_NAME} PRIVATE ${SDL2_LIBRARIES}) endif() +# The emulator's mock video device decodes a small MPEG-1 clip with pl_mpeg +# (single-header, in lib/). No ffmpeg is linked into any app binary on any OS. +if(EMULATOR_BUILD) + target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/lib) + + # Build step: turn a test video into the looping MPEG-1 clip the mock reads. + # Opt-in + graceful -- only runs if EMU_TEST_VIDEO points at a real file and + # ffmpeg is on the build host; otherwise the emulator just shows a gray FPV. + set(EMU_TEST_VIDEO "" CACHE FILEPATH "Test video to generate the emulator's loop.mpg from") + find_program(FFMPEG_BIN ffmpeg) + if(EMU_TEST_VIDEO AND EXISTS "${EMU_TEST_VIDEO}" AND FFMPEG_BIN) + add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/loop.mpg + COMMAND ${FFMPEG_BIN} -y -i "${EMU_TEST_VIDEO}" -t 5 -an + -c:v mpeg1video -b:v 6M -s 1928x1088 -r 30 + ${CMAKE_BINARY_DIR}/loop.mpg + DEPENDS "${EMU_TEST_VIDEO}" + COMMENT "Generating emulator FPV loop.mpg from ${EMU_TEST_VIDEO}") + add_custom_target(emu_video ALL DEPENDS ${CMAKE_BINARY_DIR}/loop.mpg) + endif() +endif() + +# Per-platform POSIX compatibility headers for the desktop (SDL) emulator. Each +# target OS gets an include dir of shims for the headers it lacks, so the dormant +# device layer (serial/mount/ioctl/sockets) compiles against inert stubs. Linux +# uses the real system headers, so it needs no compat dir. +if(EMULATOR_BUILD AND WIN32) + target_include_directories(${PROJECT_NAME} BEFORE PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/platform/compat/windows) + target_sources(${PROJECT_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/platform/compat/windows/posix_compat.c) +elseif(EMULATOR_BUILD AND APPLE) + target_include_directories(${PROJECT_NAME} BEFORE PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/platform/compat/macos) +endif() + if(NOT EMULATOR_BUILD) # record application file(GLOB SRC_FILES_RECORD "src/record/*.c" "src/record/*.h") diff --git a/docs/emulator.md b/docs/emulator.md new file mode 100644 index 00000000..3cb9d0fe --- /dev/null +++ b/docs/emulator.md @@ -0,0 +1,93 @@ +# HDZero Goggle Emulator + +A cross-platform desktop build of the goggle UI, for developing and previewing the +LVGL interface without hardware. It runs the **real** application code — the same +`main()`, menu system, OSD, and page logic that ships on the goggle — rendered +through SDL2 instead of the framebuffer, with the hardware peripherals replaced by +compiled-in mocks. + +Runs on **Linux, macOS, and Windows**. + + + +## Design + +Two rules keep the emulator honest and keep the firmware untouched: + +- **Build in a container, run natively.** Every target — including macOS — is + compiled inside a devcontainer image (reproducible, no host toolchain sprawl); + the resulting binary runs natively on its OS. The macOS image cross-compiles + Mach-O with LLVM/clang; since Apple's SDK isn't redistributable you stage it once + from your own Xcode (`build.sh prepare-mac`), but the compile still runs in the + container. (CI is the one place macOS builds natively — on a `macos-latest` + runner — because CI can't stage that SDK.) +- **Zero cost on the goggle.** All emulator code is behind `EMULATOR_BUILD` or in + `src/emulator/*`, which CMake only compiles for the emulator. The goggle firmware + binary contains none of it. Hardware drivers that differ (UART, I2C, RTC, the + framebuffer) are split into per-target files selected by CMake, so the goggle + driver stays free of emulator `#ifdef`s. + +The peripherals are mocked at their hardware seam, so the app is unchanged: + +| Hardware | Emulator stand-in | +|----------|-------------------| +| Display engine / framebuffer | SDL2 window + software compositor | +| DM5680 video receiver | `video_emu` — decodes a looping MPEG-1 clip (pl_mpeg) behind the OSD | +| DM5680 serial link | socketpair-backed UART + a mock that answers version/RSSI | +| I2C bus, RTC | no-op bus, in-memory clock | +| Device shell commands (`system_exec`) | logged and skipped (never touches the host) | + +## Build + +All builds go through one entry point, [`.devcontainer/build.sh`](../.devcontainer/build.sh): + +```bash +.devcontainer/build.sh images # build the devcontainer images (one-time) +.devcontainer/build.sh linux # -> build_emu_linux/HDZGOGGLE +.devcontainer/build.sh windows # -> build_win_x64/HDZGOGGLE.exe (mingw cross) +.devcontainer/build.sh mac # -> build_xmac/HDZGOGGLE (macOS, LLVM cross) +``` + +macOS needs the SDK staged from your Xcode once: `.devcontainer/build.sh prepare-mac`. + +## Run + +On the host (macOS/Linux): + +```bash +./run-native.sh # launches the built binary in an SDL window +``` + +In the window, **long-press the wheel (SPACE)** to enter the FPV view. + +### Fake FPV video feed + +Point `HDZ_VIDEO` at an MPEG-1 file and the mock receiver plays it behind the OSD +(defaults to `sdcard/loop.mpg`). Generate one from any clip: + +```bash +ffmpeg -i input.mp4 -t 5 -an -c:v mpeg1video -b:v 6M -s 1928x1088 -r 30 sdcard/loop.mpg +``` + +`ffmpeg` is only used offline to make the clip — no ffmpeg is linked into any app +binary; the emulator decodes with the vendored single-header `pl_mpeg`. + +### Resource paths + +The app's filesystem roots are configurable so the emulator finds resources on the +host (the goggle keeps its compiled-in `/mnt/app`, `/mnt/extsd` defaults): + +- `HDZ_APP_ROOT` — built-in resources (OSD fonts, language) +- `HDZ_EXTSD_ROOT` — the SD card +- `HDZ_CONFIG` — an ini file with `[paths] app_root`/`extsd_root` + +`run-native.sh` sets sensible defaults. + +## CI & releases + +[`.github/workflows/build_emulator.yml`](../.github/workflows/build_emulator.yml) +builds all three targets on every push / PR (with a headless Linux screenshot as a +regression check) — Linux and Windows in the devcontainer images, macOS natively on +a `macos-latest` runner (CI can't stage the Apple SDK the cross image needs). +Pushing a **semver tag** (`v*`) additionally publishes a GitHub Release with the +per-platform binaries attached. diff --git a/lib/log/src/log.c b/lib/log/src/log.c index 4db33c22..e0f4fda0 100644 --- a/lib/log/src/log.c +++ b/lib/log/src/log.c @@ -14,7 +14,7 @@ static int fd_log_file = -1; static char offline_buffer[1 * 1024 * 1024]; static size_t offline_offset = 0; -static atomic_bool log_mutex_init = ATOMIC_VAR_INIT(false); +static atomic_bool log_mutex_init = false; // was ATOMIC_VAR_INIT(false) — macro removed in C23 / modern GCC static pthread_mutex_t log_mutex; static const char *log_level_names[] = { diff --git a/lib/lvgl/lv_conf.h b/lib/lvgl/lv_conf.h index c608919b..e8a6d4f2 100644 --- a/lib/lvgl/lv_conf.h +++ b/lib/lvgl/lv_conf.h @@ -47,7 +47,14 @@ *=========================*/ /*1: use custom malloc/free, 0: use the built-in `lv_mem_alloc()` and `lv_mem_free()`*/ +#ifdef EMULATOR_BUILD +/* Desktop emulator: malloc-based memory. The 128MB static pool below is fine as + .bss on the goggle (ELF, unstored), but mingw materialises it into the PE .data + section -> a ~170MB .exe. malloc avoids the giant static entirely. */ +#define LV_MEM_CUSTOM 1 +#else #define LV_MEM_CUSTOM 0 +#endif #if LV_MEM_CUSTOM == 0 /*Size of the memory available for `lv_mem_alloc()` in bytes (>= 2kB)*/ #define LV_MEM_SIZE (128U * 1024U * 1024U) /*[bytes]*/ diff --git a/lib/pl_mpeg/pl_mpeg.h b/lib/pl_mpeg/pl_mpeg.h new file mode 100644 index 00000000..b42f53f8 --- /dev/null +++ b/lib/pl_mpeg/pl_mpeg.h @@ -0,0 +1,4439 @@ +/* +PL_MPEG - MPEG1 Video decoder, MP2 Audio decoder, MPEG-PS demuxer +SPDX-License-Identifier: MIT + +Dominic Szablewski - https://phoboslab.org + + +-- Synopsis + +// Define `PL_MPEG_IMPLEMENTATION` in *one* C/C++ file before including this +// library to create the implementation. + +#define PL_MPEG_IMPLEMENTATION +#include "plmpeg.h" + +// This function gets called for each decoded video frame +void my_video_callback(plm_t *plm, plm_frame_t *frame, void *user) { + // Do something with frame->y.data, frame->cr.data, frame->cb.data +} + +// This function gets called for each decoded audio frame +void my_audio_callback(plm_t *plm, plm_samples_t *frame, void *user) { + // Do something with samples->interleaved +} + +// Load a .mpg (MPEG Program Stream) file +plm_t *plm = plm_create_with_filename("some-file.mpg"); + +// Install the video & audio decode callbacks +plm_set_video_decode_callback(plm, my_video_callback, my_data); +plm_set_audio_decode_callback(plm, my_audio_callback, my_data); + + +// Decode +do { + plm_decode(plm, time_since_last_call); +} while (!plm_has_ended(plm)); + +// All done +plm_destroy(plm); + + + +-- Documentation + +This library provides several interfaces to load, demux and decode MPEG video +and audio data. A high-level API combines the demuxer, video & audio decoders +in an easy to use wrapper. + +Lower-level APIs for accessing the demuxer, video decoder and audio decoder, +as well as providing different data sources are also available. + +Interfaces are written in an object oriented style, meaning you create object +instances via various different constructor functions (plm_*create()), +do some work on them and later dispose them via plm_*destroy(). + +plm_* ......... the high-level interface, combining demuxer and decoders +plm_buffer_* .. the data source used by all interfaces +plm_demux_* ... the MPEG-PS demuxer +plm_video_* ... the MPEG1 Video ("mpeg1") decoder +plm_audio_* ... the MPEG1 Audio Layer II ("mp2") decoder + + +With the high-level interface you have two options to decode video & audio: + + 1. Use plm_decode() and just hand over the delta time since the last call. + It will decode everything needed and call your callbacks (specified through + plm_set_{video|audio}_decode_callback()) any number of times. + + 2. Use plm_decode_video() and plm_decode_audio() to decode exactly one + frame of video or audio data at a time. How you handle the synchronization + of both streams is up to you. + +If you only want to decode video *or* audio through these functions, you should +disable the other stream (plm_set_{video|audio}_enabled(FALSE)) + +Video data is decoded into a struct with all 3 planes (Y, Cr, Cb) stored in +separate buffers. You can either convert this to RGB on the CPU (slow) via the +plm_frame_to_rgb() function or do it on the GPU with the following matrix: + +mat4 bt601 = mat4( + 1.16438, 0.00000, 1.59603, -0.87079, + 1.16438, -0.39176, -0.81297, 0.52959, + 1.16438, 2.01723, 0.00000, -1.08139, + 0, 0, 0, 1 +); +gl_FragColor = vec4(y, cb, cr, 1.0) * bt601; + +Audio data is decoded into a struct with either one single float array with the +samples for the left and right channel interleaved, or if the +PLM_AUDIO_SEPARATE_CHANNELS is defined *before* including this library, into +two separate float arrays - one for each channel. + + +Data can be supplied to the high level interface, the demuxer and the decoders +in three different ways: + + 1. Using plm_create_from_filename() or with a file handle with + plm_create_from_file(). + + 2. Using plm_create_with_memory() and supplying a pointer to memory that + contains the whole file. + + 3. Using plm_create_with_buffer(), supplying your own plm_buffer_t instance and + periodically writing to this buffer. + +When using your own plm_buffer_t instance, you can fill this buffer using +plm_buffer_write(). You can either monitor plm_buffer_get_remaining() and push +data when appropriate, or install a callback on the buffer with +plm_buffer_set_load_callback() that gets called whenever the buffer needs more +data. + +A buffer created with plm_buffer_create_with_capacity() is treated as a ring +buffer, meaning that data that has already been read, will be discarded. In +contrast, a buffer created with plm_buffer_create_for_appending() will keep all +data written to it in memory. This enables seeking in the already loaded data. + + +There should be no need to use the lower level plm_demux_*, plm_video_* and +plm_audio_* functions, if all you want to do is read/decode an MPEG-PS file. +However, if you get raw mpeg1video data or raw mp2 audio data from a different +source, these functions can be used to decode the raw data directly. Similarly, +if you only want to analyze an MPEG-PS file or extract raw video or audio +packets from it, you can use the plm_demux_* functions. + + +This library uses malloc(), realloc() and free() to manage memory. Typically +all allocation happens up-front when creating the interface. However, the +default buffer size may be too small for certain inputs. In these cases plmpeg +will realloc() the buffer with a larger size whenever needed. You can configure +the default buffer size by defining PLM_BUFFER_DEFAULT_SIZE *before* +including this library. + +You can also define PLM_MALLOC, PLM_REALLOC and PLM_FREE to provide your own +memory management functions. + + +See below for detailed the API documentation. + +*/ + + +#ifndef PL_MPEG_H +#define PL_MPEG_H + +#include + + +#ifdef __cplusplus +extern "C" { +#endif + +// ----------------------------------------------------------------------------- +// Public Data Types + + +// Object types for the various interfaces + +typedef struct plm_t plm_t; +typedef struct plm_buffer_t plm_buffer_t; +typedef struct plm_demux_t plm_demux_t; +typedef struct plm_video_t plm_video_t; +typedef struct plm_audio_t plm_audio_t; + + +// Demuxed MPEG PS packet +// The type maps directly to the various MPEG-PES start codes. PTS is the +// presentation time stamp of the packet in seconds. Note that not all packets +// have a PTS value, indicated by PLM_PACKET_INVALID_TS. + +#define PLM_PACKET_INVALID_TS -1 + +typedef struct { + int type; + double pts; + size_t length; + uint8_t *data; +} plm_packet_t; + + +// Decoded Video Plane +// The byte length of the data is width * height. Note that different planes +// have different sizes: the Luma plane (Y) is double the size of each of +// the two Chroma planes (Cr, Cb) - i.e. 4 times the byte length. +// Also note that the size of the plane does *not* denote the size of the +// displayed frame. The sizes of planes are always rounded up to the nearest +// macroblock (16px). + +typedef struct { + unsigned int width; + unsigned int height; + uint8_t *data; +} plm_plane_t; + + +// Decoded Video Frame +// width and height denote the desired display size of the frame. This may be +// different from the internal size of the 3 planes. + +typedef struct { + double time; + unsigned int width; + unsigned int height; + plm_plane_t y; + plm_plane_t cr; + plm_plane_t cb; +} plm_frame_t; + + +// Callback function type for decoded video frames used by the high-level +// plm_* interface + +typedef void(*plm_video_decode_callback) + (plm_t *self, plm_frame_t *frame, void *user); + + +// Decoded Audio Samples +// Samples are stored as normalized (-1, 1) float either interleaved, or if +// PLM_AUDIO_SEPARATE_CHANNELS is defined, in two separate arrays. +// The `count` is always PLM_AUDIO_SAMPLES_PER_FRAME and just there for +// convenience. + +#define PLM_AUDIO_SAMPLES_PER_FRAME 1152 + +typedef struct { + double time; + unsigned int count; + #ifdef PLM_AUDIO_SEPARATE_CHANNELS + float left[PLM_AUDIO_SAMPLES_PER_FRAME]; + float right[PLM_AUDIO_SAMPLES_PER_FRAME]; + #else + float interleaved[PLM_AUDIO_SAMPLES_PER_FRAME * 2]; + #endif +} plm_samples_t; + + +// Callback function type for decoded audio samples used by the high-level +// plm_* interface + +typedef void(*plm_audio_decode_callback) + (plm_t *self, plm_samples_t *samples, void *user); + + +// Callback function for plm_buffer when it needs more data + +typedef void(*plm_buffer_load_callback)(plm_buffer_t *self, void *user); + + +// Callback function for plm_buffer when it needs to seek + +typedef void(*plm_buffer_seek_callback)(plm_buffer_t *self, size_t offset, void *user); + + +// Callback function for plm_buffer when it needs to tell the position + +typedef size_t(*plm_buffer_tell_callback)(plm_buffer_t *self, void *user); + + +// ----------------------------------------------------------------------------- +// plm_* public API +// High-Level API for loading/demuxing/decoding MPEG-PS data + +#ifndef PLM_NO_STDIO + +// Create a plmpeg instance with a filename. Returns NULL if the file could not +// be opened. + +plm_t *plm_create_with_filename(const char *filename); + + +// Create a plmpeg instance with a file handle. Pass TRUE to close_when_done to +// let plmpeg call fclose() on the handle when plm_destroy() is called. + +plm_t *plm_create_with_file(FILE *fh, int close_when_done); + +#endif // PLM_NO_STDIO + + +// Create a plmpeg instance with a pointer to memory as source. This assumes the +// whole file is in memory. The memory is not copied. Pass TRUE to +// free_when_done to let plmpeg call free() on the pointer when plm_destroy() +// is called. + +plm_t *plm_create_with_memory(uint8_t *bytes, size_t length, int free_when_done); + + +// Create a plmpeg instance with a plm_buffer as source. Pass TRUE to +// destroy_when_done to let plmpeg call plm_buffer_destroy() on the buffer when +// plm_destroy() is called. + +plm_t *plm_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done); + + +// Destroy a plmpeg instance and free all data. + +void plm_destroy(plm_t *self); + + +// Get whether we have headers on all available streams and we can report the +// number of video/audio streams, video dimensions, framerate and audio +// samplerate. +// This returns FALSE if the file is not an MPEG-PS file or - when not using a +// file as source - when not enough data is available yet. + +int plm_has_headers(plm_t *self); + + +// Probe the MPEG-PS data to find the actual number of video and audio streams +// within the buffer. For certain files (e.g. VideoCD) this can be more accurate +// than just reading the number of streams from the headers. +// This should only be used when the underlying plm_buffer is seekable, i.e. for +// files, fixed memory buffers or _for_appending buffers. If used with dynamic +// memory buffers it will skip decoding the probesize! +// The necessary probesize is dependent on the files you expect to read. Usually +// a few hundred KB should be enough to find all streams. +// Use plm_get_num_{audio|video}_streams() afterwards to get the number of +// streams in the file. +// Returns TRUE if any streams were found within the probesize. + +int plm_probe(plm_t *self, size_t probesize); + + +// Get or set whether video decoding is enabled. Default TRUE. + +int plm_get_video_enabled(plm_t *self); +void plm_set_video_enabled(plm_t *self, int enabled); + + +// Get the number of video streams (0--1) reported in the system header. + +int plm_get_num_video_streams(plm_t *self); + + +// Get the display width/height of the video stream. + +int plm_get_width(plm_t *self); +int plm_get_height(plm_t *self); +double plm_get_pixel_aspect_ratio(plm_t *self); + + +// Get the framerate of the video stream in frames per second. + +double plm_get_framerate(plm_t *self); + + +// Get or set whether audio decoding is enabled. Default TRUE. + +int plm_get_audio_enabled(plm_t *self); +void plm_set_audio_enabled(plm_t *self, int enabled); + + +// Get the number of audio streams (0--4) reported in the system header. + +int plm_get_num_audio_streams(plm_t *self); + + +// Set the desired audio stream (0--3). Default 0. + +void plm_set_audio_stream(plm_t *self, int stream_index); + + +// Get the samplerate of the audio stream in samples per second. + +int plm_get_samplerate(plm_t *self); + + +// Get or set the audio lead time in seconds - the time in which audio samples +// are decoded in advance (or behind) the video decode time. Typically this +// should be set to the duration of the buffer of the audio API that you use +// for output. E.g. for SDL2: (SDL_AudioSpec.samples / samplerate) + +double plm_get_audio_lead_time(plm_t *self); +void plm_set_audio_lead_time(plm_t *self, double lead_time); + + +// Get the current internal time in seconds. + +double plm_get_time(plm_t *self); + + +// Get the video duration of the underlying source in seconds. + +double plm_get_duration(plm_t *self); + + +// Rewind all buffers back to the beginning. + +void plm_rewind(plm_t *self); + + +// Get or set looping. Default FALSE. + +int plm_get_loop(plm_t *self); +void plm_set_loop(plm_t *self, int loop); + + +// Get whether the file has ended. If looping is enabled, this will always +// return FALSE. + +int plm_has_ended(plm_t *self); + + +// Set the callback for decoded video frames used with plm_decode(). If no +// callback is set, video data will be ignored and not be decoded. The *user +// Parameter will be passed to your callback. + +void plm_set_video_decode_callback(plm_t *self, plm_video_decode_callback fp, void *user); + + +// Set the callback for decoded audio samples used with plm_decode(). If no +// callback is set, audio data will be ignored and not be decoded. The *user +// Parameter will be passed to your callback. + +void plm_set_audio_decode_callback(plm_t *self, plm_audio_decode_callback fp, void *user); + + +// Advance the internal timer by seconds and decode video/audio up to this time. +// This will call the video_decode_callback and audio_decode_callback any number +// of times. A frame-skip is not implemented, i.e. everything up to current time +// will be decoded. + +void plm_decode(plm_t *self, double seconds); + + +// Decode and return one video frame. Returns NULL if no frame could be decoded +// (either because the source ended or data is corrupt). If you only want to +// decode video, you should disable audio via plm_set_audio_enabled(). +// The returned plm_frame_t is valid until the next call to plm_decode_video() +// or until plm_destroy() is called. + +plm_frame_t *plm_decode_video(plm_t *self); + + +// Decode and return one audio frame. Returns NULL if no frame could be decoded +// (either because the source ended or data is corrupt). If you only want to +// decode audio, you should disable video via plm_set_video_enabled(). +// The returned plm_samples_t is valid until the next call to plm_decode_audio() +// or until plm_destroy() is called. + +plm_samples_t *plm_decode_audio(plm_t *self); + + +// Seek to the specified time, clamped between 0 -- duration. This can only be +// used when the underlying plm_buffer is seekable, i.e. for files, fixed +// memory buffers or _for_appending buffers. +// If seek_exact is TRUE this will seek to the exact time, otherwise it will +// seek to the last intra frame just before the desired time. Exact seeking can +// be slow, because all frames up to the seeked one have to be decoded on top of +// the previous intra frame. +// If seeking succeeds, this function will call the video_decode_callback +// exactly once with the target frame. If audio is enabled, it will also call +// the audio_decode_callback any number of times, until the audio_lead_time is +// satisfied. +// Returns TRUE if seeking succeeded or FALSE if no frame could be found. + +int plm_seek(plm_t *self, double time, int seek_exact); + + +// Similar to plm_seek(), but will not call the video_decode_callback, +// audio_decode_callback or make any attempts to sync audio. +// Returns the found frame or NULL if no frame could be found. + +plm_frame_t *plm_seek_frame(plm_t *self, double time, int seek_exact); + + + +// ----------------------------------------------------------------------------- +// plm_buffer public API +// Provides the data source for all other plm_* interfaces + + +// The default size for buffers created from files or by the high-level API + +#ifndef PLM_BUFFER_DEFAULT_SIZE +#define PLM_BUFFER_DEFAULT_SIZE (128 * 1024) +#endif + +#ifndef PLM_NO_STDIO + +// Create a buffer instance with a filename. Returns NULL if the file could not +// be opened. + +plm_buffer_t *plm_buffer_create_with_filename(const char *filename); + + +// Create a buffer instance with a file handle. Pass TRUE to close_when_done +// to let plmpeg call fclose() on the handle when plm_destroy() is called. + +plm_buffer_t *plm_buffer_create_with_file(FILE *fh, int close_when_done); + +#endif // PLM_NO_STDIO + + +// Create a buffer instance with custom callbacks for loading, seeking and +// telling the position. This behaves like a file handle, but with user-defined +// callbacks, useful for file handles that don't use the standard FILE API. +// Setting the length and closing/freeing has to be done manually. + +plm_buffer_t *plm_buffer_create_with_callbacks( + plm_buffer_load_callback load_callback, + plm_buffer_seek_callback seek_callback, + plm_buffer_tell_callback tell_callback, + size_t length, + void *user +); + + +// Create a buffer instance with a pointer to memory as source. This assumes +// the whole file is in memory. The bytes are not copied. Pass 1 to +// free_when_done to let plmpeg call free() on the pointer when plm_destroy() +// is called. + +plm_buffer_t *plm_buffer_create_with_memory(uint8_t *bytes, size_t length, int free_when_done); + + +// Create an empty buffer with an initial capacity. The buffer will grow +// as needed. Data that has already been read, will be discarded. + +plm_buffer_t *plm_buffer_create_with_capacity(size_t capacity); + + +// Create an empty buffer with an initial capacity. The buffer will grow +// as needed. Decoded data will *not* be discarded. This can be used when +// loading a file over the network, without needing to throttle the download. +// It also allows for seeking in the already loaded data. + +plm_buffer_t *plm_buffer_create_for_appending(size_t initial_capacity); + + +// Destroy a buffer instance and free all data + +void plm_buffer_destroy(plm_buffer_t *self); + + +// Copy data into the buffer. If the data to be written is larger than the +// available space, the buffer will realloc() with a larger capacity. +// Returns the number of bytes written. This will always be the same as the +// passed in length, except when the buffer was created _with_memory() for +// which _write() is forbidden. + +size_t plm_buffer_write(plm_buffer_t *self, uint8_t *bytes, size_t length); + + +// Mark the current byte length as the end of this buffer and signal that no +// more data is expected to be written to it. This function should be called +// just after the last plm_buffer_write(). +// For _with_capacity buffers, this is cleared on a plm_buffer_rewind(). + +void plm_buffer_signal_end(plm_buffer_t *self); + + +// Set a callback that is called whenever the buffer needs more data + +void plm_buffer_set_load_callback(plm_buffer_t *self, plm_buffer_load_callback fp, void *user); + + +// Rewind the buffer back to the beginning. When loading from a file handle, +// this also seeks to the beginning of the file. + +void plm_buffer_rewind(plm_buffer_t *self); + + +// Get the total size. For files, this returns the file size. For all other +// types it returns the number of bytes currently in the buffer. + +size_t plm_buffer_get_size(plm_buffer_t *self); + + +// Get the number of remaining (yet unread) bytes in the buffer. This can be +// useful to throttle writing. + +size_t plm_buffer_get_remaining(plm_buffer_t *self); + + +// Get whether the read position of the buffer is at the end and no more data +// is expected. + +int plm_buffer_has_ended(plm_buffer_t *self); + + + +// ----------------------------------------------------------------------------- +// plm_demux public API +// Demux an MPEG Program Stream (PS) data into separate packages + + +// Various Packet Types + +static const int PLM_DEMUX_PACKET_PRIVATE = 0xBD; +static const int PLM_DEMUX_PACKET_AUDIO_1 = 0xC0; +static const int PLM_DEMUX_PACKET_AUDIO_2 = 0xC1; +static const int PLM_DEMUX_PACKET_AUDIO_3 = 0xC2; +static const int PLM_DEMUX_PACKET_AUDIO_4 = 0xC3; +static const int PLM_DEMUX_PACKET_VIDEO_1 = 0xE0; + + +// Create a demuxer with a plm_buffer as source. This will also attempt to read +// the pack and system headers from the buffer. + +plm_demux_t *plm_demux_create(plm_buffer_t *buffer, int destroy_when_done); + + +// Destroy a demuxer and free all data. + +void plm_demux_destroy(plm_demux_t *self); + + +// Returns TRUE/FALSE whether pack and system headers have been found. This will +// attempt to read the headers if non are present yet. + +int plm_demux_has_headers(plm_demux_t *self); + + +// Probe the file for the actual number of video/audio streams. See +// plm_probe() for the details. + +int plm_demux_probe(plm_demux_t *self, size_t probesize); + + +// Returns the number of video streams found in the system header. This will +// attempt to read the system header if non is present yet. + +int plm_demux_get_num_video_streams(plm_demux_t *self); + + +// Returns the number of audio streams found in the system header. This will +// attempt to read the system header if non is present yet. + +int plm_demux_get_num_audio_streams(plm_demux_t *self); + + +// Rewind the internal buffer. See plm_buffer_rewind(). + +void plm_demux_rewind(plm_demux_t *self); + + +// Get whether the file has ended. This will be cleared on seeking or rewind. + +int plm_demux_has_ended(plm_demux_t *self); + + +// Seek to a packet of the specified type with a PTS just before specified time. +// If force_intra is TRUE, only packets containing an intra frame will be +// considered - this only makes sense when the type is PLM_DEMUX_PACKET_VIDEO_1. +// Note that the specified time is considered 0-based, regardless of the first +// PTS in the data source. + +plm_packet_t *plm_demux_seek(plm_demux_t *self, double time, int type, int force_intra); + + +// Get the PTS of the first packet of this type. Returns PLM_PACKET_INVALID_TS +// if not packet of this packet type can be found. + +double plm_demux_get_start_time(plm_demux_t *self, int type); + + +// Get the duration for the specified packet type - i.e. the span between the +// the first PTS and the last PTS in the data source. This only makes sense when +// the underlying data source is a file or fixed memory. + +double plm_demux_get_duration(plm_demux_t *self, int type); + + +// Decode and return the next packet. The returned packet_t is valid until +// the next call to plm_demux_decode() or until the demuxer is destroyed. + +plm_packet_t *plm_demux_decode(plm_demux_t *self); + + + +// ----------------------------------------------------------------------------- +// plm_video public API +// Decode MPEG1 Video ("mpeg1") data into raw YCrCb frames + + +// Create a video decoder with a plm_buffer as source. + +plm_video_t *plm_video_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done); + + +// Destroy a video decoder and free all data. + +void plm_video_destroy(plm_video_t *self); + + +// Get whether a sequence header was found and we can accurately report on +// dimensions and framerate. + +int plm_video_has_header(plm_video_t *self); + + +// Get the framerate in frames per second. + +double plm_video_get_framerate(plm_video_t *self); +double plm_video_get_pixel_aspect_ratio(plm_video_t *self); + + +// Get the display width/height. + +int plm_video_get_width(plm_video_t *self); +int plm_video_get_height(plm_video_t *self); + + +// Set "no delay" mode. When enabled, the decoder assumes that the video does +// *not* contain any B-Frames. This is useful for reducing lag when streaming. +// The default is FALSE. + +void plm_video_set_no_delay(plm_video_t *self, int no_delay); + + +// Get the current internal time in seconds. + +double plm_video_get_time(plm_video_t *self); + + +// Set the current internal time in seconds. This is only useful when you +// manipulate the underlying video buffer and want to enforce a correct +// timestamps. + +void plm_video_set_time(plm_video_t *self, double time); + + +// Rewind the internal buffer. See plm_buffer_rewind(). + +void plm_video_rewind(plm_video_t *self); + + +// Get whether the file has ended. This will be cleared on rewind. + +int plm_video_has_ended(plm_video_t *self); + + +// Decode and return one frame of video and advance the internal time by +// 1/framerate seconds. The returned frame_t is valid until the next call of +// plm_video_decode() or until the video decoder is destroyed. + +plm_frame_t *plm_video_decode(plm_video_t *self); + + +// Convert the YCrCb data of a frame into interleaved R G B data. The stride +// specifies the width in bytes of the destination buffer. I.e. the number of +// bytes from one line to the next. The stride must be at least +// (frame->width * bytes_per_pixel). The buffer pointed to by *dest must have a +// size of at least (stride * frame->height). +// Note that the alpha component of the dest buffer is always left untouched. + +void plm_frame_to_rgb(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_bgr(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_rgba(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_bgra(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_argb(plm_frame_t *frame, uint8_t *dest, int stride); +void plm_frame_to_abgr(plm_frame_t *frame, uint8_t *dest, int stride); + + +// ----------------------------------------------------------------------------- +// plm_audio public API +// Decode MPEG-1 Audio Layer II ("mp2") data into raw samples + + +// Create an audio decoder with a plm_buffer as source. + +plm_audio_t *plm_audio_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done); + + +// Destroy an audio decoder and free all data. + +void plm_audio_destroy(plm_audio_t *self); + + +// Get whether a frame header was found and we can accurately report on +// samplerate. + +int plm_audio_has_header(plm_audio_t *self); + + +// Get the samplerate in samples per second. + +int plm_audio_get_samplerate(plm_audio_t *self); + + +// Get the current internal time in seconds. + +double plm_audio_get_time(plm_audio_t *self); + + +// Set the current internal time in seconds. This is only useful when you +// manipulate the underlying video buffer and want to enforce a correct +// timestamps. + +void plm_audio_set_time(plm_audio_t *self, double time); + + +// Rewind the internal buffer. See plm_buffer_rewind(). + +void plm_audio_rewind(plm_audio_t *self); + + +// Get whether the file has ended. This will be cleared on rewind. + +int plm_audio_has_ended(plm_audio_t *self); + + +// Decode and return one "frame" of audio and advance the internal time by +// (PLM_AUDIO_SAMPLES_PER_FRAME/samplerate) seconds. The returned samples_t +// is valid until the next call of plm_audio_decode() or until the audio +// decoder is destroyed. + +plm_samples_t *plm_audio_decode(plm_audio_t *self); + + + +#ifdef __cplusplus +} +#endif + +#endif // PL_MPEG_H + + + + + +// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- +// IMPLEMENTATION + +#ifdef PL_MPEG_IMPLEMENTATION + +#include +#include +#ifndef PLM_NO_STDIO +#include +#endif + +#ifndef TRUE +#define TRUE 1 +#define FALSE 0 +#endif + +#ifndef PLM_MALLOC + #define PLM_MALLOC(sz) malloc(sz) + #define PLM_FREE(p) free(p) + #define PLM_REALLOC(p, sz) realloc(p, sz) +#endif + +#define PLM_UNUSED(expr) (void)(expr) +#ifdef _MSC_VER + #pragma warning(disable:4996) +#endif + +// ----------------------------------------------------------------------------- +// plm (high-level interface) implementation + +struct plm_t { + plm_demux_t *demux; + double time; + int has_ended; + int loop; + int has_decoders; + + int video_enabled; + int video_packet_type; + plm_buffer_t *video_buffer; + plm_video_t *video_decoder; + + int audio_enabled; + int audio_stream_index; + int audio_packet_type; + double audio_lead_time; + plm_buffer_t *audio_buffer; + plm_audio_t *audio_decoder; + + plm_video_decode_callback video_decode_callback; + void *video_decode_callback_user_data; + + plm_audio_decode_callback audio_decode_callback; + void *audio_decode_callback_user_data; +}; + +int plm_init_decoders(plm_t *self); +void plm_handle_end(plm_t *self); +void plm_read_video_packet(plm_buffer_t *buffer, void *user); +void plm_read_audio_packet(plm_buffer_t *buffer, void *user); +void plm_read_packets(plm_t *self, int requested_type); + +#ifndef PLM_NO_STDIO + +plm_t *plm_create_with_filename(const char *filename) { + plm_buffer_t *buffer = plm_buffer_create_with_filename(filename); + if (!buffer) { + return NULL; + } + return plm_create_with_buffer(buffer, TRUE); +} + +plm_t *plm_create_with_file(FILE *fh, int close_when_done) { + plm_buffer_t *buffer = plm_buffer_create_with_file(fh, close_when_done); + return plm_create_with_buffer(buffer, TRUE); +} + +#endif // PLM_NO_STDIO + +plm_t *plm_create_with_memory(uint8_t *bytes, size_t length, int free_when_done) { + plm_buffer_t *buffer = plm_buffer_create_with_memory(bytes, length, free_when_done); + return plm_create_with_buffer(buffer, TRUE); +} + +plm_t *plm_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done) { + plm_t *self = (plm_t *)PLM_MALLOC(sizeof(plm_t)); + memset(self, 0, sizeof(plm_t)); + + self->demux = plm_demux_create(buffer, destroy_when_done); + self->video_enabled = TRUE; + self->audio_enabled = TRUE; + plm_init_decoders(self); + + return self; +} + +int plm_init_decoders(plm_t *self) { + if (self->has_decoders) { + return TRUE; + } + + if (!plm_demux_has_headers(self->demux)) { + return FALSE; + } + + if (plm_demux_get_num_video_streams(self->demux) > 0) { + if (self->video_enabled) { + self->video_packet_type = PLM_DEMUX_PACKET_VIDEO_1; + } + if (!self->video_decoder) { + self->video_buffer = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE); + plm_buffer_set_load_callback(self->video_buffer, plm_read_video_packet, self); + self->video_decoder = plm_video_create_with_buffer(self->video_buffer, TRUE); + } + } + + if (plm_demux_get_num_audio_streams(self->demux) > 0) { + if (self->audio_enabled) { + self->audio_packet_type = PLM_DEMUX_PACKET_AUDIO_1 + self->audio_stream_index; + } + if (!self->audio_decoder) { + self->audio_buffer = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE); + plm_buffer_set_load_callback(self->audio_buffer, plm_read_audio_packet, self); + self->audio_decoder = plm_audio_create_with_buffer(self->audio_buffer, TRUE); + } + } + + self->has_decoders = TRUE; + return TRUE; +} + +void plm_destroy(plm_t *self) { + if (self->video_decoder) { + plm_video_destroy(self->video_decoder); + } + if (self->audio_decoder) { + plm_audio_destroy(self->audio_decoder); + } + + plm_demux_destroy(self->demux); + PLM_FREE(self); +} + +int plm_get_audio_enabled(plm_t *self) { + return self->audio_enabled; +} + +int plm_has_headers(plm_t *self) { + if (!plm_demux_has_headers(self->demux)) { + return FALSE; + } + + if (!plm_init_decoders(self)) { + return FALSE; + } + + if ( + (self->video_decoder && !plm_video_has_header(self->video_decoder)) || + (self->audio_decoder && !plm_audio_has_header(self->audio_decoder)) + ) { + return FALSE; + } + + return TRUE; +} + +int plm_probe(plm_t *self, size_t probesize) { + int found_streams = plm_demux_probe(self->demux, probesize); + if (!found_streams) { + return FALSE; + } + + // Re-init decoders + self->has_decoders = FALSE; + self->video_packet_type = 0; + self->audio_packet_type = 0; + return plm_init_decoders(self); +} + +void plm_set_audio_enabled(plm_t *self, int enabled) { + self->audio_enabled = enabled; + + if (!enabled) { + self->audio_packet_type = 0; + return; + } + + self->audio_packet_type = (plm_init_decoders(self) && self->audio_decoder) + ? PLM_DEMUX_PACKET_AUDIO_1 + self->audio_stream_index + : 0; +} + +void plm_set_audio_stream(plm_t *self, int stream_index) { + if (stream_index < 0 || stream_index > 3) { + return; + } + self->audio_stream_index = stream_index; + + // Set the correct audio_packet_type + plm_set_audio_enabled(self, self->audio_enabled); +} + +int plm_get_video_enabled(plm_t *self) { + return self->video_enabled; +} + +void plm_set_video_enabled(plm_t *self, int enabled) { + self->video_enabled = enabled; + + if (!enabled) { + self->video_packet_type = 0; + return; + } + + self->video_packet_type = (plm_init_decoders(self) && self->video_decoder) + ? PLM_DEMUX_PACKET_VIDEO_1 + : 0; +} + +int plm_get_num_video_streams(plm_t *self) { + return plm_demux_get_num_video_streams(self->demux); +} + +int plm_get_width(plm_t *self) { + return (plm_init_decoders(self) && self->video_decoder) + ? plm_video_get_width(self->video_decoder) + : 0; +} + +int plm_get_height(plm_t *self) { + return (plm_init_decoders(self) && self->video_decoder) + ? plm_video_get_height(self->video_decoder) + : 0; +} + +double plm_get_framerate(plm_t *self) { + return (plm_init_decoders(self) && self->video_decoder) + ? plm_video_get_framerate(self->video_decoder) + : 0; +} + +double plm_get_pixel_aspect_ratio(plm_t *self) { + return (plm_init_decoders(self) && self->video_decoder) + ? plm_video_get_pixel_aspect_ratio(self->video_decoder) + : 0; +} + +int plm_get_num_audio_streams(plm_t *self) { + return plm_demux_get_num_audio_streams(self->demux); +} + +int plm_get_samplerate(plm_t *self) { + return (plm_init_decoders(self) && self->audio_decoder) + ? plm_audio_get_samplerate(self->audio_decoder) + : 0; +} + +double plm_get_audio_lead_time(plm_t *self) { + return self->audio_lead_time; +} + +void plm_set_audio_lead_time(plm_t *self, double lead_time) { + self->audio_lead_time = lead_time; +} + +double plm_get_time(plm_t *self) { + return self->time; +} + +double plm_get_duration(plm_t *self) { + return plm_demux_get_duration(self->demux, PLM_DEMUX_PACKET_VIDEO_1); +} + +void plm_rewind(plm_t *self) { + if (self->video_decoder) { + plm_video_rewind(self->video_decoder); + } + + if (self->audio_decoder) { + plm_audio_rewind(self->audio_decoder); + } + + plm_demux_rewind(self->demux); + self->time = 0; + self->has_ended = FALSE; +} + +int plm_get_loop(plm_t *self) { + return self->loop; +} + +void plm_set_loop(plm_t *self, int loop) { + self->loop = loop; +} + +int plm_has_ended(plm_t *self) { + return self->has_ended; +} + +void plm_set_video_decode_callback(plm_t *self, plm_video_decode_callback fp, void *user) { + self->video_decode_callback = fp; + self->video_decode_callback_user_data = user; +} + +void plm_set_audio_decode_callback(plm_t *self, plm_audio_decode_callback fp, void *user) { + self->audio_decode_callback = fp; + self->audio_decode_callback_user_data = user; +} + +void plm_decode(plm_t *self, double tick) { + if (!plm_init_decoders(self)) { + return; + } + + int decode_video = (self->video_decode_callback && self->video_packet_type); + int decode_audio = (self->audio_decode_callback && self->audio_packet_type); + + if (!decode_video && !decode_audio) { + // Nothing to do here + return; + } + + int did_decode = FALSE; + int decode_video_failed = FALSE; + int decode_audio_failed = FALSE; + + double video_target_time = self->time + tick; + double audio_target_time = self->time + tick + self->audio_lead_time; + + do { + did_decode = FALSE; + + if (decode_video && plm_video_get_time(self->video_decoder) < video_target_time) { + plm_frame_t *frame = plm_video_decode(self->video_decoder); + if (frame) { + self->video_decode_callback(self, frame, self->video_decode_callback_user_data); + did_decode = TRUE; + } + else { + decode_video_failed = TRUE; + } + } + + if (decode_audio && plm_audio_get_time(self->audio_decoder) < audio_target_time) { + plm_samples_t *samples = plm_audio_decode(self->audio_decoder); + if (samples) { + self->audio_decode_callback(self, samples, self->audio_decode_callback_user_data); + did_decode = TRUE; + } + else { + decode_audio_failed = TRUE; + } + } + } while (did_decode); + + // Did all sources we wanted to decode fail and the demuxer is at the end? + if ( + (!decode_video || decode_video_failed) && + (!decode_audio || decode_audio_failed) && + plm_demux_has_ended(self->demux) + ) { + plm_handle_end(self); + return; + } + + self->time += tick; +} + +plm_frame_t *plm_decode_video(plm_t *self) { + if (!plm_init_decoders(self)) { + return NULL; + } + + if (!self->video_packet_type) { + return NULL; + } + + plm_frame_t *frame = plm_video_decode(self->video_decoder); + if (frame) { + self->time = frame->time; + } + else if (plm_demux_has_ended(self->demux)) { + plm_handle_end(self); + } + return frame; +} + +plm_samples_t *plm_decode_audio(plm_t *self) { + if (!plm_init_decoders(self)) { + return NULL; + } + + if (!self->audio_packet_type) { + return NULL; + } + + plm_samples_t *samples = plm_audio_decode(self->audio_decoder); + if (samples) { + self->time = samples->time; + } + else if (plm_demux_has_ended(self->demux)) { + plm_handle_end(self); + } + return samples; +} + +void plm_handle_end(plm_t *self) { + if (self->loop) { + plm_rewind(self); + } + else { + self->has_ended = TRUE; + } +} + +void plm_read_video_packet(plm_buffer_t *buffer, void *user) { + PLM_UNUSED(buffer); + plm_t *self = (plm_t *)user; + plm_read_packets(self, self->video_packet_type); +} + +void plm_read_audio_packet(plm_buffer_t *buffer, void *user) { + PLM_UNUSED(buffer); + plm_t *self = (plm_t *)user; + plm_read_packets(self, self->audio_packet_type); +} + +void plm_read_packets(plm_t *self, int requested_type) { + plm_packet_t *packet; + while ((packet = plm_demux_decode(self->demux))) { + if (packet->type == self->video_packet_type) { + plm_buffer_write(self->video_buffer, packet->data, packet->length); + } + else if (packet->type == self->audio_packet_type) { + plm_buffer_write(self->audio_buffer, packet->data, packet->length); + } + + if (packet->type == requested_type) { + return; + } + } + + if (plm_demux_has_ended(self->demux)) { + if (self->video_buffer) { + plm_buffer_signal_end(self->video_buffer); + } + if (self->audio_buffer) { + plm_buffer_signal_end(self->audio_buffer); + } + } +} + +plm_frame_t *plm_seek_frame(plm_t *self, double time, int seek_exact) { + if (!plm_init_decoders(self)) { + return NULL; + } + + if (!self->video_packet_type) { + return NULL; + } + + int type = self->video_packet_type; + + double start_time = plm_demux_get_start_time(self->demux, type); + double duration = plm_demux_get_duration(self->demux, type); + + if (time < 0) { + time = 0; + } + else if (time > duration) { + time = duration; + } + + plm_packet_t *packet = plm_demux_seek(self->demux, time, type, TRUE); + if (!packet) { + return NULL; + } + + // Disable writing to the audio buffer while decoding video + int previous_audio_packet_type = self->audio_packet_type; + self->audio_packet_type = 0; + + // Clear video buffer and decode the found packet + plm_video_rewind(self->video_decoder); + plm_video_set_time(self->video_decoder, packet->pts - start_time); + plm_buffer_write(self->video_buffer, packet->data, packet->length); + plm_frame_t *frame = plm_video_decode(self->video_decoder); + + // If we want to seek to an exact frame, we have to decode all frames + // on top of the intra frame we just jumped to. + if (seek_exact) { + while (frame && frame->time < time) { + frame = plm_video_decode(self->video_decoder); + } + } + + // Enable writing to the audio buffer again? + self->audio_packet_type = previous_audio_packet_type; + + if (frame) { + self->time = frame->time; + } + + self->has_ended = FALSE; + return frame; +} + +int plm_seek(plm_t *self, double time, int seek_exact) { + plm_frame_t *frame = plm_seek_frame(self, time, seek_exact); + + if (!frame) { + return FALSE; + } + + if (self->video_decode_callback) { + self->video_decode_callback(self, frame, self->video_decode_callback_user_data); + } + + // If audio is not enabled we are done here. + if (!self->audio_packet_type) { + return TRUE; + } + + // Sync up Audio. This demuxes more packets until the first audio packet + // with a PTS greater than the current time is found. plm_decode() is then + // called to decode enough audio data to satisfy the audio_lead_time. + + double start_time = plm_demux_get_start_time(self->demux, self->video_packet_type); + plm_audio_rewind(self->audio_decoder); + + plm_packet_t *packet = NULL; + while ((packet = plm_demux_decode(self->demux))) { + if (packet->type == self->video_packet_type) { + plm_buffer_write(self->video_buffer, packet->data, packet->length); + } + else if ( + packet->type == self->audio_packet_type && + packet->pts - start_time > self->time + ) { + plm_audio_set_time(self->audio_decoder, packet->pts - start_time); + plm_buffer_write(self->audio_buffer, packet->data, packet->length); + plm_decode(self, 0); + break; + } + } + + return TRUE; +} + + + +// ----------------------------------------------------------------------------- +// plm_buffer implementation + +enum plm_buffer_mode { + PLM_BUFFER_MODE_FILE, + PLM_BUFFER_MODE_FIXED_MEM, + PLM_BUFFER_MODE_RING, + PLM_BUFFER_MODE_APPEND +}; + +struct plm_buffer_t { + size_t bit_index; + size_t capacity; + size_t length; + size_t total_size; + int discard_read_bytes; + int has_ended; + int free_when_done; +#ifndef PLM_NO_STDIO + int close_when_done; + FILE *fh; +#endif + plm_buffer_load_callback load_callback; + plm_buffer_seek_callback seek_callback; + plm_buffer_tell_callback tell_callback; + void *load_callback_user_data; + uint8_t *bytes; + enum plm_buffer_mode mode; +}; + +typedef struct { + int16_t index; + int16_t value; +} plm_vlc_t; + +typedef struct { + int16_t index; + uint16_t value; +} plm_vlc_uint_t; + + +void plm_buffer_seek(plm_buffer_t *self, size_t pos); +size_t plm_buffer_tell(plm_buffer_t *self); +void plm_buffer_discard_read_bytes(plm_buffer_t *self); + +#ifndef PLM_NO_STDIO +void plm_buffer_load_file_callback(plm_buffer_t *self, void *user); +void plm_buffer_seek_file_callback(plm_buffer_t *self, size_t offset, void *user); +size_t plm_buffer_tell_file_callback(plm_buffer_t *self, void *user); +#endif + +int plm_buffer_has(plm_buffer_t *self, size_t count); +int plm_buffer_read(plm_buffer_t *self, int count); +void plm_buffer_align(plm_buffer_t *self); +void plm_buffer_skip(plm_buffer_t *self, size_t count); +int plm_buffer_skip_bytes(plm_buffer_t *self, uint8_t v); +int plm_buffer_next_start_code(plm_buffer_t *self); +int plm_buffer_find_start_code(plm_buffer_t *self, int code); +int plm_buffer_no_start_code(plm_buffer_t *self); +int16_t plm_buffer_read_vlc(plm_buffer_t *self, const plm_vlc_t *table); +uint16_t plm_buffer_read_vlc_uint(plm_buffer_t *self, const plm_vlc_uint_t *table); + +#ifndef PLM_NO_STDIO + +plm_buffer_t *plm_buffer_create_with_filename(const char *filename) { + FILE *fh = fopen(filename, "rb"); + if (!fh) { + return NULL; + } + return plm_buffer_create_with_file(fh, TRUE); +} + +plm_buffer_t *plm_buffer_create_with_file(FILE *fh, int close_when_done) { + plm_buffer_t *self = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE); + self->fh = fh; + self->close_when_done = close_when_done; + self->mode = PLM_BUFFER_MODE_FILE; + self->discard_read_bytes = TRUE; + + fseek(self->fh, 0, SEEK_END); + self->total_size = ftell(self->fh); + fseek(self->fh, 0, SEEK_SET); + + self->load_callback = plm_buffer_load_file_callback; + self->seek_callback = plm_buffer_seek_file_callback; + self->tell_callback = plm_buffer_tell_file_callback; + return self; +} + +#endif // PLM_NO_STDIO + +plm_buffer_t *plm_buffer_create_with_callbacks( + plm_buffer_load_callback load_callback, + plm_buffer_seek_callback seek_callback, + plm_buffer_tell_callback tell_callback, + size_t length, + void *user +) { + plm_buffer_t *self = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE); + self->mode = PLM_BUFFER_MODE_FILE; + self->total_size = length; + self->load_callback = load_callback; + self->seek_callback = seek_callback; + self->tell_callback = tell_callback; + self->load_callback_user_data = user; + return self; +} + +plm_buffer_t *plm_buffer_create_with_memory(uint8_t *bytes, size_t length, int free_when_done) { + plm_buffer_t *self = (plm_buffer_t *)PLM_MALLOC(sizeof(plm_buffer_t)); + memset(self, 0, sizeof(plm_buffer_t)); + self->capacity = length; + self->length = length; + self->total_size = length; + self->free_when_done = free_when_done; + self->bytes = bytes; + self->mode = PLM_BUFFER_MODE_FIXED_MEM; + self->discard_read_bytes = FALSE; + return self; +} + +plm_buffer_t *plm_buffer_create_with_capacity(size_t capacity) { + plm_buffer_t *self = (plm_buffer_t *)PLM_MALLOC(sizeof(plm_buffer_t)); + memset(self, 0, sizeof(plm_buffer_t)); + self->capacity = capacity; + self->free_when_done = TRUE; + self->bytes = (uint8_t *)PLM_MALLOC(capacity); + self->mode = PLM_BUFFER_MODE_RING; + self->discard_read_bytes = TRUE; + return self; +} + +plm_buffer_t *plm_buffer_create_for_appending(size_t initial_capacity) { + plm_buffer_t *self = plm_buffer_create_with_capacity(initial_capacity); + self->mode = PLM_BUFFER_MODE_APPEND; + self->discard_read_bytes = FALSE; + return self; +} + +void plm_buffer_destroy(plm_buffer_t *self) { +#ifndef PLM_NO_STDIO + if (self->fh && self->close_when_done) { + fclose(self->fh); + } +#endif + if (self->free_when_done) { + PLM_FREE(self->bytes); + } + PLM_FREE(self); +} + +size_t plm_buffer_get_size(plm_buffer_t *self) { + return (self->mode == PLM_BUFFER_MODE_FILE) + ? self->total_size + : self->length; +} + +size_t plm_buffer_get_remaining(plm_buffer_t *self) { + return self->length - (self->bit_index >> 3); +} + +size_t plm_buffer_write(plm_buffer_t *self, uint8_t *bytes, size_t length) { + if (self->mode == PLM_BUFFER_MODE_FIXED_MEM) { + return 0; + } + + if (self->discard_read_bytes) { + // This should be a ring buffer, but instead it just shifts all unread + // data to the beginning of the buffer and appends new data at the end. + // Seems to be good enough. + + plm_buffer_discard_read_bytes(self); + if (self->mode == PLM_BUFFER_MODE_RING) { + self->total_size = 0; + } + } + + // Do we have to resize to fit the new data? + size_t bytes_available = self->capacity - self->length; + if (bytes_available < length) { + size_t new_size = self->capacity; + do { + new_size *= 2; + } while (new_size - self->length < length); + self->bytes = (uint8_t *)PLM_REALLOC(self->bytes, new_size); + self->capacity = new_size; + } + + memcpy(self->bytes + self->length, bytes, length); + self->length += length; + self->has_ended = FALSE; + return length; +} + +void plm_buffer_signal_end(plm_buffer_t *self) { + self->total_size = self->length; +} + +void plm_buffer_set_load_callback(plm_buffer_t *self, plm_buffer_load_callback fp, void *user) { + self->load_callback = fp; + self->load_callback_user_data = user; +} + +void plm_buffer_rewind(plm_buffer_t *self) { + plm_buffer_seek(self, 0); +} + +void plm_buffer_seek(plm_buffer_t *self, size_t pos) { + self->has_ended = FALSE; + + if (self->seek_callback) { + self->seek_callback(self, pos, self->load_callback_user_data); + self->bit_index = 0; + self->length = 0; + } + else if (self->mode == PLM_BUFFER_MODE_RING) { + if (pos != 0) { + // Seeking to non-0 is forbidden for dynamic-mem buffers + return; + } + self->bit_index = 0; + self->length = 0; + self->total_size = 0; + } + else if (pos < self->length) { + self->bit_index = pos << 3; + } +} + +size_t plm_buffer_tell(plm_buffer_t *self) { + return self->tell_callback + ? self->tell_callback(self, self->load_callback_user_data) + (self->bit_index >> 3) - self->length + : self->bit_index >> 3; +} + +void plm_buffer_discard_read_bytes(plm_buffer_t *self) { + size_t byte_pos = self->bit_index >> 3; + if (byte_pos == self->length) { + self->bit_index = 0; + self->length = 0; + } + else if (byte_pos > 0) { + memmove(self->bytes, self->bytes + byte_pos, self->length - byte_pos); + self->bit_index -= byte_pos << 3; + self->length -= byte_pos; + } +} + +#ifndef PLM_NO_STDIO + +void plm_buffer_load_file_callback(plm_buffer_t *self, void *user) { + PLM_UNUSED(user); + + if (self->discard_read_bytes) { + plm_buffer_discard_read_bytes(self); + } + + size_t bytes_available = self->capacity - self->length; + size_t bytes_read = fread(self->bytes + self->length, 1, bytes_available, self->fh); + self->length += bytes_read; + + if (bytes_read == 0) { + self->has_ended = TRUE; + } +} + +void plm_buffer_seek_file_callback(plm_buffer_t *self, size_t offset, void *user) { + PLM_UNUSED(user); + fseek(self->fh, offset, SEEK_SET); +} + +size_t plm_buffer_tell_file_callback(plm_buffer_t *self, void *user) { + PLM_UNUSED(user); + return ftell(self->fh); +} + +#endif // PLM_NO_STDIO + +int plm_buffer_has_ended(plm_buffer_t *self) { + return self->has_ended; +} + +int plm_buffer_has(plm_buffer_t *self, size_t count) { + if (((self->length << 3) - self->bit_index) >= count) { + return TRUE; + } + + if (self->load_callback) { + self->load_callback(self, self->load_callback_user_data); + + if (((self->length << 3) - self->bit_index) >= count) { + return TRUE; + } + } + + if (self->total_size != 0 && self->length == self->total_size) { + self->has_ended = TRUE; + } + return FALSE; +} + +int plm_buffer_read(plm_buffer_t *self, int count) { + if (!plm_buffer_has(self, count)) { + return 0; + } + + int value = 0; + while (count) { + int current_byte = self->bytes[self->bit_index >> 3]; + + int remaining = 8 - (self->bit_index & 7); // Remaining bits in byte + int read = remaining < count ? remaining : count; // Bits in self run + int shift = remaining - read; + int mask = (0xff >> (8 - read)); + + value = (value << read) | ((current_byte & (mask << shift)) >> shift); + + self->bit_index += read; + count -= read; + } + + return value; +} + +void plm_buffer_align(plm_buffer_t *self) { + self->bit_index = ((self->bit_index + 7) >> 3) << 3; // Align to next byte +} + +void plm_buffer_skip(plm_buffer_t *self, size_t count) { + if (plm_buffer_has(self, count)) { + self->bit_index += count; + } +} + +int plm_buffer_skip_bytes(plm_buffer_t *self, uint8_t v) { + plm_buffer_align(self); + int skipped = 0; + while (plm_buffer_has(self, 8) && self->bytes[self->bit_index >> 3] == v) { + self->bit_index += 8; + skipped++; + } + return skipped; +} + +int plm_buffer_next_start_code(plm_buffer_t *self) { + plm_buffer_align(self); + + while (plm_buffer_has(self, (5 << 3))) { + size_t byte_index = (self->bit_index) >> 3; + if ( + self->bytes[byte_index] == 0x00 && + self->bytes[byte_index + 1] == 0x00 && + self->bytes[byte_index + 2] == 0x01 + ) { + self->bit_index = (byte_index + 4) << 3; + return self->bytes[byte_index + 3]; + } + self->bit_index += 8; + } + return -1; +} + +int plm_buffer_find_start_code(plm_buffer_t *self, int code) { + int current = 0; + while (TRUE) { + current = plm_buffer_next_start_code(self); + if (current == code || current == -1) { + return current; + } + } + return -1; +} + +int plm_buffer_has_start_code(plm_buffer_t *self, int code) { + size_t previous_bit_index = self->bit_index; + int previous_discard_read_bytes = self->discard_read_bytes; + + self->discard_read_bytes = FALSE; + int current = plm_buffer_find_start_code(self, code); + + self->bit_index = previous_bit_index; + self->discard_read_bytes = previous_discard_read_bytes; + return current; +} + +int plm_buffer_peek_non_zero(plm_buffer_t *self, int bit_count) { + if (!plm_buffer_has(self, bit_count)) { + return FALSE; + } + + int val = plm_buffer_read(self, bit_count); + self->bit_index -= bit_count; + return val != 0; +} + +int16_t plm_buffer_read_vlc(plm_buffer_t *self, const plm_vlc_t *table) { + plm_vlc_t state = {0, 0}; + do { + state = table[state.index + plm_buffer_read(self, 1)]; + } while (state.index > 0); + return state.value; +} + +uint16_t plm_buffer_read_vlc_uint(plm_buffer_t *self, const plm_vlc_uint_t *table) { + return (uint16_t)plm_buffer_read_vlc(self, (const plm_vlc_t *)table); +} + + + +// ---------------------------------------------------------------------------- +// plm_demux implementation + +static const int PLM_START_PACK = 0xBA; +static const int PLM_START_END = 0xB9; +static const int PLM_START_SYSTEM = 0xBB; + +struct plm_demux_t { + plm_buffer_t *buffer; + int destroy_buffer_when_done; + double system_clock_ref; + + size_t last_file_size; + double last_decoded_pts; + double start_time; + double duration; + + int start_code; + int has_pack_header; + int has_system_header; + int has_headers; + + int num_audio_streams; + int num_video_streams; + plm_packet_t current_packet; + plm_packet_t next_packet; +}; + + +void plm_demux_buffer_seek(plm_demux_t *self, size_t pos); +double plm_demux_decode_time(plm_demux_t *self); +plm_packet_t *plm_demux_decode_packet(plm_demux_t *self, int type); +plm_packet_t *plm_demux_get_packet(plm_demux_t *self); + +plm_demux_t *plm_demux_create(plm_buffer_t *buffer, int destroy_when_done) { + plm_demux_t *self = (plm_demux_t *)PLM_MALLOC(sizeof(plm_demux_t)); + memset(self, 0, sizeof(plm_demux_t)); + + self->buffer = buffer; + self->destroy_buffer_when_done = destroy_when_done; + + self->start_time = PLM_PACKET_INVALID_TS; + self->duration = PLM_PACKET_INVALID_TS; + self->start_code = -1; + + plm_demux_has_headers(self); + return self; +} + +void plm_demux_destroy(plm_demux_t *self) { + if (self->destroy_buffer_when_done) { + plm_buffer_destroy(self->buffer); + } + PLM_FREE(self); +} + +int plm_demux_has_headers(plm_demux_t *self) { + if (self->has_headers) { + return TRUE; + } + + // Decode pack header + if (!self->has_pack_header) { + if ( + self->start_code != PLM_START_PACK && + plm_buffer_find_start_code(self->buffer, PLM_START_PACK) == -1 + ) { + return FALSE; + } + + self->start_code = PLM_START_PACK; + if (!plm_buffer_has(self->buffer, 64)) { + return FALSE; + } + self->start_code = -1; + + if (plm_buffer_read(self->buffer, 4) != 0x02) { + return FALSE; + } + + self->system_clock_ref = plm_demux_decode_time(self); + plm_buffer_skip(self->buffer, 1); + plm_buffer_skip(self->buffer, 22); // mux_rate * 50 + plm_buffer_skip(self->buffer, 1); + + self->has_pack_header = TRUE; + } + + // Decode system header + if (!self->has_system_header) { + if ( + self->start_code != PLM_START_SYSTEM && + plm_buffer_find_start_code(self->buffer, PLM_START_SYSTEM) == -1 + ) { + return FALSE; + } + + self->start_code = PLM_START_SYSTEM; + if (!plm_buffer_has(self->buffer, 56)) { + return FALSE; + } + self->start_code = -1; + + plm_buffer_skip(self->buffer, 16); // header_length + plm_buffer_skip(self->buffer, 24); // rate bound + self->num_audio_streams = plm_buffer_read(self->buffer, 6); + plm_buffer_skip(self->buffer, 5); // misc flags + self->num_video_streams = plm_buffer_read(self->buffer, 5); + + self->has_system_header = TRUE; + } + + self->has_headers = TRUE; + return TRUE; +} + +int plm_demux_probe(plm_demux_t *self, size_t probesize) { + int previous_pos = plm_buffer_tell(self->buffer); + + int video_stream = FALSE; + int audio_streams[4] = {FALSE, FALSE, FALSE, FALSE}; + do { + self->start_code = plm_buffer_next_start_code(self->buffer); + if (self->start_code == PLM_DEMUX_PACKET_VIDEO_1) { + video_stream = TRUE; + } + else if ( + self->start_code >= PLM_DEMUX_PACKET_AUDIO_1 && + self->start_code <= PLM_DEMUX_PACKET_AUDIO_4 + ) { + audio_streams[self->start_code - PLM_DEMUX_PACKET_AUDIO_1] = TRUE; + } + } while ( + self->start_code != -1 && + plm_buffer_tell(self->buffer) - previous_pos < probesize + ); + + self->num_video_streams = video_stream ? 1 : 0; + self->num_audio_streams = 0; + for (int i = 0; i < 4; i++) { + if (audio_streams[i]) { + self->num_audio_streams++; + } + } + + plm_demux_buffer_seek(self, previous_pos); + return (self->num_video_streams || self->num_audio_streams); +} + +int plm_demux_get_num_video_streams(plm_demux_t *self) { + return plm_demux_has_headers(self) + ? self->num_video_streams + : 0; +} + +int plm_demux_get_num_audio_streams(plm_demux_t *self) { + return plm_demux_has_headers(self) + ? self->num_audio_streams + : 0; +} + +void plm_demux_rewind(plm_demux_t *self) { + plm_buffer_rewind(self->buffer); + self->current_packet.length = 0; + self->next_packet.length = 0; + self->start_code = -1; +} + +int plm_demux_has_ended(plm_demux_t *self) { + return plm_buffer_has_ended(self->buffer); +} + +void plm_demux_buffer_seek(plm_demux_t *self, size_t pos) { + plm_buffer_seek(self->buffer, pos); + self->current_packet.length = 0; + self->next_packet.length = 0; + self->start_code = -1; +} + +double plm_demux_get_start_time(plm_demux_t *self, int type) { + if (self->start_time != PLM_PACKET_INVALID_TS) { + return self->start_time; + } + + int previous_pos = plm_buffer_tell(self->buffer); + int previous_start_code = self->start_code; + + // Find first video PTS + plm_demux_rewind(self); + do { + plm_packet_t *packet = plm_demux_decode(self); + if (!packet) { + break; + } + if (packet->type == type) { + self->start_time = packet->pts; + } + } while (self->start_time == PLM_PACKET_INVALID_TS); + + plm_demux_buffer_seek(self, previous_pos); + self->start_code = previous_start_code; + return self->start_time; +} + +double plm_demux_get_duration(plm_demux_t *self, int type) { + size_t file_size = plm_buffer_get_size(self->buffer); + + if ( + self->duration != PLM_PACKET_INVALID_TS && + self->last_file_size == file_size + ) { + return self->duration; + } + + size_t previous_pos = plm_buffer_tell(self->buffer); + int previous_start_code = self->start_code; + + // Find last video PTS. Start searching 64kb from the end and go further + // back if needed. + long start_range = 64 * 1024; + long max_range = 4096 * 1024; + for (long range = start_range; range <= max_range; range *= 2) { + long seek_pos = file_size - range; + if (seek_pos < 0) { + seek_pos = 0; + range = max_range; // Make sure to bail after this round + } + plm_demux_buffer_seek(self, seek_pos); + self->current_packet.length = 0; + + double last_pts = PLM_PACKET_INVALID_TS; + plm_packet_t *packet = NULL; + while ((packet = plm_demux_decode(self))) { + if (packet->pts != PLM_PACKET_INVALID_TS && packet->type == type) { + last_pts = packet->pts; + } + } + if (last_pts != PLM_PACKET_INVALID_TS) { + self->duration = last_pts - plm_demux_get_start_time(self, type); + break; + } + } + + plm_demux_buffer_seek(self, previous_pos); + self->start_code = previous_start_code; + self->last_file_size = file_size; + return self->duration; +} + +plm_packet_t *plm_demux_seek(plm_demux_t *self, double seek_time, int type, int force_intra) { + if (!plm_demux_has_headers(self)) { + return NULL; + } + + // Using the current time, current byte position and the average bytes per + // second for this file, try to jump to a byte position that hopefully has + // packets containing timestamps within one second before to the desired + // seek_time. + + // If we hit close to the seek_time scan through all packets to find the + // last one (just before the seek_time) containing an intra frame. + // Otherwise we should at least be closer than before. Calculate the bytes + // per second for the jumped range and jump again. + + // The number of retries here is hard-limited to a generous amount. Usually + // the correct range is found after 1--5 jumps, even for files with very + // variable bitrates. If significantly more jumps are needed, there's + // probably something wrong with the file and we just avoid getting into an + // infinite loop. 32 retries should be enough for anybody. + + double duration = plm_demux_get_duration(self, type); + long file_size = plm_buffer_get_size(self->buffer); + long byterate = file_size / duration; + + double cur_time = self->last_decoded_pts; + double scan_span = 1; + + if (seek_time > duration) { + seek_time = duration; + } + else if (seek_time < 0) { + seek_time = 0; + } + seek_time += self->start_time; + + for (int retry = 0; retry < 32; retry++) { + int found_packet_with_pts = FALSE; + int found_packet_in_range = FALSE; + long last_valid_packet_start = -1; + double first_packet_time = PLM_PACKET_INVALID_TS; + + long cur_pos = plm_buffer_tell(self->buffer); + + // Estimate byte offset and jump to it. + long offset = (seek_time - cur_time - scan_span) * byterate; + long seek_pos = cur_pos + offset; + if (seek_pos < 0) { + seek_pos = 0; + } + else if (seek_pos > file_size - 256) { + seek_pos = file_size - 256; + } + + plm_demux_buffer_seek(self, seek_pos); + + // Scan through all packets up to the seek_time to find the last packet + // containing an intra frame. + while (plm_buffer_find_start_code(self->buffer, type) != -1) { + long packet_start = plm_buffer_tell(self->buffer); + plm_packet_t *packet = plm_demux_decode_packet(self, type); + + // Skip packet if it has no PTS + if (!packet || packet->pts == PLM_PACKET_INVALID_TS) { + continue; + } + + // Bail scanning through packets if we hit one that is outside + // seek_time - scan_span. + // We also adjust the cur_time and byterate values here so the next + // iteration can be a bit more precise. + if (packet->pts > seek_time || packet->pts < seek_time - scan_span) { + found_packet_with_pts = TRUE; + byterate = (seek_pos - cur_pos) / (packet->pts - cur_time); + cur_time = packet->pts; + break; + } + + // If we are still here, it means this packet is in close range to + // the seek_time. If this is the first packet for this jump position + // record the PTS. If we later have to back off, when there was no + // intra frame in this range, we can lower the seek_time to not scan + // this range again. + if (!found_packet_in_range) { + found_packet_in_range = TRUE; + first_packet_time = packet->pts; + } + + // Check if this is an intra frame packet. If so, record the buffer + // position of the start of this packet. We want to jump back to it + // later, when we know it's the last intra frame before desired + // seek time. + if (force_intra) { + for (size_t i = 0; i < packet->length - 6; i++) { + // Find the START_PICTURE code + if ( + packet->data[i] == 0x00 && + packet->data[i + 1] == 0x00 && + packet->data[i + 2] == 0x01 && + packet->data[i + 3] == 0x00 + ) { + // Bits 11--13 in the picture header contain the frame + // type, where 1=Intra + if ((packet->data[i + 5] & 0x38) == 8) { + last_valid_packet_start = packet_start; + } + break; + } + } + } + + // If we don't want intra frames, just use the last PTS found. + else { + last_valid_packet_start = packet_start; + } + } + + // If there was at least one intra frame in the range scanned above, + // our search is over. Jump back to the packet and decode it again. + if (last_valid_packet_start != -1) { + plm_demux_buffer_seek(self, last_valid_packet_start); + return plm_demux_decode_packet(self, type); + } + + // If we hit the right range, but still found no intra frame, we have + // to increases the scan_span. This is done exponentially to also handle + // video files with very few intra frames. + else if (found_packet_in_range) { + scan_span *= 2; + seek_time = first_packet_time; + } + + // If we didn't find any packet with a PTS, it probably means we reached + // the end of the file. Estimate byterate and cur_time accordingly. + else if (!found_packet_with_pts) { + byterate = (seek_pos - cur_pos) / (duration - cur_time); + cur_time = duration; + } + } + + return NULL; +} + +plm_packet_t *plm_demux_decode(plm_demux_t *self) { + if (!plm_demux_has_headers(self)) { + return NULL; + } + + if (self->current_packet.length) { + size_t bits_till_next_packet = self->current_packet.length << 3; + if (!plm_buffer_has(self->buffer, bits_till_next_packet)) { + return NULL; + } + plm_buffer_skip(self->buffer, bits_till_next_packet); + self->current_packet.length = 0; + } + + // Pending packet waiting for data? + if (self->next_packet.length) { + return plm_demux_get_packet(self); + } + + // Pending packet waiting for header? + if (self->start_code != -1) { + return plm_demux_decode_packet(self, self->start_code); + } + + do { + self->start_code = plm_buffer_next_start_code(self->buffer); + if ( + self->start_code == PLM_DEMUX_PACKET_VIDEO_1 || + self->start_code == PLM_DEMUX_PACKET_PRIVATE || ( + self->start_code >= PLM_DEMUX_PACKET_AUDIO_1 && + self->start_code <= PLM_DEMUX_PACKET_AUDIO_4 + ) + ) { + return plm_demux_decode_packet(self, self->start_code); + } + } while (self->start_code != -1); + + return NULL; +} + +double plm_demux_decode_time(plm_demux_t *self) { + int64_t clock = plm_buffer_read(self->buffer, 3) << 30; + plm_buffer_skip(self->buffer, 1); + clock |= plm_buffer_read(self->buffer, 15) << 15; + plm_buffer_skip(self->buffer, 1); + clock |= plm_buffer_read(self->buffer, 15); + plm_buffer_skip(self->buffer, 1); + return (double)clock / 90000.0; +} + +plm_packet_t *plm_demux_decode_packet(plm_demux_t *self, int type) { + if (!plm_buffer_has(self->buffer, 16 << 3)) { + return NULL; + } + + self->start_code = -1; + + self->next_packet.type = type; + self->next_packet.length = plm_buffer_read(self->buffer, 16); + self->next_packet.length -= plm_buffer_skip_bytes(self->buffer, 0xff); // stuffing + + // skip P-STD + if (plm_buffer_read(self->buffer, 2) == 0x01) { + plm_buffer_skip(self->buffer, 16); + self->next_packet.length -= 2; + } + + int pts_dts_marker = plm_buffer_read(self->buffer, 2); + if (pts_dts_marker == 0x03) { + self->next_packet.pts = plm_demux_decode_time(self); + self->last_decoded_pts = self->next_packet.pts; + plm_buffer_skip(self->buffer, 40); // skip dts + self->next_packet.length -= 10; + } + else if (pts_dts_marker == 0x02) { + self->next_packet.pts = plm_demux_decode_time(self); + self->last_decoded_pts = self->next_packet.pts; + self->next_packet.length -= 5; + } + else if (pts_dts_marker == 0x00) { + self->next_packet.pts = PLM_PACKET_INVALID_TS; + plm_buffer_skip(self->buffer, 4); + self->next_packet.length -= 1; + } + else { + return NULL; // invalid + } + + return plm_demux_get_packet(self); +} + +plm_packet_t *plm_demux_get_packet(plm_demux_t *self) { + if (!plm_buffer_has(self->buffer, self->next_packet.length << 3)) { + return NULL; + } + + self->current_packet.data = self->buffer->bytes + (self->buffer->bit_index >> 3); + self->current_packet.length = self->next_packet.length; + self->current_packet.type = self->next_packet.type; + self->current_packet.pts = self->next_packet.pts; + + self->next_packet.length = 0; + return &self->current_packet; +} + + + +// ----------------------------------------------------------------------------- +// plm_video implementation + +// Inspired by Java MPEG-1 Video Decoder and Player by Zoltan Korandi +// https://sourceforge.net/projects/javampeg1video/ + +static const int PLM_VIDEO_PICTURE_TYPE_INTRA = 1; +static const int PLM_VIDEO_PICTURE_TYPE_PREDICTIVE = 2; +static const int PLM_VIDEO_PICTURE_TYPE_B = 3; + +static const int PLM_START_SEQUENCE = 0xB3; +static const int PLM_START_SLICE_FIRST = 0x01; +static const int PLM_START_SLICE_LAST = 0xAF; +static const int PLM_START_PICTURE = 0x00; +static const int PLM_START_EXTENSION = 0xB5; +static const int PLM_START_USER_DATA = 0xB2; + +#define PLM_START_IS_SLICE(c) \ + (c >= PLM_START_SLICE_FIRST && c <= PLM_START_SLICE_LAST) + +static const float PLM_VIDEO_PIXEL_ASPECT_RATIO[] = { + 1.0000, /* square pixels */ + 0.6735, /* 3:4? */ + 0.7031, /* MPEG-1 / MPEG-2 video encoding divergence? */ + 0.7615, 0.8055, 0.8437, 0.8935, 0.9157, 0.9815, + 1.0255, 1.0695, 1.0950, 1.1575, 1.2051, +}; + +static const double PLM_VIDEO_PICTURE_RATE[] = { + 0.000, 23.976, 24.000, 25.000, 29.970, 30.000, 50.000, 59.940, + 60.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000 +}; + +static const uint8_t PLM_VIDEO_ZIG_ZAG[] = { + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63 +}; + +static const uint8_t PLM_VIDEO_INTRA_QUANT_MATRIX[] = { + 8, 16, 19, 22, 26, 27, 29, 34, + 16, 16, 22, 24, 27, 29, 34, 37, + 19, 22, 26, 27, 29, 34, 34, 38, + 22, 22, 26, 27, 29, 34, 37, 40, + 22, 26, 27, 29, 32, 35, 40, 48, + 26, 27, 29, 32, 35, 40, 48, 58, + 26, 27, 29, 34, 38, 46, 56, 69, + 27, 29, 35, 38, 46, 56, 69, 83 +}; + +static const uint8_t PLM_VIDEO_NON_INTRA_QUANT_MATRIX[] = { + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16, + 16, 16, 16, 16, 16, 16, 16, 16 +}; + +static const uint8_t PLM_VIDEO_PREMULTIPLIER_MATRIX[] = { + 32, 44, 42, 38, 32, 25, 17, 9, + 44, 62, 58, 52, 44, 35, 24, 12, + 42, 58, 55, 49, 42, 33, 23, 12, + 38, 52, 49, 44, 38, 30, 20, 10, + 32, 44, 42, 38, 32, 25, 17, 9, + 25, 35, 33, 30, 25, 20, 14, 7, + 17, 24, 23, 20, 17, 14, 9, 5, + 9, 12, 12, 10, 9, 7, 5, 2 +}; + +static const plm_vlc_t PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT[] = { + { 1 << 1, 0}, { 0, 1}, // 0: x + { 2 << 1, 0}, { 3 << 1, 0}, // 1: 0x + { 4 << 1, 0}, { 5 << 1, 0}, // 2: 00x + { 0, 3}, { 0, 2}, // 3: 01x + { 6 << 1, 0}, { 7 << 1, 0}, // 4: 000x + { 0, 5}, { 0, 4}, // 5: 001x + { 8 << 1, 0}, { 9 << 1, 0}, // 6: 0000x + { 0, 7}, { 0, 6}, // 7: 0001x + { 10 << 1, 0}, { 11 << 1, 0}, // 8: 0000 0x + { 12 << 1, 0}, { 13 << 1, 0}, // 9: 0000 1x + { 14 << 1, 0}, { 15 << 1, 0}, // 10: 0000 00x + { 16 << 1, 0}, { 17 << 1, 0}, // 11: 0000 01x + { 18 << 1, 0}, { 19 << 1, 0}, // 12: 0000 10x + { 0, 9}, { 0, 8}, // 13: 0000 11x + { -1, 0}, { 20 << 1, 0}, // 14: 0000 000x + { -1, 0}, { 21 << 1, 0}, // 15: 0000 001x + { 22 << 1, 0}, { 23 << 1, 0}, // 16: 0000 010x + { 0, 15}, { 0, 14}, // 17: 0000 011x + { 0, 13}, { 0, 12}, // 18: 0000 100x + { 0, 11}, { 0, 10}, // 19: 0000 101x + { 24 << 1, 0}, { 25 << 1, 0}, // 20: 0000 0001x + { 26 << 1, 0}, { 27 << 1, 0}, // 21: 0000 0011x + { 28 << 1, 0}, { 29 << 1, 0}, // 22: 0000 0100x + { 30 << 1, 0}, { 31 << 1, 0}, // 23: 0000 0101x + { 32 << 1, 0}, { -1, 0}, // 24: 0000 0001 0x + { -1, 0}, { 33 << 1, 0}, // 25: 0000 0001 1x + { 34 << 1, 0}, { 35 << 1, 0}, // 26: 0000 0011 0x + { 36 << 1, 0}, { 37 << 1, 0}, // 27: 0000 0011 1x + { 38 << 1, 0}, { 39 << 1, 0}, // 28: 0000 0100 0x + { 0, 21}, { 0, 20}, // 29: 0000 0100 1x + { 0, 19}, { 0, 18}, // 30: 0000 0101 0x + { 0, 17}, { 0, 16}, // 31: 0000 0101 1x + { 0, 35}, { -1, 0}, // 32: 0000 0001 00x + { -1, 0}, { 0, 34}, // 33: 0000 0001 11x + { 0, 33}, { 0, 32}, // 34: 0000 0011 00x + { 0, 31}, { 0, 30}, // 35: 0000 0011 01x + { 0, 29}, { 0, 28}, // 36: 0000 0011 10x + { 0, 27}, { 0, 26}, // 37: 0000 0011 11x + { 0, 25}, { 0, 24}, // 38: 0000 0100 00x + { 0, 23}, { 0, 22}, // 39: 0000 0100 01x +}; + +static const plm_vlc_t PLM_VIDEO_MACROBLOCK_TYPE_INTRA[] = { + { 1 << 1, 0}, { 0, 0x01}, // 0: x + { -1, 0}, { 0, 0x11}, // 1: 0x +}; + +static const plm_vlc_t PLM_VIDEO_MACROBLOCK_TYPE_PREDICTIVE[] = { + { 1 << 1, 0}, { 0, 0x0a}, // 0: x + { 2 << 1, 0}, { 0, 0x02}, // 1: 0x + { 3 << 1, 0}, { 0, 0x08}, // 2: 00x + { 4 << 1, 0}, { 5 << 1, 0}, // 3: 000x + { 6 << 1, 0}, { 0, 0x12}, // 4: 0000x + { 0, 0x1a}, { 0, 0x01}, // 5: 0001x + { -1, 0}, { 0, 0x11}, // 6: 0000 0x +}; + +static const plm_vlc_t PLM_VIDEO_MACROBLOCK_TYPE_B[] = { + { 1 << 1, 0}, { 2 << 1, 0}, // 0: x + { 3 << 1, 0}, { 4 << 1, 0}, // 1: 0x + { 0, 0x0c}, { 0, 0x0e}, // 2: 1x + { 5 << 1, 0}, { 6 << 1, 0}, // 3: 00x + { 0, 0x04}, { 0, 0x06}, // 4: 01x + { 7 << 1, 0}, { 8 << 1, 0}, // 5: 000x + { 0, 0x08}, { 0, 0x0a}, // 6: 001x + { 9 << 1, 0}, { 10 << 1, 0}, // 7: 0000x + { 0, 0x1e}, { 0, 0x01}, // 8: 0001x + { -1, 0}, { 0, 0x11}, // 9: 0000 0x + { 0, 0x16}, { 0, 0x1a}, // 10: 0000 1x +}; + +static const plm_vlc_t *PLM_VIDEO_MACROBLOCK_TYPE[] = { + NULL, + PLM_VIDEO_MACROBLOCK_TYPE_INTRA, + PLM_VIDEO_MACROBLOCK_TYPE_PREDICTIVE, + PLM_VIDEO_MACROBLOCK_TYPE_B +}; + +static const plm_vlc_t PLM_VIDEO_CODE_BLOCK_PATTERN[] = { + { 1 << 1, 0}, { 2 << 1, 0}, // 0: x + { 3 << 1, 0}, { 4 << 1, 0}, // 1: 0x + { 5 << 1, 0}, { 6 << 1, 0}, // 2: 1x + { 7 << 1, 0}, { 8 << 1, 0}, // 3: 00x + { 9 << 1, 0}, { 10 << 1, 0}, // 4: 01x + { 11 << 1, 0}, { 12 << 1, 0}, // 5: 10x + { 13 << 1, 0}, { 0, 60}, // 6: 11x + { 14 << 1, 0}, { 15 << 1, 0}, // 7: 000x + { 16 << 1, 0}, { 17 << 1, 0}, // 8: 001x + { 18 << 1, 0}, { 19 << 1, 0}, // 9: 010x + { 20 << 1, 0}, { 21 << 1, 0}, // 10: 011x + { 22 << 1, 0}, { 23 << 1, 0}, // 11: 100x + { 0, 32}, { 0, 16}, // 12: 101x + { 0, 8}, { 0, 4}, // 13: 110x + { 24 << 1, 0}, { 25 << 1, 0}, // 14: 0000x + { 26 << 1, 0}, { 27 << 1, 0}, // 15: 0001x + { 28 << 1, 0}, { 29 << 1, 0}, // 16: 0010x + { 30 << 1, 0}, { 31 << 1, 0}, // 17: 0011x + { 0, 62}, { 0, 2}, // 18: 0100x + { 0, 61}, { 0, 1}, // 19: 0101x + { 0, 56}, { 0, 52}, // 20: 0110x + { 0, 44}, { 0, 28}, // 21: 0111x + { 0, 40}, { 0, 20}, // 22: 1000x + { 0, 48}, { 0, 12}, // 23: 1001x + { 32 << 1, 0}, { 33 << 1, 0}, // 24: 0000 0x + { 34 << 1, 0}, { 35 << 1, 0}, // 25: 0000 1x + { 36 << 1, 0}, { 37 << 1, 0}, // 26: 0001 0x + { 38 << 1, 0}, { 39 << 1, 0}, // 27: 0001 1x + { 40 << 1, 0}, { 41 << 1, 0}, // 28: 0010 0x + { 42 << 1, 0}, { 43 << 1, 0}, // 29: 0010 1x + { 0, 63}, { 0, 3}, // 30: 0011 0x + { 0, 36}, { 0, 24}, // 31: 0011 1x + { 44 << 1, 0}, { 45 << 1, 0}, // 32: 0000 00x + { 46 << 1, 0}, { 47 << 1, 0}, // 33: 0000 01x + { 48 << 1, 0}, { 49 << 1, 0}, // 34: 0000 10x + { 50 << 1, 0}, { 51 << 1, 0}, // 35: 0000 11x + { 52 << 1, 0}, { 53 << 1, 0}, // 36: 0001 00x + { 54 << 1, 0}, { 55 << 1, 0}, // 37: 0001 01x + { 56 << 1, 0}, { 57 << 1, 0}, // 38: 0001 10x + { 58 << 1, 0}, { 59 << 1, 0}, // 39: 0001 11x + { 0, 34}, { 0, 18}, // 40: 0010 00x + { 0, 10}, { 0, 6}, // 41: 0010 01x + { 0, 33}, { 0, 17}, // 42: 0010 10x + { 0, 9}, { 0, 5}, // 43: 0010 11x + { -1, 0}, { 60 << 1, 0}, // 44: 0000 000x + { 61 << 1, 0}, { 62 << 1, 0}, // 45: 0000 001x + { 0, 58}, { 0, 54}, // 46: 0000 010x + { 0, 46}, { 0, 30}, // 47: 0000 011x + { 0, 57}, { 0, 53}, // 48: 0000 100x + { 0, 45}, { 0, 29}, // 49: 0000 101x + { 0, 38}, { 0, 26}, // 50: 0000 110x + { 0, 37}, { 0, 25}, // 51: 0000 111x + { 0, 43}, { 0, 23}, // 52: 0001 000x + { 0, 51}, { 0, 15}, // 53: 0001 001x + { 0, 42}, { 0, 22}, // 54: 0001 010x + { 0, 50}, { 0, 14}, // 55: 0001 011x + { 0, 41}, { 0, 21}, // 56: 0001 100x + { 0, 49}, { 0, 13}, // 57: 0001 101x + { 0, 35}, { 0, 19}, // 58: 0001 110x + { 0, 11}, { 0, 7}, // 59: 0001 111x + { 0, 39}, { 0, 27}, // 60: 0000 0001x + { 0, 59}, { 0, 55}, // 61: 0000 0010x + { 0, 47}, { 0, 31}, // 62: 0000 0011x +}; + +static const plm_vlc_t PLM_VIDEO_MOTION[] = { + { 1 << 1, 0}, { 0, 0}, // 0: x + { 2 << 1, 0}, { 3 << 1, 0}, // 1: 0x + { 4 << 1, 0}, { 5 << 1, 0}, // 2: 00x + { 0, 1}, { 0, -1}, // 3: 01x + { 6 << 1, 0}, { 7 << 1, 0}, // 4: 000x + { 0, 2}, { 0, -2}, // 5: 001x + { 8 << 1, 0}, { 9 << 1, 0}, // 6: 0000x + { 0, 3}, { 0, -3}, // 7: 0001x + { 10 << 1, 0}, { 11 << 1, 0}, // 8: 0000 0x + { 12 << 1, 0}, { 13 << 1, 0}, // 9: 0000 1x + { -1, 0}, { 14 << 1, 0}, // 10: 0000 00x + { 15 << 1, 0}, { 16 << 1, 0}, // 11: 0000 01x + { 17 << 1, 0}, { 18 << 1, 0}, // 12: 0000 10x + { 0, 4}, { 0, -4}, // 13: 0000 11x + { -1, 0}, { 19 << 1, 0}, // 14: 0000 001x + { 20 << 1, 0}, { 21 << 1, 0}, // 15: 0000 010x + { 0, 7}, { 0, -7}, // 16: 0000 011x + { 0, 6}, { 0, -6}, // 17: 0000 100x + { 0, 5}, { 0, -5}, // 18: 0000 101x + { 22 << 1, 0}, { 23 << 1, 0}, // 19: 0000 0011x + { 24 << 1, 0}, { 25 << 1, 0}, // 20: 0000 0100x + { 26 << 1, 0}, { 27 << 1, 0}, // 21: 0000 0101x + { 28 << 1, 0}, { 29 << 1, 0}, // 22: 0000 0011 0x + { 30 << 1, 0}, { 31 << 1, 0}, // 23: 0000 0011 1x + { 32 << 1, 0}, { 33 << 1, 0}, // 24: 0000 0100 0x + { 0, 10}, { 0, -10}, // 25: 0000 0100 1x + { 0, 9}, { 0, -9}, // 26: 0000 0101 0x + { 0, 8}, { 0, -8}, // 27: 0000 0101 1x + { 0, 16}, { 0, -16}, // 28: 0000 0011 00x + { 0, 15}, { 0, -15}, // 29: 0000 0011 01x + { 0, 14}, { 0, -14}, // 30: 0000 0011 10x + { 0, 13}, { 0, -13}, // 31: 0000 0011 11x + { 0, 12}, { 0, -12}, // 32: 0000 0100 00x + { 0, 11}, { 0, -11}, // 33: 0000 0100 01x +}; + +static const plm_vlc_t PLM_VIDEO_DCT_SIZE_LUMINANCE[] = { + { 1 << 1, 0}, { 2 << 1, 0}, // 0: x + { 0, 1}, { 0, 2}, // 1: 0x + { 3 << 1, 0}, { 4 << 1, 0}, // 2: 1x + { 0, 0}, { 0, 3}, // 3: 10x + { 0, 4}, { 5 << 1, 0}, // 4: 11x + { 0, 5}, { 6 << 1, 0}, // 5: 111x + { 0, 6}, { 7 << 1, 0}, // 6: 1111x + { 0, 7}, { 8 << 1, 0}, // 7: 1111 1x + { 0, 8}, { -1, 0}, // 8: 1111 11x +}; + +static const plm_vlc_t PLM_VIDEO_DCT_SIZE_CHROMINANCE[] = { + { 1 << 1, 0}, { 2 << 1, 0}, // 0: x + { 0, 0}, { 0, 1}, // 1: 0x + { 0, 2}, { 3 << 1, 0}, // 2: 1x + { 0, 3}, { 4 << 1, 0}, // 3: 11x + { 0, 4}, { 5 << 1, 0}, // 4: 111x + { 0, 5}, { 6 << 1, 0}, // 5: 1111x + { 0, 6}, { 7 << 1, 0}, // 6: 1111 1x + { 0, 7}, { 8 << 1, 0}, // 7: 1111 11x + { 0, 8}, { -1, 0}, // 8: 1111 111x +}; + +static const plm_vlc_t *PLM_VIDEO_DCT_SIZE[] = { + PLM_VIDEO_DCT_SIZE_LUMINANCE, + PLM_VIDEO_DCT_SIZE_CHROMINANCE, + PLM_VIDEO_DCT_SIZE_CHROMINANCE +}; + + +// dct_coeff bitmap: +// 0xff00 run +// 0x00ff level + +// Decoded values are unsigned. Sign bit follows in the stream. + +static const plm_vlc_uint_t PLM_VIDEO_DCT_COEFF[] = { + { 1 << 1, 0}, { 0, 0x0001}, // 0: x + { 2 << 1, 0}, { 3 << 1, 0}, // 1: 0x + { 4 << 1, 0}, { 5 << 1, 0}, // 2: 00x + { 6 << 1, 0}, { 0, 0x0101}, // 3: 01x + { 7 << 1, 0}, { 8 << 1, 0}, // 4: 000x + { 9 << 1, 0}, { 10 << 1, 0}, // 5: 001x + { 0, 0x0002}, { 0, 0x0201}, // 6: 010x + { 11 << 1, 0}, { 12 << 1, 0}, // 7: 0000x + { 13 << 1, 0}, { 14 << 1, 0}, // 8: 0001x + { 15 << 1, 0}, { 0, 0x0003}, // 9: 0010x + { 0, 0x0401}, { 0, 0x0301}, // 10: 0011x + { 16 << 1, 0}, { 0, 0xffff}, // 11: 0000 0x + { 17 << 1, 0}, { 18 << 1, 0}, // 12: 0000 1x + { 0, 0x0701}, { 0, 0x0601}, // 13: 0001 0x + { 0, 0x0102}, { 0, 0x0501}, // 14: 0001 1x + { 19 << 1, 0}, { 20 << 1, 0}, // 15: 0010 0x + { 21 << 1, 0}, { 22 << 1, 0}, // 16: 0000 00x + { 0, 0x0202}, { 0, 0x0901}, // 17: 0000 10x + { 0, 0x0004}, { 0, 0x0801}, // 18: 0000 11x + { 23 << 1, 0}, { 24 << 1, 0}, // 19: 0010 00x + { 25 << 1, 0}, { 26 << 1, 0}, // 20: 0010 01x + { 27 << 1, 0}, { 28 << 1, 0}, // 21: 0000 000x + { 29 << 1, 0}, { 30 << 1, 0}, // 22: 0000 001x + { 0, 0x0d01}, { 0, 0x0006}, // 23: 0010 000x + { 0, 0x0c01}, { 0, 0x0b01}, // 24: 0010 001x + { 0, 0x0302}, { 0, 0x0103}, // 25: 0010 010x + { 0, 0x0005}, { 0, 0x0a01}, // 26: 0010 011x + { 31 << 1, 0}, { 32 << 1, 0}, // 27: 0000 0000x + { 33 << 1, 0}, { 34 << 1, 0}, // 28: 0000 0001x + { 35 << 1, 0}, { 36 << 1, 0}, // 29: 0000 0010x + { 37 << 1, 0}, { 38 << 1, 0}, // 30: 0000 0011x + { 39 << 1, 0}, { 40 << 1, 0}, // 31: 0000 0000 0x + { 41 << 1, 0}, { 42 << 1, 0}, // 32: 0000 0000 1x + { 43 << 1, 0}, { 44 << 1, 0}, // 33: 0000 0001 0x + { 45 << 1, 0}, { 46 << 1, 0}, // 34: 0000 0001 1x + { 0, 0x1001}, { 0, 0x0502}, // 35: 0000 0010 0x + { 0, 0x0007}, { 0, 0x0203}, // 36: 0000 0010 1x + { 0, 0x0104}, { 0, 0x0f01}, // 37: 0000 0011 0x + { 0, 0x0e01}, { 0, 0x0402}, // 38: 0000 0011 1x + { 47 << 1, 0}, { 48 << 1, 0}, // 39: 0000 0000 00x + { 49 << 1, 0}, { 50 << 1, 0}, // 40: 0000 0000 01x + { 51 << 1, 0}, { 52 << 1, 0}, // 41: 0000 0000 10x + { 53 << 1, 0}, { 54 << 1, 0}, // 42: 0000 0000 11x + { 55 << 1, 0}, { 56 << 1, 0}, // 43: 0000 0001 00x + { 57 << 1, 0}, { 58 << 1, 0}, // 44: 0000 0001 01x + { 59 << 1, 0}, { 60 << 1, 0}, // 45: 0000 0001 10x + { 61 << 1, 0}, { 62 << 1, 0}, // 46: 0000 0001 11x + { -1, 0}, { 63 << 1, 0}, // 47: 0000 0000 000x + { 64 << 1, 0}, { 65 << 1, 0}, // 48: 0000 0000 001x + { 66 << 1, 0}, { 67 << 1, 0}, // 49: 0000 0000 010x + { 68 << 1, 0}, { 69 << 1, 0}, // 50: 0000 0000 011x + { 70 << 1, 0}, { 71 << 1, 0}, // 51: 0000 0000 100x + { 72 << 1, 0}, { 73 << 1, 0}, // 52: 0000 0000 101x + { 74 << 1, 0}, { 75 << 1, 0}, // 53: 0000 0000 110x + { 76 << 1, 0}, { 77 << 1, 0}, // 54: 0000 0000 111x + { 0, 0x000b}, { 0, 0x0802}, // 55: 0000 0001 000x + { 0, 0x0403}, { 0, 0x000a}, // 56: 0000 0001 001x + { 0, 0x0204}, { 0, 0x0702}, // 57: 0000 0001 010x + { 0, 0x1501}, { 0, 0x1401}, // 58: 0000 0001 011x + { 0, 0x0009}, { 0, 0x1301}, // 59: 0000 0001 100x + { 0, 0x1201}, { 0, 0x0105}, // 60: 0000 0001 101x + { 0, 0x0303}, { 0, 0x0008}, // 61: 0000 0001 110x + { 0, 0x0602}, { 0, 0x1101}, // 62: 0000 0001 111x + { 78 << 1, 0}, { 79 << 1, 0}, // 63: 0000 0000 0001x + { 80 << 1, 0}, { 81 << 1, 0}, // 64: 0000 0000 0010x + { 82 << 1, 0}, { 83 << 1, 0}, // 65: 0000 0000 0011x + { 84 << 1, 0}, { 85 << 1, 0}, // 66: 0000 0000 0100x + { 86 << 1, 0}, { 87 << 1, 0}, // 67: 0000 0000 0101x + { 88 << 1, 0}, { 89 << 1, 0}, // 68: 0000 0000 0110x + { 90 << 1, 0}, { 91 << 1, 0}, // 69: 0000 0000 0111x + { 0, 0x0a02}, { 0, 0x0902}, // 70: 0000 0000 1000x + { 0, 0x0503}, { 0, 0x0304}, // 71: 0000 0000 1001x + { 0, 0x0205}, { 0, 0x0107}, // 72: 0000 0000 1010x + { 0, 0x0106}, { 0, 0x000f}, // 73: 0000 0000 1011x + { 0, 0x000e}, { 0, 0x000d}, // 74: 0000 0000 1100x + { 0, 0x000c}, { 0, 0x1a01}, // 75: 0000 0000 1101x + { 0, 0x1901}, { 0, 0x1801}, // 76: 0000 0000 1110x + { 0, 0x1701}, { 0, 0x1601}, // 77: 0000 0000 1111x + { 92 << 1, 0}, { 93 << 1, 0}, // 78: 0000 0000 0001 0x + { 94 << 1, 0}, { 95 << 1, 0}, // 79: 0000 0000 0001 1x + { 96 << 1, 0}, { 97 << 1, 0}, // 80: 0000 0000 0010 0x + { 98 << 1, 0}, { 99 << 1, 0}, // 81: 0000 0000 0010 1x + {100 << 1, 0}, {101 << 1, 0}, // 82: 0000 0000 0011 0x + {102 << 1, 0}, {103 << 1, 0}, // 83: 0000 0000 0011 1x + { 0, 0x001f}, { 0, 0x001e}, // 84: 0000 0000 0100 0x + { 0, 0x001d}, { 0, 0x001c}, // 85: 0000 0000 0100 1x + { 0, 0x001b}, { 0, 0x001a}, // 86: 0000 0000 0101 0x + { 0, 0x0019}, { 0, 0x0018}, // 87: 0000 0000 0101 1x + { 0, 0x0017}, { 0, 0x0016}, // 88: 0000 0000 0110 0x + { 0, 0x0015}, { 0, 0x0014}, // 89: 0000 0000 0110 1x + { 0, 0x0013}, { 0, 0x0012}, // 90: 0000 0000 0111 0x + { 0, 0x0011}, { 0, 0x0010}, // 91: 0000 0000 0111 1x + {104 << 1, 0}, {105 << 1, 0}, // 92: 0000 0000 0001 00x + {106 << 1, 0}, {107 << 1, 0}, // 93: 0000 0000 0001 01x + {108 << 1, 0}, {109 << 1, 0}, // 94: 0000 0000 0001 10x + {110 << 1, 0}, {111 << 1, 0}, // 95: 0000 0000 0001 11x + { 0, 0x0028}, { 0, 0x0027}, // 96: 0000 0000 0010 00x + { 0, 0x0026}, { 0, 0x0025}, // 97: 0000 0000 0010 01x + { 0, 0x0024}, { 0, 0x0023}, // 98: 0000 0000 0010 10x + { 0, 0x0022}, { 0, 0x0021}, // 99: 0000 0000 0010 11x + { 0, 0x0020}, { 0, 0x010e}, // 100: 0000 0000 0011 00x + { 0, 0x010d}, { 0, 0x010c}, // 101: 0000 0000 0011 01x + { 0, 0x010b}, { 0, 0x010a}, // 102: 0000 0000 0011 10x + { 0, 0x0109}, { 0, 0x0108}, // 103: 0000 0000 0011 11x + { 0, 0x0112}, { 0, 0x0111}, // 104: 0000 0000 0001 000x + { 0, 0x0110}, { 0, 0x010f}, // 105: 0000 0000 0001 001x + { 0, 0x0603}, { 0, 0x1002}, // 106: 0000 0000 0001 010x + { 0, 0x0f02}, { 0, 0x0e02}, // 107: 0000 0000 0001 011x + { 0, 0x0d02}, { 0, 0x0c02}, // 108: 0000 0000 0001 100x + { 0, 0x0b02}, { 0, 0x1f01}, // 109: 0000 0000 0001 101x + { 0, 0x1e01}, { 0, 0x1d01}, // 110: 0000 0000 0001 110x + { 0, 0x1c01}, { 0, 0x1b01}, // 111: 0000 0000 0001 111x +}; + +typedef struct { + int full_px; + int is_set; + int r_size; + int h; + int v; +} plm_video_motion_t; + +struct plm_video_t { + double framerate; + double pixel_aspect_ratio; + double time; + int frames_decoded; + int width; + int height; + int mb_width; + int mb_height; + int mb_size; + + int luma_width; + int luma_height; + + int chroma_width; + int chroma_height; + + int start_code; + int picture_type; + + plm_video_motion_t motion_forward; + plm_video_motion_t motion_backward; + + int has_sequence_header; + + int quantizer_scale; + int slice_begin; + int macroblock_address; + + int mb_row; + int mb_col; + + int macroblock_type; + int macroblock_intra; + + int dc_predictor[3]; + + plm_buffer_t *buffer; + int destroy_buffer_when_done; + + plm_frame_t frame_current; + plm_frame_t frame_forward; + plm_frame_t frame_backward; + + uint8_t *frames_data; + + int block_data[64]; + uint8_t intra_quant_matrix[64]; + uint8_t non_intra_quant_matrix[64]; + + int has_reference_frame; + int assume_no_b_frames; +}; + +static inline uint8_t plm_clamp(int n) { + if (n > 255) { + n = 255; + } + else if (n < 0) { + n = 0; + } + return n; +} + +int plm_video_decode_sequence_header(plm_video_t *self); +void plm_video_init_frame(plm_video_t *self, plm_frame_t *frame, uint8_t *base); +void plm_video_decode_picture(plm_video_t *self); +void plm_video_decode_slice(plm_video_t *self, int slice); +void plm_video_decode_macroblock(plm_video_t *self); +void plm_video_decode_motion_vectors(plm_video_t *self); +int plm_video_decode_motion_vector(plm_video_t *self, int r_size, int motion); +void plm_video_predict_macroblock(plm_video_t *self); +void plm_video_copy_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v); +void plm_video_interpolate_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v); +void plm_video_process_macroblock(plm_video_t *self, uint8_t *s, uint8_t *d, int mh, int mb, int bs, int interp); +void plm_video_decode_block(plm_video_t *self, int block); +void plm_video_idct(int *block); + +plm_video_t * plm_video_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done) { + plm_video_t *self = (plm_video_t *)PLM_MALLOC(sizeof(plm_video_t)); + memset(self, 0, sizeof(plm_video_t)); + + self->buffer = buffer; + self->destroy_buffer_when_done = destroy_when_done; + + // Attempt to decode the sequence header + self->start_code = plm_buffer_find_start_code(self->buffer, PLM_START_SEQUENCE); + if (self->start_code != -1) { + plm_video_decode_sequence_header(self); + } + return self; +} + +void plm_video_destroy(plm_video_t *self) { + if (self->destroy_buffer_when_done) { + plm_buffer_destroy(self->buffer); + } + + if (self->has_sequence_header) { + PLM_FREE(self->frames_data); + } + + PLM_FREE(self); +} + +double plm_video_get_framerate(plm_video_t *self) { + return plm_video_has_header(self) + ? self->framerate + : 0; +} + +double plm_video_get_pixel_aspect_ratio(plm_video_t *self) { + return plm_video_has_header(self) + ? self->pixel_aspect_ratio + : 0; +} + +int plm_video_get_width(plm_video_t *self) { + return plm_video_has_header(self) + ? self->width + : 0; +} + +int plm_video_get_height(plm_video_t *self) { + return plm_video_has_header(self) + ? self->height + : 0; +} + +void plm_video_set_no_delay(plm_video_t *self, int no_delay) { + self->assume_no_b_frames = no_delay; +} + +double plm_video_get_time(plm_video_t *self) { + return self->time; +} + +void plm_video_set_time(plm_video_t *self, double time) { + self->frames_decoded = self->framerate * time; + self->time = time; +} + +void plm_video_rewind(plm_video_t *self) { + plm_buffer_rewind(self->buffer); + self->time = 0; + self->frames_decoded = 0; + self->has_reference_frame = FALSE; + self->start_code = -1; +} + +int plm_video_has_ended(plm_video_t *self) { + return plm_buffer_has_ended(self->buffer); +} + +plm_frame_t *plm_video_decode(plm_video_t *self) { + if (!plm_video_has_header(self)) { + return NULL; + } + + plm_frame_t *frame = NULL; + do { + if (self->start_code != PLM_START_PICTURE) { + self->start_code = plm_buffer_find_start_code(self->buffer, PLM_START_PICTURE); + + if (self->start_code == -1) { + // If we reached the end of the file and the previously decoded + // frame was a reference frame, we still have to return it. + if ( + self->has_reference_frame && + !self->assume_no_b_frames && + plm_buffer_has_ended(self->buffer) && ( + self->picture_type == PLM_VIDEO_PICTURE_TYPE_INTRA || + self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE + ) + ) { + self->has_reference_frame = FALSE; + frame = &self->frame_backward; + break; + } + + return NULL; + } + } + + // Make sure we have a full picture in the buffer before attempting to + // decode it. Sadly, this can only be done by seeking for the start code + // of the next picture. Also, if we didn't find the start code for the + // next picture, but the source has ended, we assume that this last + // picture is in the buffer. + if ( + plm_buffer_has_start_code(self->buffer, PLM_START_PICTURE) == -1 && + !plm_buffer_has_ended(self->buffer) + ) { + return NULL; + } + plm_buffer_discard_read_bytes(self->buffer); + + plm_video_decode_picture(self); + + if (self->assume_no_b_frames) { + frame = &self->frame_backward; + } + else if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) { + frame = &self->frame_current; + } + else if (self->has_reference_frame) { + frame = &self->frame_forward; + } + else { + self->has_reference_frame = TRUE; + } + } while (!frame); + + frame->time = self->time; + self->frames_decoded++; + self->time = (double)self->frames_decoded / self->framerate; + + return frame; +} + +int plm_video_has_header(plm_video_t *self) { + if (self->has_sequence_header) { + return TRUE; + } + + if (self->start_code != PLM_START_SEQUENCE) { + self->start_code = plm_buffer_find_start_code(self->buffer, PLM_START_SEQUENCE); + } + if (self->start_code == -1) { + return FALSE; + } + + if (!plm_video_decode_sequence_header(self)) { + return FALSE; + } + + return TRUE; +} + +int plm_video_decode_sequence_header(plm_video_t *self) { + int max_header_size = 64 + 2 * 64 * 8; // 64 bit header + 2x 64 byte matrix + if (!plm_buffer_has(self->buffer, max_header_size)) { + return FALSE; + } + + self->width = plm_buffer_read(self->buffer, 12); + self->height = plm_buffer_read(self->buffer, 12); + + if (self->width <= 0 || self->height <= 0) { + return FALSE; + } + + // Get pixel aspect ratio + int pixel_aspect_ratio_code; + pixel_aspect_ratio_code = plm_buffer_read(self->buffer, 4); + pixel_aspect_ratio_code -= 1; + if (pixel_aspect_ratio_code < 0) { + pixel_aspect_ratio_code = 0; + } + int par_last = (sizeof(PLM_VIDEO_PIXEL_ASPECT_RATIO) / + sizeof(PLM_VIDEO_PIXEL_ASPECT_RATIO[0]) - 1); + if (pixel_aspect_ratio_code > par_last) { + pixel_aspect_ratio_code = par_last; + } + self->pixel_aspect_ratio = + PLM_VIDEO_PIXEL_ASPECT_RATIO[pixel_aspect_ratio_code]; + + // Get frame rate + self->framerate = PLM_VIDEO_PICTURE_RATE[plm_buffer_read(self->buffer, 4)]; + + // Skip bit_rate, marker, buffer_size and constrained bit + plm_buffer_skip(self->buffer, 18 + 1 + 10 + 1); + + // Load custom intra quant matrix? + if (plm_buffer_read(self->buffer, 1)) { + for (int i = 0; i < 64; i++) { + int idx = PLM_VIDEO_ZIG_ZAG[i]; + self->intra_quant_matrix[idx] = plm_buffer_read(self->buffer, 8); + } + } + else { + memcpy(self->intra_quant_matrix, PLM_VIDEO_INTRA_QUANT_MATRIX, 64); + } + + // Load custom non intra quant matrix? + if (plm_buffer_read(self->buffer, 1)) { + for (int i = 0; i < 64; i++) { + int idx = PLM_VIDEO_ZIG_ZAG[i]; + self->non_intra_quant_matrix[idx] = plm_buffer_read(self->buffer, 8); + } + } + else { + memcpy(self->non_intra_quant_matrix, PLM_VIDEO_NON_INTRA_QUANT_MATRIX, 64); + } + + self->mb_width = (self->width + 15) >> 4; + self->mb_height = (self->height + 15) >> 4; + self->mb_size = self->mb_width * self->mb_height; + + self->luma_width = self->mb_width << 4; + self->luma_height = self->mb_height << 4; + + self->chroma_width = self->mb_width << 3; + self->chroma_height = self->mb_height << 3; + + + // Allocate one big chunk of data for all 3 frames = 9 planes + size_t luma_plane_size = self->luma_width * self->luma_height; + size_t chroma_plane_size = self->chroma_width * self->chroma_height; + size_t frame_data_size = (luma_plane_size + 2 * chroma_plane_size); + + self->frames_data = (uint8_t*)PLM_MALLOC(frame_data_size * 3); + plm_video_init_frame(self, &self->frame_current, self->frames_data + frame_data_size * 0); + plm_video_init_frame(self, &self->frame_forward, self->frames_data + frame_data_size * 1); + plm_video_init_frame(self, &self->frame_backward, self->frames_data + frame_data_size * 2); + + self->has_sequence_header = TRUE; + return TRUE; +} + +void plm_video_init_frame(plm_video_t *self, plm_frame_t *frame, uint8_t *base) { + size_t luma_plane_size = self->luma_width * self->luma_height; + size_t chroma_plane_size = self->chroma_width * self->chroma_height; + + frame->width = self->width; + frame->height = self->height; + frame->y.width = self->luma_width; + frame->y.height = self->luma_height; + frame->y.data = base; + + frame->cr.width = self->chroma_width; + frame->cr.height = self->chroma_height; + frame->cr.data = base + luma_plane_size; + + frame->cb.width = self->chroma_width; + frame->cb.height = self->chroma_height; + frame->cb.data = base + luma_plane_size + chroma_plane_size; +} + +void plm_video_decode_picture(plm_video_t *self) { + plm_buffer_skip(self->buffer, 10); // skip temporalReference + self->picture_type = plm_buffer_read(self->buffer, 3); + plm_buffer_skip(self->buffer, 16); // skip vbv_delay + + // D frames or unknown coding type + if (self->picture_type <= 0 || self->picture_type > PLM_VIDEO_PICTURE_TYPE_B) { + return; + } + + // Forward full_px, f_code + if ( + self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE || + self->picture_type == PLM_VIDEO_PICTURE_TYPE_B + ) { + self->motion_forward.full_px = plm_buffer_read(self->buffer, 1); + int f_code = plm_buffer_read(self->buffer, 3); + if (f_code == 0) { + // Ignore picture with zero f_code + return; + } + self->motion_forward.r_size = f_code - 1; + } + + // Backward full_px, f_code + if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) { + self->motion_backward.full_px = plm_buffer_read(self->buffer, 1); + int f_code = plm_buffer_read(self->buffer, 3); + if (f_code == 0) { + // Ignore picture with zero f_code + return; + } + self->motion_backward.r_size = f_code - 1; + } + + plm_frame_t frame_temp = self->frame_forward; + if ( + self->picture_type == PLM_VIDEO_PICTURE_TYPE_INTRA || + self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE + ) { + self->frame_forward = self->frame_backward; + } + + + // Find first slice start code; skip extension and user data + do { + self->start_code = plm_buffer_next_start_code(self->buffer); + } while ( + self->start_code == PLM_START_EXTENSION || + self->start_code == PLM_START_USER_DATA + ); + + // Decode all slices + while (PLM_START_IS_SLICE(self->start_code)) { + plm_video_decode_slice(self, self->start_code & 0x000000FF); + if (self->macroblock_address >= self->mb_size - 1) { + break; + } + self->start_code = plm_buffer_next_start_code(self->buffer); + } + + // If this is a reference picture rotate the prediction pointers + if ( + self->picture_type == PLM_VIDEO_PICTURE_TYPE_INTRA || + self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE + ) { + self->frame_backward = self->frame_current; + self->frame_current = frame_temp; + } +} + +void plm_video_decode_slice(plm_video_t *self, int slice) { + self->slice_begin = TRUE; + self->macroblock_address = (slice - 1) * self->mb_width - 1; + + // Reset motion vectors and DC predictors + self->motion_backward.h = self->motion_forward.h = 0; + self->motion_backward.v = self->motion_forward.v = 0; + self->dc_predictor[0] = 128; + self->dc_predictor[1] = 128; + self->dc_predictor[2] = 128; + + self->quantizer_scale = plm_buffer_read(self->buffer, 5); + + // Skip extra + while (plm_buffer_read(self->buffer, 1)) { + plm_buffer_skip(self->buffer, 8); + } + + do { + plm_video_decode_macroblock(self); + } while ( + self->macroblock_address < self->mb_size - 1 && + plm_buffer_peek_non_zero(self->buffer, 23) + ); +} + +void plm_video_decode_macroblock(plm_video_t *self) { + // Decode increment + int increment = 0; + int t = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT); + + while (t == 34) { + // macroblock_stuffing + t = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT); + } + while (t == 35) { + // macroblock_escape + increment += 33; + t = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT); + } + increment += t; + + // Process any skipped macroblocks + if (self->slice_begin) { + // The first increment of each slice is relative to beginning of the + // previous row, not the previous macroblock + self->slice_begin = FALSE; + self->macroblock_address += increment; + } + else { + if (self->macroblock_address + increment >= self->mb_size) { + return; // invalid + } + if (increment > 1) { + // Skipped macroblocks reset DC predictors + self->dc_predictor[0] = 128; + self->dc_predictor[1] = 128; + self->dc_predictor[2] = 128; + + // Skipped macroblocks in P-pictures reset motion vectors + if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE) { + self->motion_forward.h = 0; + self->motion_forward.v = 0; + } + } + + // Predict skipped macroblocks + while (increment > 1) { + self->macroblock_address++; + self->mb_row = self->macroblock_address / self->mb_width; + self->mb_col = self->macroblock_address % self->mb_width; + + plm_video_predict_macroblock(self); + increment--; + } + self->macroblock_address++; + } + + self->mb_row = self->macroblock_address / self->mb_width; + self->mb_col = self->macroblock_address % self->mb_width; + + if (self->mb_col >= self->mb_width || self->mb_row >= self->mb_height) { + return; // corrupt stream; + } + + // Process the current macroblock + const plm_vlc_t *table = PLM_VIDEO_MACROBLOCK_TYPE[self->picture_type]; + self->macroblock_type = plm_buffer_read_vlc(self->buffer, table); + + self->macroblock_intra = (self->macroblock_type & 0x01); + self->motion_forward.is_set = (self->macroblock_type & 0x08); + self->motion_backward.is_set = (self->macroblock_type & 0x04); + + // Quantizer scale + if ((self->macroblock_type & 0x10) != 0) { + self->quantizer_scale = plm_buffer_read(self->buffer, 5); + } + + if (self->macroblock_intra) { + // Intra-coded macroblocks reset motion vectors + self->motion_backward.h = self->motion_forward.h = 0; + self->motion_backward.v = self->motion_forward.v = 0; + } + else { + // Non-intra macroblocks reset DC predictors + self->dc_predictor[0] = 128; + self->dc_predictor[1] = 128; + self->dc_predictor[2] = 128; + + plm_video_decode_motion_vectors(self); + plm_video_predict_macroblock(self); + } + + // Decode blocks + int cbp = ((self->macroblock_type & 0x02) != 0) + ? plm_buffer_read_vlc(self->buffer, PLM_VIDEO_CODE_BLOCK_PATTERN) + : (self->macroblock_intra ? 0x3f : 0); + + for (int block = 0, mask = 0x20; block < 6; block++) { + if ((cbp & mask) != 0) { + plm_video_decode_block(self, block); + } + mask >>= 1; + } +} + +void plm_video_decode_motion_vectors(plm_video_t *self) { + + // Forward + if (self->motion_forward.is_set) { + int r_size = self->motion_forward.r_size; + self->motion_forward.h = plm_video_decode_motion_vector(self, r_size, self->motion_forward.h); + self->motion_forward.v = plm_video_decode_motion_vector(self, r_size, self->motion_forward.v); + } + else if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE) { + // No motion information in P-picture, reset vectors + self->motion_forward.h = 0; + self->motion_forward.v = 0; + } + + if (self->motion_backward.is_set) { + int r_size = self->motion_backward.r_size; + self->motion_backward.h = plm_video_decode_motion_vector(self, r_size, self->motion_backward.h); + self->motion_backward.v = plm_video_decode_motion_vector(self, r_size, self->motion_backward.v); + } +} + +int plm_video_decode_motion_vector(plm_video_t *self, int r_size, int motion) { + int fscale = 1 << r_size; + int m_code = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MOTION); + int r = 0; + int d; + + if ((m_code != 0) && (fscale != 1)) { + r = plm_buffer_read(self->buffer, r_size); + d = ((abs(m_code) - 1) << r_size) + r + 1; + if (m_code < 0) { + d = -d; + } + } + else { + d = m_code; + } + + motion += d; + if (motion > (fscale << 4) - 1) { + motion -= fscale << 5; + } + else if (motion < (int)((unsigned)(-fscale) << 4)) { + motion += fscale << 5; + } + + return motion; +} + +void plm_video_predict_macroblock(plm_video_t *self) { + int fw_h = self->motion_forward.h; + int fw_v = self->motion_forward.v; + + if (self->motion_forward.full_px) { + fw_h <<= 1; + fw_v <<= 1; + } + + if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) { + int bw_h = self->motion_backward.h; + int bw_v = self->motion_backward.v; + + if (self->motion_backward.full_px) { + bw_h <<= 1; + bw_v <<= 1; + } + + if (self->motion_forward.is_set) { + plm_video_copy_macroblock(self, &self->frame_forward, fw_h, fw_v); + if (self->motion_backward.is_set) { + plm_video_interpolate_macroblock(self, &self->frame_backward, bw_h, bw_v); + } + } + else { + plm_video_copy_macroblock(self, &self->frame_backward, bw_h, bw_v); + } + } + else { + plm_video_copy_macroblock(self, &self->frame_forward, fw_h, fw_v); + } +} + +void plm_video_copy_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v) { + plm_frame_t *d = &self->frame_current; + plm_video_process_macroblock(self, s->y.data, d->y.data, motion_h, motion_v, 16, FALSE); + plm_video_process_macroblock(self, s->cr.data, d->cr.data, motion_h / 2, motion_v / 2, 8, FALSE); + plm_video_process_macroblock(self, s->cb.data, d->cb.data, motion_h / 2, motion_v / 2, 8, FALSE); +} + +void plm_video_interpolate_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v) { + plm_frame_t *d = &self->frame_current; + plm_video_process_macroblock(self, s->y.data, d->y.data, motion_h, motion_v, 16, TRUE); + plm_video_process_macroblock(self, s->cr.data, d->cr.data, motion_h / 2, motion_v / 2, 8, TRUE); + plm_video_process_macroblock(self, s->cb.data, d->cb.data, motion_h / 2, motion_v / 2, 8, TRUE); +} + +#define PLM_BLOCK_SET(DEST, DEST_INDEX, DEST_WIDTH, SOURCE_INDEX, SOURCE_WIDTH, BLOCK_SIZE, OP) do { \ + int dest_scan = DEST_WIDTH - BLOCK_SIZE; \ + int source_scan = SOURCE_WIDTH - BLOCK_SIZE; \ + for (int y = 0; y < BLOCK_SIZE; y++) { \ + for (int x = 0; x < BLOCK_SIZE; x++) { \ + DEST[DEST_INDEX] = OP; \ + SOURCE_INDEX++; DEST_INDEX++; \ + } \ + SOURCE_INDEX += source_scan; \ + DEST_INDEX += dest_scan; \ + }} while(FALSE) + +void plm_video_process_macroblock( + plm_video_t *self, uint8_t *s, uint8_t *d, + int motion_h, int motion_v, int block_size, int interpolate +) { + int dw = self->mb_width * block_size; + + int hp = motion_h >> 1; + int vp = motion_v >> 1; + int odd_h = (motion_h & 1) == 1; + int odd_v = (motion_v & 1) == 1; + + unsigned int si = ((self->mb_row * block_size) + vp) * dw + (self->mb_col * block_size) + hp; + unsigned int di = (self->mb_row * dw + self->mb_col) * block_size; + + unsigned int max_address = (dw * (self->mb_height * block_size - block_size + 1) - block_size); + if (si > max_address || di > max_address) { + return; // corrupt video + } + + #define PLM_MB_CASE(INTERPOLATE, ODD_H, ODD_V, OP) \ + case ((INTERPOLATE << 2) | (ODD_H << 1) | (ODD_V)): \ + PLM_BLOCK_SET(d, di, dw, si, dw, block_size, OP); \ + break + + switch ((interpolate << 2) | (odd_h << 1) | (odd_v)) { + PLM_MB_CASE(0, 0, 0, (s[si])); + PLM_MB_CASE(0, 0, 1, (s[si] + s[si + dw] + 1) >> 1); + PLM_MB_CASE(0, 1, 0, (s[si] + s[si + 1] + 1) >> 1); + PLM_MB_CASE(0, 1, 1, (s[si] + s[si + 1] + s[si + dw] + s[si + dw + 1] + 2) >> 2); + + PLM_MB_CASE(1, 0, 0, (d[di] + (s[si]) + 1) >> 1); + PLM_MB_CASE(1, 0, 1, (d[di] + ((s[si] + s[si + dw] + 1) >> 1) + 1) >> 1); + PLM_MB_CASE(1, 1, 0, (d[di] + ((s[si] + s[si + 1] + 1) >> 1) + 1) >> 1); + PLM_MB_CASE(1, 1, 1, (d[di] + ((s[si] + s[si + 1] + s[si + dw] + s[si + dw + 1] + 2) >> 2) + 1) >> 1); + } + + #undef PLM_MB_CASE +} + +void plm_video_decode_block(plm_video_t *self, int block) { + + int n = 0; + uint8_t *quant_matrix; + + // Decode DC coefficient of intra-coded blocks + if (self->macroblock_intra) { + int predictor; + int dct_size; + + // DC prediction + int plane_index = block > 3 ? block - 3 : 0; + predictor = self->dc_predictor[plane_index]; + dct_size = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_DCT_SIZE[plane_index]); + + // Read DC coeff + if (dct_size > 0) { + int differential = plm_buffer_read(self->buffer, dct_size); + if ((differential & (1 << (dct_size - 1))) != 0) { + self->block_data[0] = predictor + differential; + } + else { + self->block_data[0] = predictor + (-(1 << dct_size) | (differential + 1)); + } + } + else { + self->block_data[0] = predictor; + } + + // Save predictor value + self->dc_predictor[plane_index] = self->block_data[0]; + + // Dequantize + premultiply + self->block_data[0] <<= (3 + 5); + + quant_matrix = self->intra_quant_matrix; + n = 1; + } + else { + quant_matrix = self->non_intra_quant_matrix; + } + + // Decode AC coefficients (+DC for non-intra) + int level = 0; + while (TRUE) { + int run = 0; + uint16_t coeff = plm_buffer_read_vlc_uint(self->buffer, PLM_VIDEO_DCT_COEFF); + + if ((coeff == 0x0001) && (n > 0) && (plm_buffer_read(self->buffer, 1) == 0)) { + // end_of_block + break; + } + if (coeff == 0xffff) { + // escape + run = plm_buffer_read(self->buffer, 6); + level = plm_buffer_read(self->buffer, 8); + if (level == 0) { + level = plm_buffer_read(self->buffer, 8); + } + else if (level == 128) { + level = plm_buffer_read(self->buffer, 8) - 256; + } + else if (level > 128) { + level = level - 256; + } + } + else { + run = coeff >> 8; + level = coeff & 0xff; + if (plm_buffer_read(self->buffer, 1)) { + level = -level; + } + } + + n += run; + if (n < 0 || n >= 64) { + return; // invalid + } + + int de_zig_zagged = PLM_VIDEO_ZIG_ZAG[n]; + n++; + + // Dequantize, oddify, clip + level = (unsigned)level << 1; + if (!self->macroblock_intra) { + level += (level < 0 ? -1 : 1); + } + level = (level * self->quantizer_scale * quant_matrix[de_zig_zagged]) >> 4; + if ((level & 1) == 0) { + level -= level > 0 ? 1 : -1; + } + if (level > 2047) { + level = 2047; + } + else if (level < -2048) { + level = -2048; + } + + // Save premultiplied coefficient + self->block_data[de_zig_zagged] = level * PLM_VIDEO_PREMULTIPLIER_MATRIX[de_zig_zagged]; + } + + // Move block to its place + uint8_t *d; + int dw; + int di; + + if (block < 4) { + d = self->frame_current.y.data; + dw = self->luma_width; + di = (self->mb_row * self->luma_width + self->mb_col) << 4; + if ((block & 1) != 0) { + di += 8; + } + if ((block & 2) != 0) { + di += self->luma_width << 3; + } + } + else { + d = (block == 4) ? self->frame_current.cb.data : self->frame_current.cr.data; + dw = self->chroma_width; + di = ((self->mb_row * self->luma_width) << 2) + (self->mb_col << 3); + } + + int *s = self->block_data; + int si = 0; + if (self->macroblock_intra) { + // Overwrite (no prediction) + if (n == 1) { + int clamped = plm_clamp((s[0] + 128) >> 8); + PLM_BLOCK_SET(d, di, dw, si, 8, 8, clamped); + s[0] = 0; + } + else { + plm_video_idct(s); + PLM_BLOCK_SET(d, di, dw, si, 8, 8, plm_clamp(s[si])); + memset(self->block_data, 0, sizeof(self->block_data)); + } + } + else { + // Add data to the predicted macroblock + if (n == 1) { + int value = (s[0] + 128) >> 8; + PLM_BLOCK_SET(d, di, dw, si, 8, 8, plm_clamp(d[di] + value)); + s[0] = 0; + } + else { + plm_video_idct(s); + PLM_BLOCK_SET(d, di, dw, si, 8, 8, plm_clamp(d[di] + s[si])); + memset(self->block_data, 0, sizeof(self->block_data)); + } + } +} + +void plm_video_idct(int *block) { + int + b1, b3, b4, b6, b7, tmp1, tmp2, m0, + x0, x1, x2, x3, x4, y3, y4, y5, y6, y7; + + // Transform columns + for (int i = 0; i < 8; ++i) { + b1 = block[4 * 8 + i]; + b3 = block[2 * 8 + i] + block[6 * 8 + i]; + b4 = block[5 * 8 + i] - block[3 * 8 + i]; + tmp1 = block[1 * 8 + i] + block[7 * 8 + i]; + tmp2 = block[3 * 8 + i] + block[5 * 8 + i]; + b6 = block[1 * 8 + i] - block[7 * 8 + i]; + b7 = tmp1 + tmp2; + m0 = block[0 * 8 + i]; + x4 = ((b6 * 473 - b4 * 196 + 128) >> 8) - b7; + x0 = x4 - (((tmp1 - tmp2) * 362 + 128) >> 8); + x1 = m0 - b1; + x2 = (((block[2 * 8 + i] - block[6 * 8 + i]) * 362 + 128) >> 8) - b3; + x3 = m0 + b1; + y3 = x1 + x2; + y4 = x3 + b3; + y5 = x1 - x2; + y6 = x3 - b3; + y7 = -x0 - ((b4 * 473 + b6 * 196 + 128) >> 8); + block[0 * 8 + i] = b7 + y4; + block[1 * 8 + i] = x4 + y3; + block[2 * 8 + i] = y5 - x0; + block[3 * 8 + i] = y6 - y7; + block[4 * 8 + i] = y6 + y7; + block[5 * 8 + i] = x0 + y5; + block[6 * 8 + i] = y3 - x4; + block[7 * 8 + i] = y4 - b7; + } + + // Transform rows + for (int i = 0; i < 64; i += 8) { + b1 = block[4 + i]; + b3 = block[2 + i] + block[6 + i]; + b4 = block[5 + i] - block[3 + i]; + tmp1 = block[1 + i] + block[7 + i]; + tmp2 = block[3 + i] + block[5 + i]; + b6 = block[1 + i] - block[7 + i]; + b7 = tmp1 + tmp2; + m0 = block[0 + i]; + x4 = ((b6 * 473 - b4 * 196 + 128) >> 8) - b7; + x0 = x4 - (((tmp1 - tmp2) * 362 + 128) >> 8); + x1 = m0 - b1; + x2 = (((block[2 + i] - block[6 + i]) * 362 + 128) >> 8) - b3; + x3 = m0 + b1; + y3 = x1 + x2; + y4 = x3 + b3; + y5 = x1 - x2; + y6 = x3 - b3; + y7 = -x0 - ((b4 * 473 + b6 * 196 + 128) >> 8); + block[0 + i] = (b7 + y4 + 128) >> 8; + block[1 + i] = (x4 + y3 + 128) >> 8; + block[2 + i] = (y5 - x0 + 128) >> 8; + block[3 + i] = (y6 - y7 + 128) >> 8; + block[4 + i] = (y6 + y7 + 128) >> 8; + block[5 + i] = (x0 + y5 + 128) >> 8; + block[6 + i] = (y3 - x4 + 128) >> 8; + block[7 + i] = (y4 - b7 + 128) >> 8; + } +} + +// YCbCr conversion following the BT.601 standard: +// https://infogalactic.com/info/YCbCr#ITU-R_BT.601_conversion + +#define PLM_PUT_PIXEL(RI, GI, BI, Y_OFFSET, DEST_OFFSET) \ + y = ((frame->y.data[y_index + Y_OFFSET]-16) * 76309) >> 16; \ + dest[d_index + DEST_OFFSET + RI] = plm_clamp(y + r); \ + dest[d_index + DEST_OFFSET + GI] = plm_clamp(y - g); \ + dest[d_index + DEST_OFFSET + BI] = plm_clamp(y + b); + +#define PLM_DEFINE_FRAME_CONVERT_FUNCTION(NAME, BYTES_PER_PIXEL, RI, GI, BI) \ + void NAME(plm_frame_t *frame, uint8_t *dest, int stride) { \ + int cols = frame->width >> 1; \ + int rows = frame->height >> 1; \ + int yw = frame->y.width; \ + int cw = frame->cb.width; \ + for (int row = 0; row < rows; row++) { \ + int c_index = row * cw; \ + int y_index = row * 2 * yw; \ + int d_index = row * 2 * stride; \ + for (int col = 0; col < cols; col++) { \ + int y; \ + int cr = frame->cr.data[c_index] - 128; \ + int cb = frame->cb.data[c_index] - 128; \ + int r = (cr * 104597) >> 16; \ + int g = (cb * 25674 + cr * 53278) >> 16; \ + int b = (cb * 132201) >> 16; \ + PLM_PUT_PIXEL(RI, GI, BI, 0, 0); \ + PLM_PUT_PIXEL(RI, GI, BI, 1, BYTES_PER_PIXEL); \ + PLM_PUT_PIXEL(RI, GI, BI, yw, stride); \ + PLM_PUT_PIXEL(RI, GI, BI, yw + 1, stride + BYTES_PER_PIXEL); \ + c_index += 1; \ + y_index += 2; \ + d_index += 2 * BYTES_PER_PIXEL; \ + } \ + } \ + } + +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_rgb, 3, 0, 1, 2) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_bgr, 3, 2, 1, 0) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_rgba, 4, 0, 1, 2) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_bgra, 4, 2, 1, 0) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_argb, 4, 1, 2, 3) +PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_abgr, 4, 3, 2, 1) + + +#undef PLM_PUT_PIXEL +#undef PLM_DEFINE_FRAME_CONVERT_FUNCTION + + + +// ----------------------------------------------------------------------------- +// plm_audio implementation + +// Based on kjmp2 by Martin J. Fiedler +// http://keyj.emphy.de/kjmp2/ + +static const int PLM_AUDIO_FRAME_SYNC = 0x7ff; + +static const int PLM_AUDIO_MPEG_2_5 = 0x0; +static const int PLM_AUDIO_MPEG_2 = 0x2; +static const int PLM_AUDIO_MPEG_1 = 0x3; + +static const int PLM_AUDIO_LAYER_III = 0x1; +static const int PLM_AUDIO_LAYER_II = 0x2; +static const int PLM_AUDIO_LAYER_I = 0x3; + +static const int PLM_AUDIO_MODE_STEREO = 0x0; +static const int PLM_AUDIO_MODE_JOINT_STEREO = 0x1; +static const int PLM_AUDIO_MODE_DUAL_CHANNEL = 0x2; +static const int PLM_AUDIO_MODE_MONO = 0x3; + +static const unsigned short PLM_AUDIO_SAMPLE_RATE[] = { + 44100, 48000, 32000, 0, // MPEG-1 + 22050, 24000, 16000, 0 // MPEG-2 +}; + +static const short PLM_AUDIO_BIT_RATE[] = { + 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, // MPEG-1 + 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 // MPEG-2 +}; + +static const int PLM_AUDIO_SCALEFACTOR_BASE[] = { + 0x02000000, 0x01965FEA, 0x01428A30 +}; + +static const float PLM_AUDIO_SYNTHESIS_WINDOW[] = { + 0.0, -0.5, -0.5, -0.5, -0.5, -0.5, + -0.5, -1.0, -1.0, -1.0, -1.0, -1.5, + -1.5, -2.0, -2.0, -2.5, -2.5, -3.0, + -3.5, -3.5, -4.0, -4.5, -5.0, -5.5, + -6.5, -7.0, -8.0, -8.5, -9.5, -10.5, + -12.0, -13.0, -14.5, -15.5, -17.5, -19.0, + -20.5, -22.5, -24.5, -26.5, -29.0, -31.5, + -34.0, -36.5, -39.5, -42.5, -45.5, -48.5, + -52.0, -55.5, -58.5, -62.5, -66.0, -69.5, + -73.5, -77.0, -80.5, -84.5, -88.0, -91.5, + -95.0, -98.0, -101.0, -104.0, 106.5, 109.0, + 111.0, 112.5, 113.5, 114.0, 114.0, 113.5, + 112.0, 110.5, 107.5, 104.0, 100.0, 94.5, + 88.5, 81.5, 73.0, 63.5, 53.0, 41.5, + 28.5, 14.5, -1.0, -18.0, -36.0, -55.5, + -76.5, -98.5, -122.0, -147.0, -173.5, -200.5, + -229.5, -259.5, -290.5, -322.5, -355.5, -389.5, + -424.0, -459.5, -495.5, -532.0, -568.5, -605.0, + -641.5, -678.0, -714.0, -749.0, -783.5, -817.0, + -849.0, -879.5, -908.5, -935.0, -959.5, -981.0, + -1000.5, -1016.0, -1028.5, -1037.5, -1042.5, -1043.5, + -1040.0, -1031.5, 1018.5, 1000.0, 976.0, 946.5, + 911.0, 869.5, 822.0, 767.5, 707.0, 640.0, + 565.5, 485.0, 397.0, 302.5, 201.0, 92.5, + -22.5, -144.0, -272.5, -407.0, -547.5, -694.0, + -846.0, -1003.0, -1165.0, -1331.5, -1502.0, -1675.5, + -1852.5, -2031.5, -2212.5, -2394.0, -2576.5, -2758.5, + -2939.5, -3118.5, -3294.5, -3467.5, -3635.5, -3798.5, + -3955.0, -4104.5, -4245.5, -4377.5, -4499.0, -4609.5, + -4708.0, -4792.5, -4863.5, -4919.0, -4958.0, -4979.5, + -4983.0, -4967.5, -4931.5, -4875.0, -4796.0, -4694.5, + -4569.5, -4420.0, -4246.0, -4046.0, -3820.0, -3567.0, + 3287.0, 2979.5, 2644.0, 2280.5, 1888.0, 1467.5, + 1018.5, 541.0, 35.0, -499.0, -1061.0, -1650.0, + -2266.5, -2909.0, -3577.0, -4270.0, -4987.5, -5727.5, + -6490.0, -7274.0, -8077.5, -8899.5, -9739.0, -10594.5, + -11464.5, -12347.0, -13241.0, -14144.5, -15056.0, -15973.5, + -16895.5, -17820.0, -18744.5, -19668.0, -20588.0, -21503.0, + -22410.5, -23308.5, -24195.0, -25068.5, -25926.5, -26767.0, + -27589.0, -28389.0, -29166.5, -29919.0, -30644.5, -31342.0, + -32009.5, -32645.0, -33247.0, -33814.5, -34346.0, -34839.5, + -35295.0, -35710.0, -36084.5, -36417.5, -36707.5, -36954.0, + -37156.5, -37315.0, -37428.0, -37496.0, 37519.0, 37496.0, + 37428.0, 37315.0, 37156.5, 36954.0, 36707.5, 36417.5, + 36084.5, 35710.0, 35295.0, 34839.5, 34346.0, 33814.5, + 33247.0, 32645.0, 32009.5, 31342.0, 30644.5, 29919.0, + 29166.5, 28389.0, 27589.0, 26767.0, 25926.5, 25068.5, + 24195.0, 23308.5, 22410.5, 21503.0, 20588.0, 19668.0, + 18744.5, 17820.0, 16895.5, 15973.5, 15056.0, 14144.5, + 13241.0, 12347.0, 11464.5, 10594.5, 9739.0, 8899.5, + 8077.5, 7274.0, 6490.0, 5727.5, 4987.5, 4270.0, + 3577.0, 2909.0, 2266.5, 1650.0, 1061.0, 499.0, + -35.0, -541.0, -1018.5, -1467.5, -1888.0, -2280.5, + -2644.0, -2979.5, 3287.0, 3567.0, 3820.0, 4046.0, + 4246.0, 4420.0, 4569.5, 4694.5, 4796.0, 4875.0, + 4931.5, 4967.5, 4983.0, 4979.5, 4958.0, 4919.0, + 4863.5, 4792.5, 4708.0, 4609.5, 4499.0, 4377.5, + 4245.5, 4104.5, 3955.0, 3798.5, 3635.5, 3467.5, + 3294.5, 3118.5, 2939.5, 2758.5, 2576.5, 2394.0, + 2212.5, 2031.5, 1852.5, 1675.5, 1502.0, 1331.5, + 1165.0, 1003.0, 846.0, 694.0, 547.5, 407.0, + 272.5, 144.0, 22.5, -92.5, -201.0, -302.5, + -397.0, -485.0, -565.5, -640.0, -707.0, -767.5, + -822.0, -869.5, -911.0, -946.5, -976.0, -1000.0, + 1018.5, 1031.5, 1040.0, 1043.5, 1042.5, 1037.5, + 1028.5, 1016.0, 1000.5, 981.0, 959.5, 935.0, + 908.5, 879.5, 849.0, 817.0, 783.5, 749.0, + 714.0, 678.0, 641.5, 605.0, 568.5, 532.0, + 495.5, 459.5, 424.0, 389.5, 355.5, 322.5, + 290.5, 259.5, 229.5, 200.5, 173.5, 147.0, + 122.0, 98.5, 76.5, 55.5, 36.0, 18.0, + 1.0, -14.5, -28.5, -41.5, -53.0, -63.5, + -73.0, -81.5, -88.5, -94.5, -100.0, -104.0, + -107.5, -110.5, -112.0, -113.5, -114.0, -114.0, + -113.5, -112.5, -111.0, -109.0, 106.5, 104.0, + 101.0, 98.0, 95.0, 91.5, 88.0, 84.5, + 80.5, 77.0, 73.5, 69.5, 66.0, 62.5, + 58.5, 55.5, 52.0, 48.5, 45.5, 42.5, + 39.5, 36.5, 34.0, 31.5, 29.0, 26.5, + 24.5, 22.5, 20.5, 19.0, 17.5, 15.5, + 14.5, 13.0, 12.0, 10.5, 9.5, 8.5, + 8.0, 7.0, 6.5, 5.5, 5.0, 4.5, + 4.0, 3.5, 3.5, 3.0, 2.5, 2.5, + 2.0, 2.0, 1.5, 1.5, 1.0, 1.0, + 1.0, 1.0, 0.5, 0.5, 0.5, 0.5, + 0.5, 0.5 +}; + +// Quantizer lookup, step 1: bitrate classes +static const uint8_t PLM_AUDIO_QUANT_LUT_STEP_1[2][16] = { + // 32, 48, 56, 64, 80, 96,112,128,160,192,224,256,320,384 <- bitrate + { 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2 }, // mono + // 16, 24, 28, 32, 40, 48, 56, 64, 80, 96,112,128,160,192 <- bitrate / chan + { 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2 } // stereo +}; + +// Quantizer lookup, step 2: bitrate class, sample rate -> B2 table idx, sblimit +#define PLM_AUDIO_QUANT_TAB_A (27 | 64) // Table 3-B.2a: high-rate, sblimit = 27 +#define PLM_AUDIO_QUANT_TAB_B (30 | 64) // Table 3-B.2b: high-rate, sblimit = 30 +#define PLM_AUDIO_QUANT_TAB_C 8 // Table 3-B.2c: low-rate, sblimit = 8 +#define PLM_AUDIO_QUANT_TAB_D 12 // Table 3-B.2d: low-rate, sblimit = 12 + +static const uint8_t QUANT_LUT_STEP_2[3][3] = { + //44.1 kHz, 48 kHz, 32 kHz + { PLM_AUDIO_QUANT_TAB_C, PLM_AUDIO_QUANT_TAB_C, PLM_AUDIO_QUANT_TAB_D }, // 32 - 48 kbit/sec/ch + { PLM_AUDIO_QUANT_TAB_A, PLM_AUDIO_QUANT_TAB_A, PLM_AUDIO_QUANT_TAB_A }, // 56 - 80 kbit/sec/ch + { PLM_AUDIO_QUANT_TAB_B, PLM_AUDIO_QUANT_TAB_A, PLM_AUDIO_QUANT_TAB_B } // 96+ kbit/sec/ch +}; + +// Quantizer lookup, step 3: B2 table, subband -> nbal, row index +// (upper 4 bits: nbal, lower 4 bits: row index) +static const uint8_t PLM_AUDIO_QUANT_LUT_STEP_3[3][32] = { + // Low-rate table (3-B.2c and 3-B.2d) + { + 0x44,0x44, + 0x34,0x34,0x34,0x34,0x34,0x34,0x34,0x34,0x34,0x34 + }, + // High-rate table (3-B.2a and 3-B.2b) + { + 0x43,0x43,0x43, + 0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x42, + 0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31, + 0x20,0x20,0x20,0x20,0x20,0x20,0x20 + }, + // MPEG-2 LSR table (B.2 in ISO 13818-3) + { + 0x45,0x45,0x45,0x45, + 0x34,0x34,0x34,0x34,0x34,0x34,0x34, + 0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24, + 0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24 + } +}; + +// Quantizer lookup, step 4: table row, allocation[] value -> quant table index +static const uint8_t PLM_AUDIO_QUANT_LUT_STEP_4[6][16] = { + { 0, 1, 2, 17 }, + { 0, 1, 2, 3, 4, 5, 6, 17 }, + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17 }, + { 0, 1, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 }, + { 0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17 }, + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 } +}; + +typedef struct plm_quantizer_spec_t { + unsigned short levels; + unsigned char group; + unsigned char bits; +} plm_quantizer_spec_t; + +static const plm_quantizer_spec_t PLM_AUDIO_QUANT_TAB[] = { + { 3, 1, 5 }, // 1 + { 5, 1, 7 }, // 2 + { 7, 0, 3 }, // 3 + { 9, 1, 10 }, // 4 + { 15, 0, 4 }, // 5 + { 31, 0, 5 }, // 6 + { 63, 0, 6 }, // 7 + { 127, 0, 7 }, // 8 + { 255, 0, 8 }, // 9 + { 511, 0, 9 }, // 10 + { 1023, 0, 10 }, // 11 + { 2047, 0, 11 }, // 12 + { 4095, 0, 12 }, // 13 + { 8191, 0, 13 }, // 14 + { 16383, 0, 14 }, // 15 + { 32767, 0, 15 }, // 16 + { 65535, 0, 16 } // 17 +}; + +struct plm_audio_t { + double time; + int samples_decoded; + int samplerate_index; + int bitrate_index; + int version; + int layer; + int mode; + int bound; + int v_pos; + int next_frame_data_size; + int has_header; + + plm_buffer_t *buffer; + int destroy_buffer_when_done; + + const plm_quantizer_spec_t *allocation[2][32]; + uint8_t scale_factor_info[2][32]; + int scale_factor[2][32][3]; + int sample[2][32][3]; + + plm_samples_t samples; + float D[1024]; + float V[2][1024]; + float U[32]; +}; + +int plm_audio_find_frame_sync(plm_audio_t *self); +int plm_audio_decode_header(plm_audio_t *self); +void plm_audio_decode_frame(plm_audio_t *self); +const plm_quantizer_spec_t *plm_audio_read_allocation(plm_audio_t *self, int sb, int tab3); +void plm_audio_read_samples(plm_audio_t *self, int ch, int sb, int part); +void plm_audio_idct36(int s[32][3], int ss, float *d, int dp); + +plm_audio_t *plm_audio_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done) { + plm_audio_t *self = (plm_audio_t *)PLM_MALLOC(sizeof(plm_audio_t)); + memset(self, 0, sizeof(plm_audio_t)); + + self->samples.count = PLM_AUDIO_SAMPLES_PER_FRAME; + self->buffer = buffer; + self->destroy_buffer_when_done = destroy_when_done; + self->samplerate_index = 3; // Indicates 0 + + memcpy(self->D, PLM_AUDIO_SYNTHESIS_WINDOW, 512 * sizeof(float)); + memcpy(self->D + 512, PLM_AUDIO_SYNTHESIS_WINDOW, 512 * sizeof(float)); + + // Attempt to decode first header + self->next_frame_data_size = plm_audio_decode_header(self); + + return self; +} + +void plm_audio_destroy(plm_audio_t *self) { + if (self->destroy_buffer_when_done) { + plm_buffer_destroy(self->buffer); + } + PLM_FREE(self); +} + +int plm_audio_has_header(plm_audio_t *self) { + if (self->has_header) { + return TRUE; + } + + self->next_frame_data_size = plm_audio_decode_header(self); + return self->has_header; +} + +int plm_audio_get_samplerate(plm_audio_t *self) { + return plm_audio_has_header(self) + ? PLM_AUDIO_SAMPLE_RATE[self->samplerate_index] + : 0; +} + +double plm_audio_get_time(plm_audio_t *self) { + return self->time; +} + +void plm_audio_set_time(plm_audio_t *self, double time) { + self->samples_decoded = time * + (double)PLM_AUDIO_SAMPLE_RATE[self->samplerate_index]; + self->time = time; +} + +void plm_audio_rewind(plm_audio_t *self) { + plm_buffer_rewind(self->buffer); + self->time = 0; + self->samples_decoded = 0; + self->next_frame_data_size = 0; +} + +int plm_audio_has_ended(plm_audio_t *self) { + return plm_buffer_has_ended(self->buffer); +} + +plm_samples_t *plm_audio_decode(plm_audio_t *self) { + // Do we have at least enough information to decode the frame header? + if (!self->next_frame_data_size) { + if (!plm_buffer_has(self->buffer, 48)) { + return NULL; + } + self->next_frame_data_size = plm_audio_decode_header(self); + } + + if ( + self->next_frame_data_size == 0 || + !plm_buffer_has(self->buffer, self->next_frame_data_size << 3) + ) { + return NULL; + } + + plm_audio_decode_frame(self); + self->next_frame_data_size = 0; + + self->samples.time = self->time; + + self->samples_decoded += PLM_AUDIO_SAMPLES_PER_FRAME; + self->time = (double)self->samples_decoded / + (double)PLM_AUDIO_SAMPLE_RATE[self->samplerate_index]; + + return &self->samples; +} + +int plm_audio_find_frame_sync(plm_audio_t *self) { + size_t i; + for (i = self->buffer->bit_index >> 3; i < self->buffer->length-1; i++) { + if ( + self->buffer->bytes[i] == 0xFF && + (self->buffer->bytes[i+1] & 0xFE) == 0xFC + ) { + self->buffer->bit_index = ((i+1) << 3) + 3; + return TRUE; + } + } + self->buffer->bit_index = (i + 1) << 3; + return FALSE; +} + +int plm_audio_decode_header(plm_audio_t *self) { + if (!plm_buffer_has(self->buffer, 48)) { + return 0; + } + + plm_buffer_skip_bytes(self->buffer, 0x00); + int sync = plm_buffer_read(self->buffer, 11); + + + // Attempt to resync if no syncword was found. This sucks balls. The MP2 + // stream contains a syncword just before every frame (11 bits set to 1). + // However, this syncword is not guaranteed to not occur elsewhere in the + // stream. So, if we have to resync, we also have to check if the header + // (samplerate, bitrate) differs from the one we had before. This all + // may still lead to garbage data being decoded :/ + + if (sync != PLM_AUDIO_FRAME_SYNC && !plm_audio_find_frame_sync(self)) { + return 0; + } + + self->version = plm_buffer_read(self->buffer, 2); + self->layer = plm_buffer_read(self->buffer, 2); + int hasCRC = !plm_buffer_read(self->buffer, 1); + + if ( + self->version != PLM_AUDIO_MPEG_1 || + self->layer != PLM_AUDIO_LAYER_II + ) { + return 0; + } + + int bitrate_index = plm_buffer_read(self->buffer, 4) - 1; + if (bitrate_index > 13) { + return 0; + } + + int samplerate_index = plm_buffer_read(self->buffer, 2); + if (samplerate_index == 3) { + return 0; + } + + int padding = plm_buffer_read(self->buffer, 1); + plm_buffer_skip(self->buffer, 1); // f_private + int mode = plm_buffer_read(self->buffer, 2); + + // If we already have a header, make sure the samplerate, bitrate and mode + // are still the same, otherwise we might have missed sync. + if ( + self->has_header && ( + self->bitrate_index != bitrate_index || + self->samplerate_index != samplerate_index || + self->mode != mode + ) + ) { + return 0; + } + + self->bitrate_index = bitrate_index; + self->samplerate_index = samplerate_index; + self->mode = mode; + self->has_header = TRUE; + + // Parse the mode_extension, set up the stereo bound + if (mode == PLM_AUDIO_MODE_JOINT_STEREO) { + self->bound = (plm_buffer_read(self->buffer, 2) + 1) << 2; + } + else { + plm_buffer_skip(self->buffer, 2); + self->bound = (mode == PLM_AUDIO_MODE_MONO) ? 0 : 32; + } + + // Discard the last 4 bits of the header and the CRC value, if present + plm_buffer_skip(self->buffer, 4); // copyright(1), original(1), emphasis(2) + if (hasCRC) { + plm_buffer_skip(self->buffer, 16); + } + + // Compute frame size, check if we have enough data to decode the whole + // frame. + int bitrate = PLM_AUDIO_BIT_RATE[self->bitrate_index]; + int samplerate = PLM_AUDIO_SAMPLE_RATE[self->samplerate_index]; + int frame_size = (144000 * bitrate / samplerate) + padding; + return frame_size - (hasCRC ? 6 : 4); +} + +void plm_audio_decode_frame(plm_audio_t *self) { + // Prepare the quantizer table lookups + int tab3 = 0; + int sblimit = 0; + + int tab1 = (self->mode == PLM_AUDIO_MODE_MONO) ? 0 : 1; + int tab2 = PLM_AUDIO_QUANT_LUT_STEP_1[tab1][self->bitrate_index]; + tab3 = QUANT_LUT_STEP_2[tab2][self->samplerate_index]; + sblimit = tab3 & 63; + tab3 >>= 6; + + if (self->bound > sblimit) { + self->bound = sblimit; + } + + // Read the allocation information + for (int sb = 0; sb < self->bound; sb++) { + self->allocation[0][sb] = plm_audio_read_allocation(self, sb, tab3); + self->allocation[1][sb] = plm_audio_read_allocation(self, sb, tab3); + } + + for (int sb = self->bound; sb < sblimit; sb++) { + self->allocation[0][sb] = + self->allocation[1][sb] = + plm_audio_read_allocation(self, sb, tab3); + } + + // Read scale factor selector information + int channels = (self->mode == PLM_AUDIO_MODE_MONO) ? 1 : 2; + for (int sb = 0; sb < sblimit; sb++) { + for (int ch = 0; ch < channels; ch++) { + if (self->allocation[ch][sb]) { + self->scale_factor_info[ch][sb] = plm_buffer_read(self->buffer, 2); + } + } + if (self->mode == PLM_AUDIO_MODE_MONO) { + self->scale_factor_info[1][sb] = self->scale_factor_info[0][sb]; + } + } + + // Read scale factors + for (int sb = 0; sb < sblimit; sb++) { + for (int ch = 0; ch < channels; ch++) { + if (self->allocation[ch][sb]) { + int *sf = self->scale_factor[ch][sb]; + switch (self->scale_factor_info[ch][sb]) { + case 0: + sf[0] = plm_buffer_read(self->buffer, 6); + sf[1] = plm_buffer_read(self->buffer, 6); + sf[2] = plm_buffer_read(self->buffer, 6); + break; + case 1: + sf[0] = + sf[1] = plm_buffer_read(self->buffer, 6); + sf[2] = plm_buffer_read(self->buffer, 6); + break; + case 2: + sf[0] = + sf[1] = + sf[2] = plm_buffer_read(self->buffer, 6); + break; + case 3: + sf[0] = plm_buffer_read(self->buffer, 6); + sf[1] = + sf[2] = plm_buffer_read(self->buffer, 6); + break; + } + } + } + if (self->mode == PLM_AUDIO_MODE_MONO) { + self->scale_factor[1][sb][0] = self->scale_factor[0][sb][0]; + self->scale_factor[1][sb][1] = self->scale_factor[0][sb][1]; + self->scale_factor[1][sb][2] = self->scale_factor[0][sb][2]; + } + } + + // Coefficient input and reconstruction + int out_pos = 0; + for (int part = 0; part < 3; part++) { + for (int granule = 0; granule < 4; granule++) { + + // Read the samples + for (int sb = 0; sb < self->bound; sb++) { + plm_audio_read_samples(self, 0, sb, part); + plm_audio_read_samples(self, 1, sb, part); + } + for (int sb = self->bound; sb < sblimit; sb++) { + plm_audio_read_samples(self, 0, sb, part); + self->sample[1][sb][0] = self->sample[0][sb][0]; + self->sample[1][sb][1] = self->sample[0][sb][1]; + self->sample[1][sb][2] = self->sample[0][sb][2]; + } + for (int sb = sblimit; sb < 32; sb++) { + self->sample[0][sb][0] = 0; + self->sample[0][sb][1] = 0; + self->sample[0][sb][2] = 0; + self->sample[1][sb][0] = 0; + self->sample[1][sb][1] = 0; + self->sample[1][sb][2] = 0; + } + + // Synthesis loop + for (int p = 0; p < 3; p++) { + // Shifting step + self->v_pos = (self->v_pos - 64) & 1023; + + for (int ch = 0; ch < 2; ch++) { + plm_audio_idct36(self->sample[ch], p, self->V[ch], self->v_pos); + + // Build U, windowing, calculate output + memset(self->U, 0, sizeof(self->U)); + + int d_index = 512 - (self->v_pos >> 1); + int v_index = (self->v_pos % 128) >> 1; + while (v_index < 1024) { + for (int i = 0; i < 32; ++i) { + self->U[i] += self->D[d_index++] * self->V[ch][v_index++]; + } + + v_index += 128 - 32; + d_index += 64 - 32; + } + + d_index -= (512 - 32); + v_index = (128 - 32 + 1024) - v_index; + while (v_index < 1024) { + for (int i = 0; i < 32; ++i) { + self->U[i] += self->D[d_index++] * self->V[ch][v_index++]; + } + + v_index += 128 - 32; + d_index += 64 - 32; + } + + // Output samples + #ifdef PLM_AUDIO_SEPARATE_CHANNELS + float *out_channel = ch == 0 + ? self->samples.left + : self->samples.right; + for (int j = 0; j < 32; j++) { + out_channel[out_pos + j] = self->U[j] / -1090519040.0f; + } + #else + for (int j = 0; j < 32; j++) { + self->samples.interleaved[((out_pos + j) << 1) + ch] = + self->U[j] / -1090519040.0f; + } + #endif + } // End of synthesis channel loop + out_pos += 32; + } // End of synthesis sub-block loop + + } // Decoding of the granule finished + } + + plm_buffer_align(self->buffer); +} + +const plm_quantizer_spec_t *plm_audio_read_allocation(plm_audio_t *self, int sb, int tab3) { + int tab4 = PLM_AUDIO_QUANT_LUT_STEP_3[tab3][sb]; + int qtab = PLM_AUDIO_QUANT_LUT_STEP_4[tab4 & 15][plm_buffer_read(self->buffer, tab4 >> 4)]; + return qtab ? (&PLM_AUDIO_QUANT_TAB[qtab - 1]) : 0; +} + +void plm_audio_read_samples(plm_audio_t *self, int ch, int sb, int part) { + const plm_quantizer_spec_t *q = self->allocation[ch][sb]; + int sf = self->scale_factor[ch][sb][part]; + int *sample = self->sample[ch][sb]; + int val = 0; + + if (!q) { + // No bits allocated for this subband + sample[0] = sample[1] = sample[2] = 0; + return; + } + + // Resolve scalefactor + if (sf == 63) { + sf = 0; + } + else { + int shift = (sf / 3) | 0; + sf = (PLM_AUDIO_SCALEFACTOR_BASE[sf % 3] + ((1 << shift) >> 1)) >> shift; + } + + // Decode samples + int adj = q->levels; + if (q->group) { + // Decode grouped samples + val = plm_buffer_read(self->buffer, q->bits); + sample[0] = val % adj; + val /= adj; + sample[1] = val % adj; + sample[2] = val / adj; + } + else { + // Decode direct samples + sample[0] = plm_buffer_read(self->buffer, q->bits); + sample[1] = plm_buffer_read(self->buffer, q->bits); + sample[2] = plm_buffer_read(self->buffer, q->bits); + } + + // Postmultiply samples + int scale = 65536 / (adj + 1); + adj = ((adj + 1) >> 1) - 1; + + val = (adj - sample[0]) * scale; + sample[0] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12; + + val = (adj - sample[1]) * scale; + sample[1] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12; + + val = (adj - sample[2]) * scale; + sample[2] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12; +} + +void plm_audio_idct36(int s[32][3], int ss, float *d, int dp) { + float t01, t02, t03, t04, t05, t06, t07, t08, t09, t10, t11, t12, + t13, t14, t15, t16, t17, t18, t19, t20, t21, t22, t23, t24, + t25, t26, t27, t28, t29, t30, t31, t32, t33; + + t01 = (float)(s[0][ss] + s[31][ss]); t02 = (float)(s[0][ss] - s[31][ss]) * 0.500602998235f; + t03 = (float)(s[1][ss] + s[30][ss]); t04 = (float)(s[1][ss] - s[30][ss]) * 0.505470959898f; + t05 = (float)(s[2][ss] + s[29][ss]); t06 = (float)(s[2][ss] - s[29][ss]) * 0.515447309923f; + t07 = (float)(s[3][ss] + s[28][ss]); t08 = (float)(s[3][ss] - s[28][ss]) * 0.53104259109f; + t09 = (float)(s[4][ss] + s[27][ss]); t10 = (float)(s[4][ss] - s[27][ss]) * 0.553103896034f; + t11 = (float)(s[5][ss] + s[26][ss]); t12 = (float)(s[5][ss] - s[26][ss]) * 0.582934968206f; + t13 = (float)(s[6][ss] + s[25][ss]); t14 = (float)(s[6][ss] - s[25][ss]) * 0.622504123036f; + t15 = (float)(s[7][ss] + s[24][ss]); t16 = (float)(s[7][ss] - s[24][ss]) * 0.674808341455f; + t17 = (float)(s[8][ss] + s[23][ss]); t18 = (float)(s[8][ss] - s[23][ss]) * 0.744536271002f; + t19 = (float)(s[9][ss] + s[22][ss]); t20 = (float)(s[9][ss] - s[22][ss]) * 0.839349645416f; + t21 = (float)(s[10][ss] + s[21][ss]); t22 = (float)(s[10][ss] - s[21][ss]) * 0.972568237862f; + t23 = (float)(s[11][ss] + s[20][ss]); t24 = (float)(s[11][ss] - s[20][ss]) * 1.16943993343f; + t25 = (float)(s[12][ss] + s[19][ss]); t26 = (float)(s[12][ss] - s[19][ss]) * 1.48416461631f; + t27 = (float)(s[13][ss] + s[18][ss]); t28 = (float)(s[13][ss] - s[18][ss]) * 2.05778100995f; + t29 = (float)(s[14][ss] + s[17][ss]); t30 = (float)(s[14][ss] - s[17][ss]) * 3.40760841847f; + t31 = (float)(s[15][ss] + s[16][ss]); t32 = (float)(s[15][ss] - s[16][ss]) * 10.1900081235f; + + t33 = t01 + t31; t31 = (t01 - t31) * 0.502419286188f; + t01 = t03 + t29; t29 = (t03 - t29) * 0.52249861494f; + t03 = t05 + t27; t27 = (t05 - t27) * 0.566944034816f; + t05 = t07 + t25; t25 = (t07 - t25) * 0.64682178336f; + t07 = t09 + t23; t23 = (t09 - t23) * 0.788154623451f; + t09 = t11 + t21; t21 = (t11 - t21) * 1.06067768599f; + t11 = t13 + t19; t19 = (t13 - t19) * 1.72244709824f; + t13 = t15 + t17; t17 = (t15 - t17) * 5.10114861869f; + t15 = t33 + t13; t13 = (t33 - t13) * 0.509795579104f; + t33 = t01 + t11; t01 = (t01 - t11) * 0.601344886935f; + t11 = t03 + t09; t09 = (t03 - t09) * 0.899976223136f; + t03 = t05 + t07; t07 = (t05 - t07) * 2.56291544774f; + t05 = t15 + t03; t15 = (t15 - t03) * 0.541196100146f; + t03 = t33 + t11; t11 = (t33 - t11) * 1.30656296488f; + t33 = t05 + t03; t05 = (t05 - t03) * 0.707106781187f; + t03 = t15 + t11; t15 = (t15 - t11) * 0.707106781187f; + t03 += t15; + t11 = t13 + t07; t13 = (t13 - t07) * 0.541196100146f; + t07 = t01 + t09; t09 = (t01 - t09) * 1.30656296488f; + t01 = t11 + t07; t07 = (t11 - t07) * 0.707106781187f; + t11 = t13 + t09; t13 = (t13 - t09) * 0.707106781187f; + t11 += t13; t01 += t11; + t11 += t07; t07 += t13; + t09 = t31 + t17; t31 = (t31 - t17) * 0.509795579104f; + t17 = t29 + t19; t29 = (t29 - t19) * 0.601344886935f; + t19 = t27 + t21; t21 = (t27 - t21) * 0.899976223136f; + t27 = t25 + t23; t23 = (t25 - t23) * 2.56291544774f; + t25 = t09 + t27; t09 = (t09 - t27) * 0.541196100146f; + t27 = t17 + t19; t19 = (t17 - t19) * 1.30656296488f; + t17 = t25 + t27; t27 = (t25 - t27) * 0.707106781187f; + t25 = t09 + t19; t19 = (t09 - t19) * 0.707106781187f; + t25 += t19; + t09 = t31 + t23; t31 = (t31 - t23) * 0.541196100146f; + t23 = t29 + t21; t21 = (t29 - t21) * 1.30656296488f; + t29 = t09 + t23; t23 = (t09 - t23) * 0.707106781187f; + t09 = t31 + t21; t31 = (t31 - t21) * 0.707106781187f; + t09 += t31; t29 += t09; t09 += t23; t23 += t31; + t17 += t29; t29 += t25; t25 += t09; t09 += t27; + t27 += t23; t23 += t19; t19 += t31; + t21 = t02 + t32; t02 = (t02 - t32) * 0.502419286188f; + t32 = t04 + t30; t04 = (t04 - t30) * 0.52249861494f; + t30 = t06 + t28; t28 = (t06 - t28) * 0.566944034816f; + t06 = t08 + t26; t08 = (t08 - t26) * 0.64682178336f; + t26 = t10 + t24; t10 = (t10 - t24) * 0.788154623451f; + t24 = t12 + t22; t22 = (t12 - t22) * 1.06067768599f; + t12 = t14 + t20; t20 = (t14 - t20) * 1.72244709824f; + t14 = t16 + t18; t16 = (t16 - t18) * 5.10114861869f; + t18 = t21 + t14; t14 = (t21 - t14) * 0.509795579104f; + t21 = t32 + t12; t32 = (t32 - t12) * 0.601344886935f; + t12 = t30 + t24; t24 = (t30 - t24) * 0.899976223136f; + t30 = t06 + t26; t26 = (t06 - t26) * 2.56291544774f; + t06 = t18 + t30; t18 = (t18 - t30) * 0.541196100146f; + t30 = t21 + t12; t12 = (t21 - t12) * 1.30656296488f; + t21 = t06 + t30; t30 = (t06 - t30) * 0.707106781187f; + t06 = t18 + t12; t12 = (t18 - t12) * 0.707106781187f; + t06 += t12; + t18 = t14 + t26; t26 = (t14 - t26) * 0.541196100146f; + t14 = t32 + t24; t24 = (t32 - t24) * 1.30656296488f; + t32 = t18 + t14; t14 = (t18 - t14) * 0.707106781187f; + t18 = t26 + t24; t24 = (t26 - t24) * 0.707106781187f; + t18 += t24; t32 += t18; + t18 += t14; t26 = t14 + t24; + t14 = t02 + t16; t02 = (t02 - t16) * 0.509795579104f; + t16 = t04 + t20; t04 = (t04 - t20) * 0.601344886935f; + t20 = t28 + t22; t22 = (t28 - t22) * 0.899976223136f; + t28 = t08 + t10; t10 = (t08 - t10) * 2.56291544774f; + t08 = t14 + t28; t14 = (t14 - t28) * 0.541196100146f; + t28 = t16 + t20; t20 = (t16 - t20) * 1.30656296488f; + t16 = t08 + t28; t28 = (t08 - t28) * 0.707106781187f; + t08 = t14 + t20; t20 = (t14 - t20) * 0.707106781187f; + t08 += t20; + t14 = t02 + t10; t02 = (t02 - t10) * 0.541196100146f; + t10 = t04 + t22; t22 = (t04 - t22) * 1.30656296488f; + t04 = t14 + t10; t10 = (t14 - t10) * 0.707106781187f; + t14 = t02 + t22; t02 = (t02 - t22) * 0.707106781187f; + t14 += t02; t04 += t14; t14 += t10; t10 += t02; + t16 += t04; t04 += t08; t08 += t14; t14 += t28; + t28 += t10; t10 += t20; t20 += t02; t21 += t16; + t16 += t32; t32 += t04; t04 += t06; t06 += t08; + t08 += t18; t18 += t14; t14 += t30; t30 += t28; + t28 += t26; t26 += t10; t10 += t12; t12 += t20; + t20 += t24; t24 += t02; + + d[dp + 48] = -t33; + d[dp + 49] = d[dp + 47] = -t21; + d[dp + 50] = d[dp + 46] = -t17; + d[dp + 51] = d[dp + 45] = -t16; + d[dp + 52] = d[dp + 44] = -t01; + d[dp + 53] = d[dp + 43] = -t32; + d[dp + 54] = d[dp + 42] = -t29; + d[dp + 55] = d[dp + 41] = -t04; + d[dp + 56] = d[dp + 40] = -t03; + d[dp + 57] = d[dp + 39] = -t06; + d[dp + 58] = d[dp + 38] = -t25; + d[dp + 59] = d[dp + 37] = -t08; + d[dp + 60] = d[dp + 36] = -t11; + d[dp + 61] = d[dp + 35] = -t18; + d[dp + 62] = d[dp + 34] = -t09; + d[dp + 63] = d[dp + 33] = -t14; + d[dp + 32] = -t05; + d[dp + 0] = t05; d[dp + 31] = -t30; + d[dp + 1] = t30; d[dp + 30] = -t27; + d[dp + 2] = t27; d[dp + 29] = -t28; + d[dp + 3] = t28; d[dp + 28] = -t07; + d[dp + 4] = t07; d[dp + 27] = -t26; + d[dp + 5] = t26; d[dp + 26] = -t23; + d[dp + 6] = t23; d[dp + 25] = -t10; + d[dp + 7] = t10; d[dp + 24] = -t15; + d[dp + 8] = t15; d[dp + 23] = -t12; + d[dp + 9] = t12; d[dp + 22] = -t19; + d[dp + 10] = t19; d[dp + 21] = -t20; + d[dp + 11] = t20; d[dp + 20] = -t13; + d[dp + 12] = t13; d[dp + 19] = -t24; + d[dp + 13] = t24; d[dp + 18] = -t31; + d[dp + 14] = t31; d[dp + 17] = -t02; + d[dp + 15] = t02; d[dp + 16] = 0.0; +} + + +#endif // PL_MPEG_IMPLEMENTATION diff --git a/run-native.sh b/run-native.sh new file mode 100755 index 00000000..327b6b5b --- /dev/null +++ b/run-native.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Launch the natively-built HDZero emulator on the HOST (macOS/Linux) in a real +# SDL window. This is the interactive counterpart to .devcontainer/emu.sh, which +# runs headless in the container for CI screenshots. +# +# Build first IN THE CONTAINER (nothing is compiled on the host): +# .devcontainer/build.sh mac # -> build_xmac/HDZGOGGLE (macOS) +# .devcontainer/build.sh linux # -> build_emu_linux/HDZGOGGLE (Linux) +# Then: ./run-native.sh +# +# In the window, long-press the wheel (D) to enter the FPV view; the mock +# video device plays $HDZ_VIDEO (default sdcard/loop.mpg) behind the OSD. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Default to the container-built binary for this OS (override with BUILD_DIR=...). +if [ -z "${BUILD_DIR:-}" ]; then + case "$(uname -s)" in + Darwin) BUILD_DIR="$REPO_ROOT/build_xmac" ;; + *) BUILD_DIR="$REPO_ROOT/build_emu_linux" ;; + esac +fi +BIN="$BUILD_DIR/HDZGOGGLE" +LOG_DIR="${LOG_DIR:-$REPO_ROOT/tmp/logs}" +mkdir -p "$LOG_DIR" + +[ -x "$BIN" ] || { echo "not built: $BIN — run cmake + make first (see header)"; exit 1; } + +# Same configurable roots the goggle build compiles away: point them at the repo's +# built-in resources and the safe, gitignored sdcard copy. +: "${HDZ_SDCARD:=$REPO_ROOT/sdcard}" +export HDZ_APP_ROOT="${HDZ_APP_ROOT:-$REPO_ROOT/mkapp/app}" +export HDZ_EXTSD_ROOT="${HDZ_EXTSD_ROOT:-$HDZ_SDCARD}" +export HDZ_MEDIA_DIR="${HDZ_MEDIA_DIR:-$HDZ_SDCARD/DCIM/100HDZRO}" +export HDZ_VIDEO="${HDZ_VIDEO:-$HDZ_SDCARD/loop.mpg}" + +echo ">> launching $BIN" +echo " video feed : $HDZ_VIDEO" +echo " FPV view : long-press D (wheel) once the menu is up" +echo " log : $LOG_DIR/native-run.log" +( cd "$BUILD_DIR" && "./$(basename "$BIN")" ) 2>&1 | tee "$LOG_DIR/native-run.log" diff --git a/src/core/elrs.c b/src/core/elrs.c index 02e2a26a..3e0e0d9e 100644 --- a/src/core/elrs.c +++ b/src/core/elrs.c @@ -12,9 +12,47 @@ #include #include #include -#include +#if !defined(_WIN32) +#include // serial config (device only); mingw/Windows has no +#endif #include +// Desktop (SDL) emulator byte-order shims: neither Darwin nor Windows has the +// le*toh() macros the code uses. +#if defined(__APPLE__) +#include +#define le16toh(x) OSSwapLittleToHostInt16(x) +#define le32toh(x) OSSwapLittleToHostInt32(x) +#define le64toh(x) OSSwapLittleToHostInt64(x) +#elif defined(_WIN32) +// mingw targets little-endian x86_64/arm64, so host order == little-endian. +#define le16toh(x) (x) +#define le32toh(x) (x) +#define le64toh(x) (x) +#endif + +#if defined(__APPLE__) +// sem_timedwait() is not implemented on macOS, so poll sem_trywait(). +#include +static inline int compat_sem_timedwait(sem_t *sem, const struct timespec *abs_timeout) { + for (;;) { + if (sem_trywait(sem) == 0) + return 0; + if (errno != EAGAIN) + return -1; + struct timespec now; + clock_gettime(CLOCK_REALTIME, &now); + if (now.tv_sec > abs_timeout->tv_sec || + (now.tv_sec == abs_timeout->tv_sec && now.tv_nsec >= abs_timeout->tv_nsec)) { + errno = ETIMEDOUT; + return -1; + } + usleep(1000); + } +} +#define sem_timedwait(sem, ts) compat_sem_timedwait((sem), (ts)) +#endif + #include #include "core/app_state.h" diff --git a/src/core/esp32_flash.c b/src/core/esp32_flash.c index 12958e98..d731cda0 100644 --- a/src/core/esp32_flash.c +++ b/src/core/esp32_flash.c @@ -9,7 +9,10 @@ #include #include #include -#include +#include // clock() / CLOCKS_PER_SEC (not pulled in transitively on older mingw) +#if !defined(_WIN32) +#include // serial config (device only); mingw/Windows has no +#endif #include #include "core/common.hh" @@ -63,6 +66,13 @@ esp_loader_error_t loader_port_serial_write(const uint8_t *data, uint16_t size, } static esp_loader_error_t read_char(uint8_t *c, uint32_t timeout) { +#if defined(_WIN32) + // ESP32 serial flasher is device-only; POSIX select() on a serial fd is not + // available under mingw, so this is inert in the Windows emulator build. + (void)c; + (void)timeout; + return ESP_LOADER_ERROR_FAIL; +#else struct timeval tv = {timeout / 1000, (timeout % 1000) * 1000}; fd_set rd; FD_ZERO(&rd); @@ -82,6 +92,7 @@ static esp_loader_error_t read_char(uint8_t *c, uint32_t timeout) { } else { return ESP_LOADER_ERROR_FAIL; } +#endif } static esp_loader_error_t read_data(uint8_t *buffer, uint32_t size) { diff --git a/src/core/ht.c b/src/core/ht.c index 9cb27e4f..03f9dfd3 100644 --- a/src/core/ht.c +++ b/src/core/ht.c @@ -3,6 +3,9 @@ #include #include #include +#if !defined(__linux__) +#include "platform/timer_compat.h" // POSIX interval-timer shim (macOS/Windows) +#endif #include #include diff --git a/src/core/input_device.c b/src/core/input_device.c index d972dcb9..e1a222af 100644 --- a/src/core/input_device.c +++ b/src/core/input_device.c @@ -1,25 +1,17 @@ -#include #include -#include -#include #include #include #include #include -#include #include -#include #include #include #include -#ifdef EMULATOR_BUILD -#include "SDLaccess.h" -#endif - #include "defines.h" #include "input_device.h" +#include "input_device_internal.h" #include "common.hh" #include "ht.h" @@ -58,12 +50,10 @@ static uint8_t tune_state = 0; // 0=init; 1=waiting for key; 2=tuning static uint16_t tune_timer = 0; -#define EPOLL_FD_CNT 4 - -static int epfd; static pthread_t input_device_pid; -static int btn_value = 0; +// Shared with the input backends (input_device_internal.h): right button held state. +int btn_value = 0; // action: 1 = tune up, 2 = tune down, 3 = confirm void exit_tune_channel() { @@ -216,16 +206,11 @@ void tune_channel_timer() { } /////////////////////////////////////////////////////////////////////////////// -static int roller_up_acc = 0; -static int roller_down_acc = 0; - -static bool scroll_sim_mode = false; -static bool scroll_sim_mode_pending = false; - -#define SCROLL_REPEAT_NONE 0 -#define SCROLL_REPEAT_UP 1 -#define SCROLL_REPEAT_DOWN 2 -static int scroll_sim_mode_repeat = SCROLL_REPEAT_NONE; +// Shared with the input backends (input_device_internal.h): the no_dial scroll- +// simulation state, driven by the hardware backend together with rbtn_click(). +bool scroll_sim_mode = false; +bool scroll_sim_mode_pending = false; +int scroll_sim_mode_repeat = SCROLL_REPEAT_NONE; void (*btn_click_callback)() = &osd_toggle; void (*btn_press_callback)() = &app_switch_to_menu; @@ -236,10 +221,7 @@ void (*rbtn_double_click_callback)() = &ht_set_center_position; void (*roller_callback)(uint8_t key) = &tune_channel; -static void roller_up(void); -static void roller_down(void); - -static void btn_press(void) // long press left key +void btn_press(void) // long press left key { LOGI("btn_press (%d)", g_app_state); if (g_scanning || (g_init_done != 1)) // no long pree Enter before done with init @@ -301,7 +283,7 @@ static void btn_press(void) // long press left key pthread_mutex_unlock(&lvgl_mutex); } -static void btn_click(void) // short press enter key +void btn_click(void) // short press enter key { LOGI("btn_click (%d)", g_app_state); if (g_init_done != 1) // no short pree Enter before done with init @@ -428,7 +410,7 @@ void rbtn_click(right_button_t click_type) { } } -static void roller_up(void) { +void roller_up(void) { LOGI("roller up (%d)", g_app_state); if (g_scanning) @@ -463,7 +445,7 @@ static void roller_up(void) { pthread_mutex_unlock(&lvgl_mutex); } -static void roller_down(void) { +void roller_down(void) { LOGI("roller down (%d)", g_app_state); if (g_scanning) @@ -497,272 +479,9 @@ static void roller_down(void) { pthread_mutex_unlock(&lvgl_mutex); } -static void get_event(int fd) { - struct input_event event; - static int event_type_last = 0; - static int btn_press_time = 0; - - static int roller_value = 0; - - // time (sec, usec) difference above which will the next scroll wheel event be accepted - // 10000 usec = 10msec is more than short enough (100Hz) - // scroll events - const struct timeval scroll_time_diff = {0, 10000}; - // direction change events - const struct timeval rel_time_diff = {0, 20000}; - // expected timestamp in the future beyond that will the events be accepted - static struct timeval next_scroll = {0, 0}; - static struct timeval next_rel = {0, 0}; - static bool discard_scroll = false; - - read(fd, &event, sizeof(event)); - - switch (event.type) { - case EV_SYN: - if (event.code == SYN_REPORT) { - if (event_type_last == EV_REL) { - if (g_setting.ease.no_dial) - break; - - if (!discard_scroll && timercmp(&event.time, &next_scroll, >)) { - timeradd(&event.time, &scroll_time_diff, &next_scroll); - if (roller_value == 1) { - roller_up_acc++; - roller_down_acc = 0; - } else if (roller_value == -1) { - roller_down_acc++; - roller_up_acc = 0; - } - - if (roller_up_acc == DIAL_SENSITIVITY) { - roller_up(); - g_key = DIAL_KEY_UP; - roller_up_acc = 0; - } else if (roller_down_acc == DIAL_SENSITIVITY) { - roller_down(); - g_key = DIAL_KEY_DOWN; - roller_down_acc = 0; - } - } else { - // LOGI("discard EV_SYN"); - } - } else if (event_type_last == EV_KEY) { - if (btn_value) { - if (!g_setting.ease.no_dial) { - if (btn_press_time == 10) { - btn_press(); - g_key = DIAL_KEY_PRESS; - } - } else { - if (scroll_sim_mode_repeat == SCROLL_REPEAT_DOWN) { - roller_down(); - } else if (scroll_sim_mode_repeat == SCROLL_REPEAT_UP) { - roller_up(); - } - } - if (scroll_sim_mode_pending) - btn_press_time = 0; - else - btn_press_time++; - // LOGI("btn down"); - } else { - if (scroll_sim_mode_pending) { - scroll_sim_mode_pending = false; - } else { - if (scroll_sim_mode_repeat == SCROLL_REPEAT_NONE) { - if (btn_press_time < 10) { - btn_click(); - g_key = DIAL_KEY_CLICK; - } else if (g_setting.ease.no_dial) { - if (btn_press_time < 50) { - btn_press(); - g_key = DIAL_KEY_PRESS; - } - // else if(btn_press_time > 200){ - // btn_super_press(); - // } - } - } - } - btn_press_time = 0; - } - } - // LOGI("------------ syn report ----------"); - } else if (event.code == SYN_MT_REPORT) { - // LOGI("----------- syn mt report ------------"); - } - break; - case EV_KEY: - // LOGI("key code%d is %s!", event.code, event.value?"down":"up"); - btn_value = event.value; - event_type_last = EV_KEY; - break; - case EV_ABS: - if ((event.code == ABS_X) || - (event.code == ABS_MT_POSITION_X)) { - // LOGI("abs,x = %d", event.value); - } else if ((event.code == ABS_Y) || - (event.code == ABS_MT_POSITION_Y)) { - // LOGI("abs,y = %d", event.value); - } else if ((event.code == ABS_PRESSURE) || - (event.code == ABS_MT_PRESSURE)) { - // LOGI("pressure value: %d", event.value); - } - break; - case EV_REL: - if (timercmp(&event.time, &next_rel, >)) { - discard_scroll = false; - if (event.code == REL_X) { - // LOGI("x = %d", event.value); - } else if (event.code == REL_Y) { - if (roller_value != event.value) { - timeradd(&event.time, &rel_time_diff, &next_rel); - roller_value = event.value; - // LOGI("y = %d", event.value); - } - event_type_last = EV_REL; - } - } else { - discard_scroll = true; - LOGI("discard EV_REL"); - } - break; - default: - // LOGI("unknown [type=%d, code=%d value=%d]", event.type, event.code, event.value); - break; - } -} - -static void add_to_epfd(int epfd, int fd) { - struct epoll_event event = { - .events = EPOLLIN, - .data = { - .fd = fd, - }, - }; - - int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); - assert(ret == 0); -} - -static void *thread_input_device(void *ptr) { -#ifndef EMULATOR_BUILD - for (;;) { - struct epoll_event events[EPOLL_FD_CNT]; - - int ret = epoll_wait(epfd, events, EPOLL_FD_CNT, DIAL_SENSITIVTY_TIMEOUT_MS); - if (ret < 0) { - perror("epoll_wait"); - continue; - } - - if (ret > 0) { - for (int i = 0; i < ret; i++) { - if (events[i].events & EPOLLIN) { - get_event(events[i].data.fd); - } - } - } else { - roller_up_acc = 0; - roller_down_acc = 0; - if (scroll_sim_mode_repeat != SCROLL_REPEAT_NONE) - beep(); - } - } - return NULL; -#else - static uint32_t btn_d_start = 0; - static uint32_t btn_a_start = 0; - - while (true) { - SDL_Event event; - SDL_LockMutex(global_sdl_mutex); - while (SDL_PollEvent(&event)) { - SDL_UnlockMutex(global_sdl_mutex); - switch (event.type) { - case SDL_QUIT: - exit(0); - - case SDL_KEYDOWN: - switch (event.key.keysym.sym) { - case SDLK_d: - if (!btn_d_start) { - btn_d_start = event.key.timestamp; - } - break; - - case SDLK_a: - if (!btn_a_start) { - btn_a_start = event.key.timestamp; - } - break; - } - break; - - case SDL_KEYUP: - switch (event.key.keysym.sym) { - case SDLK_s: - roller_up(); - g_key = DIAL_KEY_UP; - break; - - case SDLK_w: - roller_down(); - g_key = DIAL_KEY_DOWN; - break; - - case SDLK_d: - if (event.key.timestamp - btn_d_start > 500) { - btn_press(); - g_key = DIAL_KEY_PRESS; - } else { - btn_click(); - g_key = DIAL_KEY_CLICK; - } - btn_d_start = 0; - break; - - case SDLK_a: - if (event.key.timestamp - btn_a_start > 500) { - rbtn_click(RIGHT_LONG_PRESS); - g_key = RIGHT_KEY_PRESS; - } else { - rbtn_click(RIGHT_CLICK); - g_key = RIGHT_KEY_CLICK; - } - btn_a_start = 0; - break; - } - break; - } - SDL_LockMutex(global_sdl_mutex); - } - SDL_UnlockMutex(global_sdl_mutex); - usleep(50000); // Sorry, this will break windows, but it's not like it is working now anyway :-( - } -#endif -} - void input_device_init() { -#ifndef EMULATOR_BUILD - epfd = epoll_create(EPOLL_FD_CNT); - assert(epfd > 0); - - char buf[64]; - for (int i = 0; i < EPOLL_FD_CNT; i++) { - snprintf(buf, 64, "/dev/input/event%d", i); - - int fd = open(buf, O_RDONLY); - if (fd >= 0) { - add_to_epfd(epfd, fd); - LOGI("opened %s", buf); - } - } - app_state_push(APP_STATE_MAINMENU); -#else - if (SDL_Init(SDL_INIT_VIDEO) != 0) { - printf("Error initializing SDL: %s\n", SDL_GetError()); - } -#endif - pthread_create(&input_device_pid, NULL, thread_input_device, NULL); + // Bring up whichever input backend was compiled in (evdev on the goggle, + // SDL in the emulator) and run its read loop on a background thread. + input_backend_init(); + pthread_create(&input_device_pid, NULL, input_backend_thread, NULL); } diff --git a/src/core/input_device.h b/src/core/input_device.h index 81923eae..016c6880 100644 --- a/src/core/input_device.h +++ b/src/core/input_device.h @@ -5,6 +5,7 @@ extern "C" { #endif #include +#include // uint8_t in the prototypes below typedef enum { RIGHT_CLICK = 0, @@ -13,6 +14,10 @@ typedef enum { } right_button_t; void input_device_init(); +#ifdef EMULATOR_BUILD +void input_device_pump(void); // pump SDL events (call from main thread on macOS) +void input_device_print_help(void); // print the keyboard<->goggle control map on launch +#endif void tune_channel(uint8_t key); void tune_channel_timer(); void tune_channel_confirm(); diff --git a/src/core/input_device_internal.h b/src/core/input_device_internal.h new file mode 100644 index 00000000..8bd62fe4 --- /dev/null +++ b/src/core/input_device_internal.h @@ -0,0 +1,33 @@ +#pragma once + +// Internal contract between the shared input logic (src/core/input_device.c) and +// the per-target input backend that feeds it raw events: +// - src/driver/input_evdev.c goggle hardware (Linux evdev / epoll) +// - src/emulator/input_device_emu.c desktop emulator (SDL keyboard) +// Exactly one backend is compiled in (CMake swaps them, like uart/rtc), so the +// shared logic and the hardware path carry no #ifdef EMULATOR_BUILD. This is not +// a public API -- only these files include it. + +#include + +// Semantic handlers a backend calls once it has decoded a raw event into a goggle +// action. (rbtn_click() is the third and lives in the public input_device.h.) +void roller_up(void); +void roller_down(void); +void btn_click(void); +void btn_press(void); + +// Right-button "scroll simulation" state for the no_dial accessibility mode. The +// hardware backend drives these in lock-step with rbtn_click(); the emulator has a +// real wheel and leaves them untouched. +#define SCROLL_REPEAT_NONE 0 +#define SCROLL_REPEAT_UP 1 +#define SCROLL_REPEAT_DOWN 2 +extern int btn_value; // right button currently held (1) or released (0) +extern bool scroll_sim_mode; // scroll-sim mode active +extern bool scroll_sim_mode_pending; // a mode transition is settling +extern int scroll_sim_mode_repeat; // SCROLL_REPEAT_* + +// The backend, implemented by whichever backend file is compiled in. +void input_backend_init(void); // open the event source (evdev fds / SDL) +void *input_backend_thread(void *); // read loop, run on a background pthread diff --git a/src/core/main.c b/src/core/main.c index c042ff4b..36aa6bb7 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -27,6 +27,7 @@ SDL_mutex *global_sdl_mutex; #include "core/osd.h" #include "core/self_test.h" #include "core/settings.h" +#include "platform/paths.h" #include "core/sleep_mode.h" #include "core/thread.h" #include "driver/beep.h" @@ -143,7 +144,7 @@ void lvgl_init() { lv_theme_t *theme = lv_theme_default_init(dispp, lv_color_make(0xff, 0xff, 0xff), lv_palette_main(LV_PALETTE_RED), false, LV_FONT_DEFAULT); lv_disp_set_theme(dispp, theme); - lv_obj_set_style_bg_color(lv_scr_act(), lv_color_make(64, 64, 64), 0); + lv_obj_set_style_bg_color(lv_scr_act(), lv_color_make(12, 15, 18), 0); } int main(int argc, char *argv[]) { @@ -156,6 +157,10 @@ int main(int argc, char *argv[]) { } #endif + // 0. Resolve filesystem roots (optional config file / env; defaults to the + // on-device paths, so the goggle firmware build is unchanged). + paths_init(); + // 1. Recall configuration settings_init(); settings_load(); @@ -219,18 +224,28 @@ int main(int argc, char *argv[]) { // 10. Execute main loop g_init_done = 1; +#if defined(EMULATOR_BUILD) + input_device_print_help(); // announce the keyboard<->goggle control map +#endif for (;;) { pthread_mutex_lock(&lvgl_mutex); main_menu_update(); sleep_reminder(); statubar_update(); osd_hdzero_update(); +#if defined(EMULATOR_BUILD) + if (gif_cnt % 200 == 0) + osd_inject_mock_fc(); // ~1s: mock Betaflight OSD so the FPV view is representative +#endif ims_update(); ui_osd_element_pos_update(); ht_detect_motion(); lv_timer_handler(); source_status_timer(); pthread_mutex_unlock(&lvgl_mutex); +#if defined(EMULATOR_BUILD) && (defined(__APPLE__) || defined(_WIN32)) + input_device_pump(); // macOS + Windows: SDL events must be pumped on the main (window) thread +#endif usleep(5000); gif_cnt++; } diff --git a/src/core/osd.c b/src/core/osd.c index e4c65fdc..737028ac 100644 --- a/src/core/osd.c +++ b/src/core/osd.c @@ -23,9 +23,10 @@ #include "core/elrs.h" #include "core/msp_displayport.h" #include "core/settings.h" +#include "platform/paths.h" + #include "driver/dm5680.h" #include "driver/fans.h" -#include "driver/fbtools.h" #include "driver/hardware.h" #include "driver/i2c.h" #include "driver/nct75.h" @@ -75,7 +76,8 @@ void osd_resource_path(char *buf, const char *fmt, osd_resource_t osd_resource_t vsnprintf(filename, sizeof(filename), fmt, args); va_end(args); strcpy(buf2, buf); - strcpy(buf, RESOURCE_PATH_SDCARD); + strcpy(buf, path_extsd()); + strcat(buf, "/resource/OSD/GOGGLE/"); if (osd_resource_type == OSD_RESOURCE_1080) { strcat(buf, "FHD/"); @@ -84,7 +86,8 @@ void osd_resource_path(char *buf, const char *fmt, osd_resource_t osd_resource_t if (access(buf, F_OK) != 0) { strcpy(buf, buf2); - strcpy(buf, RESOURCE_PATH); + strcpy(buf, path_app()); + strcat(buf, "/resource/OSD/GOGGLE/"); if (osd_resource_type == OSD_RESOURCE_1080) { strcat(buf, "FHD/"); } @@ -1111,13 +1114,13 @@ void load_fc_osd_font(uint8_t fhd) { int i; if (fhd) { - snprintf(fp[0], sizeof(fp[0]), "%s%s_FHD_000.bmp", FC_OSD_SDCARD_PATH, fc_variant); - snprintf(fp[1], sizeof(fp[1]), "%s%s_FHD_000.bmp", FC_OSD_LOCAL_PATH, fc_variant); - snprintf(fp[2], sizeof(fp[2]), "%sBTFL_FHD_000.bmp", FC_OSD_LOCAL_PATH); + snprintf(fp[0], sizeof(fp[0]), "%s/resource/OSD/FC/%s_FHD_000.bmp", path_extsd(), fc_variant); + snprintf(fp[1], sizeof(fp[1]), "%s/resource/OSD/FC/%s_FHD_000.bmp", path_app(), fc_variant); + snprintf(fp[2], sizeof(fp[2]), "%s/resource/OSD/FC/BTFL_FHD_000.bmp", path_app()); } else { - snprintf(fp[0], sizeof(fp[0]), "%s%s_000.bmp", FC_OSD_SDCARD_PATH, fc_variant); - snprintf(fp[1], sizeof(fp[1]), "%s%s_000.bmp", FC_OSD_LOCAL_PATH, fc_variant); - snprintf(fp[2], sizeof(fp[2]), "%sBTFL_000.bmp", FC_OSD_LOCAL_PATH); + snprintf(fp[0], sizeof(fp[0]), "%s/resource/OSD/FC/%s_000.bmp", path_extsd(), fc_variant); + snprintf(fp[1], sizeof(fp[1]), "%s/resource/OSD/FC/%s_000.bmp", path_app(), fc_variant); + snprintf(fp[2], sizeof(fp[2]), "%s/resource/OSD/FC/BTFL_000.bmp", path_app()); } // Optimized for runtime execution @@ -1181,6 +1184,43 @@ void osd_signal_update() { sem_post(&osd_semaphore); } +#ifdef EMULATOR_BUILD +// Mock a Betaflight MSP-DisplayPort OSD so the emulator's FPV view is +// representative for design work — no flight controller required. +// Edit the PUT(row, col, "text") lines to try layouts, rebuild, and see it render. +// Grid is HD_VMAX(18) rows x HD_HMAX(50) cols; (0,0) is top-left. +void osd_inject_mock_fc(void) { + for (int i = 0; i < HD_VMAX; i++) + for (int j = 0; j < HD_HMAX; j++) + fc_osd[i][j] = 0x20; // clear to spaces +#define PUT(r, c, s) \ + do { \ + const char *_p = (s); \ + for (int _k = 0; _p[_k] && ((c) + _k) < HD_HMAX; _k++) \ + fc_osd[(r)][(c) + _k] = (uint16_t)(uint8_t)_p[_k]; \ + } while (0) + // ---- top row: pack voltage / timer / link ---- + PUT(0, 1, "16.8V"); + PUT(0, 22, "05:23"); + PUT(0, 43, "RSSI 98"); + PUT(1, 1, "12.4A"); + PUT(1, 45, "LQ 99"); + // ---- middle: altitude / crosshair / speed ---- + PUT(8, 1, "ALT 42M"); + PUT(8, 24, "-+-"); + PUT(8, 44, "58 KMH"); + // ---- bottom: throttle / mode / capacity / gps / home / rec ---- + PUT(16, 1, "THR 42%"); + PUT(16, 23, "ACRO"); + PUT(16, 42, "1250MAH"); + PUT(17, 1, "GPS 12"); + PUT(17, 20, "HOME 128M"); + PUT(17, 46, "REC"); +#undef PUT + osd_signal_update(); +} +#endif + void *thread_osd(void *ptr) { static uint8_t fhd_d = 0; for (;;) { diff --git a/src/core/osd.h b/src/core/osd.h index ff0044e6..6b0d6c1e 100644 --- a/src/core/osd.h +++ b/src/core/osd.h @@ -92,6 +92,9 @@ int osd_init(void); int osd_clear(void); void osd_fhd(uint8_t); void osd_signal_update(); +#ifdef EMULATOR_BUILD +void osd_inject_mock_fc(void); // emulator: mock a Betaflight OSD for the FPV view +#endif void osd_hdzero_update(void); void osd_rec_update(bool enable); void osd_show(bool show); diff --git a/src/core/thread.c b/src/core/thread.c index d9f52897..904a664b 100644 --- a/src/core/thread.c +++ b/src/core/thread.c @@ -5,7 +5,11 @@ #include #include #include +#if defined(__linux__) #include +#else +#include +#endif #include #include diff --git a/src/driver/dm5680.c b/src/driver/dm5680.c index 75088d57..5efc5b36 100644 --- a/src/driver/dm5680.c +++ b/src/driver/dm5680.c @@ -12,6 +12,7 @@ #include #include #include +#include // fd_set/select (transitive via sys/types on glibc; explicit for portability) #include #include #include /* POSIX Terminal Control Definitions */ @@ -28,6 +29,10 @@ #include "driver/uart.h" #include "ui/page_common.h" +#ifdef EMULATOR_BUILD +#include "emulator/dm5680_emu.h" // mock receiver that answers on the UART socketpairs +#endif + ///////////////////////////////////////////////////////////////////// // global rx_status_t rx_status[2]; // global, 0=UART1 from Right DM5680, 1=UART2 from Left DM5680, @@ -239,8 +244,24 @@ int uart_init() { fd_dm5680r = uart_open(1); fd_dm5680l = uart_open(2); +#ifdef EMULATOR_BUILD + // Emulator: stand up the mock receiver on the UART peer ends before the recv + // threads start, so their first blocking select() has a device to answer. On the + // Windows emulator uart_emu_peer_fd() returns -1, so this attaches nothing. + dm5680_emu_start(); +#endif + +#if !defined(_WIN32) + // These recv threads block in select() on the UART fd -- real serial on the + // goggle, a socketpair under the POSIX emulator -- so select() sleeps until data + // arrives (no busy-spin). The Windows emulator has no selectable UART fd (its + // compat select() is a stub), so its recv threads stay off. pthread_create(&tid_l, NULL, pthread_recv_dm5680l, NULL); pthread_create(&tid_r, NULL, pthread_recv_dm5680r, NULL); +#else + (void)tid_l, (void)tid_r; +#endif + pthread_mutex_init(&cmd_to5680_mutex, NULL); is_inited = true; diff --git a/src/driver/esp32.c b/src/driver/esp32.c index aefe1721..098404ca 100644 --- a/src/driver/esp32.c +++ b/src/driver/esp32.c @@ -12,6 +12,7 @@ #include #include #include +#include // fd_set/select (transitive via sys/types on glibc; explicit for portability) #include #include #include /* POSIX Terminal Control Definitions */ diff --git a/src/driver/fbtools.c b/src/driver/fbtools.c index bc01dcd4..b52ea115 100644 --- a/src/driver/fbtools.c +++ b/src/driver/fbtools.c @@ -1,5 +1,9 @@ /****************** File name : fbtools.c + + Goggle-only: the Linux framebuffer driver. The SDL emulator renders through SDL + instead and never links this file (CMake drops it for EMULATOR_BUILD), so there + is no host-portability stub here. */ #include "fbtools.h" diff --git a/src/driver/fbtools.h b/src/driver/fbtools.h index a9863737..308999aa 100644 --- a/src/driver/fbtools.h +++ b/src/driver/fbtools.h @@ -8,11 +8,12 @@ extern "C" { #endif -#include #include #include -// a framebuffer device structure; +// Goggle-only: the Linux framebuffer. Emulator builds render via SDL and never +// include this header (guarded in ui_porting.c), so there is no non-Linux variant. +#include typedef struct fbdev { int fb; unsigned long fb_mem_offset; diff --git a/src/driver/i2c.c b/src/driver/i2c.c index 41216802..941b3bb2 100644 --- a/src/driver/i2c.c +++ b/src/driver/i2c.c @@ -1,3 +1,5 @@ +// Goggle-only: the Linux I2C bus driver. The emulator has no bus and links +// src/emulator/i2c_emu.c instead (CMake drops this file for EMULATOR_BUILD). #include "i2c.h" #include diff --git a/src/driver/input_evdev.c b/src/driver/input_evdev.c new file mode 100644 index 00000000..9217939a --- /dev/null +++ b/src/driver/input_evdev.c @@ -0,0 +1,223 @@ +// Goggle hardware input backend: reads the jog wheel + centre button from the +// Linux evdev devices (/dev/input/event*) via epoll and feeds them to the shared +// handlers in core/input_device.c. Compiled only for the real goggle -- CMake drops +// this file for EMULATOR_BUILD and swaps in src/emulator/input_device_emu.c. (The +// right-side function button arrives separately over UART, in driver/dm5680.c.) + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "defines.h" + +#include "common.hh" + +#include "core/app_state.h" +#include "core/input_device_internal.h" +#include "core/settings.h" +#include "driver/beep.h" + +#define EPOLL_FD_CNT 4 + +static int epfd; + +static int roller_up_acc = 0; +static int roller_down_acc = 0; + +static void get_event(int fd) { + struct input_event event; + static int event_type_last = 0; + static int btn_press_time = 0; + + static int roller_value = 0; + + // time (sec, usec) difference above which will the next scroll wheel event be accepted + // 10000 usec = 10msec is more than short enough (100Hz) + // scroll events + const struct timeval scroll_time_diff = {0, 10000}; + // direction change events + const struct timeval rel_time_diff = {0, 20000}; + // expected timestamp in the future beyond that will the events be accepted + static struct timeval next_scroll = {0, 0}; + static struct timeval next_rel = {0, 0}; + static bool discard_scroll = false; + + read(fd, &event, sizeof(event)); + + switch (event.type) { + case EV_SYN: + if (event.code == SYN_REPORT) { + if (event_type_last == EV_REL) { + if (g_setting.ease.no_dial) + break; + + if (!discard_scroll && timercmp(&event.time, &next_scroll, >)) { + timeradd(&event.time, &scroll_time_diff, &next_scroll); + if (roller_value == 1) { + roller_up_acc++; + roller_down_acc = 0; + } else if (roller_value == -1) { + roller_down_acc++; + roller_up_acc = 0; + } + + if (roller_up_acc == DIAL_SENSITIVITY) { + roller_up(); + g_key = DIAL_KEY_UP; + roller_up_acc = 0; + } else if (roller_down_acc == DIAL_SENSITIVITY) { + roller_down(); + g_key = DIAL_KEY_DOWN; + roller_down_acc = 0; + } + } else { + // LOGI("discard EV_SYN"); + } + } else if (event_type_last == EV_KEY) { + if (btn_value) { + if (!g_setting.ease.no_dial) { + if (btn_press_time == 10) { + btn_press(); + g_key = DIAL_KEY_PRESS; + } + } else { + if (scroll_sim_mode_repeat == SCROLL_REPEAT_DOWN) { + roller_down(); + } else if (scroll_sim_mode_repeat == SCROLL_REPEAT_UP) { + roller_up(); + } + } + if (scroll_sim_mode_pending) + btn_press_time = 0; + else + btn_press_time++; + // LOGI("btn down"); + } else { + if (scroll_sim_mode_pending) { + scroll_sim_mode_pending = false; + } else { + if (scroll_sim_mode_repeat == SCROLL_REPEAT_NONE) { + if (btn_press_time < 10) { + btn_click(); + g_key = DIAL_KEY_CLICK; + } else if (g_setting.ease.no_dial) { + if (btn_press_time < 50) { + btn_press(); + g_key = DIAL_KEY_PRESS; + } + // else if(btn_press_time > 200){ + // btn_super_press(); + // } + } + } + } + btn_press_time = 0; + } + } + // LOGI("------------ syn report ----------"); + } else if (event.code == SYN_MT_REPORT) { + // LOGI("----------- syn mt report ------------"); + } + break; + case EV_KEY: + // LOGI("key code%d is %s!", event.code, event.value?"down":"up"); + btn_value = event.value; + event_type_last = EV_KEY; + break; + case EV_ABS: + if ((event.code == ABS_X) || + (event.code == ABS_MT_POSITION_X)) { + // LOGI("abs,x = %d", event.value); + } else if ((event.code == ABS_Y) || + (event.code == ABS_MT_POSITION_Y)) { + // LOGI("abs,y = %d", event.value); + } else if ((event.code == ABS_PRESSURE) || + (event.code == ABS_MT_PRESSURE)) { + // LOGI("pressure value: %d", event.value); + } + break; + case EV_REL: + if (timercmp(&event.time, &next_rel, >)) { + discard_scroll = false; + if (event.code == REL_X) { + // LOGI("x = %d", event.value); + } else if (event.code == REL_Y) { + if (roller_value != event.value) { + timeradd(&event.time, &rel_time_diff, &next_rel); + roller_value = event.value; + // LOGI("y = %d", event.value); + } + event_type_last = EV_REL; + } + } else { + discard_scroll = true; + LOGI("discard EV_REL"); + } + break; + default: + // LOGI("unknown [type=%d, code=%d value=%d]", event.type, event.code, event.value); + break; + } +} + +static void add_to_epfd(int epfd, int fd) { + struct epoll_event event = { + .events = EPOLLIN, + .data = { + .fd = fd, + }, + }; + + int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); + assert(ret == 0); +} + +void input_backend_init(void) { + epfd = epoll_create(EPOLL_FD_CNT); + assert(epfd > 0); + + char buf[64]; + for (int i = 0; i < EPOLL_FD_CNT; i++) { + snprintf(buf, 64, "/dev/input/event%d", i); + + int fd = open(buf, O_RDONLY); + if (fd >= 0) { + add_to_epfd(epfd, fd); + LOGI("opened %s", buf); + } + } + app_state_push(APP_STATE_MAINMENU); +} + +void *input_backend_thread(void *ptr) { + (void)ptr; + for (;;) { + struct epoll_event events[EPOLL_FD_CNT]; + + int ret = epoll_wait(epfd, events, EPOLL_FD_CNT, DIAL_SENSITIVTY_TIMEOUT_MS); + if (ret < 0) { + perror("epoll_wait"); + continue; + } + + if (ret > 0) { + for (int i = 0; i < ret; i++) { + if (events[i].events & EPOLLIN) { + get_event(events[i].data.fd); + } + } + } else { + roller_up_acc = 0; + roller_down_acc = 0; + if (scroll_sim_mode_repeat != SCROLL_REPEAT_NONE) + beep(); + } + } + return NULL; +} diff --git a/src/driver/rtc.c b/src/driver/rtc.c index 6dbc4586..b811b6a9 100644 --- a/src/driver/rtc.c +++ b/src/driver/rtc.c @@ -4,18 +4,20 @@ * Provides management interfaces for accessing the * Real-Time-Clock. All date/time representations * are performed in UTC. + * + * This file is the shared, hardware-agnostic half: date math, validation and + * OSD/log formatting, with NO platform or build #ifdefs. The two functions that + * actually touch the clock -- rtc_get_clock / rtc_set_clock -- delegate to the + * per-target backend in rtc_hw.h (goggle: /dev/rtc; emulator: in-memory). */ #include "rtc.h" +#include "rtc_hw.h" #include -#include -#include -#include #include -#include +#include #include -#include #include #include @@ -31,7 +33,6 @@ #define RTC_OSD_FORMAT_TIME "%02u:%02u:%02u" #define LEAPS_THRU_END_OF(y) ((y) / 4 - (y) / 100 + (y) / 400) -static const char *RTC_DEV = "/dev/rtc"; static const unsigned char RTC_DAYS_PER_MONTH[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; @@ -40,10 +41,6 @@ static const unsigned char RTC_DAYS_PER_MONTH[] = { */ static int g_rtc_has_battery = 0; -#ifdef EMULATOR_BUILD -static struct rtc_date g_rtc_date = {1970, 1, 1, 0, 0, 0}; -#endif - /** * Convert Gregorian date to seconds since 1970-01-01 00:00:00 */ @@ -113,26 +110,6 @@ void rtc_print(const struct rtc_date *rd) { rd->sec); } -/** - * Conversion Utils used to interact with internal data types. - */ -void rtc_rd2rt(const struct rtc_date *rd, struct rtc_time *rt) { - rt->tm_year = rd->year - 1900; - rt->tm_mon = rd->month - 1; - rt->tm_mday = rd->day; - rt->tm_hour = rd->hour; - rt->tm_min = rd->min; - rt->tm_sec = rd->sec; -} -void rtc_rt2rd(const struct rtc_time *rt, struct rtc_date *rd) { - rd->year = rt->tm_year + 1900; - rd->month = rt->tm_mon + 1; - rd->day = rt->tm_mday; - rd->hour = rt->tm_hour; - rd->min = rt->tm_min; - rd->sec = rt->tm_sec; -} - /** * Conversion Utils used to interact with system data types. */ @@ -141,37 +118,30 @@ void rtc_rd2tv(const struct rtc_date *rd, struct timeval *tv) { tv->tv_usec = 0; } void rtc_tv2rd(const struct timeval *tv, struct rtc_date *rd) { - struct rtc_time rt; unsigned int month, year; unsigned long secs; int days = tv->tv_sec / 86400; secs = tv->tv_sec - (unsigned int)days * 86400; - rt.tm_wday = (days + 4) % 7; year = 1970 + days / 365; days -= (year - 1970) * 365 + LEAPS_THRU_END_OF(year - 1) - LEAPS_THRU_END_OF(1970 - 1); if (days < 0) { year -= 1; days += 365 + rtc_is_leap_year(year); } - rt.tm_year = year - 1900; - rt.tm_yday = days + 1; for (month = 0; month < 11; month++) { - int newdays; - - newdays = days - rtc_days_per_month(year, month); + int newdays = days - rtc_days_per_month(year, month); if (newdays < 0) { break; } days = newdays; } - rt.tm_mon = month; - rt.tm_mday = days + 1; - rt.tm_hour = secs / 3600; - secs -= rt.tm_hour * 3600; - rt.tm_min = secs / 60; - rt.tm_sec = secs - rt.tm_min * 60; - rt.tm_isdst = 0; - rtc_rt2rd(&rt, rd); + rd->year = year; + rd->month = month + 1; + rd->day = days + 1; + rd->hour = secs / 3600; + secs -= (unsigned long)rd->hour * 3600; + rd->min = secs / 60; + rd->sec = secs - rd->min * 60; } /** @@ -218,49 +188,14 @@ void rtc_timestamp() { * Set Hardware Clock and synchronize OS Clock in UTC. */ void rtc_set_clock(const struct rtc_date *rd) { -#ifdef EMULATOR_BUILD - g_rtc_date = *rd; -#else - int fd = open(RTC_DEV, O_WRONLY); - if (fd >= 0) { - struct rtc_time rt; - struct timeval tv; - rtc_rd2rt(rd, &rt); - rtc_rd2tv(rd, &tv); - - if (settimeofday(&tv, NULL) != 0) { - LOGE("settimeofday(&tv, NULL) failed with errno(%d)", errno); - } - if (ioctl(fd, RTC_SET_TIME, &rt) != 0) { - LOGE("ioctl(%d,RTC_SET_TIME,&rt) failed with errno(%d)", fd, errno); - } - close(fd); - } else { - LOGE("rtc_set_clock failed to open(%s, O_RDWR)", RTC_DEV); - } -#endif + rtc_hw_write(rd); } /** * Get Hardware Clock */ void rtc_get_clock(struct rtc_date *rd) { -#ifdef EMULATOR_BUILD - *rd = g_rtc_date; -#else - int fd = open(RTC_DEV, O_RDONLY); - if (fd >= 0) { - struct rtc_time rt; - if (ioctl(fd, RTC_RD_TIME, &rt) == 0) { - rtc_rt2rd(&rt, rd); - } else { - LOGE("ioctl(%d,RTC_RD_TIME,rt) failed with errno(%d)", fd, errno); - } - close(fd); - } else { - LOGE("rtc_get_clock failed to open(%s, O_RDWR)", RTC_DEV); - } -#endif + rtc_hw_read(rd); } /** diff --git a/src/driver/rtc_hw.c b/src/driver/rtc_hw.c new file mode 100644 index 00000000..fc274a51 --- /dev/null +++ b/src/driver/rtc_hw.c @@ -0,0 +1,73 @@ +// rtc_hw.c - Goggle backend for the physical clock (see rtc_hw.h). +// +// Reads/writes /dev/rtc via the kernel rtc ioctls and mirrors the wall clock into +// the OS via settimeofday(). The struct rtc_time <-> struct rtc_date conversions +// live here because only this hardware path needs the kernel type; the shared +// rtc.c deals purely in struct rtc_date. The emulator supplies rtc_hw_emu.c instead. +#include "rtc_hw.h" + +#include +#include +#include +#include +#include // settimeofday, struct timeval +#include + +#include + +#include "rtc.h" // rtc_rd2tv + +static const char *RTC_DEV = "/dev/rtc"; + +static void rd2rt(const struct rtc_date *rd, struct rtc_time *rt) { + rt->tm_year = rd->year - 1900; + rt->tm_mon = rd->month - 1; + rt->tm_mday = rd->day; + rt->tm_hour = rd->hour; + rt->tm_min = rd->min; + rt->tm_sec = rd->sec; +} + +static void rt2rd(const struct rtc_time *rt, struct rtc_date *rd) { + rd->year = rt->tm_year + 1900; + rd->month = rt->tm_mon + 1; + rd->day = rt->tm_mday; + rd->hour = rt->tm_hour; + rd->min = rt->tm_min; + rd->sec = rt->tm_sec; +} + +void rtc_hw_read(struct rtc_date *rd) { + int fd = open(RTC_DEV, O_RDONLY); + if (fd >= 0) { + struct rtc_time rt; + if (ioctl(fd, RTC_RD_TIME, &rt) == 0) { + rt2rd(&rt, rd); + } else { + LOGE("ioctl(%d,RTC_RD_TIME,rt) failed with errno(%d)", fd, errno); + } + close(fd); + } else { + LOGE("rtc_get_clock failed to open(%s, O_RDONLY)", RTC_DEV); + } +} + +void rtc_hw_write(const struct rtc_date *rd) { + int fd = open(RTC_DEV, O_WRONLY); + if (fd >= 0) { + struct rtc_time rt; + struct timeval tv; + rd2rt(rd, &rt); + rtc_rd2tv(rd, &tv); + + if (settimeofday(&tv, NULL) != 0) { + LOGE("settimeofday(&tv, NULL) failed with errno(%d)", errno); + } + if (ioctl(fd, RTC_SET_TIME, &rt) != 0) { + LOGE("ioctl(%d,RTC_SET_TIME,&rt) failed with errno(%d)", fd, errno); + } + close(fd); + } else { + LOGE("rtc_set_clock failed to open(%s, O_WRONLY)", RTC_DEV); + } +} diff --git a/src/driver/rtc_hw.h b/src/driver/rtc_hw.h new file mode 100644 index 00000000..faef2f17 --- /dev/null +++ b/src/driver/rtc_hw.h @@ -0,0 +1,21 @@ +#pragma once +// Internal per-target backend for the physical clock. rtc.c owns all the shared +// date math + formatting with no build/platform #ifdefs; these two calls are the +// only hardware-touching seam: +// goggle -> src/driver/rtc_hw.c (/dev/rtc ioctls + settimeofday) +// emulator -> src/emulator/rtc_hw_emu.c (in-memory clock) +// CMake compiles exactly one of the two. +#include "rtc.h" // struct rtc_date + +#ifdef __cplusplus +extern "C" { +#endif + +// Read the hardware clock into *rd (leaves *rd untouched on failure). +void rtc_hw_read(struct rtc_date *rd); +// Write *rd to the hardware clock (and, on the goggle, the OS clock). +void rtc_hw_write(const struct rtc_date *rd); + +#ifdef __cplusplus +} +#endif diff --git a/src/driver/uart.c b/src/driver/uart.c index 1dc89548..be5282ce 100644 --- a/src/driver/uart.c +++ b/src/driver/uart.c @@ -11,6 +11,11 @@ #include #include #include /* POSIX Terminal Control Definitions */ + +// macOS/BSD termios uses literal baud values and lacks the Linux B460800 alias. +#ifndef B460800 +#define B460800 460800 +#endif #include #include /* UNIX Standard Definitions */ diff --git a/src/driver/uart.h b/src/driver/uart.h index bb99d9d3..5ef9e22f 100644 --- a/src/driver/uart.h +++ b/src/driver/uart.h @@ -15,6 +15,12 @@ void uart_close(int fd); int uart_read(int fd, uint8_t *data, int len); int uart_write(int fd, uint8_t *data, int len); +#ifdef EMULATOR_BUILD +// Device (peer) end of the emulator UART socketpair for `port`, or -1. A mock +// device reads host commands and writes replies here. Absent on the goggle build. +int uart_emu_peer_fd(int port); +#endif + #ifdef __cplusplus } #endif diff --git a/src/emulator/dm5680_emu.c b/src/emulator/dm5680_emu.c new file mode 100644 index 00000000..af8f4e74 --- /dev/null +++ b/src/emulator/dm5680_emu.c @@ -0,0 +1,156 @@ +// dm5680_emu.c - Emulator-only mock of the DM5680 receiver's serial link. +// +// On the goggle, two DM5680 chips talk to the app over UART. The app polls them +// (version, valid-channel, RSSI) and they answer; dm5680.c's recv threads block in +// select() on the real serial fd until a reply arrives. The desktop emulator has no +// DM5680, so uart_open() hands back a socketpair instead of a tty (see uart.c). This +// module sits on the *device* end of those socketpairs and plays the receiver: it +// reads the goggle's command frames and writes back plausible telemetry, so the app +// sees a connected receiver -- and its recv threads block on real data at 0% CPU +// rather than spinning on a dead fd. +// +// Wire format (see dm5680.c): +// host -> device command: 0xAA 0x55 LEN CMD [payload...] (LEN = bytes after LEN) +// device -> host response: 0xCC 0x33 LEN CMD [data...] (LEN = bytes after LEN) + +#ifdef EMULATOR_BUILD + +#include "dm5680_emu.h" + +#include +#include +#include +#include +#include + +#include + +#include "driver/uart.h" + +// Canned telemetry the mock reports. Values chosen to look like a healthy link. +#define EMU_RX_VER 0x0F // receiver firmware version byte +#define EMU_RSSI_0 90 +#define EMU_RSSI_1 88 +#define EMU_DLQ 100 // downlink quality (0..100) +#define EMU_STAT 0 +// Whether the mock reports a locked HDZero channel. 0 = present but no lock, so the +// app does NOT auto-tune out of the menu on boot/scan (the FPV view still shows the +// fake video -- compositing is gated on the view, not this flag). Set to 1 to +// emulate a live signal that autoscan will lock onto and switch to video. +#define EMU_CHANNEL_VALID 0 + +// DM5680 UART ports (must match uart_open() calls in uart_init()). +static const int kPorts[2] = {1, 2}; + +typedef struct { + int fd; // device end of the UART socketpair + pthread_t tid; +} emu_port_t; + +static emu_port_t g_ports[2]; + +// Frame a response payload (CMD + data) as 0xCC 0x33 LEN and send it. +static void send_resp(int fd, const uint8_t *payload, uint8_t len) { + uint8_t frame[64]; + if (len > sizeof(frame) - 3) + return; + frame[0] = 0xCC; + frame[1] = 0x33; + frame[2] = len; + memcpy(&frame[3], payload, len); + ssize_t rc = write(fd, frame, (size_t)len + 3); + (void)rc; +} + +// Answer one command from the goggle. Only the polling queries need a reply; +// config writes (fan speed, resets, etc.) are accepted silently, like real HW. +static void handle_cmd(int fd, uint8_t cmd) { + switch (cmd) { + case 0x01: { // version request + uint8_t d[] = {0x01, EMU_RX_VER}; + send_resp(fd, d, sizeof(d)); + break; + } + case 0x11: { // valid-channel request -> report the channel as valid + uint8_t d[] = {0x11, 1}; + send_resp(fd, d, sizeof(d)); + break; + } + case 0x12: { // RSSI request -> rssi0, rssi1, DLQ, Stat, crc(sum of the four) + uint8_t crc = (uint8_t)(EMU_RSSI_0 + EMU_RSSI_1 + EMU_DLQ + EMU_STAT); + uint8_t d[] = {0x12, EMU_RSSI_0, EMU_RSSI_1, EMU_DLQ, EMU_STAT, crc}; + send_resp(fd, d, sizeof(d)); + break; + } + case 0x14: { // VTX info -> type, ver, stat, crc(sum of the three) + uint8_t type = 1, ver = 1, stat = 1; + uint8_t crc = (uint8_t)(type + ver + stat); + uint8_t d[] = {0x14, type, ver, stat, crc}; + send_resp(fd, d, sizeof(d)); + break; + } + default: + break; // no reply expected + } +} + +// Read host command bytes and drive a tiny 0xAA/0x55/LEN/CMD/payload parser. The +// read() blocks until the goggle actually sends something, so an idle link costs +// no CPU -- exactly like waiting on a real UART. +static void *port_thread(void *arg) { + emu_port_t *p = (emu_port_t *)arg; + uint8_t buf[128]; + int state = 0; + uint8_t remaining = 0; + + for (;;) { + ssize_t n = read(p->fd, buf, sizeof(buf)); + if (n <= 0) { + if (n == 0) + break; // peer closed + usleep(5000); + continue; + } + for (ssize_t i = 0; i < n; i++) { + uint8_t b = buf[i]; + switch (state) { + case 0: + if (b == 0xAA) + state = 1; + break; + case 1: + state = (b == 0x55) ? 2 : 0; + break; + case 2: // LEN = number of bytes after LEN (CMD + payload) + remaining = b; + state = remaining ? 3 : 0; + break; + case 3: // CMD + handle_cmd(p->fd, b); + remaining--; + state = remaining ? 4 : 0; + break; + case 4: // consume this command's payload bytes + if (--remaining == 0) + state = 0; + break; + } + } + } + return NULL; +} + +void dm5680_emu_start(void) { + int started = 0; + for (int i = 0; i < 2; i++) { + int fd = uart_emu_peer_fd(kPorts[i]); + if (fd < 0) + continue; + g_ports[i].fd = fd; + if (pthread_create(&g_ports[i].tid, NULL, port_thread, &g_ports[i]) == 0) + started++; + } + LOGI("dm5680_emu: mock receiver attached on %d UART(s) (version, valid channel, RSSI)", started); +} + +#endif // EMULATOR_BUILD diff --git a/src/emulator/dm5680_emu.h b/src/emulator/dm5680_emu.h new file mode 100644 index 00000000..59e864c6 --- /dev/null +++ b/src/emulator/dm5680_emu.h @@ -0,0 +1,19 @@ +#pragma once +// Emulator-only mock of the DM5680 receiver's serial link. Compiled ONLY into the +// emulator (this whole file is #ifdef EMULATOR_BUILD, and src/emulator/*.c is added +// to the build only when EMULATOR_BUILD) -- nothing here reaches the goggle firmware. +#ifdef EMULATOR_BUILD +#ifdef __cplusplus +extern "C" { +#endif + +// Attach the mock receiver to the DM5680 UART socketpairs (uart_emu_peer_fd of the +// ports opened in uart_init). It reads the goggle's polling commands and replies +// with plausible telemetry (firmware version, valid channel, RSSI), so the app sees +// a connected receiver instead of a dead link. Safe to call once, after uart_open. +void dm5680_emu_start(void); + +#ifdef __cplusplus +} +#endif +#endif // EMULATOR_BUILD diff --git a/src/emulator/i2c_emu.c b/src/emulator/i2c_emu.c new file mode 100644 index 00000000..e36befb6 --- /dev/null +++ b/src/emulator/i2c_emu.c @@ -0,0 +1,41 @@ +// i2c_emu.c - Emulator-only I2C driver. +// +// CMake compiles this instead of src/driver/i2c.c when EMULATOR_BUILD, so the +// hardware driver stays free of host/emulator #ifdefs. The emulator has no I2C bus, +// so every access no-ops -- exactly the "device not available" behaviour the goggle +// driver already exhibits on a dev box with no /dev/i2c-* nodes (reads return 0, +// writes report failure). Callers (bmi270, fans, page_version, ...) tolerate this. +#ifdef EMULATOR_BUILD + +#include "i2c.h" + +#include + +bool iic_is_port_ready(int port) { + (void)port; + return false; +} + +void iic_init(void) {} + +uint8_t i2c_read(int port, uint8_t slave_address, uint8_t addr) { + (void)port, (void)slave_address, (void)addr; + return 0; +} + +int i2c_write(int port, uint8_t slave_address, uint8_t addr, uint8_t val) { + (void)port, (void)slave_address, (void)addr, (void)val; + return -1; +} + +int8_t i2c_read_n(int port, uint8_t slave_address, uint8_t addr, uint8_t *data, uint16_t len) { + (void)port, (void)slave_address, (void)addr, (void)data, (void)len; + return -1; +} + +int8_t i2c_write_n(int port, uint8_t slave_address, uint8_t addr, uint8_t *val, uint16_t len) { + (void)port, (void)slave_address, (void)addr, (void)val, (void)len; + return -1; +} + +#endif // EMULATOR_BUILD diff --git a/src/emulator/input_device_emu.c b/src/emulator/input_device_emu.c new file mode 100644 index 00000000..669e9d29 --- /dev/null +++ b/src/emulator/input_device_emu.c @@ -0,0 +1,158 @@ +// Desktop emulator input backend: maps SDL keyboard events onto the goggle's jog +// wheel + centre button + right function button, and feeds them to the shared +// handlers in core/input_device.c. Compiled only for EMULATOR_BUILD -- CMake swaps +// this in for the goggle's driver/input_evdev.c, so neither the shared logic nor +// the hardware path carries any emulator #ifdef. + +#include +#include +#include +#include + +#include "SDLaccess.h" + +#include "common.hh" + +#include "core/input_device.h" +#include "core/input_device_internal.h" + +// Right-button double-click window (ms). Matches the dm5680.c short-click timeout +// on the real goggle, so a single tap is deferred this long to disambiguate it +// from a double-tap. +#define RBTN_DOUBLE_CLICK_MS 250 + +// Drain pending SDL events (keyboard -> goggle buttons/wheel). macOS Cocoa +// requires this to run on the main thread, so main.c calls it from the main +// loop; other platforms call it from the background input thread below. +void input_device_pump(void) { + static uint32_t btn_wheel_start = 0; + static uint32_t btn_fn_start = 0; + static uint32_t btn_fn_click_ts = 0; // timestamp of a deferred (pending) right click + static bool btn_fn_pending = false; // a right tap is waiting to see if it doubles + + SDL_Event event; + SDL_LockMutex(global_sdl_mutex); + while (SDL_PollEvent(&event)) { + SDL_UnlockMutex(global_sdl_mutex); + switch (event.type) { + case SDL_QUIT: + exit(0); + + case SDL_KEYDOWN: + switch (event.key.keysym.sym) { + case SDLK_d: // D = wheel press (centre button: enter/select) + if (!btn_wheel_start) { + btn_wheel_start = event.key.timestamp; + } + break; + + case SDLK_l: // L = function button (right side of the goggle) + if (!btn_fn_start) { + btn_fn_start = event.key.timestamp; + } + break; + } + break; + + case SDL_KEYUP: + switch (event.key.keysym.sym) { + case SDLK_s: // S = jog wheel down (highlight moves down) + roller_up(); + g_key = DIAL_KEY_UP; + break; + + case SDLK_w: // W = jog wheel up (highlight moves up) + roller_down(); + g_key = DIAL_KEY_DOWN; + break; + + case SDLK_d: // D = wheel press (centre button: enter/select) + if (event.key.timestamp - btn_wheel_start > 500) { + btn_press(); + g_key = DIAL_KEY_PRESS; + } else { + btn_click(); + g_key = DIAL_KEY_CLICK; + } + btn_wheel_start = 0; + break; + + case SDLK_l: // L = function button (right side of the goggle) + if (event.key.timestamp - btn_fn_start > 500) { + btn_fn_pending = false; // long press supersedes any pending single + rbtn_click(RIGHT_LONG_PRESS); + g_key = RIGHT_KEY_PRESS; + } else if (btn_fn_pending && + event.key.timestamp - btn_fn_click_ts <= RBTN_DOUBLE_CLICK_MS) { + btn_fn_pending = false; // second tap inside the window -> double click + rbtn_click(RIGHT_DOUBLE_CLICK); + g_key = RIGHT_KEY_CLICK; + } else { + // first tap: defer the single click until the double-click window + // lapses (fired from the timeout check below), matching dm5680.c + btn_fn_pending = true; + btn_fn_click_ts = event.key.timestamp; + } + btn_fn_start = 0; + break; + } + break; + } + SDL_LockMutex(global_sdl_mutex); + } + SDL_UnlockMutex(global_sdl_mutex); + + // Fire a deferred single right-click once the double-click window lapses with + // no second tap (mirrors the dm5680.c short-click timeout on the real goggle). + if (btn_fn_pending && SDL_GetTicks() - btn_fn_click_ts > RBTN_DOUBLE_CLICK_MS) { + btn_fn_pending = false; + rbtn_click(RIGHT_CLICK); + g_key = RIGHT_KEY_CLICK; + } +} + +// Print the keyboard<->goggle control map to the terminal on launch. The real +// goggle is driven by a jog wheel (rotate + press) plus a right-side function +// button, each distinguishing tap vs long-press; here those are keyboard keys, +// which isn't obvious without being told. Emulator-only -- the firmware has no +// console and no keyboard. Keep this in sync with input_device_pump() above. +void input_device_print_help(void) { + printf( + "\n" + "======================== HDZero Goggle emulator ========================\n" + " keyboard -> goggle control\n" + " ----------------------------------------------------------------------\n" + " W ................. jog wheel up move highlight up\n" + " S ................. jog wheel down move highlight down\n" + " D ................. wheel press tap = select / confirm\n" + " (centre button) hold >= 0.5s = long press\n" + " L ................. function button tap = click, hold >= 0.5s = long press\n" + " (right of goggle) double-tap = recentre head tracker (video)\n" + " close window ...... quit\n" + " ----------------------------------------------------------------------\n" + " in a menu: wheel navigates, tap selects, long-press wheel = back/exit\n" + " in video : tap = OSD on/off, long-press = open menu, Fn = DVR toggle\n" + "========================================================================\n" + "\n"); + fflush(stdout); +} + +void input_backend_init(void) { + if (SDL_Init(SDL_INIT_VIDEO) != 0) { + printf("Error initializing SDL: %s\n", SDL_GetError()); + } +} + +void *input_backend_thread(void *ptr) { + (void)ptr; + // macOS Cocoa and Windows require SDL events to be pumped on the main (window) + // thread, so there main.c drives input_device_pump() from its loop and this + // thread idles. Elsewhere (Linux/X11) the background thread pumps. +#if !defined(__APPLE__) && !defined(_WIN32) + while (true) { + input_device_pump(); + usleep(50000); + } +#endif + return NULL; +} diff --git a/src/emulator/rtc_hw_emu.c b/src/emulator/rtc_hw_emu.c new file mode 100644 index 00000000..1b795c8e --- /dev/null +++ b/src/emulator/rtc_hw_emu.c @@ -0,0 +1,21 @@ +// rtc_hw_emu.c - Emulator backend for the physical clock (see rtc_hw.h). +// +// No /dev/rtc on a dev box, so the clock is just an in-memory value the app sets +// and reads back. Starts at the epoch so rtc_init() treats it as "no battery" and +// seeds it from settings, exactly like a fresh goggle. Compiled instead of +// src/driver/rtc_hw.c when EMULATOR_BUILD. +#ifdef EMULATOR_BUILD + +#include "rtc_hw.h" + +static struct rtc_date g_rtc_date = {1970, 1, 1, 0, 0, 0}; + +void rtc_hw_read(struct rtc_date *rd) { + *rd = g_rtc_date; +} + +void rtc_hw_write(const struct rtc_date *rd) { + g_rtc_date = *rd; +} + +#endif // EMULATOR_BUILD diff --git a/src/emulator/uart_emu.c b/src/emulator/uart_emu.c new file mode 100644 index 00000000..acdce6c3 --- /dev/null +++ b/src/emulator/uart_emu.c @@ -0,0 +1,84 @@ +// uart_emu.c - Emulator-only UART driver. +// +// CMake compiles THIS instead of the goggle's src/driver/uart.c when EMULATOR_BUILD, +// so the hardware driver stays completely free of emulator special-casing. It +// implements the same uart.h interface, plus uart_emu_peer_fd() for a mock device. +// +// POSIX (Linux / macOS): each UART is backed by a socketpair, giving the fd true +// UART semantics -- read()/select() block in the kernel until bytes arrive. The +// DM5680 recv threads then sleep at 0% CPU instead of busy-spinning on a dead fd, +// and a mock device (dm5680_emu) answers commands on the peer end. +// +// Windows: the compat select() is a stub (no real fd multiplexing), so there is no +// selectable UART to back. uart_open() reports "no device" (-1); the DM5680 recv +// threads stay off (guarded in dm5680.c) and the UART is simply inert. This keeps +// the behaviour Windows already had -- no busy-spin -- without a socket layer. + +#ifdef EMULATOR_BUILD + +#include "driver/uart.h" + +#include +#include +#ifndef _WIN32 +#include +#endif + +#ifndef _WIN32 +// Device (peer) end of each UART socketpair; the mock reads/writes here. -1 = closed. +static int uart_emu_peer[UART_PORTS] = {-1, -1, -1, -1}; +#endif + +int uart_emu_peer_fd(int port_num) { +#ifndef _WIN32 + if (port_num < 0 || port_num >= UART_PORTS) + return -1; + return uart_emu_peer[port_num]; +#else + (void)port_num; + return -1; // no mock device on the Windows emulator +#endif +} + +int uart_open(int port_num) { +#ifndef _WIN32 + if (port_num < 0 || port_num >= UART_PORTS) + return -1; + int sp[2]; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sp) != 0) + return -1; + uart_emu_peer[port_num] = sp[1]; // device side, for the mock + return sp[0]; // app side, the "UART fd" +#else + (void)port_num; + return -1; +#endif +} + +int uart_set_opt(int fd, int nSpeed, int nBits, char nEvent, int nStop) { + // The emulator UART is a socketpair, not a tty; no line configuration applies. + (void)fd, (void)nSpeed, (void)nBits, (void)nEvent, (void)nStop; + return 0; +} + +int uart_read(int fd, uint8_t *data, int len) { + return (int)read(fd, data, len); +} + +int uart_write(int fd, uint8_t *data, int len) { + return (int)write(fd, data, len); +} + +void uart_close(int fd) { + close(fd); +} + +int uart_read_byte(int fd, uint8_t *data) { + return uart_read(fd, data, 1); +} + +int uart_write_byte(int fd, uint8_t data) { + return uart_write(fd, &data, 1); +} + +#endif // EMULATOR_BUILD diff --git a/src/emulator/video_emu.c b/src/emulator/video_emu.c new file mode 100644 index 00000000..ad9e7959 --- /dev/null +++ b/src/emulator/video_emu.c @@ -0,0 +1,220 @@ +// video_emu.c - Emulator-only mock of the HDZero video device. +// +// On the goggle, the DM5680 receiver decodes the RF and the display engine +// overlays the decoded video *under* the LVGL UI in hardware -- the app never +// touches the pixels. The desktop emulator has no receiver, so this module stands +// in for that hardware at the same seam: it decodes a small looping MPEG-1 clip +// and hands out the most recent frame as tightly-packed RGB24 for the SDL +// compositor to draw behind the FPV/OSD view, exactly where the overlay would go. +// +// Decoding uses pl_mpeg (a single-header MPEG-1 decoder), so the module compiles +// and runs identically on Linux / macOS / Windows with NO ffmpeg in the app. The +// clip is produced offline (a build step runs ffmpeg once to turn a test video +// into loop.mpg); ffmpeg is never linked into any app binary. +// +// Design: +// * Decode runs on its own pthread: plm_decode_video() -> plm_frame_to_rgb() +// straight into an RGB24 back buffer. +// * Double buffer: the decoder writes the "back" buffer, then takes the mutex +// only to flip the front index. video_emu_get() copies the front buffer out. +// * Looping is handled by pl_mpeg (plm_set_loop), so playback repeats forever. +// * Pacing: sleep the remainder of the frame interval (from the clip's rate). + +#ifdef EMULATOR_BUILD + +#include "video_emu.h" + +#include +#include +#include +#include +#include +#include + +#define PL_MPEG_IMPLEMENTATION +#include + +#include + +typedef struct { + int w; // frame geometry (taken from the clip) + int h; + + plm_t *plm; + + // RGB24 double buffer, each w*h*3 bytes, no row padding + uint8_t *buf[2]; + int front_idx; + bool has_frame; + + pthread_t tid; + pthread_mutex_t mutex; + volatile bool running; + volatile bool paused; // true = decode thread idles (not in the video view) + bool started; + + int64_t frame_interval_ns; +} video_emu_ctx_t; + +static video_emu_ctx_t g_video_emu; + +// Release the decoder + buffers. Idempotent; does not touch the mutex. +static void video_emu_free_resources(video_emu_ctx_t *s) { + if (s->plm) { + plm_destroy(s->plm); + s->plm = NULL; + } + for (int i = 0; i < 2; i++) { + free(s->buf[i]); + s->buf[i] = NULL; + } +} + +// Decode loop: pull frames forever (pl_mpeg loops for us), convert to RGB into +// the back buffer, flip to front, and pace to the clip's frame rate. +static void *video_emu_thread(void *arg) { + video_emu_ctx_t *s = (video_emu_ctx_t *)arg; + + while (s->running) { + // Not in the video view: idle instead of decoding. No frames are pulled, + // converted, or paced -- the "receiver" is effectively off, so the menu + // costs no video CPU. A short sleep keeps resume latency low. + if (s->paused) { + usleep(20000); + continue; + } + + struct timespec t_start; + clock_gettime(CLOCK_MONOTONIC, &t_start); + + plm_frame_t *frame = plm_decode_video(s->plm); + if (frame == NULL) { + // With looping enabled this shouldn't occur; back off to avoid a spin. + usleep(10000); + continue; + } + + int back = 1 - s->front_idx; + plm_frame_to_rgb(frame, s->buf[back], s->w * 3); + + pthread_mutex_lock(&s->mutex); + s->front_idx = back; + s->has_frame = true; + pthread_mutex_unlock(&s->mutex); + + struct timespec t_now; + clock_gettime(CLOCK_MONOTONIC, &t_now); + int64_t elapsed_ns = (int64_t)(t_now.tv_sec - t_start.tv_sec) * 1000000000LL + + (int64_t)(t_now.tv_nsec - t_start.tv_nsec); + int64_t sleep_ns = s->frame_interval_ns - elapsed_ns; + if (sleep_ns > 0) + usleep((useconds_t)(sleep_ns / 1000)); + } + + return NULL; +} + +int video_emu_start(const char *path, int w, int h) { + video_emu_ctx_t *s = &g_video_emu; + + if (path == NULL || path[0] == '\0' || w <= 0 || h <= 0) + return -1; + if (s->started) // already playing + return -1; + + memset(s, 0, sizeof(*s)); + + s->plm = plm_create_with_filename(path); + if (s->plm == NULL) { + // Missing/invalid clip: the feature is simply absent (gray FPV), not broken. + LOGE("video_emu: cannot open '%s'", path); + return -1; + } + plm_set_audio_enabled(s->plm, 0); + plm_set_loop(s->plm, 1); + + // The clip is authored at display resolution (the build step encodes it that + // way), so its frames drop straight into the compositor with no scaling. + s->w = plm_get_width(s->plm); + s->h = plm_get_height(s->plm); + if (s->w <= 0 || s->h <= 0) { + s->w = w; + s->h = h; + } + if (s->w != w || s->h != h) + LOGI("video_emu: clip is %dx%d but compositor wants %dx%d (re-encode to match for video)", s->w, s->h, w, h); + + size_t buf_size = (size_t)s->w * (size_t)s->h * 3; + s->buf[0] = (uint8_t *)calloc(1, buf_size); + s->buf[1] = (uint8_t *)calloc(1, buf_size); + if (s->buf[0] == NULL || s->buf[1] == NULL) { + video_emu_free_resources(s); + return -1; + } + + double fps = plm_get_framerate(s->plm); + if (fps <= 0.0) + fps = 30.0; + s->frame_interval_ns = (int64_t)(1000000000.0 / fps); + + if (pthread_mutex_init(&s->mutex, NULL) != 0) { + video_emu_free_resources(s); + return -1; + } + + s->running = true; + s->paused = true; // start idle; the flush path resumes us once the video view is shown + if (pthread_create(&s->tid, NULL, video_emu_thread, s) != 0) { + s->running = false; + pthread_mutex_destroy(&s->mutex); + video_emu_free_resources(s); + return -1; + } + + s->started = true; + LOGI("video_emu: playing '%s' -> %dx%d RGB24 @ %.2f fps", path, s->w, s->h, fps); + return 0; +} + +bool video_emu_get(uint8_t *dst, int w, int h) { + video_emu_ctx_t *s = &g_video_emu; + + if (dst == NULL || !s->started) + return false; + // Geometry must match what playback produced; refuse rather than garble. + if (w != s->w || h != s->h) + return false; + + bool copied = false; + pthread_mutex_lock(&s->mutex); + if (s->has_frame) { + memcpy(dst, s->buf[s->front_idx], (size_t)s->w * (size_t)s->h * 3); + copied = true; + } + pthread_mutex_unlock(&s->mutex); + return copied; +} + +void video_emu_set_paused(bool paused) { + video_emu_ctx_t *s = &g_video_emu; + if (s->started) + s->paused = paused; +} + +void video_emu_stop(void) { + video_emu_ctx_t *s = &g_video_emu; + + if (!s->started) + return; + + s->running = false; + pthread_join(s->tid, NULL); + + video_emu_free_resources(s); + pthread_mutex_destroy(&s->mutex); + + s->started = false; + s->has_frame = false; +} + +#endif // EMULATOR_BUILD diff --git a/src/emulator/video_emu.h b/src/emulator/video_emu.h new file mode 100644 index 00000000..74ac9a1d --- /dev/null +++ b/src/emulator/video_emu.h @@ -0,0 +1,21 @@ +#pragma once +#ifdef EMULATOR_BUILD +#include +#include +#ifdef __cplusplus +extern "C" { +#endif +// Start a background decode thread for `path` (a .ts / mp4). Playback loops forever. +// Frames are converted+scaled to w x h, packed RGB24 (3 bytes/pixel, tightly packed, no row padding). +// Returns 0 on success; non-zero on error or if path is NULL/empty (feature then inert). +int video_emu_start(const char *path, int w, int h); +// Copy the latest decoded frame into dst (caller-owned, must hold w*h*3 bytes). Returns true if a frame was copied. +bool video_emu_get(uint8_t *dst, int w, int h); +// Pause/resume decoding. While paused the decode thread idles (no decode, no CPU), +// mirroring the real receiver being closed when the app leaves the video view. +void video_emu_set_paused(bool paused); +void video_emu_stop(void); +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/platform/compat/windows/arpa/inet.h b/src/platform/compat/windows/arpa/inet.h new file mode 100644 index 00000000..895a6fee --- /dev/null +++ b/src/platform/compat/windows/arpa/inet.h @@ -0,0 +1,2 @@ +#pragma once +/* Empty arpa/inet.h stub for the Windows (mingw) emulator (wifi is device-only; its socket use is guarded). */ diff --git a/src/platform/compat/windows/net/if.h b/src/platform/compat/windows/net/if.h new file mode 100644 index 00000000..3d6d00e2 --- /dev/null +++ b/src/platform/compat/windows/net/if.h @@ -0,0 +1,2 @@ +#pragma once +/* Empty net/if.h stub for the Windows (mingw) emulator (wifi is device-only; its socket use is guarded). */ diff --git a/src/platform/compat/windows/netinet/in.h b/src/platform/compat/windows/netinet/in.h new file mode 100644 index 00000000..896702da --- /dev/null +++ b/src/platform/compat/windows/netinet/in.h @@ -0,0 +1,2 @@ +#pragma once +/* Empty netinet/in.h stub for the Windows (mingw) emulator (wifi is device-only; its socket use is guarded). */ diff --git a/src/platform/compat/windows/posix_compat.c b/src/platform/compat/windows/posix_compat.c new file mode 100644 index 00000000..156af923 --- /dev/null +++ b/src/platform/compat/windows/posix_compat.c @@ -0,0 +1,87 @@ +/* POSIX functions mingw/Windows lacks, for the Windows emulator build only. + Compiled solely for WIN32 (see CMakeLists.txt). getline/scandir back real + emulator paths (settings parsing, playback listing); sync is a best-effort + flush; loader_port_debug_print gives the (weak-in-lib) esp-loader symbol a + strong definition the mingw linker will resolve. */ +#if defined(EMULATOR_BUILD) && defined(_WIN32) + +#include +#include +#include +#include +#include + +ssize_t getline(char **lineptr, size_t *n, FILE *stream) { + if (!lineptr || !n || !stream) + return -1; + if (*lineptr == NULL || *n == 0) { + *n = 128; + *lineptr = (char *)malloc(*n); + if (*lineptr == NULL) + return -1; + } + size_t pos = 0; + int c; + while ((c = fgetc(stream)) != EOF) { + if (pos + 1 >= *n) { + size_t nn = *n * 2; + char *np = (char *)realloc(*lineptr, nn); + if (np == NULL) + return -1; + *lineptr = np; + *n = nn; + } + (*lineptr)[pos++] = (char)c; + if (c == '\n') + break; + } + if (pos == 0 && c == EOF) + return -1; + (*lineptr)[pos] = '\0'; + return (ssize_t)pos; +} + +int scandir(const char *dirp, struct dirent ***namelist, + int (*filter)(const struct dirent *), + int (*compar)(const struct dirent **, const struct dirent **)) { + DIR *d = opendir(dirp); + if (d == NULL) + return -1; + struct dirent **list = NULL; + size_t count = 0, cap = 0; + struct dirent *ent; + while ((ent = readdir(d)) != NULL) { + if (filter != NULL && filter(ent) == 0) + continue; + if (count >= cap) { + size_t ncap = cap ? cap * 2 : 16; + struct dirent **nl = (struct dirent **)realloc(list, ncap * sizeof(*list)); + if (nl == NULL) + break; + list = nl; + cap = ncap; + } + struct dirent *copy = (struct dirent *)malloc(sizeof(struct dirent)); + if (copy == NULL) + break; + memcpy(copy, ent, sizeof(struct dirent)); + list[count++] = copy; + } + closedir(d); + if (compar != NULL && count > 1) + qsort(list, count, sizeof(*list), (int (*)(const void *, const void *))compar); + *namelist = list; + return (int)count; +} + +void sync(void) { + _flushall(); // best-effort: flush all open C streams +} + +// esp-loader declares this weak; give it a strong (silent) definition so the +// mingw linker resolves it. The ESP32 flasher is device-only and inert here. +void loader_port_debug_print(const char *str) { + (void)str; +} + +#endif // EMULATOR_BUILD && _WIN32 diff --git a/src/platform/compat/windows/sys/ioctl.h b/src/platform/compat/windows/sys/ioctl.h new file mode 100644 index 00000000..9f089260 --- /dev/null +++ b/src/platform/compat/windows/sys/ioctl.h @@ -0,0 +1,3 @@ +#pragma once +/* stub for the Windows (mingw) emulator build (device ioctls dormant). */ +static inline int ioctl(int fd, unsigned long request, ...) { (void)fd; (void)request; return -1; } diff --git a/src/platform/compat/windows/sys/mount.h b/src/platform/compat/windows/sys/mount.h new file mode 100644 index 00000000..b0a5d68b --- /dev/null +++ b/src/platform/compat/windows/sys/mount.h @@ -0,0 +1,6 @@ +#pragma once +/* stub for the Windows (mingw) emulator build (statfs/mount dormant). */ +struct statfs { unsigned long f_bsize, f_blocks, f_bfree, f_bavail, f_files, f_ffree; }; +static inline int statfs(const char *p, struct statfs *b) { (void)p; if (b) { b->f_bsize = 0; b->f_bavail = 0; } return -1; } +static inline int mount(const char *s, const char *t, const char *f, unsigned long fl, const void *d) { (void)s; (void)t; (void)f; (void)fl; (void)d; return -1; } +static inline int umount(const char *t) { (void)t; return -1; } diff --git a/src/platform/compat/windows/sys/select.h b/src/platform/compat/windows/sys/select.h new file mode 100644 index 00000000..2cc3ade0 --- /dev/null +++ b/src/platform/compat/windows/sys/select.h @@ -0,0 +1,15 @@ +#pragma once +/* stub for the Windows (mingw) emulator build. The device select() + paths that include it never have real fds in the emulator, so this is inert. */ +#include +#ifndef FD_SETSIZE +#define FD_SETSIZE 64 +#endif +typedef struct { int fd_count; int fd_array[FD_SETSIZE]; } fd_set; +#define FD_ZERO(s) ((s)->fd_count = 0) +#define FD_SET(fd, s) ((void)(fd), (void)(s)) +#define FD_CLR(fd, s) ((void)(fd), (void)(s)) +#define FD_ISSET(fd, s) (((void)(fd), (void)(s)), 0) +static inline int select(int n, fd_set *r, fd_set *w, fd_set *e, struct timeval *t) { + (void)n; (void)r; (void)w; (void)e; (void)t; return -1; +} diff --git a/src/platform/compat/windows/sys/socket.h b/src/platform/compat/windows/sys/socket.h new file mode 100644 index 00000000..f03290c3 --- /dev/null +++ b/src/platform/compat/windows/sys/socket.h @@ -0,0 +1,2 @@ +#pragma once +/* Empty sys/socket.h stub for the Windows (mingw) emulator (wifi is device-only; its socket use is guarded). */ diff --git a/src/platform/compat/windows/sys/sockio.h b/src/platform/compat/windows/sys/sockio.h new file mode 100644 index 00000000..f3d8e37c --- /dev/null +++ b/src/platform/compat/windows/sys/sockio.h @@ -0,0 +1,2 @@ +#pragma once +/* Empty sys/sockio.h stub for the Windows (mingw) emulator (wifi is device-only; its socket use is guarded). */ diff --git a/src/platform/compat/windows/termios.h b/src/platform/compat/windows/termios.h new file mode 100644 index 00000000..1501d354 --- /dev/null +++ b/src/platform/compat/windows/termios.h @@ -0,0 +1,98 @@ +#pragma once +/* stub for the Windows (mingw) emulator build. The serial drivers + that configure a tty (uart, dm5680, esp32, ...) never open a real port in the + emulator, so these are inert stubs that let that device code compile. */ + +typedef unsigned char cc_t; +typedef unsigned int speed_t; +typedef unsigned int tcflag_t; + +#define NCCS 32 +struct termios { + tcflag_t c_iflag; + tcflag_t c_oflag; + tcflag_t c_cflag; + tcflag_t c_lflag; + cc_t c_line; + cc_t c_cc[NCCS]; + speed_t c_ispeed; + speed_t c_ospeed; +}; + +/* c_cc subscripts */ +#define VINTR 0 +#define VQUIT 1 +#define VERASE 2 +#define VKILL 3 +#define VEOF 4 +#define VTIME 5 +#define VMIN 6 + +/* c_iflag */ +#define IGNBRK 0000001 +#define BRKINT 0000002 +#define IGNPAR 0000004 +#define PARMRK 0000010 +#define INPCK 0000020 +#define ISTRIP 0000040 +#define INLCR 0000100 +#define IGNCR 0000200 +#define ICRNL 0000400 +#define IXON 0002000 +#define IXOFF 0010000 + +/* c_oflag */ +#define OPOST 0000001 + +/* c_cflag */ +#define CSIZE 0000060 +#define CS5 0000000 +#define CS6 0000020 +#define CS7 0000040 +#define CS8 0000060 +#define CSTOPB 0000100 +#define CREAD 0000200 +#define PARENB 0000400 +#define PARODD 0001000 +#define HUPCL 0002000 +#define CLOCAL 0004000 +#define CRTSCTS 020000000000 + +/* c_lflag */ +#define ISIG 0000001 +#define ICANON 0000002 +#define ECHO 0000010 + +/* baud rates (opaque tags here; values mirror Linux ) */ +#define B0 0000000 +#define B2400 0000013 +#define B4800 0000014 +#define B9600 0000015 +#define B19200 0000016 +#define B38400 0000017 +#define B57600 0010001 +#define B115200 0010002 +#define B230400 0010003 +#define B460800 0010004 +#define B921600 0010007 + +/* tcsetattr optional_actions */ +#define TCSANOW 0 +#define TCSADRAIN 1 +#define TCSAFLUSH 2 + +/* tcflush queue_selector */ +#define TCIFLUSH 0 +#define TCOFLUSH 1 +#define TCIOFLUSH 2 + +static inline int tcgetattr(int fd, struct termios *t) { (void)fd; (void)t; return -1; } +static inline int tcsetattr(int fd, int act, const struct termios *t) { (void)fd; (void)act; (void)t; return -1; } +static inline int tcflush(int fd, int q) { (void)fd; (void)q; return -1; } +static inline int tcdrain(int fd) { (void)fd; return -1; } +static inline int tcsendbreak(int fd, int d) { (void)fd; (void)d; return -1; } +static inline int cfsetispeed(struct termios *t, speed_t s) { (void)t; (void)s; return 0; } +static inline int cfsetospeed(struct termios *t, speed_t s) { (void)t; (void)s; return 0; } +static inline speed_t cfgetispeed(const struct termios *t) { (void)t; return 0; } +static inline speed_t cfgetospeed(const struct termios *t) { (void)t; return 0; } +static inline void cfmakeraw(struct termios *t) { (void)t; } diff --git a/src/platform/paths.c b/src/platform/paths.c new file mode 100644 index 00000000..36610ac2 --- /dev/null +++ b/src/platform/paths.c @@ -0,0 +1,46 @@ +#include "platform/paths.h" + +// On-device defaults. The goggle firmware keeps exactly these (see paths_init). +static char s_app[256] = "/mnt/app"; +static char s_extsd[256] = "/mnt/extsd"; + +#if defined(EMULATOR_BUILD) +#include +#include + +#include + +static void set_root(const char *value, char *dst, size_t n) { + if (value && *value) { + strncpy(dst, value, n - 1); + dst[n - 1] = '\0'; + } +} +#endif + +void paths_init(void) { +#if defined(EMULATOR_BUILD) + // Emulator only: resolve the roots from an optional minIni config file + // (HDZ_CONFIG) plus env overrides. The goggle build compiles this away, so it + // does no file I/O and never links minIni for paths — zero cost on-device. + const char *cfg = getenv("HDZ_CONFIG"); + if (cfg && *cfg) { + char buf[256]; + if (ini_gets("paths", "app_root", "", buf, sizeof(buf), cfg) > 0) + set_root(buf, s_app, sizeof(s_app)); + if (ini_gets("paths", "extsd_root", "", buf, sizeof(buf), cfg) > 0) + set_root(buf, s_extsd, sizeof(s_extsd)); + } + // Env wins over the file (quick per-run tweaks). + set_root(getenv("HDZ_APP_ROOT"), s_app, sizeof(s_app)); + set_root(getenv("HDZ_EXTSD_ROOT"), s_extsd, sizeof(s_extsd)); +#endif +} + +const char *path_app(void) { + return s_app; +} + +const char *path_extsd(void) { + return s_extsd; +} diff --git a/src/platform/paths.h b/src/platform/paths.h new file mode 100644 index 00000000..a6501c47 --- /dev/null +++ b/src/platform/paths.h @@ -0,0 +1,27 @@ +#pragma once +#ifdef __cplusplus +extern "C" { +#endif + +// Configurable filesystem roots. +// +// Defaults are the on-device paths, so the goggle firmware build is byte-for-byte +// unchanged (no config present -> "/mnt/app" and "/mnt/extsd"). The desktop +// emulator overrides them so it can load resources/media from anywhere: +// +// * a minIni config file pointed to by the HDZ_CONFIG env var, section [paths]: +// [paths] +// app_root = /path/to/app ; replaces /mnt/app +// extsd_root = /path/to/sdcard ; replaces /mnt/extsd +// * or the HDZ_APP_ROOT / HDZ_EXTSD_ROOT env vars (these win over the file), +// handy for quick per-run tweaks. +// +// Call paths_init() once, early in startup, before anything reads resources. +void paths_init(void); + +const char *path_app(void); // app install root (default "/mnt/app") +const char *path_extsd(void); // sd-card mount root (default "/mnt/extsd") + +#ifdef __cplusplus +} +#endif diff --git a/src/platform/timer_compat.c b/src/platform/timer_compat.c new file mode 100644 index 00000000..75c44dd9 --- /dev/null +++ b/src/platform/timer_compat.c @@ -0,0 +1,95 @@ +#include "platform/timer_compat.h" + +#if !defined(__linux__) + +#include +#include +#include + +struct pt_timer { + pthread_t thread; + struct itimerspec spec; + void (*fn)(union sigval); + union sigval val; + volatile int running; +}; + +static void *pt_timer_thread(void *arg) { + struct pt_timer *t = (struct pt_timer *)arg; + + struct timespec delay = t->spec.it_value; + if (delay.tv_sec || delay.tv_nsec) + nanosleep(&delay, NULL); + if (t->running && t->fn) + t->fn(t->val); + + for (;;) { + struct timespec iv = t->spec.it_interval; + if ((iv.tv_sec == 0 && iv.tv_nsec == 0) || !t->running) + break; // one-shot, or stopped + nanosleep(&iv, NULL); + if (!t->running) + break; + if (t->fn) + t->fn(t->val); + } + return NULL; +} + +int timer_create(clockid_t clockid, struct sigevent *sevp, timer_t *timerid) { + (void)clockid; + if (!timerid) { + errno = EINVAL; + return -1; + } + struct pt_timer *t = (struct pt_timer *)calloc(1, sizeof(*t)); + if (!t) { + errno = ENOMEM; + return -1; + } + if (sevp) { + t->fn = sevp->sigev_notify_function; + t->val = sevp->sigev_value; + } + *timerid = t; + return 0; +} + +int timer_settime(timer_t timerid, int flags, + const struct itimerspec *new_value, + struct itimerspec *old_value) { + (void)flags; + if (!timerid || !new_value) { + errno = EINVAL; + return -1; + } + if (old_value) { + old_value->it_interval.tv_sec = 0; + old_value->it_interval.tv_nsec = 0; + old_value->it_value.tv_sec = 0; + old_value->it_value.tv_nsec = 0; + } + timerid->spec = *new_value; + if (!timerid->running) { + timerid->running = 1; + if (pthread_create(&timerid->thread, NULL, pt_timer_thread, timerid) != 0) { + timerid->running = 0; + return -1; + } + } + return 0; +} + +int timer_delete(timer_t timerid) { + if (!timerid) { + errno = EINVAL; + return -1; + } + timerid->running = 0; + pthread_detach(timerid->thread); + // Intentionally not freed: the worker thread may still be waking, so leaking + // a tiny struct at teardown is safer than a use-after-free. ht.c never deletes. + return 0; +} + +#endif // !__linux__ diff --git a/src/platform/timer_compat.h b/src/platform/timer_compat.h new file mode 100644 index 00000000..88c15d68 --- /dev/null +++ b/src/platform/timer_compat.h @@ -0,0 +1,53 @@ +#pragma once +// Portable subset of the POSIX per-process interval-timer API (timer_create / +// timer_settime / timer_delete) for platforms that lack it (macOS, Windows). +// Only the SIGEV_THREAD periodic-timer behaviour used by the head tracker is +// implemented, backed by a pthread. On Linux the real /librt API is used +// instead, so this shim compiles to nothing there. +#if !defined(__linux__) + +#include // struct sigevent, union sigval, SIGEV_THREAD +#include // struct timespec, clockid_t + +#ifdef __cplusplus +extern "C" { +#endif + +// macOS lacks struct itimerspec; Windows (mingw + winpthread) already defines it. +#if !defined(_WIN32) +struct itimerspec { + struct timespec it_interval; + struct timespec it_value; +}; +#endif + +// Windows (mingw) lacks sigevent / SIGEV_THREAD; macOS's provides them. +#if defined(_WIN32) +#ifndef SIGEV_THREAD +#define SIGEV_THREAD 2 +#endif +union sigval { + int sival_int; + void *sival_ptr; +}; +struct sigevent { + int sigev_notify; + union sigval sigev_value; + void (*sigev_notify_function)(union sigval); + void *sigev_notify_attributes; +}; +#endif + +typedef struct pt_timer *timer_t; + +int timer_create(clockid_t clockid, struct sigevent *sevp, timer_t *timerid); +int timer_settime(timer_t timerid, int flags, + const struct itimerspec *new_value, + struct itimerspec *old_value); +int timer_delete(timer_t timerid); + +#ifdef __cplusplus +} +#endif + +#endif // !__linux__ diff --git a/src/ui/page_playback.c b/src/ui/page_playback.c index f580ca7b..466ca34d 100644 --- a/src/ui/page_playback.c +++ b/src/ui/page_playback.c @@ -24,8 +24,19 @@ #include "util/filesystem.h" #include "util/math.h" #include "util/system.h" +#if defined(EMULATOR_BUILD) +// Emulator: point the DVR folder at a local mock directory so you can drop a copy +// of your goggle's DCIM/100HDZRO/ folder (videos + .jpg thumbnails + .star.txt) +// anywhere and test playback on the desktop. Set HDZ_MEDIA_DIR (MUST end with +// '/'); defaults to ./mock-sd/ relative to the working directory. +static inline const char *emu_media_dir(void) { + const char *d = getenv("HDZ_MEDIA_DIR"); + return (d && *d) ? d : "mock-sd/"; +} +#define MEDIA_FILES_DIR emu_media_dir() +#else #define MEDIA_FILES_DIR REC_diskPATH REC_packPATH // "/mnt/extsd/movies" --> "/mnt/extsd" "/movies/" -// #define MEDIA_FILES_DIR "/mnt/extsd/movies/"--Useful for testing playback page +#endif bool status_displayed = false; lv_obj_t *status; LV_IMG_DECLARE(img_star); diff --git a/src/ui/page_scannow.c b/src/ui/page_scannow.c index 368a6295..54fd3af0 100644 --- a/src/ui/page_scannow.c +++ b/src/ui/page_scannow.c @@ -20,7 +20,6 @@ #include "core/settings.h" #include "driver/dm5680.h" #include "driver/dm6302.h" -#include "driver/fbtools.h" #include "driver/hardware.h" #include "driver/i2c.h" #include "driver/screen.h" diff --git a/src/ui/page_version.c b/src/ui/page_version.c index ced67aa7..9e34f912 100644 --- a/src/ui/page_version.c +++ b/src/ui/page_version.c @@ -132,7 +132,7 @@ static bool is_need_update_progress = false; static bool reboot_flag = false; static lv_obj_t *cur_ver_label; static int reset_all_settings_confirm = CONFIRMATION_UNCONFIRMED; -static atomic_bool autoscan_filesystem = ATOMIC_VAR_INIT(true); +static atomic_bool autoscan_filesystem = true; // was ATOMIC_VAR_INIT(true) — macro removed in C23 / modern GCC #undef RETURN_ON_ERROR #define RETURN_ON_ERROR(m, x) \ @@ -411,7 +411,11 @@ static const char *page_version_find_latest_fw(const char *path) { if (dir) { struct dirent *entry = readdir(dir); while (entry != NULL) { +#if defined(_WIN32) + if (0) { // fw scan disabled on Windows (mingw dirent lacks d_type; inert in emulator) +#else if (entry->d_type == DT_DIR) { +#endif if (dname != NULL) { if (str_compare_versions(entry->d_name, dname) > 0) { dname = entry->d_name; @@ -472,7 +476,11 @@ static int page_version_get_latest_fw_files(fw_select_t *fw_select, const char * if (dir) { struct dirent *entry = readdir(dir); while (entry != NULL) { +#if defined(_WIN32) + if (0) { // fw scan disabled on Windows (mingw dirent lacks d_type; inert in emulator) +#else if (entry->d_type == DT_REG) { +#endif if (strstr(entry->d_name, pattern)) { bool match = false; for (int j = 0; j < fw_select->count; ++j) { diff --git a/src/ui/page_wifi.c b/src/ui/page_wifi.c index 4d54e562..01854b68 100644 --- a/src/ui/page_wifi.c +++ b/src/ui/page_wifi.c @@ -2,7 +2,13 @@ #include #include +#if defined(__linux__) #include +#else +#include +#include +#include +#endif #include #include #include @@ -348,6 +354,10 @@ static void page_wifi_update_settings() { * Acquire the actual address in use. */ static const char *page_wifi_get_real_address() { +#if defined(_WIN32) + // wlan0 SIOCGIFADDR has no Windows equivalent; wifi is device-only, inert here. + return NULL; +#else const char *address = NULL; int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); @@ -364,6 +374,7 @@ static const char *page_wifi_get_real_address() { } return address; +#endif } /** @@ -617,8 +628,16 @@ static void page_wifi_apply_settings_timer_cb(struct _lv_timer_t *timer) { * Verify if user entered text contains a valid network adresses. */ static bool page_wifi_is_network_address_valid(const char *address) { +#if defined(_WIN32) + // Portable IPv4 validation (avoids winsock); equivalent to inet_pton for AF_INET. + unsigned a, b, c, d; + char extra; + return sscanf(address, "%u.%u.%u.%u%c", &a, &b, &c, &d, &extra) == 4 && + a < 256 && b < 256 && c < 256 && d < 256; +#else struct sockaddr_in sa; return 1 == inet_pton(AF_INET, address, &(sa.sin_addr)); +#endif } /** diff --git a/src/ui/ui_porting.c b/src/ui/ui_porting.c index e69a2c86..1cbb4859 100644 --- a/src/ui/ui_porting.c +++ b/src/ui/ui_porting.c @@ -6,11 +6,17 @@ #include "../core/common.hh" #include "../core/settings.h" -#include "fbtools.h" +#ifndef EMULATOR_BUILD +#include "fbtools.h" // Linux framebuffer — goggle display path only (emulator uses SDL) +#endif #include "lvgl/lvgl.h" #ifdef EMULATOR_BUILD +#include // getenv, malloc, free + #include "SDLaccess.h" +#include "core/app_state.h" // g_app_state — video only composites in the video view +#include "emulator/video_emu.h" static void *fb1, *fb2; SDL_Window *window = NULL; SDL_Renderer *renderer = NULL; @@ -18,6 +24,34 @@ SDL_Texture *texture = NULL; #ifndef WINDOW_NAME #define WINDOW_NAME "HDZero" #endif + +// --------------------------------------------------------------------------- +// Fake video-feed compositing (EMULATOR only, opt-in via the HDZ_VIDEO env var) +// --------------------------------------------------------------------------- +// When HDZ_VIDEO points at a video file, a decoded video is played as the FPV +// BACKGROUND, composited UNDERNEATH the LVGL OSD so the FPV viewer is +// representative for UI design. The OSD paints its video region solid gray +// 0x7f7f7f; we chroma-key that gray to transparent so the video shows through. +// +// This is entirely inert unless `video_active` is true (which only happens when +// HDZ_VIDEO is set AND the decoder starts OK). When inactive, the flush path is +// byte-for-byte identical to the original opaque LVGL copy: no video texture, no +// per-pixel conversion, no extra allocations. +#define VIDEO_CHROMA_KEY 127 // gray 0x7f7f7f channel value of the OSD video region +#define VIDEO_CHROMA_TOL 4 // per-channel tolerance (covers e.g. RGB565 quantisation of the key) + +static SDL_Texture *video_texture = NULL; // RGB24 streaming texture: latest decoded video frame (background) +static SDL_Texture *lvgl_argb_texture = NULL; // ARGB8888 streaming texture: keyed LVGL frame drawn on top +static uint8_t *video_rgb_buf = NULL; // scratch RGB24 decoder frame (W*H*3) +static uint8_t *lvgl_argb_buf = NULL; // scratch ARGB8888 keyed frame (W*H*4) +static bool video_active = false; // true only when the fake video feed is running + +// True when (r,g,b) is within tolerance of the gray chroma key (127,127,127). +static inline bool video_is_chroma_key(uint8_t r, uint8_t g, uint8_t b) { + return (r >= VIDEO_CHROMA_KEY - VIDEO_CHROMA_TOL) && (r <= VIDEO_CHROMA_KEY + VIDEO_CHROMA_TOL) && + (g >= VIDEO_CHROMA_KEY - VIDEO_CHROMA_TOL) && (g <= VIDEO_CHROMA_KEY + VIDEO_CHROMA_TOL) && + (b >= VIDEO_CHROMA_KEY - VIDEO_CHROMA_TOL) && (b <= VIDEO_CHROMA_KEY + VIDEO_CHROMA_TOL); +} #endif typedef enum { @@ -29,9 +63,18 @@ typedef enum { ORBIT_FLUSH = 0x10, } orbit_state_t; -FBDEV fbdev; +#ifndef EMULATOR_BUILD +FBDEV fbdev; // goggle framebuffer handle; the emulator flushes via SDL instead +#endif static lv_disp_draw_buf_t draw_buf; +#ifdef EMULATOR_BUILD +// The emulator malloc's the draw buffer at init (below) instead of a large static: +// mingw stores a zero-init static in the PE .data section (~33MB in the .exe), +// whereas ELF keeps it in .bss (unstored). Same buffer, allocated on the heap. +static lv_color_t *disp_buf; +#else static lv_color_t disp_buf[DRAW_HOR_RES_FHD * DRAW_VER_RES_FHD * 4]; +#endif static lv_disp_drv_t disp_drv; static orbit_state_t disp_orbit_state = ORBIT_NONE; static int disp_orbit_x, disp_orbit_y; @@ -70,13 +113,73 @@ static void hdz_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_ .h = disp_drv.ver_res - DISP_OVERSCAN, }; - SDL_UpdateTexture(texture, &src, color_p, disp_drv.hor_res * ((LV_COLOR_DEPTH + 7) / 8)); - src.x = disp_orbit_x; - src.w = dst.w; - src.y = disp_orbit_y; - src.h = dst.h; - SDL_RenderCopy(renderer, texture, &src, &dst); - SDL_RenderPresent(renderer); + // The mock video plane only exists in the video view, exactly like the real + // DM5680 overlay (the app paints the 0x7f7f7f "video hole" only there, and + // calls HDZero_Close() on the way to the menu). Outside APP_STATE_VIDEO we + // must NOT chroma-key -- otherwise menu pixels near gray get keyed and show + // video -- and we pause the decoder so the menu costs no video CPU. + bool show_video = video_active && (g_app_state == APP_STATE_VIDEO); + if (video_active) + video_emu_set_paused(!show_video); + + if (show_video) { + // ---- Fake video-feed compositing path (only when HDZ_VIDEO is set) ---- + // 1) Refresh the background video texture with the latest decoded frame. + // video_emu_get may return false (no frame yet); we then keep the + // previously uploaded frame, so the background never flickers. + if (video_emu_get(video_rgb_buf, DRAW_HOR_RES_FHD, DRAW_VER_RES_FHD)) { + SDL_UpdateTexture(video_texture, NULL, video_rgb_buf, DRAW_HOR_RES_FHD * 3); + } + + // 2) Convert the LVGL framebuffer to ARGB8888, chroma-keying the gray + // video region (RGB 127,127,127 == 0x7f7f7f) to alpha 0x00 so the + // video shows through; every other pixel is fully opaque (0xFF). + // + // Pixel conversion: lv_color_t is LV_COLOR_DEPTH bits. lv_color_to32() + // normalises ANY depth to 0xAARRGGBB (for LV_COLOR_DEPTH==16 it expands + // the packed RGB565 5/6/5 channels to 8-bit each; for ==32 it is the + // value as-is). We then repack as ARGB8888 with our computed alpha. + // On little-endian hosts the uint32 0xAARRGGBB is stored as bytes + // B,G,R,A -- exactly what SDL_PIXELFORMAT_ARGB8888 expects. + { + lv_color_t *cp = color_p; + uint32_t *out = (uint32_t *)lvgl_argb_buf; + int count = disp_drv.hor_res * disp_drv.ver_res; + for (int i = 0; i < count; i++) { + uint32_t px = lv_color_to32(cp[i]); // 0xAARRGGBB, alpha bits ignored below + uint8_t r = (uint8_t)((px >> 16) & 0xFF); + uint8_t g = (uint8_t)((px >> 8) & 0xFF); + uint8_t b = (uint8_t)(px & 0xFF); + uint8_t a = video_is_chroma_key(r, g, b) ? 0x00 : 0xFF; + out[i] = ((uint32_t)a << 24) | ((uint32_t)r << 16) | + ((uint32_t)g << 8) | (uint32_t)b; + } + } + // Upload the keyed frame (same sub-rect + pitch convention as the + // original opaque copy, but ARGB8888 is fixed 4 bytes/pixel). + SDL_UpdateTexture(lvgl_argb_texture, &src, lvgl_argb_buf, disp_drv.hor_res * 4); + src.x = disp_orbit_x; + src.w = dst.w; + src.y = disp_orbit_y; + src.h = dst.h; + + // 3) Composite: clear, video underneath (full window), keyed OSD on top. + // lvgl_argb_texture has SDL_BLENDMODE_BLEND so alpha==0 pixels reveal + // the video background. + SDL_RenderClear(renderer); + SDL_RenderCopy(renderer, video_texture, NULL, NULL); + SDL_RenderCopy(renderer, lvgl_argb_texture, &src, &dst); + SDL_RenderPresent(renderer); + } else { + // ---- Default path: unchanged opaque LVGL copy via the original texture ---- + SDL_UpdateTexture(texture, &src, color_p, disp_drv.hor_res * ((LV_COLOR_DEPTH + 7) / 8)); + src.x = disp_orbit_x; + src.w = dst.w; + src.y = disp_orbit_y; + src.h = dst.h; + SDL_RenderCopy(renderer, texture, &src, &dst); + SDL_RenderPresent(renderer); + } SDL_UnlockMutex(global_sdl_mutex); #endif @@ -147,10 +250,60 @@ int lvgl_init_porting() { SDL_TEXTUREACCESS_STREAMING, DRAW_HOR_RES_FHD, DRAW_VER_RES_FHD); + + // Fake video feed: opt-in via HDZ_VIDEO. Left completely inert (video_active + // stays false) otherwise, so the default emulator is unchanged. + const char *vpath = getenv("HDZ_VIDEO"); + if (vpath && *vpath) { + // Background texture the decoded RGB24 video frames are uploaded into. + video_texture = SDL_CreateTexture(renderer, + SDL_PIXELFORMAT_RGB24, + SDL_TEXTUREACCESS_STREAMING, + DRAW_HOR_RES_FHD, + DRAW_VER_RES_FHD); + // Dedicated ARGB8888 texture for the LVGL frame drawn ON TOP of the video. + // BLEND mode makes chroma-keyed (alpha==0) pixels transparent so the + // video shows through. Kept separate from `texture` so the default path + // (which never sets a blend mode) is untouched. + lvgl_argb_texture = SDL_CreateTexture(renderer, + SDL_PIXELFORMAT_ARGB8888, + SDL_TEXTUREACCESS_STREAMING, + DRAW_HOR_RES_FHD, + DRAW_VER_RES_FHD); + if (lvgl_argb_texture) { + SDL_SetTextureBlendMode(lvgl_argb_texture, SDL_BLENDMODE_BLEND); + } + video_rgb_buf = (uint8_t *)malloc((size_t)DRAW_HOR_RES_FHD * DRAW_VER_RES_FHD * 3); + lvgl_argb_buf = (uint8_t *)malloc((size_t)DRAW_HOR_RES_FHD * DRAW_VER_RES_FHD * 4); + if (video_texture && lvgl_argb_texture && video_rgb_buf && lvgl_argb_buf && + video_emu_start(vpath, DRAW_HOR_RES_FHD, DRAW_VER_RES_FHD) == 0) { + video_active = true; + LOGI("HDZ_VIDEO: fake video feed active (%s) at %dx%d", vpath, DRAW_HOR_RES_FHD, DRAW_VER_RES_FHD); + } else { + // On any failure, roll back so the flush path stays on the original + // opaque code path (video_active remains false). + LOGE("HDZ_VIDEO: failed to start fake video feed (%s); running without it", vpath); + if (video_texture) { + SDL_DestroyTexture(video_texture); + video_texture = NULL; + } + if (lvgl_argb_texture) { + SDL_DestroyTexture(lvgl_argb_texture); + lvgl_argb_texture = NULL; + } + free(video_rgb_buf); + video_rgb_buf = NULL; + free(lvgl_argb_buf); + lvgl_argb_buf = NULL; + video_active = false; + } + } SDL_UnlockMutex(global_sdl_mutex); fb1 = malloc(DRAW_HOR_RES_FHD * DRAW_VER_RES_FHD * ((LV_COLOR_DEPTH + 7) / 8)); fb2 = malloc(DRAW_HOR_RES_FHD * DRAW_VER_RES_FHD * ((LV_COLOR_DEPTH + 7) / 8)); + // LVGL draw buffer (heap, not a large static — keeps it out of the mingw .data). + disp_buf = malloc((size_t)DRAW_HOR_RES_FHD * DRAW_VER_RES_FHD * 4 * sizeof(lv_color_t)); lv_disp_draw_buf_init(&draw_buf, fb1, fb2, DRAW_HOR_RES_FHD * DRAW_VER_RES_FHD); lv_disp_drv_init(&disp_drv); diff --git a/src/util/sdcard.c b/src/util/sdcard.c index 43d686fc..2d8e4d8b 100644 --- a/src/util/sdcard.c +++ b/src/util/sdcard.c @@ -1,7 +1,11 @@ #include "sdcard.h" #include +#if defined(__linux__) #include +#else +#include +#endif #include static int g_sdcard_free_size = 0; diff --git a/src/util/system.c b/src/util/system.c index 34948f09..c34e228f 100644 --- a/src/util/system.c +++ b/src/util/system.c @@ -10,11 +10,27 @@ int system_exec(const char *command) { LOGI("System Execute: %s", command); +#ifdef EMULATOR_BUILD + // The emulator stands in for the goggle hardware, not the host OS. These + // commands are device operations (display driver `dispw`, register pokes + // `aww`, wifi bring-up + root-password scripts, writes under /etc). Running + // them on a dev machine ranges from useless to destructive (the passwd + // script tried to change the host user's password), so we log and report + // success without touching the host. Real file I/O goes through fopen/minIni, + // not this path, so nothing the emulator legitimately needs is lost. + LOGI(" [emulator] not executed (host-safe no-op)"); + return 0; +#else return system(command); +#endif } int system_script(const char *command) { LOGI("System Script: %s", command); +#ifdef EMULATOR_BUILD + LOGI(" [emulator] not executed (host-safe no-op)"); + return 0; +#else // basename may edit argument const char *script = fs_basename(command); @@ -45,4 +61,5 @@ int system_script(const char *command) { } return retval; +#endif }