From 8178396211c590415d0184bb5cc1a035111fee29 Mon Sep 17 00:00:00 2001 From: Paul Gregoire Date: Fri, 24 Apr 2026 13:00:59 -0700 Subject: [PATCH 1/7] Add macOS and Windows release scripts --- INSTALL.md | 38 ++++++ Makefile | 4 + scripts/build-macos.sh | 89 ++++++++++++++ scripts/build-windows.sh | 245 +++++++++++++++++++++++++++++++++++++++ src/funnelcake.c | 46 ++++---- src/funnelcake_hdr.c | 110 +++++++++--------- src/internal.h | 25 ++++ src/log.h | 5 +- src/tonemap.c | 10 +- 9 files changed, 491 insertions(+), 81 deletions(-) create mode 100755 scripts/build-macos.sh create mode 100755 scripts/build-windows.sh diff --git a/INSTALL.md b/INSTALL.md index 0468dc4..4f16a1e 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -125,3 +125,41 @@ Artifacts are written to `dist/` as per-architecture tarballs containing release archives are built with `CC=clang LTO=0` so the static library contains standard object files suitable for downstream linkers that do not understand Clang LTO bitcode. + +## macOS Release Artifacts + +To build a native macOS release archive: + + ./scripts/build-macos.sh + +The script must be run on macOS with Xcode command line tools installed. It +writes `dist/funnelcake-macos-.tar.gz` containing `libfunnelcake.a`, +`include/funnelcake.h`, the README, install notes, and `BUILD_INFO`. + +## Windows Release Artifacts + +To build Windows release archives: + + ./scripts/build-windows.sh + +By default the script builds every Windows target whose toolchain is available. +MinGW-w64 artifacts contain `libfunnelcake.a`; MSVC artifacts contain +`funnelcake.lib`. Both package layouts include `include/funnelcake.h`, the +README, install notes, and `BUILD_INFO`. + +For MinGW-w64 only: + + ./scripts/build-windows.sh --mingw + +For `x86_64` MinGW, install tools that provide `x86_64-w64-mingw32-gcc` and +`x86_64-w64-mingw32-ar`. For Windows on ARM64 MinGW, install tools that provide +`aarch64-w64-mingw32-gcc` and `aarch64-w64-mingw32-ar`. + +For MSVC only, run from a Visual Studio developer shell where `cl.exe` and +`lib.exe` are in `PATH`: + + ./scripts/build-windows.sh --msvc + +The MSVC package currently builds the portable scalar static library. The +MinGW packages use the normal Makefile source selection, including AVX2 on +`x86_64` and NEON on `aarch64`. diff --git a/Makefile b/Makefile index 954f6d5..95bd259 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,10 @@ TEST_OPT = -O2 UNAME_M := $(shell uname -m) UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Windows_NT) + CFLAGS_BASE += -D__USE_MINGW_ANSI_STDIO=1 +endif + # --- Tuning options --- # SCALAR_ARCH: controls -march for kernels_scalar.c only diff --git a/scripts/build-macos.sh b/scripts/build-macos.sh new file mode 100755 index 0000000..08b17ad --- /dev/null +++ b/scripts/build-macos.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +DIST_DIR="${REPO_ROOT}/dist" +BUILD_DATE="$(date -u +%Y%m%dT%H%M%SZ)" +HOST_OS="$(uname -s)" +HOST_ARCH="$(uname -m)" + +usage() { + cat <<'EOF' +Usage: scripts/build-macos.sh + +Build a native macOS release archive. This script must run on macOS with +Xcode command line tools installed. +EOF +} + +if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then + usage + exit 0 +fi + +if [ "$#" -gt 0 ]; then + echo "error: unknown option: $1" >&2 + usage >&2 + exit 1 +fi + +require_tool() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "error: required tool not found: $1" >&2 + exit 1 + fi +} + +if [ "${HOST_OS}" != "Darwin" ]; then + echo "error: macOS artifacts require a Darwin host" >&2 + exit 1 +fi + +require_tool clang +require_tool make +require_tool shasum +require_tool tar + +mkdir -p "${DIST_DIR}" + +package_dir="funnelcake-macos-${HOST_ARCH}" + +echo "==> Building macOS ${HOST_ARCH} artifact" +( + cd "${REPO_ROOT}" + make clean + make lib CC=clang LTO=0 UNAME_S=Darwin UNAME_M="${HOST_ARCH}" +) + +rm -rf "${DIST_DIR}/${package_dir}" +mkdir -p "${DIST_DIR}/${package_dir}/include" +cp "${REPO_ROOT}/libfunnelcake.a" "${DIST_DIR}/${package_dir}/" +cp "${REPO_ROOT}/include/funnelcake.h" "${DIST_DIR}/${package_dir}/include/" +cp "${REPO_ROOT}/README.md" "${REPO_ROOT}/INSTALL.md" "${DIST_DIR}/${package_dir}/" + +{ + printf '%s\n' "name=funnelcake" + printf '%s\n' "target_os=macos" + printf '%s\n' "target_arch=${HOST_ARCH}" + printf '%s\n' "compiler=clang" + printf '%s\n' "lto=0" + printf '%s\n' "build_date=${BUILD_DATE}" +} > "${DIST_DIR}/${package_dir}/BUILD_INFO" + +rm -f "${DIST_DIR}/${package_dir}.tar.gz" "${DIST_DIR}/${package_dir}.tar.gz.sha256" +( + cd "${DIST_DIR}" + tar -czf "${package_dir}.tar.gz" "${package_dir}" + shasum -a 256 "${package_dir}.tar.gz" > "${package_dir}.tar.gz.sha256" +) + +( + cd "${REPO_ROOT}" + make clean +) + +echo "" +echo "Artifacts written to ${DIST_DIR}:" +echo " ${package_dir}.tar.gz" +echo " ${package_dir}.tar.gz.sha256" diff --git a/scripts/build-windows.sh b/scripts/build-windows.sh new file mode 100755 index 0000000..7605b8d --- /dev/null +++ b/scripts/build-windows.sh @@ -0,0 +1,245 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +DIST_DIR="${REPO_ROOT}/dist" +BUILD_DIR="${REPO_ROOT}/build/windows" +BUILD_DATE="$(date -u +%Y%m%dT%H%M%SZ)" +ARTIFACTS=() + +build_mingw=1 +build_msvc=1 + +usage() { + cat <<'EOF' +Usage: scripts/build-windows.sh [--all] [--mingw] [--msvc] + +Build Windows release archives: + --all build every supported Windows target available on this host (default) + --mingw build MinGW-w64 static library packages + --msvc build an MSVC static library package from a Visual Studio shell + +MinGW targets require cross compilers such as x86_64-w64-mingw32-gcc. +The MSVC target requires cl.exe and lib.exe in PATH, usually by running from +"x64 Native Tools Command Prompt for VS" or an equivalent Developer PowerShell. +EOF +} + +if [ "$#" -gt 0 ]; then + build_mingw=0 + build_msvc=0 +fi + +while [ "$#" -gt 0 ]; do + case "$1" in + --all) + build_mingw=1 + build_msvc=1 + ;; + --mingw) + build_mingw=1 + ;; + --msvc) + build_msvc=1 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "error: unknown option: $1" >&2 + usage >&2 + exit 1 + ;; + esac + shift +done + +have_tool() { + command -v "$1" >/dev/null 2>&1 +} + +hash_file() { + local file="$1" + + if have_tool shasum; then + shasum -a 256 "${file}" + elif have_tool sha256sum; then + sha256sum "${file}" + else + echo "error: shasum or sha256sum is required to write checksums" >&2 + exit 1 + fi +} + +native_path() { + if have_tool cygpath; then + cygpath -w "$1" + else + printf '%s\n' "$1" + fi +} + +make_package_dir() { + local package_dir="$1" + local target_arch="$2" + local compiler="$3" + local library_path="$4" + local library_name="$5" + + rm -rf "${DIST_DIR}/${package_dir}" + mkdir -p "${DIST_DIR}/${package_dir}/include" + cp "${library_path}" "${DIST_DIR}/${package_dir}/${library_name}" + cp "${REPO_ROOT}/include/funnelcake.h" "${DIST_DIR}/${package_dir}/include/" + cp "${REPO_ROOT}/README.md" "${REPO_ROOT}/INSTALL.md" "${DIST_DIR}/${package_dir}/" + + { + printf '%s\n' "name=funnelcake" + printf '%s\n' "target_os=windows" + printf '%s\n' "base_distribution=${compiler}" + printf '%s\n' "target_arch=${target_arch}" + printf '%s\n' "compiler=${compiler}" + printf '%s\n' "lto=0" + printf '%s\n' "build_date=${BUILD_DATE}" + } > "${DIST_DIR}/${package_dir}/BUILD_INFO" +} + +make_zip() { + local package_dir="$1" + + if ! have_tool zip; then + echo "error: zip is required for Windows release archives" >&2 + exit 1 + fi + + rm -f "${DIST_DIR}/${package_dir}.zip" "${DIST_DIR}/${package_dir}.zip.sha256" + ( + cd "${DIST_DIR}" + zip -qr "${package_dir}.zip" "${package_dir}" + hash_file "${package_dir}.zip" > "${package_dir}.zip.sha256" + ) + ARTIFACTS+=("${package_dir}.zip" "${package_dir}.zip.sha256") +} + +build_windows_mingw() { + local arch="$1" + local cc="$2" + local ar="$3" + local package_dir="funnelcake-windows-mingw-${arch}" + + if ! have_tool "${cc}" || ! have_tool "${ar}" || ! have_tool make; then + echo "==> Skipping MinGW ${arch}: ${cc}, ${ar}, and make are required" + return 0 + fi + + echo "==> Building Windows MinGW ${arch} artifact" + ( + cd "${REPO_ROOT}" + make clean + make lib CC="${cc}" AR="${ar}" LTO=0 UNAME_S=Windows_NT UNAME_M="${arch}" + ) + + make_package_dir "${package_dir}" "${arch}" "mingw-w64" \ + "${REPO_ROOT}/libfunnelcake.a" "libfunnelcake.a" + make_zip "${package_dir}" +} + +msvc_arch_name() { + case "${VSCMD_ARG_TGT_ARCH:-${PROCESSOR_ARCHITECTURE:-unknown}}" in + x64|AMD64|amd64) printf '%s\n' "x64" ;; + arm64|ARM64) printf '%s\n' "arm64" ;; + *) printf '%s\n' "unknown" ;; + esac +} + +build_windows_msvc() { + local arch + local obj_dir + local lib_path + local package_dir + local src + local obj + local objs=() + local include_dir + local src_dir + local src_path + local obj_path + local lib_path_native + local common_sources=( + src/funnelcake.c + src/funnelcake_hdr.c + src/log.c + src/detect.c + src/kernels_scalar.c + src/kernels_hdr_scalar.c + src/tonemap.c + src/kernels_upscale_scalar.c + ) + + if ! have_tool cl || ! have_tool lib; then + echo "==> Skipping MSVC: cl.exe and lib.exe are required" + return 0 + fi + + arch="$(msvc_arch_name)" + if [ "${arch}" = "unknown" ]; then + echo "==> Skipping MSVC: unable to determine Visual Studio target architecture" + return 0 + fi + + package_dir="funnelcake-windows-msvc-${arch}" + obj_dir="${BUILD_DIR}/msvc-${arch}" + lib_path="${obj_dir}/funnelcake.lib" + include_dir="$(native_path "${REPO_ROOT}/include")" + src_dir="$(native_path "${REPO_ROOT}/src")" + lib_path_native="$(native_path "${lib_path}")" + + echo "==> Building Windows MSVC ${arch} artifact" + rm -rf "${obj_dir}" + mkdir -p "${obj_dir}" + + for src in "${common_sources[@]}"; do + obj="${obj_dir}/$(basename "${src}" .c).obj" + src_path="$(native_path "${REPO_ROOT}/${src}")" + obj_path="$(native_path "${obj}")" + MSYS2_ARG_CONV_EXCL='*' cl /nologo /std:c11 /O2 /W4 /WX \ + /D_CRT_SECURE_NO_WARNINGS /D_POSIX_C_SOURCE=200112L \ + /I"${include_dir}" /I"${src_dir}" \ + /Fo"${obj_path}" /c "${src_path}" + objs+=("$(native_path "${obj}")") + done + + MSYS2_ARG_CONV_EXCL='*' lib /nologo /OUT:"${lib_path_native}" "${objs[@]}" + make_package_dir "${package_dir}" "${arch}" "msvc" "${lib_path}" "funnelcake.lib" + make_zip "${package_dir}" +} + +mkdir -p "${DIST_DIR}" "${BUILD_DIR}" + +if [ "${build_mingw}" -eq 1 ]; then + build_windows_mingw "x86_64" "x86_64-w64-mingw32-gcc" "x86_64-w64-mingw32-ar" + build_windows_mingw "aarch64" "aarch64-w64-mingw32-gcc" "aarch64-w64-mingw32-ar" +fi + +if [ "${build_msvc}" -eq 1 ]; then + build_windows_msvc +fi + +( + cd "${REPO_ROOT}" + make clean +) + +if [ "${#ARTIFACTS[@]}" -eq 0 ]; then + echo "" + echo "No Windows artifacts were built." + echo "Install MinGW-w64, or run --msvc from a Visual Studio developer shell." + exit 1 +fi + +echo "" +echo "Artifacts written to ${DIST_DIR}:" +for artifact in "${ARTIFACTS[@]}"; do + echo " ${artifact}" +done diff --git a/src/funnelcake.c b/src/funnelcake.c index de8fa25..217a3a5 100644 --- a/src/funnelcake.c +++ b/src/funnelcake.c @@ -332,11 +332,11 @@ int fused_scaler_init(fused_scaler_ctx_t *ctx) int chroma_h = out_h / 2; void *py = NULL, *pu = NULL, *pv = NULL; - if (posix_memalign(&py, 32, (size_t)y_stride * (size_t)out_h) != 0 || - posix_memalign(&pu, 32, (size_t)uv_stride * (size_t)chroma_h) != 0 || - posix_memalign(&pv, 32, (size_t)uv_stride * (size_t)chroma_h) != 0) { + if (fused_aligned_alloc(&py, 32, (size_t)y_stride * (size_t)out_h) != 0 || + fused_aligned_alloc(&pu, 32, (size_t)uv_stride * (size_t)chroma_h) != 0 || + fused_aligned_alloc(&pv, 32, (size_t)uv_stride * (size_t)chroma_h) != 0) { /* Allocation failure - free what we got and reject this step */ - free(py); free(pu); free(pv); + fused_aligned_free(py); fused_aligned_free(pu); fused_aligned_free(pv); fused_log(&ctx->log_warnings, FUSED_LOG_WARN, "funnelcake: %s rejected: out-of-memory allocating output planes\n", sd->name); @@ -397,10 +397,10 @@ int fused_scaler_init(fused_scaler_ctx_t *ctx) int chroma_h = up_h / 2; void *py = NULL, *pu = NULL, *pv = NULL; - if (posix_memalign(&py, 32, (size_t)y_stride * (size_t)up_h) != 0 || - posix_memalign(&pu, 32, (size_t)uv_stride * (size_t)chroma_h) != 0 || - posix_memalign(&pv, 32, (size_t)uv_stride * (size_t)chroma_h) != 0) { - free(py); free(pu); free(pv); + if (fused_aligned_alloc(&py, 32, (size_t)y_stride * (size_t)up_h) != 0 || + fused_aligned_alloc(&pu, 32, (size_t)uv_stride * (size_t)chroma_h) != 0 || + fused_aligned_alloc(&pv, 32, (size_t)uv_stride * (size_t)chroma_h) != 0) { + fused_aligned_free(py); fused_aligned_free(pu); fused_aligned_free(pv); fused_log(&ctx->log_warnings, FUSED_LOG_WARN, "funnelcake: upscale level %dx rejected: out-of-memory\n", (1 << (k + 1))); @@ -469,10 +469,10 @@ int fused_scaler_init(fused_scaler_ctx_t *ctx) int chroma_h = tail_h / 2; void *py = NULL, *pu = NULL, *pv = NULL; - if (posix_memalign(&py, 32, (size_t)y_stride * (size_t)tail_h) != 0 || - posix_memalign(&pu, 32, (size_t)uv_stride * (size_t)chroma_h) != 0 || - posix_memalign(&pv, 32, (size_t)uv_stride * (size_t)chroma_h) != 0) { - free(py); free(pu); free(pv); + if (fused_aligned_alloc(&py, 32, (size_t)y_stride * (size_t)tail_h) != 0 || + fused_aligned_alloc(&pu, 32, (size_t)uv_stride * (size_t)chroma_h) != 0 || + fused_aligned_alloc(&pv, 32, (size_t)uv_stride * (size_t)chroma_h) != 0) { + fused_aligned_free(py); fused_aligned_free(pu); fused_aligned_free(pv); fused_log(&ctx->log_warnings, FUSED_LOG_WARN, "funnelcake: upscale 1.5x tail rejected: out-of-memory\n"); warn_bits |= FUSED_WARN_BIT_PARTIAL; @@ -602,7 +602,7 @@ int fused_scaler_init(fused_scaler_ctx_t *ctx) if (max_scratch_w > 0) { size_t bytes = (size_t)((max_scratch_w + 63) & ~63); void *sp = NULL; - if (posix_memalign(&sp, 64, bytes) == 0) { + if (fused_aligned_alloc(&sp, 64, bytes) == 0) { p->upscale_scratch = (uint8_t *)sp; } else { fused_log(&ctx->log_warnings, FUSED_LOG_WARN, @@ -646,13 +646,13 @@ int fused_scaler_init(fused_scaler_ctx_t *ctx) if (pool_bytes > 0) { void *sp = NULL; size_t aligned_bytes = (pool_bytes + 63) & ~(size_t)63; - if (posix_memalign(&sp, 64, aligned_bytes) == 0) { + if (fused_aligned_alloc(&sp, 64, aligned_bytes) == 0) { p->scratch_pool = (uint8_t *)sp; p->scratch_pool_size = aligned_bytes; } else { fused_log(&ctx->log_warnings, FUSED_LOG_WARN, "funnelcake: failed to allocate downscale scratch pool " - "(%zu bytes)\n", aligned_bytes); + "(%llu bytes)\n", (unsigned long long)aligned_bytes); } } } @@ -744,22 +744,22 @@ void fused_scaler_free(fused_scaler_ctx_t *ctx) { if (!ctx) return; for (int i = 0; i < 8; i++) { - free(ctx->outputs[i].plane_y); - free(ctx->outputs[i].plane_u); - free(ctx->outputs[i].plane_v); + fused_aligned_free(ctx->outputs[i].plane_y); + fused_aligned_free(ctx->outputs[i].plane_u); + fused_aligned_free(ctx->outputs[i].plane_v); memset(&ctx->outputs[i], 0, sizeof(fused_scale_output_t)); } for (int i = 0; i < FUSED_MAX_UPSCALE_STEPS; i++) { - free(ctx->upscale_outputs[i].plane_y); - free(ctx->upscale_outputs[i].plane_u); - free(ctx->upscale_outputs[i].plane_v); + fused_aligned_free(ctx->upscale_outputs[i].plane_y); + fused_aligned_free(ctx->upscale_outputs[i].plane_u); + fused_aligned_free(ctx->upscale_outputs[i].plane_v); memset(&ctx->upscale_outputs[i], 0, sizeof(fused_scale_output_t)); } if (ctx->_internal) { fused_internal_t *state = (fused_internal_t *)ctx->_internal; - free(state->params.upscale_scratch); + fused_aligned_free(state->params.upscale_scratch); state->params.upscale_scratch = NULL; - free(state->params.scratch_pool); + fused_aligned_free(state->params.scratch_pool); state->params.scratch_pool = NULL; state->params.scratch_pool_size = 0; } diff --git a/src/funnelcake_hdr.c b/src/funnelcake_hdr.c index e5827a5..97d39b1 100644 --- a/src/funnelcake_hdr.c +++ b/src/funnelcake_hdr.c @@ -391,10 +391,10 @@ int fused_hdr_init(fused_hdr_ctx_t *ctx) int uv_stride = stride_for_hdr(chroma_w); void *py = NULL, *pu = NULL, *pv = NULL; - if (posix_memalign(&py, 32, (size_t)y_stride * (size_t)out_h) != 0 || - posix_memalign(&pu, 32, (size_t)uv_stride * (size_t)chroma_h) != 0 || - posix_memalign(&pv, 32, (size_t)uv_stride * (size_t)chroma_h) != 0) { - free(py); free(pu); free(pv); + if (fused_aligned_alloc(&py, 32, (size_t)y_stride * (size_t)out_h) != 0 || + fused_aligned_alloc(&pu, 32, (size_t)uv_stride * (size_t)chroma_h) != 0 || + fused_aligned_alloc(&pv, 32, (size_t)uv_stride * (size_t)chroma_h) != 0) { + fused_aligned_free(py); fused_aligned_free(pu); fused_aligned_free(pv); fused_log(&ctx->log_warnings, FUSED_LOG_WARN, "funnelcake-hdr: %s rejected: out-of-memory allocating HDR output planes\n", sd->name); @@ -420,18 +420,18 @@ int fused_hdr_init(fused_hdr_ctx_t *ctx) int uv_stride = stride_for(chroma_w); void *py = NULL, *pu = NULL, *pv = NULL; - if (posix_memalign(&py, 32, (size_t)y_stride * (size_t)out_h) != 0 || - posix_memalign(&pu, 32, (size_t)uv_stride * (size_t)chroma_h) != 0 || - posix_memalign(&pv, 32, (size_t)uv_stride * (size_t)chroma_h) != 0) { - free(py); free(pu); free(pv); + if (fused_aligned_alloc(&py, 32, (size_t)y_stride * (size_t)out_h) != 0 || + fused_aligned_alloc(&pu, 32, (size_t)uv_stride * (size_t)chroma_h) != 0 || + fused_aligned_alloc(&pv, 32, (size_t)uv_stride * (size_t)chroma_h) != 0) { + fused_aligned_free(py); fused_aligned_free(pu); fused_aligned_free(pv); fused_log(&ctx->log_warnings, FUSED_LOG_WARN, "funnelcake-hdr: %s rejected: out-of-memory allocating SDR output planes\n", sd->name); /* If we already allocated HDR planes for this step, free them */ if (step_wants_hdr && (achieved_hdr & sd->flag)) { - free(ctx->hdr_outputs[i].plane_y); - free(ctx->hdr_outputs[i].plane_u); - free(ctx->hdr_outputs[i].plane_v); + fused_aligned_free(ctx->hdr_outputs[i].plane_y); + fused_aligned_free(ctx->hdr_outputs[i].plane_u); + fused_aligned_free(ctx->hdr_outputs[i].plane_v); memset(&ctx->hdr_outputs[i], 0, sizeof(fused_hdr_output_t)); achieved_hdr &= ~sd->flag; } @@ -457,14 +457,14 @@ int fused_hdr_init(fused_hdr_ctx_t *ctx) int hdr_uv_stride = stride_for_hdr(chroma_w); void *ty = NULL, *tu = NULL, *tv = NULL; - if (posix_memalign(&ty, 32, (size_t)hdr_y_stride * (size_t)out_h) != 0 || - posix_memalign(&tu, 32, (size_t)hdr_uv_stride * (size_t)chroma_h) != 0 || - posix_memalign(&tv, 32, (size_t)hdr_uv_stride * (size_t)chroma_h) != 0) { - free(ty); free(tu); free(tv); + if (fused_aligned_alloc(&ty, 32, (size_t)hdr_y_stride * (size_t)out_h) != 0 || + fused_aligned_alloc(&tu, 32, (size_t)hdr_uv_stride * (size_t)chroma_h) != 0 || + fused_aligned_alloc(&tv, 32, (size_t)hdr_uv_stride * (size_t)chroma_h) != 0) { + fused_aligned_free(ty); fused_aligned_free(tu); fused_aligned_free(tv); /* Roll back SDR allocation for this step */ - free(ctx->sdr_outputs[i].plane_y); - free(ctx->sdr_outputs[i].plane_u); - free(ctx->sdr_outputs[i].plane_v); + fused_aligned_free(ctx->sdr_outputs[i].plane_y); + fused_aligned_free(ctx->sdr_outputs[i].plane_u); + fused_aligned_free(ctx->sdr_outputs[i].plane_v); memset(&ctx->sdr_outputs[i], 0, sizeof(fused_scale_output_t)); achieved_sdr &= ~sd->flag; fused_log(&ctx->log_warnings, FUSED_LOG_WARN, @@ -517,10 +517,10 @@ int fused_hdr_init(fused_hdr_ctx_t *ctx) int chroma_h = up_h / 2; void *py = NULL, *pu = NULL, *pv = NULL; - if (posix_memalign(&py, 32, (size_t)y_stride * (size_t)up_h) != 0 || - posix_memalign(&pu, 32, (size_t)uv_stride * (size_t)chroma_h) != 0 || - posix_memalign(&pv, 32, (size_t)uv_stride * (size_t)chroma_h) != 0) { - free(py); free(pu); free(pv); + if (fused_aligned_alloc(&py, 32, (size_t)y_stride * (size_t)up_h) != 0 || + fused_aligned_alloc(&pu, 32, (size_t)uv_stride * (size_t)chroma_h) != 0 || + fused_aligned_alloc(&pv, 32, (size_t)uv_stride * (size_t)chroma_h) != 0) { + fused_aligned_free(py); fused_aligned_free(pu); fused_aligned_free(pv); fused_log(&ctx->log_warnings, FUSED_LOG_WARN, "funnelcake-hdr: upscale level %dx rejected: out-of-memory\n", (1 << (k + 1))); @@ -584,10 +584,10 @@ int fused_hdr_init(fused_hdr_ctx_t *ctx) int chroma_h = tail_h / 2; void *py = NULL, *pu = NULL, *pv = NULL; - if (posix_memalign(&py, 32, (size_t)y_stride * (size_t)tail_h) != 0 || - posix_memalign(&pu, 32, (size_t)uv_stride * (size_t)chroma_h) != 0 || - posix_memalign(&pv, 32, (size_t)uv_stride * (size_t)chroma_h) != 0) { - free(py); free(pu); free(pv); + if (fused_aligned_alloc(&py, 32, (size_t)y_stride * (size_t)tail_h) != 0 || + fused_aligned_alloc(&pu, 32, (size_t)uv_stride * (size_t)chroma_h) != 0 || + fused_aligned_alloc(&pv, 32, (size_t)uv_stride * (size_t)chroma_h) != 0) { + fused_aligned_free(py); fused_aligned_free(pu); fused_aligned_free(pv); fused_log(&ctx->log_warnings, FUSED_LOG_WARN, "funnelcake-hdr: upscale 1.5x tail rejected: out-of-memory\n"); warn_bits |= FUSED_WARN_BIT_PARTIAL; @@ -645,10 +645,10 @@ int fused_hdr_init(fused_hdr_ctx_t *ctx) int chroma_h = eff_h / 2; void *py = NULL, *pu = NULL, *pv = NULL; - if (posix_memalign(&py, 32, (size_t)y_stride * (size_t)eff_h) != 0 || - posix_memalign(&pu, 32, (size_t)uv_stride * (size_t)chroma_h) != 0 || - posix_memalign(&pv, 32, (size_t)uv_stride * (size_t)chroma_h) != 0) { - free(py); free(pu); free(pv); + if (fused_aligned_alloc(&py, 32, (size_t)y_stride * (size_t)eff_h) != 0 || + fused_aligned_alloc(&pu, 32, (size_t)uv_stride * (size_t)chroma_h) != 0 || + fused_aligned_alloc(&pv, 32, (size_t)uv_stride * (size_t)chroma_h) != 0) { + fused_aligned_free(py); fused_aligned_free(pu); fused_aligned_free(pv); fused_hdr_free(ctx); free(state); ctx->_internal = NULL; @@ -723,10 +723,10 @@ int fused_hdr_init(fused_hdr_ctx_t *ctx) int tmp_stride = stride_for_hdr(chroma_w); /* 32-byte aligned */ size_t tmp_bytes = (size_t)tmp_stride * (size_t)chroma_h; - if (posix_memalign((void **)&p->p010_tmp_u, 32, tmp_bytes) != 0 || - posix_memalign((void **)&p->p010_tmp_v, 32, tmp_bytes) != 0) { - free(p->p010_tmp_u); - free(p->p010_tmp_v); + if (fused_aligned_alloc((void **)&p->p010_tmp_u, 32, tmp_bytes) != 0 || + fused_aligned_alloc((void **)&p->p010_tmp_v, 32, tmp_bytes) != 0) { + fused_aligned_free(p->p010_tmp_u); + fused_aligned_free(p->p010_tmp_v); p->p010_tmp_u = NULL; p->p010_tmp_v = NULL; fused_log(&ctx->log_warnings, FUSED_LOG_WARN, @@ -823,7 +823,7 @@ int fused_hdr_init(fused_hdr_ctx_t *ctx) if (max_scratch_w > 0) { size_t bytes = (size_t)((max_scratch_w + 63) & ~63) * sizeof(uint16_t); void *sp = NULL; - if (posix_memalign(&sp, 64, bytes) == 0) { + if (fused_aligned_alloc(&sp, 64, bytes) == 0) { p->upscale_scratch_hdr = (uint16_t *)sp; } else { fused_log(&ctx->log_warnings, FUSED_LOG_WARN, @@ -856,7 +856,7 @@ int fused_hdr_init(fused_hdr_ctx_t *ctx) if (pool_bytes > 0) { void *sp = NULL; size_t aligned_bytes = (pool_bytes + 63) & ~(size_t)63; - if (posix_memalign(&sp, 64, aligned_bytes) == 0) { + if (fused_aligned_alloc(&sp, 64, aligned_bytes) == 0) { p->scratch_pool = (uint8_t *)sp; p->scratch_pool_size = aligned_bytes; } else { @@ -1027,17 +1027,17 @@ void fused_hdr_free(fused_hdr_ctx_t *ctx) /* Free HDR output planes */ for (int i = 0; i < 8; i++) { - free(ctx->hdr_outputs[i].plane_y); - free(ctx->hdr_outputs[i].plane_u); - free(ctx->hdr_outputs[i].plane_v); + fused_aligned_free(ctx->hdr_outputs[i].plane_y); + fused_aligned_free(ctx->hdr_outputs[i].plane_u); + fused_aligned_free(ctx->hdr_outputs[i].plane_v); memset(&ctx->hdr_outputs[i], 0, sizeof(fused_hdr_output_t)); } /* Free SDR output planes */ for (int i = 0; i < 8; i++) { - free(ctx->sdr_outputs[i].plane_y); - free(ctx->sdr_outputs[i].plane_u); - free(ctx->sdr_outputs[i].plane_v); + fused_aligned_free(ctx->sdr_outputs[i].plane_y); + fused_aligned_free(ctx->sdr_outputs[i].plane_u); + fused_aligned_free(ctx->sdr_outputs[i].plane_v); memset(&ctx->sdr_outputs[i], 0, sizeof(fused_scale_output_t)); } @@ -1045,28 +1045,28 @@ void fused_hdr_free(fused_hdr_ctx_t *ctx) fused_hdr_internal_t *state = (fused_hdr_internal_t *)ctx->_internal; if (state) { for (int i = 0; i < 8; i++) { - free(state->sdr_temp[i].y); - free(state->sdr_temp[i].u); - free(state->sdr_temp[i].v); + fused_aligned_free(state->sdr_temp[i].y); + fused_aligned_free(state->sdr_temp[i].u); + fused_aligned_free(state->sdr_temp[i].v); } - free(state->params.p010_tmp_u); - free(state->params.p010_tmp_v); - free(state->params.upscale_scratch_hdr); - free(state->params.scratch_pool); + fused_aligned_free(state->params.p010_tmp_u); + fused_aligned_free(state->params.p010_tmp_v); + fused_aligned_free(state->params.upscale_scratch_hdr); + fused_aligned_free(state->params.scratch_pool); free(state); } /* Free 1:1 tonemap output planes */ - free(ctx->output_1x.plane_y); - free(ctx->output_1x.plane_u); - free(ctx->output_1x.plane_v); + fused_aligned_free(ctx->output_1x.plane_y); + fused_aligned_free(ctx->output_1x.plane_u); + fused_aligned_free(ctx->output_1x.plane_v); memset(&ctx->output_1x, 0, sizeof(fused_scale_output_t)); /* Free upscale HDR output planes */ for (int i = 0; i < FUSED_MAX_UPSCALE_STEPS; i++) { - free(ctx->upscale_hdr_outputs[i].plane_y); - free(ctx->upscale_hdr_outputs[i].plane_u); - free(ctx->upscale_hdr_outputs[i].plane_v); + fused_aligned_free(ctx->upscale_hdr_outputs[i].plane_y); + fused_aligned_free(ctx->upscale_hdr_outputs[i].plane_u); + fused_aligned_free(ctx->upscale_hdr_outputs[i].plane_v); memset(&ctx->upscale_hdr_outputs[i], 0, sizeof(fused_hdr_output_t)); } diff --git a/src/internal.h b/src/internal.h index eaf8a3a..3506c90 100644 --- a/src/internal.h +++ b/src/internal.h @@ -5,6 +5,31 @@ #include "funnelcake.h" #include +#include +#if defined(_WIN32) +#include +#endif + +static inline int fused_aligned_alloc(void **ptr, size_t alignment, size_t size) +{ +#if defined(_WIN32) + void *p = _aligned_malloc(size ? size : alignment, alignment); + if (!p) return -1; + *ptr = p; + return 0; +#else + return posix_memalign(ptr, alignment, size); +#endif +} + +static inline void fused_aligned_free(void *ptr) +{ +#if defined(_WIN32) + _aligned_free(ptr); +#else + free(ptr); +#endif +} /* -------------------------------------------------------------------------- * Constants diff --git a/src/log.h b/src/log.h index a7599f1..017a054 100644 --- a/src/log.h +++ b/src/log.h @@ -19,6 +19,9 @@ * call config->callback(level, buf, ctx) */ void fused_log(const fused_log_config_t *config, int level, const char *fmt, ...) - __attribute__((format(printf, 3, 4))); +#if defined(__GNUC__) || defined(__clang__) + __attribute__((format(printf, 3, 4))) +#endif + ; #endif /* FUNNELCAKE_LOG_H */ diff --git a/src/tonemap.c b/src/tonemap.c index b54d59d..1bfbf9a 100644 --- a/src/tonemap.c +++ b/src/tonemap.c @@ -15,6 +15,12 @@ #include #endif +#if defined(__GNUC__) || defined(__clang__) +#define FUSED_HOT __attribute__((hot)) +#else +#define FUSED_HOT +#endif + /* -------------------------------------------------------------------------- * ST 2084 (PQ) EOTF constants @@ -568,7 +574,7 @@ static inline void tonemap_pixel_rgb( } -__attribute__((hot)) +FUSED_HOT void fused_tonemap_apply( const fused_hdr_internal_t *state, const uint16_t *src_y, int src_y_stride, @@ -698,7 +704,7 @@ void fused_tonemap_apply( * fused_tonemap_apply_p010 - interleaved P010 chroma * -------------------------------------------------------------------------- */ -__attribute__((hot)) +FUSED_HOT void fused_tonemap_apply_p010( const fused_hdr_internal_t *state, const uint16_t *src_y, int src_y_stride, From a25965aa022f71f0bba44d5f5a810cc68003a7be Mon Sep 17 00:00:00 2001 From: Paul Gregoire Date: Fri, 24 Apr 2026 13:52:02 -0700 Subject: [PATCH 2/7] Add PowerShell equivalent of build-windows.sh Mirrors the bash script's flag surface (-All/-Mingw/-Msvc) and target handling so the Windows MSVC artifact can be built from a native Developer PowerShell without needing MSYS2/Git Bash. MinGW path shells out to make (or mingw32-make); MSVC path invokes cl/lib directly over the same 8 sources, then packages with Compress-Archive + Get-FileHash to produce the same zip + .sha256 + BUILD_INFO layout. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/build-windows.ps1 | 220 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 scripts/build-windows.ps1 diff --git a/scripts/build-windows.ps1 b/scripts/build-windows.ps1 new file mode 100644 index 0000000..99404c5 --- /dev/null +++ b/scripts/build-windows.ps1 @@ -0,0 +1,220 @@ +#Requires -Version 5.1 +[CmdletBinding()] +param( + [switch]$All, + [switch]$Mingw, + [switch]$Msvc, + [switch]$Help +) + +$ErrorActionPreference = 'Stop' + +$ScriptDir = $PSScriptRoot +$RepoRoot = (Resolve-Path (Join-Path $ScriptDir '..')).Path +$DistDir = Join-Path $RepoRoot 'dist' +$BuildDir = Join-Path $RepoRoot 'build\windows' +$BuildDate = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ') +$Artifacts = New-Object System.Collections.Generic.List[string] + +function Show-Usage { +@' +Usage: scripts\build-windows.ps1 [-All] [-Mingw] [-Msvc] + +Build Windows release archives: + -All build every supported Windows target available on this host (default) + -Mingw build MinGW-w64 static library packages + -Msvc build an MSVC static library package from a Visual Studio shell + +MinGW targets require cross compilers such as x86_64-w64-mingw32-gcc. +The MSVC target requires cl.exe and lib.exe in PATH, usually by running from +"x64 Native Tools Command Prompt for VS" or an equivalent Developer PowerShell. +'@ +} + +if ($Help) { Show-Usage; return } + +# default: build everything when no flag is given +if (-not $Mingw -and -not $Msvc -and -not $All) { $Mingw = $true; $Msvc = $true } +if ($All) { $Mingw = $true; $Msvc = $true } + +function Test-Tool { + param([string]$Name) + [bool](Get-Command $Name -ErrorAction SilentlyContinue) +} + +function Resolve-MakeTool { + if (Test-Tool 'make') { return 'make' } + if (Test-Tool 'mingw32-make') { return 'mingw32-make' } + return $null +} + +function Write-AsciiFile { + param([string]$Path, [string]$Content) + $enc = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($Path, $Content, $enc) +} + +function Get-Sha256Line { + param([string]$Path) + $hash = (Get-FileHash -Algorithm SHA256 -Path $Path).Hash.ToLower() + $name = Split-Path -Leaf $Path + return "$hash $name`n" +} + +function New-PackageDir { + param( + [string]$PackageDir, + [string]$TargetArch, + [string]$Compiler, + [string]$LibraryPath, + [string]$LibraryName + ) + $dest = Join-Path $DistDir $PackageDir + if (Test-Path $dest) { Remove-Item -Recurse -Force $dest } + New-Item -ItemType Directory -Path (Join-Path $dest 'include') -Force | Out-Null + Copy-Item $LibraryPath (Join-Path $dest $LibraryName) + Copy-Item (Join-Path $RepoRoot 'include\funnelcake.h') (Join-Path $dest 'include') + Copy-Item (Join-Path $RepoRoot 'README.md') $dest + Copy-Item (Join-Path $RepoRoot 'INSTALL.md') $dest + + $info = @( + 'name=funnelcake' + 'target_os=windows' + "base_distribution=$Compiler" + "target_arch=$TargetArch" + "compiler=$Compiler" + 'lto=0' + "build_date=$BuildDate" + ) -join "`n" + Write-AsciiFile -Path (Join-Path $dest 'BUILD_INFO') -Content ($info + "`n") +} + +function New-Zip { + param([string]$PackageDir) + $zip = Join-Path $DistDir "$PackageDir.zip" + $sha = "$zip.sha256" + $source = Join-Path $DistDir $PackageDir + if (Test-Path $zip) { Remove-Item -Force $zip } + if (Test-Path $sha) { Remove-Item -Force $sha } + Compress-Archive -Path $source -DestinationPath $zip -Force + Write-AsciiFile -Path $sha -Content (Get-Sha256Line $zip) + $Artifacts.Add("$PackageDir.zip") | Out-Null + $Artifacts.Add("$PackageDir.zip.sha256") | Out-Null +} + +function Invoke-MingwBuild { + param([string]$Arch, [string]$CC, [string]$ARTool) + $package = "funnelcake-windows-mingw-$Arch" + $makeTool = Resolve-MakeTool + + if (-not (Test-Tool $CC) -or -not (Test-Tool $ARTool) -or -not $makeTool) { + Write-Host "==> Skipping MinGW ${Arch}: $CC, $ARTool, and make are required" + return + } + + Write-Host "==> Building Windows MinGW $Arch artifact" + Push-Location $RepoRoot + try { + & $makeTool clean + if ($LASTEXITCODE -ne 0) { throw "$makeTool clean failed" } + & $makeTool lib "CC=$CC" "AR=$ARTool" 'LTO=0' 'UNAME_S=Windows_NT' "UNAME_M=$Arch" + if ($LASTEXITCODE -ne 0) { throw "$makeTool lib failed" } + } + finally { Pop-Location } + + New-PackageDir -PackageDir $package -TargetArch $Arch -Compiler 'mingw-w64' ` + -LibraryPath (Join-Path $RepoRoot 'libfunnelcake.a') -LibraryName 'libfunnelcake.a' + New-Zip -PackageDir $package +} + +function Get-MsvcArchName { + $val = $env:VSCMD_ARG_TGT_ARCH + if (-not $val) { $val = $env:PROCESSOR_ARCHITECTURE } + switch -Regex ($val) { + '^(x64|AMD64|amd64)$' { return 'x64' } + '^(arm64|ARM64)$' { return 'arm64' } + default { return 'unknown' } + } +} + +function Invoke-MsvcBuild { + $commonSources = @( + 'src\funnelcake.c', + 'src\funnelcake_hdr.c', + 'src\log.c', + 'src\detect.c', + 'src\kernels_scalar.c', + 'src\kernels_hdr_scalar.c', + 'src\tonemap.c', + 'src\kernels_upscale_scalar.c' + ) + + if (-not (Test-Tool 'cl') -or -not (Test-Tool 'lib')) { + Write-Host '==> Skipping MSVC: cl.exe and lib.exe are required' + return + } + + $arch = Get-MsvcArchName + if ($arch -eq 'unknown') { + Write-Host '==> Skipping MSVC: unable to determine Visual Studio target architecture' + return + } + + $package = "funnelcake-windows-msvc-$arch" + $objDir = Join-Path $BuildDir "msvc-$arch" + $libPath = Join-Path $objDir 'funnelcake.lib' + $includeDir = Join-Path $RepoRoot 'include' + $srcDir = Join-Path $RepoRoot 'src' + + Write-Host "==> Building Windows MSVC $arch artifact" + if (Test-Path $objDir) { Remove-Item -Recurse -Force $objDir } + New-Item -ItemType Directory -Path $objDir -Force | Out-Null + + $objs = @() + foreach ($rel in $commonSources) { + $srcPath = Join-Path $RepoRoot $rel + $objPath = Join-Path $objDir ([IO.Path]::GetFileNameWithoutExtension($rel) + '.obj') + & cl /nologo /std:c11 /O2 /W4 /WX ` + /D_CRT_SECURE_NO_WARNINGS /D_POSIX_C_SOURCE=200112L ` + "/I$includeDir" "/I$srcDir" ` + "/Fo$objPath" /c $srcPath + if ($LASTEXITCODE -ne 0) { throw "cl failed for $rel" } + $objs += $objPath + } + + & lib /nologo "/OUT:$libPath" $objs + if ($LASTEXITCODE -ne 0) { throw 'lib failed' } + + New-PackageDir -PackageDir $package -TargetArch $arch -Compiler 'msvc' ` + -LibraryPath $libPath -LibraryName 'funnelcake.lib' + New-Zip -PackageDir $package +} + +New-Item -ItemType Directory -Path $DistDir -Force | Out-Null +New-Item -ItemType Directory -Path $BuildDir -Force | Out-Null + +if ($Mingw) { + Invoke-MingwBuild -Arch 'x86_64' -CC 'x86_64-w64-mingw32-gcc' -ARTool 'x86_64-w64-mingw32-ar' + Invoke-MingwBuild -Arch 'aarch64' -CC 'aarch64-w64-mingw32-gcc' -ARTool 'aarch64-w64-mingw32-ar' +} + +if ($Msvc) { + Invoke-MsvcBuild +} + +$finalMake = Resolve-MakeTool +if ($finalMake) { + Push-Location $RepoRoot + try { & $finalMake clean | Out-Null } finally { Pop-Location } +} + +if ($Artifacts.Count -eq 0) { + Write-Host '' + Write-Host 'No Windows artifacts were built.' + Write-Host 'Install MinGW-w64, or run -Msvc from a Visual Studio developer shell.' + exit 1 +} + +Write-Host '' +Write-Host "Artifacts written to ${DistDir}:" +foreach ($a in $Artifacts) { Write-Host " $a" } From f2b832ebbe1d5baa222d9bd1a5bfbbd3d7c6df36 Mon Sep 17 00:00:00 2001 From: Paul Gregoire Date: Mon, 27 Apr 2026 17:33:10 -0700 Subject: [PATCH 3/7] Add Windows ARM64 build script and enable NEON under MSVC Adds scripts/build-windows-arm64.ps1, a focused PowerShell driver that builds Windows-on-ARM64 release artifacts via either MinGW (cross compile through aarch64-w64-mingw32-gcc) or MSVC (native ARM64 from a VS ARM64 developer shell). Mirrors the package layout of build-windows.ps1. The MSVC path used to ship scalar-only because the NEON kernels and arch detection were gated on __aarch64__, which MSVC does not define. Widen the guards to (__aarch64__ || _M_ARM64) across internal.h, detect.c, the SDR and HDR dispatchers, the three NEON kernel files, and tonemap.c. detect.c gains a Windows branch alongside __APPLE__ that just sets has_neon = 1 since NEON is architecturally mandatory on Windows on ARM. The NEON kernels also relied on GCC-only constructs that MSVC rejects. Add FUSED_HOT and FUSED_PREFETCH portability macros in internal.h with MSVC ARM64 fallbacks (FUSED_PREFETCH uses __prefetch from ); replace per-variable __attribute__((aligned(N))) with the C11 _Alignas(N) keyword; drop the now-redundant local FUSED_HOT in tonemap.c. INSTALL.md documents the new script and the build-windows.ps1 variants alongside the existing bash flow. Co-Authored-By: Claude Opus 4.7 (1M context) --- INSTALL.md | 26 +++- scripts/build-windows-arm64.ps1 | 222 ++++++++++++++++++++++++++++++++ src/detect.c | 17 +-- src/funnelcake.c | 2 +- src/funnelcake_hdr.c | 2 +- src/internal.h | 27 +++- src/kernels_hdr_neon.c | 30 ++--- src/kernels_neon.c | 12 +- src/kernels_upscale_neon.c | 2 +- src/tonemap.c | 16 +-- 10 files changed, 306 insertions(+), 50 deletions(-) create mode 100644 scripts/build-windows-arm64.ps1 diff --git a/INSTALL.md b/INSTALL.md index 4f16a1e..3c86a80 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -140,7 +140,8 @@ writes `dist/funnelcake-macos-.tar.gz` containing `libfunnelcake.a`, To build Windows release archives: - ./scripts/build-windows.sh + ./scripts/build-windows.sh # bash / MSYS2 / Git Bash + scripts\build-windows.ps1 # native PowerShell 5.1+ By default the script builds every Windows target whose toolchain is available. MinGW-w64 artifacts contain `libfunnelcake.a`; MSVC artifacts contain @@ -150,6 +151,7 @@ README, install notes, and `BUILD_INFO`. For MinGW-w64 only: ./scripts/build-windows.sh --mingw + scripts\build-windows.ps1 -Mingw For `x86_64` MinGW, install tools that provide `x86_64-w64-mingw32-gcc` and `x86_64-w64-mingw32-ar`. For Windows on ARM64 MinGW, install tools that provide @@ -159,7 +161,23 @@ For MSVC only, run from a Visual Studio developer shell where `cl.exe` and `lib.exe` are in `PATH`: ./scripts/build-windows.sh --msvc + scripts\build-windows.ps1 -Msvc -The MSVC package currently builds the portable scalar static library. The -MinGW packages use the normal Makefile source selection, including AVX2 on -`x86_64` and NEON on `aarch64`. +Both MinGW and MSVC builds use the normal source selection (AVX2 on `x86_64`, +NEON on `aarch64`/ARM64). The NEON kernels guard on `__aarch64__ || _M_ARM64`, +so MSVC ARM64 picks up the same SIMD coverage as the MinGW cross-compile. + +### Windows ARM64 only + +For a Windows-on-ARM64 build without touching the x86_64 paths, use the +dedicated PowerShell driver: + + scripts\build-windows-arm64.ps1 # MinGW + MSVC, whichever is available + scripts\build-windows-arm64.ps1 -Mingw # cross-compile via aarch64-w64-mingw32-gcc + scripts\build-windows-arm64.ps1 -Msvc # native ARM64 MSVC + +The `-Msvc` path requires an "ARM64 Native Tools Command Prompt for VS" or +an equivalent Developer PowerShell with `VSCMD_ARG_TGT_ARCH=arm64`; the +script refuses to run if the shell is not configured for ARM64. Both +artifact layouts mirror `build-windows.ps1`: a per-toolchain `dist/` +package plus a `.zip.sha256`. diff --git a/scripts/build-windows-arm64.ps1 b/scripts/build-windows-arm64.ps1 new file mode 100644 index 0000000..3822b77 --- /dev/null +++ b/scripts/build-windows-arm64.ps1 @@ -0,0 +1,222 @@ +#Requires -Version 5.1 +[CmdletBinding()] +param( + [switch]$All, + [switch]$Mingw, + [switch]$Msvc, + [switch]$Help +) + +$ErrorActionPreference = 'Stop' + +$ScriptDir = $PSScriptRoot +$RepoRoot = (Resolve-Path (Join-Path $ScriptDir '..')).Path +$DistDir = Join-Path $RepoRoot 'dist' +$BuildDir = Join-Path $RepoRoot 'build\windows' +$BuildDate = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ') +$Artifacts = New-Object System.Collections.Generic.List[string] + +function Show-Usage { +@' +Usage: scripts\build-windows-arm64.ps1 [-All] [-Mingw] [-Msvc] + +Build Windows ARM64 (aarch64) release archives: + -All build every supported ARM64 target available on this host (default) + -Mingw build a MinGW-w64 aarch64 static library package (cross compile) + -Msvc build an MSVC ARM64 static library package from a Visual Studio + ARM64 developer shell + +The MinGW target requires the aarch64-w64-mingw32-gcc cross compiler. It +produces a NEON-enabled libfunnelcake.a (the GCC __aarch64__ macro selects +the NEON kernel sources). + +The MSVC target requires cl.exe and lib.exe configured for ARM64, usually by +running from "ARM64 Native Tools Command Prompt for VS" or an equivalent +Developer PowerShell with VSCMD_ARG_TGT_ARCH=arm64. The MSVC build includes +the NEON kernels (gated on _M_ARM64 in addition to __aarch64__), matching +the MinGW build's instruction-set coverage. +'@ +} + +if ($Help) { Show-Usage; return } + +# default: build everything when no flag is given +if (-not $Mingw -and -not $Msvc -and -not $All) { $Mingw = $true; $Msvc = $true } +if ($All) { $Mingw = $true; $Msvc = $true } + +function Test-Tool { + param([string]$Name) + [bool](Get-Command $Name -ErrorAction SilentlyContinue) +} + +function Resolve-MakeTool { + if (Test-Tool 'make') { return 'make' } + if (Test-Tool 'mingw32-make') { return 'mingw32-make' } + return $null +} + +function Write-AsciiFile { + param([string]$Path, [string]$Content) + $enc = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($Path, $Content, $enc) +} + +function Get-Sha256Line { + param([string]$Path) + $hash = (Get-FileHash -Algorithm SHA256 -Path $Path).Hash.ToLower() + $name = Split-Path -Leaf $Path + return "$hash $name`n" +} + +function New-PackageDir { + param( + [string]$PackageDir, + [string]$TargetArch, + [string]$Compiler, + [string]$LibraryPath, + [string]$LibraryName + ) + $dest = Join-Path $DistDir $PackageDir + if (Test-Path $dest) { Remove-Item -Recurse -Force $dest } + New-Item -ItemType Directory -Path (Join-Path $dest 'include') -Force | Out-Null + Copy-Item $LibraryPath (Join-Path $dest $LibraryName) + Copy-Item (Join-Path $RepoRoot 'include\funnelcake.h') (Join-Path $dest 'include') + Copy-Item (Join-Path $RepoRoot 'README.md') $dest + Copy-Item (Join-Path $RepoRoot 'INSTALL.md') $dest + + $info = @( + 'name=funnelcake' + 'target_os=windows' + "base_distribution=$Compiler" + "target_arch=$TargetArch" + "compiler=$Compiler" + 'lto=0' + "build_date=$BuildDate" + ) -join "`n" + Write-AsciiFile -Path (Join-Path $dest 'BUILD_INFO') -Content ($info + "`n") +} + +function New-Zip { + param([string]$PackageDir) + $zip = Join-Path $DistDir "$PackageDir.zip" + $sha = "$zip.sha256" + $source = Join-Path $DistDir $PackageDir + if (Test-Path $zip) { Remove-Item -Force $zip } + if (Test-Path $sha) { Remove-Item -Force $sha } + Compress-Archive -Path $source -DestinationPath $zip -Force + Write-AsciiFile -Path $sha -Content (Get-Sha256Line $zip) + $Artifacts.Add("$PackageDir.zip") | Out-Null + $Artifacts.Add("$PackageDir.zip.sha256") | Out-Null +} + +function Invoke-MingwArm64Build { + $arch = 'aarch64' + $cc = 'aarch64-w64-mingw32-gcc' + $arTool = 'aarch64-w64-mingw32-ar' + $package = "funnelcake-windows-mingw-$arch" + $makeTool = Resolve-MakeTool + + if (-not (Test-Tool $cc) -or -not (Test-Tool $arTool) -or -not $makeTool) { + Write-Host "==> Skipping MinGW ${arch}: $cc, $arTool, and make are required" + return + } + + Write-Host "==> Building Windows MinGW $arch artifact" + Push-Location $RepoRoot + try { + & $makeTool clean + if ($LASTEXITCODE -ne 0) { throw "$makeTool clean failed" } + & $makeTool lib "CC=$cc" "AR=$arTool" 'LTO=0' 'UNAME_S=Windows_NT' "UNAME_M=$arch" + if ($LASTEXITCODE -ne 0) { throw "$makeTool lib failed" } + } + finally { Pop-Location } + + New-PackageDir -PackageDir $package -TargetArch $arch -Compiler 'mingw-w64' ` + -LibraryPath (Join-Path $RepoRoot 'libfunnelcake.a') -LibraryName 'libfunnelcake.a' + New-Zip -PackageDir $package +} + +function Test-MsvcArm64Target { + $val = $env:VSCMD_ARG_TGT_ARCH + if (-not $val) { $val = $env:PROCESSOR_ARCHITECTURE } + return ($val -match '^(arm64|ARM64)$') +} + +function Invoke-MsvcArm64Build { + $commonSources = @( + 'src\funnelcake.c', + 'src\funnelcake_hdr.c', + 'src\log.c', + 'src\detect.c', + 'src\kernels_scalar.c', + 'src\kernels_hdr_scalar.c', + 'src\tonemap.c', + 'src\kernels_upscale_scalar.c', + 'src\kernels_neon.c', + 'src\kernels_hdr_neon.c', + 'src\kernels_upscale_neon.c' + ) + + if (-not (Test-Tool 'cl') -or -not (Test-Tool 'lib')) { + Write-Host '==> Skipping MSVC: cl.exe and lib.exe are required' + return + } + + if (-not (Test-MsvcArm64Target)) { + Write-Host '==> Skipping MSVC: this shell does not target ARM64 (set VSCMD_ARG_TGT_ARCH=arm64 or use the ARM64 Native Tools prompt)' + return + } + + $arch = 'arm64' + $package = "funnelcake-windows-msvc-$arch" + $objDir = Join-Path $BuildDir "msvc-$arch" + $libPath = Join-Path $objDir 'funnelcake.lib' + $includeDir = Join-Path $RepoRoot 'include' + $srcDir = Join-Path $RepoRoot 'src' + + Write-Host "==> Building Windows MSVC $arch artifact" + if (Test-Path $objDir) { Remove-Item -Recurse -Force $objDir } + New-Item -ItemType Directory -Path $objDir -Force | Out-Null + + $objs = @() + foreach ($rel in $commonSources) { + $srcPath = Join-Path $RepoRoot $rel + $objPath = Join-Path $objDir ([IO.Path]::GetFileNameWithoutExtension($rel) + '.obj') + & cl /nologo /std:c11 /O2 /W4 /WX ` + /D_CRT_SECURE_NO_WARNINGS /D_POSIX_C_SOURCE=200112L ` + "/I$includeDir" "/I$srcDir" ` + "/Fo$objPath" /c $srcPath + if ($LASTEXITCODE -ne 0) { throw "cl failed for $rel" } + $objs += $objPath + } + + & lib /nologo "/OUT:$libPath" $objs + if ($LASTEXITCODE -ne 0) { throw 'lib failed' } + + New-PackageDir -PackageDir $package -TargetArch $arch -Compiler 'msvc' ` + -LibraryPath $libPath -LibraryName 'funnelcake.lib' + New-Zip -PackageDir $package +} + +New-Item -ItemType Directory -Path $DistDir -Force | Out-Null +New-Item -ItemType Directory -Path $BuildDir -Force | Out-Null + +if ($Mingw) { Invoke-MingwArm64Build } +if ($Msvc) { Invoke-MsvcArm64Build } + +$finalMake = Resolve-MakeTool +if ($finalMake) { + Push-Location $RepoRoot + try { & $finalMake clean | Out-Null } finally { Pop-Location } +} + +if ($Artifacts.Count -eq 0) { + Write-Host '' + Write-Host 'No Windows ARM64 artifacts were built.' + Write-Host 'Install aarch64-w64-mingw32-gcc, or run -Msvc from an ARM64 Visual Studio developer shell.' + exit 1 +} + +Write-Host '' +Write-Host "Artifacts written to ${DistDir}:" +foreach ($a in $Artifacts) { Write-Host " $a" } diff --git a/src/detect.c b/src/detect.c index 03e4b03..a45ef74 100644 --- a/src/detect.c +++ b/src/detect.c @@ -81,20 +81,21 @@ static void detect_x86(void) #endif /* __x86_64__ */ -#if defined(__aarch64__) +#if defined(__aarch64__) || defined(_M_ARM64) -#if defined(__APPLE__) +#if defined(__APPLE__) || defined(_WIN32) /* - * On aarch64 macOS (Apple Silicon), NEON is architecturally mandatory and - * always available. No runtime detection needed. + * On aarch64 Apple Silicon and Windows on ARM, NEON (Advanced SIMD) is + * architecturally mandatory and always available. No runtime detection + * needed - just assert it. */ static void detect_aarch64(void) { g_caps.has_neon = 1; } -#else /* aarch64 Linux (and other non-Apple aarch64) */ +#else /* aarch64 Linux (and other non-Apple, non-Windows aarch64) */ #include #include @@ -126,9 +127,9 @@ static void detect_aarch64(void) fclose(f); } -#endif /* __APPLE__ */ +#endif /* __APPLE__ || _WIN32 */ -#endif /* __aarch64__ */ +#endif /* __aarch64__ || _M_ARM64 */ /* -------------------------------------------------------------------------- @@ -140,7 +141,7 @@ const fused_cpu_caps_t *fused_detect_cpu(void) if (!g_detected) { #if defined(__x86_64__) detect_x86(); -#elif defined(__aarch64__) +#elif defined(__aarch64__) || defined(_M_ARM64) detect_aarch64(); #endif g_detected = 1; diff --git a/src/funnelcake.c b/src/funnelcake.c index 217a3a5..7f3306b 100644 --- a/src/funnelcake.c +++ b/src/funnelcake.c @@ -225,7 +225,7 @@ int fused_scaler_init(fused_scaler_ctx_t *ctx) fused_kernel_fn simd_thirds_up_fn = NULL; fused_kernel_fn simd_pow2_up_fn = NULL; -#if defined(__aarch64__) +#if defined(__aarch64__) || defined(_M_ARM64) if (caps->has_neon) { has_simd = 1; simd_thirds_fn = fused_kernel_thirds_neon; diff --git a/src/funnelcake_hdr.c b/src/funnelcake_hdr.c index 97d39b1..2e8e7d9 100644 --- a/src/funnelcake_hdr.c +++ b/src/funnelcake_hdr.c @@ -271,7 +271,7 @@ int fused_hdr_init(fused_hdr_ctx_t *ctx) fused_hdr_kernel_fn simd_thirds_up_fn = NULL; fused_hdr_kernel_fn simd_pow2_up_fn = NULL; -#if defined(__aarch64__) +#if defined(__aarch64__) || defined(_M_ARM64) if (caps->has_neon) { has_simd = 1; simd_thirds_fn = fused_kernel_thirds_hdr_neon; diff --git a/src/internal.h b/src/internal.h index 3506c90..78532ff 100644 --- a/src/internal.h +++ b/src/internal.h @@ -31,6 +31,25 @@ static inline void fused_aligned_free(void *ptr) #endif } +/* -------------------------------------------------------------------------- + * Portability macros + * + * GCC/Clang ship a few extensions the NEON kernels rely on; MSVC (used for + * the Windows ARM64 build) needs equivalents. + * -------------------------------------------------------------------------- */ +#if defined(__GNUC__) || defined(__clang__) +# define FUSED_HOT __attribute__((hot)) +# define FUSED_PREFETCH(p) __builtin_prefetch(p) +#else +# define FUSED_HOT +# if defined(_M_ARM64) || defined(_M_ARM64EC) +# include +# define FUSED_PREFETCH(p) __prefetch((const void *)(p)) +# else +# define FUSED_PREFETCH(p) ((void)0) +# endif +#endif + /* -------------------------------------------------------------------------- * Constants * -------------------------------------------------------------------------- */ @@ -213,7 +232,7 @@ void fused_kernel_pow2_avx2(const fused_kernel_params_t *p, const uint8_t *src_v); #endif /* __x86_64__ */ -#if defined(__aarch64__) +#if defined(__aarch64__) || defined(_M_ARM64) /* NEON (aarch64 only) */ void fused_kernel_thirds_neon(const fused_kernel_params_t *p, const uint8_t *src_y, @@ -270,7 +289,7 @@ void fused_kernel_pow2_up_avx2(const fused_kernel_params_t *p, const uint8_t *src_v); #endif /* __x86_64__ */ -#if defined(__aarch64__) +#if defined(__aarch64__) || defined(_M_ARM64) void fused_kernel_upscale_neon(const fused_kernel_params_t *p, const uint8_t *src_y, const uint8_t *src_u, @@ -456,7 +475,7 @@ void fused_kernel_pow2_hdr_avx2(const fused_hdr_kernel_params_t *p, const uint16_t *src_v); #endif /* __x86_64__ */ -#if defined(__aarch64__) +#if defined(__aarch64__) || defined(_M_ARM64) /* NEON (aarch64 only) */ void fused_kernel_thirds_hdr_neon(const fused_hdr_kernel_params_t *p, const uint16_t *src_y, @@ -506,7 +525,7 @@ void fused_kernel_pow2_up_hdr_avx2(const fused_hdr_kernel_params_t *p, const uint16_t *src_v); #endif /* __x86_64__ */ -#if defined(__aarch64__) +#if defined(__aarch64__) || defined(_M_ARM64) void fused_kernel_upscale_hdr_neon(const fused_hdr_kernel_params_t *p, const uint16_t *src_y, const uint16_t *src_u, diff --git a/src/kernels_hdr_neon.c b/src/kernels_hdr_neon.c index 09a04dd..2c7011a 100644 --- a/src/kernels_hdr_neon.c +++ b/src/kernels_hdr_neon.c @@ -25,10 +25,10 @@ * - Chunk sizes: 48 bytes = 24 uint16_t = 8 triplets (thirds family), * 16 bytes = 8 uint16_t elements (pow2 family). * - * Guarded by __aarch64__ so this file is a no-op on other platforms. + * Guarded by __aarch64__/_M_ARM64 so this file is a no-op on other platforms. */ -#if defined(__aarch64__) +#if defined(__aarch64__) || defined(_M_ARM64) #include "internal.h" #include @@ -232,7 +232,7 @@ static void h_filter_halve_hdr(const uint16_t *restrict src, * uint16_t elements with 8 elements per Q register instead of 16. * ----------------------------------------------------------------------- */ -static void __attribute__((hot)) scale_plane_pow2_hdr_neon( +static void FUSED_HOT scale_plane_pow2_hdr_neon( const uint16_t *restrict src, int src_w, int src_h, int src_stride, uint32_t active_outputs, @@ -295,8 +295,8 @@ static void __attribute__((hot)) scale_plane_pow2_hdr_neon( * per-row stream tracker has to re-lock; the prefetches bridge that. */ if (g + 1 < num_groups) { const uint16_t *nxt = grp_base + (size_t)group_rows * (size_t)src_el_stride; - __builtin_prefetch(nxt); - __builtin_prefetch(nxt + src_el_stride); + FUSED_PREFETCH(nxt); + FUSED_PREFETCH(nxt + src_el_stride); } /* -- Vertical cascade (NEON) --------------------------------- */ @@ -495,7 +495,7 @@ static inline uint16x8x3_t deinterleave_chunk_hdr( } -static void __attribute__((hot)) scale_plane_thirds_hdr_neon( +static void FUSED_HOT scale_plane_thirds_hdr_neon( const uint16_t *restrict src, int src_w, int src_h, int src_stride, uint32_t active_outputs, @@ -565,7 +565,7 @@ static void __attribute__((hot)) scale_plane_thirds_hdr_neon( int tail_cols = src_w - tail_start; /* Deinterleave buffer (stack-allocated, 24 uint16_t = 48 bytes) */ - uint16_t __attribute__((aligned(16))) chunk_buf[24]; + _Alignas(16) uint16_t chunk_buf[24]; /* Output row cursors */ int out_row[4] = { 0, 0, 0, 0 }; @@ -585,12 +585,12 @@ static void __attribute__((hot)) scale_plane_thirds_hdr_neon( * row; this bridges the per-row stream restart at the group boundary. */ if (g6 + 1 < base6_groups) { const uint16_t *nxt = grp + (size_t)6 * (size_t)src_el_stride; - __builtin_prefetch(nxt); - __builtin_prefetch(nxt + src_el_stride); - __builtin_prefetch(nxt + 2 * src_el_stride); - __builtin_prefetch(nxt + 3 * src_el_stride); - __builtin_prefetch(nxt + 4 * src_el_stride); - __builtin_prefetch(nxt + 5 * src_el_stride); + FUSED_PREFETCH(nxt); + FUSED_PREFETCH(nxt + src_el_stride); + FUSED_PREFETCH(nxt + 2 * src_el_stride); + FUSED_PREFETCH(nxt + 3 * src_el_stride); + FUSED_PREFETCH(nxt + 4 * src_el_stride); + FUSED_PREFETCH(nxt + 5 * src_el_stride); } /* Compute output row base pointers (element pointers, not byte) */ @@ -900,7 +900,7 @@ static void __attribute__((hot)) scale_plane_thirds_hdr_neon( * into two 8-element vectors), then process U and V identically to I010. * ----------------------------------------------------------------------- */ -void __attribute__((hot)) fused_kernel_pow2_hdr_neon( +void FUSED_HOT fused_kernel_pow2_hdr_neon( const fused_hdr_kernel_params_t *p, const uint16_t *src_y, const uint16_t *src_u, @@ -1000,7 +1000,7 @@ void __attribute__((hot)) fused_kernel_pow2_hdr_neon( } -void __attribute__((hot)) fused_kernel_thirds_hdr_neon( +void FUSED_HOT fused_kernel_thirds_hdr_neon( const fused_hdr_kernel_params_t *p, const uint16_t *src_y, const uint16_t *src_u, diff --git a/src/kernels_neon.c b/src/kernels_neon.c index 03abc0f..829931e 100644 --- a/src/kernels_neon.c +++ b/src/kernels_neon.c @@ -30,7 +30,7 @@ * as the AVX2 version. */ -#if defined(__aarch64__) +#if defined(__aarch64__) || defined(_M_ARM64) #include "internal.h" #include @@ -230,7 +230,7 @@ static void h_filter_halve(const uint8_t *restrict src, * Horizontal: NEON vpaddlq_u8 + vrshrn_n_u16 cascade * ----------------------------------------------------------------------- */ -static void __attribute__((hot)) scale_plane_pow2_neon( +static void FUSED_HOT scale_plane_pow2_neon( const uint8_t *restrict src, int src_w, int src_h, int src_stride, uint32_t active_outputs, @@ -503,7 +503,7 @@ static inline uint8x16_t neon_blend_reg(uint8x16_t a, uint8x16_t b, } -static void __attribute__((hot)) scale_plane_thirds_neon( +static void FUSED_HOT scale_plane_thirds_neon( const uint8_t *restrict src, int src_w, int src_h, int src_stride, uint32_t active_outputs, @@ -573,7 +573,7 @@ static void __attribute__((hot)) scale_plane_thirds_neon( int tail_cols = src_w - tail_start; /* Deinterleave buffer (stack-allocated, 48 bytes aligned) */ - uint8_t __attribute__((aligned(16))) chunk_buf[48]; + _Alignas(16) uint8_t chunk_buf[48]; /* Output row cursors */ int out_row[4] = { 0, 0, 0, 0 }; @@ -902,7 +902,7 @@ static void __attribute__((hot)) scale_plane_thirds_neon( * and half height with the same kernel. * ----------------------------------------------------------------------- */ -void __attribute__((hot)) fused_kernel_pow2_neon(const fused_kernel_params_t *p, +void FUSED_HOT fused_kernel_pow2_neon(const fused_kernel_params_t *p, const uint8_t *src_y, const uint8_t *src_u, const uint8_t *src_v) @@ -956,7 +956,7 @@ void __attribute__((hot)) fused_kernel_pow2_neon(const fused_kernel_params_t *p, } -void __attribute__((hot)) fused_kernel_thirds_neon(const fused_kernel_params_t *p, +void FUSED_HOT fused_kernel_thirds_neon(const fused_kernel_params_t *p, const uint8_t *src_y, const uint8_t *src_u, const uint8_t *src_v) diff --git a/src/kernels_upscale_neon.c b/src/kernels_upscale_neon.c index 607b818..324a6ad 100644 --- a/src/kernels_upscale_neon.c +++ b/src/kernels_upscale_neon.c @@ -38,7 +38,7 @@ * */ -#if defined(__aarch64__) +#if defined(__aarch64__) || defined(_M_ARM64) #include "internal.h" #include "upscale_chunk.h" diff --git a/src/tonemap.c b/src/tonemap.c index 1bfbf9a..8651d6b 100644 --- a/src/tonemap.c +++ b/src/tonemap.c @@ -11,15 +11,11 @@ #if defined(__x86_64__) #include #endif -#if defined(__aarch64__) +#if defined(__aarch64__) || defined(_M_ARM64) #include #endif -#if defined(__GNUC__) || defined(__clang__) -#define FUSED_HOT __attribute__((hot)) -#else -#define FUSED_HOT -#endif +/* FUSED_HOT and FUSED_PREFETCH are provided by internal.h. */ /* -------------------------------------------------------------------------- @@ -443,7 +439,7 @@ static void tonemap_luma_avx2(const uint8_t *lut_y, #endif /* __x86_64__ */ -#if defined(__aarch64__) +#if defined(__aarch64__) || defined(_M_ARM64) /* * tonemap_luma_neon - exact LUT tone mapping, 16 pixels per iteration. @@ -465,7 +461,7 @@ static void tonemap_luma_neon(const uint8_t *lut_y, int x = 0; if (y + 1 < height) - __builtin_prefetch(sy + src_y_pitch, 0, 2); + FUSED_PREFETCH(sy + src_y_pitch); for (; x < simd_w; x += 16) { uint16x8_t v0 = vandq_u16(vld1q_u16(sy + x), mask10); @@ -607,7 +603,7 @@ void fused_tonemap_apply( tonemap_luma_avx2(lut_y, src_y, src_y_stride, dst_y, dst_y_stride, width, height); } else -#elif defined(__aarch64__) +#elif defined(__aarch64__) || defined(_M_ARM64) if (fused_detect_cpu()->has_neon) { tonemap_luma_neon(lut_y, src_y, src_y_stride, dst_y, dst_y_stride, width, height); @@ -731,7 +727,7 @@ void fused_tonemap_apply_p010( tonemap_luma_avx2(lut_y, src_y, src_y_stride, dst_y, dst_y_stride, width, height); } else -#elif defined(__aarch64__) +#elif defined(__aarch64__) || defined(_M_ARM64) if (fused_detect_cpu()->has_neon) { tonemap_luma_neon(lut_y, src_y, src_y_stride, dst_y, dst_y_stride, width, height); From c7d2d680e5bb0f59aa9c651e992512c1ed07e29a Mon Sep 17 00:00:00 2001 From: Paul Gregoire Date: Mon, 27 Apr 2026 17:40:29 -0700 Subject: [PATCH 4/7] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 24b258e..cb2ed4d 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ test/samples/ # Internal design notes and AI planning artifacts - not for distribution funnelcake.md docs/superpowers/ +.claude/settings.local.json From ba51a78a6a16d749d5947ee18bc7db8da29085a0 Mon Sep 17 00:00:00 2001 From: Paul Gregoire Date: Tue, 28 Apr 2026 07:01:57 -0700 Subject: [PATCH 5/7] Fix HDR init sdr_temp leak; add OOM error code and INFO log level fused_hdr_init's tonemap_1x and "no valid steps" error paths called fused_hdr_free(ctx) while ctx->_internal was still NULL, which skipped the cleanup of state->sdr_temp[i] buffers populated by SDR-only steps in the per-step loop. Init now attaches state to ctx->_internal immediately after allocation so any subsequent error path goes through fused_hdr_free and releases everything. Drive-by fixes uncovered by the same review: - Internal-state OOM previously returned the misleading FUSED_ERR_NO_STEPS; added FUSED_ERR_OUT_OF_MEMORY (-5) and used it in both inits. - The "no SIMD support detected" notice bypassed log_warnings and wrote to stderr directly; it now routes through fused_log so callers using FUSED_LOG_SUPPRESS / FUSED_LOG_CALLBACK actually control it. - The misaligned-source warning used a process-wide static "warned" flag; the first context to hit it silenced every other context. Moved the flag onto fused_internal_t / fused_hdr_internal_t. - Restored the "tone map LUTs generated" diagnostic at the new FUSED_LOG_INFO (2) level (it was previously WARN, which polluted warning channels) and documented that info is routed via log_warnings so callbacks can filter on level. - fused_*_free now also resets effective_width/height for consistency. - Added trailing newline to the misalignment warning format strings. Docs updated: docs/API.md tables for log levels and error codes, README's "Platform support" wording, and a new CHANGELOG.md tracking all of the above under [Unreleased]. Verified: make lib + make funnelcake_test build clean under -Wall -Wextra -Werror; full test suite 83/83 on Apple Silicon. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 71 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 5 ++-- docs/API.md | 23 ++++++++++---- include/funnelcake.h | 8 +++++ src/funnelcake.c | 20 ++++++++----- src/funnelcake_hdr.c | 32 +++++++++++--------- src/internal.h | 2 ++ src/tonemap.c | 5 +++- 8 files changed, 136 insertions(+), 30 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ab6e737 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,71 @@ +# Changelog + +All notable changes to funnelcake are recorded here. Format follows +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project does +not yet follow a tagged release cadence, so changes accumulate under +**Unreleased** until a tag is cut. + +## [Unreleased] + +### Added +- `FUSED_ERR_OUT_OF_MEMORY` (-5) hard-error code returned by + `fused_scaler_init` and `fused_hdr_init` when allocation of the internal + state struct fails. Previously these paths returned the misleading + `FUSED_ERR_NO_STEPS`. +- `FUSED_LOG_INFO` (2) log level for low-frequency status / diagnostic + messages. Routed through the existing `log_warnings` config so callers + that install a `FUSED_LOG_CALLBACK` can filter info out by inspecting + the `level` argument while still receiving warnings. + +### Changed +- The "tone map LUTs generated" diagnostic in `fused_tonemap_generate_luts` + now logs at `FUSED_LOG_INFO` instead of `FUSED_LOG_WARN`. Callers that + previously suppressed it via `log_warnings = FUSED_LOG_SUPPRESS` keep + the same behavior; callback-based loggers can now keep warnings while + dropping info. +- The "no SIMD support detected" notice from both init functions now + routes through `fused_log(&ctx->log_warnings, FUSED_LOG_WARN, …)` + instead of writing to `stderr` directly, so users with a configured + log target see it where they expect. +- `fused_scaler_free` and `fused_hdr_free` now also reset + `effective_width` and `effective_height` on the context, matching the + reset of the other result fields. + +### Fixed +- **HDR init memory leak**: `fused_hdr_init` could leak `state->sdr_temp[i]` + buffers if SDR-only steps had been allocated successfully and a later + step (the 1:1 tonemap output, or the "no valid steps" check) failed. + The error paths called `fused_hdr_free(ctx)` while `ctx->_internal` was + still NULL, skipping the cleanup of `state`'s sdr_temp pointers, and + then `free(state)` released the struct without freeing those buffers. + Init now attaches `state` to `ctx->_internal` immediately after + allocation so any subsequent error path goes through `fused_hdr_free` + and releases everything. +- The misaligned-source warning emitted by `fused_scaler_run` and + `fused_hdr_run` used a process-wide `static int warned` flag, so the + first context to encounter a misaligned source silenced the warning + for every other context in the process (and the flag was not + thread-safe). Each context now owns its own `src_misaligned_warned` + flag inside its internal state. +- The misaligned-source warning was missing a trailing newline. + +### Documentation +- `docs/API.md`: documented `FUSED_LOG_INFO`, `FUSED_ERR_OUT_OF_MEMORY`, + and the relationship between log levels and the routing config. + +--- + +## Conventions + +- **Added** — new public API surface (functions, constants, struct fields). +- **Changed** — non-breaking behavioral changes to existing API. +- **Deprecated** — APIs scheduled for removal. +- **Removed** — APIs that have been deleted. +- **Fixed** — bug fixes that don't change documented behavior. +- **Security** — vulnerability fixes. +- **Performance** — measurable speed/memory wins, with a one-line summary + of the workload and the delta. +- **Documentation** — doc-only changes worth noting. + +Group breaking changes under their own **Breaking** subsection and call +out the impact on callers. diff --git a/README.md b/README.md index 59e6449..2c81f15 100644 --- a/README.md +++ b/README.md @@ -493,8 +493,9 @@ fused_scaler_free(&scaler); | Other | Scalar | Portable C, no intrinsics | The scalar fallback is correct on all platforms but significantly slower. -On hardware without AVX2 or NEON, the library logs a one-time notice to -stderr at first init. +On hardware without AVX2 or NEON, the library logs a one-time notice +through the configured `log_warnings` channel at first init (default: +stderr). ## HDR10 support diff --git a/docs/API.md b/docs/API.md index ddf2b6b..b6a2d98 100644 --- a/docs/API.md +++ b/docs/API.md @@ -221,9 +221,9 @@ struct means write to stderr. | Field | Type | Description | |-------|------|-------------| -| `target` | `int` | One of the `FUSED_LOG_*` constants. | +| `target` | `int` | One of the `FUSED_LOG_*` target constants. | | `file` | `FILE *` | Used when `target == FUSED_LOG_FILE`. Must be a valid open file. | -| `callback` | `void (*)(int level, const char *msg, void *ctx)` | Used when `target == FUSED_LOG_CALLBACK`. `level` is `FUSED_LOG_ERROR` or `FUSED_LOG_WARN`. | +| `callback` | `void (*)(int level, const char *msg, void *ctx)` | Used when `target == FUSED_LOG_CALLBACK`. `level` is one of `FUSED_LOG_ERROR`, `FUSED_LOG_WARN`, `FUSED_LOG_INFO`. | | `callback_ctx` | `void *` | Passed through opaquely as the `ctx` argument to `callback`. | Log target constants: @@ -236,6 +236,15 @@ Log target constants: | `FUSED_LOG_SUPPRESS` | 3 | Discard all messages | | `FUSED_LOG_CALLBACK` | 4 | Call `config.callback` | +Log level constants (passed to callbacks; stderr/stdout/file targets emit +every message regardless of level): + +| Constant | Value | Meaning | +|----------|-------|---------| +| `FUSED_LOG_ERROR` | 0 | Hard error — init failed, no resources allocated. Routed via `log_errors`. | +| `FUSED_LOG_WARN` | 1 | Partial success or fallback — request still produced output. Routed via `log_warnings`. | +| `FUSED_LOG_INFO` | 2 | Low-frequency status / diagnostic. Routed via `log_warnings`; filter on `level` in a callback to drop. | + ## Scale Step Flags @@ -515,6 +524,7 @@ are valid, and `fused_scaler_run` must not be called. | `FUSED_ERR_NO_STEPS` | -2 | No valid step flags remain after filtering (all were rejected or none were set). | | `FUSED_ERR_BAD_DIMENSIONS` | -3 | `src_width` or `src_height` is <= 0, or too small for the requested steps. | | `FUSED_ERR_BAD_ALIGNMENT` | -4 | `src_y_stride` or `src_uv_stride` is not 32-byte aligned. | +| `FUSED_ERR_OUT_OF_MEMORY` | -5 | Allocation of internal state failed; output buffers (if any were allocated earlier in init) have already been released. | ## Alignment Requirements @@ -678,9 +688,12 @@ scaler.log_warnings.callback = my_log; scaler.log_warnings.callback_ctx = my_logger_instance; ``` -The `level` argument to the callback is `FUSED_LOG_ERROR` (0) or -`FUSED_LOG_WARN` (1). The `msg` string is a complete formatted message; -do not call `fused_scaler_*` functions from within the callback. +The `level` argument to the callback is `FUSED_LOG_ERROR` (0), +`FUSED_LOG_WARN` (1), or `FUSED_LOG_INFO` (2). Info-level messages +(e.g. "tone map LUTs generated") share the `log_warnings` config — to +keep warnings but drop info, install a callback and filter on `level`. +The `msg` string is a complete formatted message; do not call +`fused_scaler_*` functions from within the callback. ## HDR10 API Reference diff --git a/include/funnelcake.h b/include/funnelcake.h index f8bceec..f80cf09 100644 --- a/include/funnelcake.h +++ b/include/funnelcake.h @@ -133,14 +133,22 @@ extern "C" { #define FUSED_ERR_NO_STEPS (-2) /* no valid step flags set after filtering */ #define FUSED_ERR_BAD_DIMENSIONS (-3) /* src_width/height <= 0 or too small */ #define FUSED_ERR_BAD_ALIGNMENT (-4) /* strides not 32-byte aligned */ +#define FUSED_ERR_OUT_OF_MEMORY (-5) /* allocation of internal state failed */ /* -------------------------------------------------------------------------- * Log levels + * + * Levels are passed to FUSED_LOG_CALLBACK callbacks (which can filter on + * them); stderr/stdout/file targets emit every message regardless of level. + * Routing of info-level diagnostics shares the warnings logger config, so + * callers that want to drop info but keep warnings should install a callback + * and filter by level. * -------------------------------------------------------------------------- */ #define FUSED_LOG_ERROR 0 #define FUSED_LOG_WARN 1 +#define FUSED_LOG_INFO 2 /* low-frequency diagnostic / status messages */ /* -------------------------------------------------------------------------- diff --git a/src/funnelcake.c b/src/funnelcake.c index 7f3306b..26a8d86 100644 --- a/src/funnelcake.c +++ b/src/funnelcake.c @@ -248,11 +248,14 @@ int fused_scaler_init(fused_scaler_ctx_t *ctx) #endif if (!has_simd) { - /* One-time stderr notice */ + /* One-time notice routed through the configured warning logger so + * callers using FUSED_LOG_SUPPRESS / FUSED_LOG_CALLBACK can control + * it. The first init that hits this wins; subsequent inits stay + * silent because the CPU detection result is invariant. */ static int g_no_simd_warned = 0; if (!g_no_simd_warned) { g_no_simd_warned = 1; - fprintf(stderr, + fused_log(&ctx->log_warnings, FUSED_LOG_WARN, "funnelcake: no SIMD support detected; using scalar kernel\n"); } warn_bits |= FUSED_WARN_BIT_SCALAR; @@ -524,7 +527,7 @@ int fused_scaler_init(fused_scaler_ctx_t *ctx) fused_scaler_free(ctx); fused_log(&ctx->log_errors, FUSED_LOG_ERROR, "funnelcake: out-of-memory allocating internal state\n"); - return FUSED_ERR_NO_STEPS; + return FUSED_ERR_OUT_OF_MEMORY; } fused_kernel_params_t *p = &state->params; @@ -703,15 +706,14 @@ void fused_scaler_run(fused_scaler_ctx_t *ctx, /* Check source plane alignment */ if (((uintptr_t)src_y & 31) || ((uintptr_t)src_u & 31) || ((uintptr_t)src_v & 31)) { - /* Warn once about misaligned source planes */ - static int warned = 0; - if (!warned) { + /* Warn once per context about misaligned source planes */ + if (!state->src_misaligned_warned) { fused_log(&ctx->log_errors, FUSED_LOG_ERROR, "funnelcake: source planes are not 32-byte aligned " "(Y=%p U=%p V=%p). Falling back to scalar kernel. " - "Performance will be significantly reduced.", + "Performance will be significantly reduced.\n", (const void*)src_y, (const void*)src_u, (const void*)src_v); - warned = 1; + state->src_misaligned_warned = 1; } /* Fall back to scalar - pick the variant matching the configured * (want_down, want_up) combination. */ @@ -769,4 +771,6 @@ void fused_scaler_free(fused_scaler_ctx_t *ctx) ctx->rejected_flags = 0; ctx->achieved_upscale_flags = 0; ctx->achieved_upscale_tail = 0; + ctx->effective_width = 0; + ctx->effective_height = 0; } diff --git a/src/funnelcake_hdr.c b/src/funnelcake_hdr.c index 2e8e7d9..6c1774a 100644 --- a/src/funnelcake_hdr.c +++ b/src/funnelcake_hdr.c @@ -294,10 +294,14 @@ int fused_hdr_init(fused_hdr_ctx_t *ctx) #endif if (!has_simd) { + /* One-time notice routed through the configured warning logger so + * callers using FUSED_LOG_SUPPRESS / FUSED_LOG_CALLBACK can control + * it. The first init that hits this wins; subsequent inits stay + * silent because the CPU detection result is invariant. */ static int g_no_simd_warned = 0; if (!g_no_simd_warned) { g_no_simd_warned = 1; - fprintf(stderr, + fused_log(&ctx->log_warnings, FUSED_LOG_WARN, "funnelcake-hdr: no SIMD support detected; using scalar kernel\n"); } warn_bits |= FUSED_WARN_BIT_SCALAR; @@ -315,13 +319,17 @@ int fused_hdr_init(fused_hdr_ctx_t *ctx) memset(ctx->hdr_outputs, 0, sizeof(ctx->hdr_outputs)); memset(ctx->sdr_outputs, 0, sizeof(ctx->sdr_outputs)); - /* Allocate internal state early so we can store sdr_temp pointers */ + /* Allocate internal state early so we can store sdr_temp pointers. + * Attach to ctx->_internal immediately so any subsequent error path + * can rely on fused_hdr_free for cleanup and avoid leaking sdr_temp[] + * buffers populated during the per-step loop below. */ fused_hdr_internal_t *state = calloc(1, sizeof(fused_hdr_internal_t)); if (!state) { fused_log(&ctx->log_errors, FUSED_LOG_ERROR, "funnelcake-hdr: out-of-memory allocating internal state\n"); - return FUSED_ERR_NO_STEPS; + return FUSED_ERR_OUT_OF_MEMORY; } + ctx->_internal = state; for (int i = 0; i < 8; i++) { const step_desc_t *sd = &k_steps[i]; @@ -626,8 +634,6 @@ int fused_hdr_init(fused_hdr_ctx_t *ctx) if (achieved_any == 0 && !ctx->tonemap_1x && !hdr_want_up) { fused_hdr_free(ctx); - free(state); - ctx->_internal = NULL; fused_log(&ctx->log_errors, FUSED_LOG_ERROR, "funnelcake-hdr: no valid output steps after validation\n"); return FUSED_ERR_NO_STEPS; @@ -650,11 +656,9 @@ int fused_hdr_init(fused_hdr_ctx_t *ctx) fused_aligned_alloc(&pv, 32, (size_t)uv_stride * (size_t)chroma_h) != 0) { fused_aligned_free(py); fused_aligned_free(pu); fused_aligned_free(pv); fused_hdr_free(ctx); - free(state); - ctx->_internal = NULL; fused_log(&ctx->log_errors, FUSED_LOG_ERROR, "funnelcake-hdr: out-of-memory allocating 1:1 tonemap output\n"); - return FUSED_ERR_NO_STEPS; + return FUSED_ERR_OUT_OF_MEMORY; } ctx->output_1x.width = eff_w; @@ -893,8 +897,7 @@ int fused_hdr_init(fused_hdr_ctx_t *ctx) state->sdr_flags = achieved_sdr; state->tonemap_1x = ctx->tonemap_1x; state->is_custom_lut = (ctx->tonemap.curve == FUSED_TONEMAP_CUSTOM) ? 1 : 0; - - ctx->_internal = state; + /* ctx->_internal already set immediately after state allocation. */ return warn_bits; /* 0 == FUSED_OK if nothing was warned */ } @@ -920,14 +923,13 @@ void fused_hdr_run(fused_hdr_ctx_t *ctx, if (((uintptr_t)src_y & 31) || ((uintptr_t)src_u & 31) || (!is_p010 && src_v && ((uintptr_t)src_v & 31))) { - static int warned = 0; - if (!warned) { + if (!state->src_misaligned_warned) { fused_log(&ctx->log_errors, FUSED_LOG_ERROR, "funnelcake-hdr: source planes are not 32-byte aligned " "(Y=%p U=%p V=%p). Falling back to scalar kernel. " - "Performance will be significantly reduced.", + "Performance will be significantly reduced.\n", (const void *)src_y, (const void *)src_u, (const void *)src_v); - warned = 1; + state->src_misaligned_warned = 1; } const fused_hdr_kernel_params_t *p = &state->params; int hdr_want_down = (p->active_outputs != 0); @@ -1076,4 +1078,6 @@ void fused_hdr_free(fused_hdr_ctx_t *ctx) ctx->rejected_flags = 0; ctx->achieved_upscale_flags = 0; ctx->achieved_upscale_tail = 0; + ctx->effective_width = 0; + ctx->effective_height = 0; } diff --git a/src/internal.h b/src/internal.h index 78532ff..821a6de 100644 --- a/src/internal.h +++ b/src/internal.h @@ -197,6 +197,7 @@ typedef struct { fused_kernel_params_t params; fused_kernel_fn kernel_fn; int has_simd; /* 1 if a SIMD kernel was selected, 0 = scalar only */ + int src_misaligned_warned; /* per-context one-shot flag for run() */ } fused_internal_t; @@ -444,6 +445,7 @@ typedef struct { uint32_t sdr_flags; int tonemap_1x; int is_custom_lut; /* 1 if using FUSED_TONEMAP_CUSTOM (skip RGB chroma path) */ + int src_misaligned_warned; /* per-context one-shot flag for run() */ } fused_hdr_internal_t; diff --git a/src/tonemap.c b/src/tonemap.c index 8651d6b..e723493 100644 --- a/src/tonemap.c +++ b/src/tonemap.c @@ -351,7 +351,10 @@ void fused_tonemap_generate_luts(fused_hdr_internal_t *hdr, hdr->linear_to_sdr[i] = (uint8_t)clamp_i((int)(V * 255.0 + 0.5), 0, 255); } - fused_log(log_warn, FUSED_LOG_WARN, + /* One-time-per-init diagnostic: emitted at INFO level so callback-based + * loggers can filter it out without losing real warnings. Stderr/file + * targets will see it on every init. */ + fused_log(log_warn, FUSED_LOG_INFO, "funnelcake: tone map LUTs generated - transfer=%s curve=%d " "peak=%d target=%d\n", (src_transfer == FUSED_TRC_HLG) ? "HLG" : "PQ", From 7d9bb44b94a911c68c7d2b0aa953fa09921d6ddd Mon Sep 17 00:00:00 2001 From: Paul Gregoire Date: Sun, 17 May 2026 06:52:54 -0700 Subject: [PATCH 6/7] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index cb2ed4d..d4a1f9e 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ test/samples/ funnelcake.md docs/superpowers/ .claude/settings.local.json +*.profdata +*.profraw From 55de0a5cbd4eed3023a950268ba3a1efaf076f41 Mon Sep 17 00:00:00 2001 From: Paul Gregoire Date: Wed, 3 Jun 2026 15:36:06 -0700 Subject: [PATCH 7/7] Built new shared lib --- funnelcake.pc | 11 +++++++++++ libfunnelcake.1.dylib | Bin 0 -> 136152 bytes 2 files changed, 11 insertions(+) create mode 100644 funnelcake.pc create mode 100755 libfunnelcake.1.dylib diff --git a/funnelcake.pc b/funnelcake.pc new file mode 100644 index 0000000..f7dff74 --- /dev/null +++ b/funnelcake.pc @@ -0,0 +1,11 @@ +prefix=/usr/local +exec_prefix=${prefix} +libdir=/usr/local/lib +includedir=/usr/local/include + +Name: funnelcake +Description: SIMD YUV scaler with HDR/SDR tonemapping +Version: 0.1.0 +Cflags: -I${includedir} +Libs: -L${libdir} -lfunnelcake +Libs.private: -lm diff --git a/libfunnelcake.1.dylib b/libfunnelcake.1.dylib new file mode 100755 index 0000000000000000000000000000000000000000..f6dfd481783ed90456195ae3a9d0fcc9dd9be307 GIT binary patch literal 136152 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⁢YVte1vR(0OYkT>% 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 Xsne~$s9ELjk-Gi0qR+n8ZO8uydMi51 literal 0 HcmV?d00001