From e0b7d30fd437292c88141fb08d60681870b86c6e Mon Sep 17 00:00:00 2001 From: Tomi Valkeinen Date: Fri, 8 May 2026 17:22:58 +0300 Subject: Squashed 'subprojects/pixpat/' content from commit d444626 git-subtree-dir: subprojects/pixpat git-subtree-split: d444626e6ba988ec6d487800721e447f94b1eaf5 --- pixpat-native/src/pixpat.cpp | 355 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 pixpat-native/src/pixpat.cpp (limited to 'pixpat-native/src/pixpat.cpp') diff --git a/pixpat-native/src/pixpat.cpp b/pixpat-native/src/pixpat.cpp new file mode 100644 index 0000000..ac21fac --- /dev/null +++ b/pixpat-native/src/pixpat.cpp @@ -0,0 +1,355 @@ +// pixpat: extern "C" entry points + runtime format dispatch. +// +// The format catalog (X-macro PIXPAT_FORMAT_LIST + FormatId enum + +// s_format_table) is hand-written in format_catalog.h. The generator +// (pixpat-native/codegen/gen_pixpat.py) reads the same X-macro and +// the user TOML and emits the per-config bits: +// +// pixpat_config.h — PIXPAT_FEATURE_PATTERN / _CONVERT +// pixpat_caps.inc — s_format_caps[] (per-format readable / writable / +// hot_src / hot_dst, indexed by FormatId) and +// s_pattern_caps[] (per-pattern enabled flag). +// +// The convert and pattern dispatch (dispatch_convert in +// pixpat_convert.cpp, dispatch_draw_pattern in pixpat_pattern.cpp) is +// hand-written and consumes s_format_caps / s_pattern_caps via +// `if constexpr` on the per-row constexpr fields. +// +// s_format_info is built here, once, by re-expanding the catalog +// X-macro through unpack_for / pack_for / snk_block_h_for / +// snk_block_w_for. Those constexpr helpers use `if constexpr` on the +// per-format readable / writable flags from s_format_caps to either +// take the address of unpack_to_norm / pack_from_norm or fall back to +// nullptr (or 0). Because they're function templates, the discarded +// branch is never instantiated, so disabled-direction templates +// produce no code. +// +// Feature gating is meson-side: pixpat_pattern.cpp / pixpat_convert.cpp +// are added to the source list only when their feature is enabled. This +// file's entry points always exist; they call the bridge functions +// dispatch_draw_pattern / dispatch_convert under `if constexpr +// (kFeatureXxx)`. The discarded if-constexpr branch produces no symbol +// reference, so when the matching TU is absent the link still succeeds +// and the entry point returns -1 instead. + +#include + +#include +#include +#include + +#include "pixpat_config.h" + +#include "color.h" +#include "error.h" +#include "format_catalog.h" +#include "formats.h" +#include "io.h" +#include "layout.h" +#include "params.h" +#include "pattern.h" +#include "pixpat_internal.h" +#include "threading.h" + +namespace pixpat +{ + +inline constexpr bool kFeaturePattern = PIXPAT_FEATURE_PATTERN; +inline constexpr bool kFeatureConvert = PIXPAT_FEATURE_CONVERT; + +static FormatId lookup_format(const char* name) noexcept +{ + if (!name) + return FormatId::Unknown; + for (const auto& e : s_format_table) + if (std::strcmp(e.name, name) == 0) + return e.id; + return FormatId::Unknown; +} + +// Per-source: fill `bh` rows of normalized pixels by calling Src::read. +// Address is taken in s_format_info[] for every readable format. When +// no format is readable (convert disabled) no specialization is +// instantiated, so this template emits no code. +template +static void unpack_to_norm(uint8_t* norm, const pixpat_buffer* src, + size_t by, size_t bh, size_t W) noexcept +{ + using P = typename Src::Pixel; + auto sb = make_buffer(src); + auto* dst = reinterpret_cast(norm); + const size_t H = src->height; + for (size_t dy = 0; dy < bh; ++dy) + for (size_t x = 0; x < W; ++x) + dst[dy * W + x] = Src::read(sb, x, by + dy, W, H); +} + +// Per-sink: re-block `Snk::block_h × W` of normalized pixels and call +// Sink::write_block. Snk's block_h dictates how many normalized rows +// the caller has to have prepared. Used by the normalized pivot for +// both convert (cold path) and pattern. +template +static void pack_from_norm(const pixpat_buffer* dst, + const uint8_t* norm, + size_t by, size_t W) noexcept +{ + using P = typename Snk::Pixel; + constexpr size_t bh = Snk::block_h; + constexpr size_t bw = Snk::block_w; + auto db = make_buffer(dst); + auto* src = reinterpret_cast(norm); + for (size_t bx = 0; bx < W; bx += bw) { + P block[bh][bw]; + for (size_t dy = 0; dy < bh; ++dy) + for (size_t dx = 0; dx < bw; ++dx) + block[dy][dx] = src[dy * W + bx + dx]; + Snk::write_block(db, bx, by, block); + } +} + +// Generated: s_format_caps[] indexed by FormatId, plus s_pattern_* / +// DefaultPattern (used only by pixpat_pattern.cpp; harmless here). +#include "pixpat_caps.inc" + +static_assert(sizeof(s_format_caps) / sizeof(s_format_caps[0]) == s_format_catalog_count, + "s_format_caps must cover the full catalog"); + +// `if constexpr` keeps disabled-direction function-template bodies +// uninstantiated. Taking `&unpack_to_norm` / `&pack_from_norm` +// forces the function body to be emitted; without the gate every +// catalog format would carry unpack and pack code regardless of its +// readable / writable bit. Snk::block_h / Snk::block_w are constexpr +// scalars — no body, no emission — so they're inlined directly in the +// initializer below, without a helper. +template +static constexpr UnpackFn unpack_for() noexcept +{ + if constexpr (Read) + return &unpack_to_norm; + else + return nullptr; +} + +template +static constexpr PackFn pack_for() noexcept +{ + if constexpr (Write) + return &pack_from_norm; + else + return nullptr; +} + +const FormatInfo s_format_info[] = { +#define CAPS(name) s_format_caps[size_t(FormatId::name)] +#define X(name) \ + { \ + unpack_for(), \ + pack_for(), \ + formats::name::kind, \ + uint8_t(formats::name::h_sub), \ + uint8_t(formats::name::v_sub), \ + uint8_t(formats::name::Sink::block_h), \ + uint8_t(formats::name::Sink::block_w), \ + }, + PIXPAT_FORMAT_LIST(X) +#undef X +#undef CAPS +}; +static_assert(sizeof(s_format_info) / sizeof(s_format_info[0]) == s_format_catalog_count, + "s_format_info must cover the full catalog"); + +// validate_* / parse_spec are only reached from inside the entry points' +// `if constexpr (kFeatureXxx)` true branches. With a feature disabled, +// its caller's branch is discarded and the helper becomes unreferenced; +// require_readable is convert-only. [[maybe_unused]] keeps +// -Wunused-function (and clang's -Wunneeded-internal-declaration) quiet. +[[maybe_unused]] static void validate_buffer(const pixpat_buffer* b) +{ + if (!b) + throw invalid_argument("null buffer"); + if (b->width == 0 || b->height == 0) + throw invalid_argument("zero-sized buffer"); +} + +[[maybe_unused]] static FormatId validate_format(const char* name) +{ + auto id = lookup_format(name); + if (id == FormatId::Unknown) + throw invalid_argument("unknown format"); + return id; +} + +[[maybe_unused]] static void require_writable(FormatId id) +{ + if (s_format_info[size_t(id)].pack == nullptr) + throw invalid_argument("format not enabled as a sink in this build"); +} + +[[maybe_unused]] static void require_readable(FormatId id) +{ + if (s_format_info[size_t(id)].unpack == nullptr) + throw invalid_argument("format not enabled as a source in this build"); +} + +[[maybe_unused]] static unsigned validate_thread_count(int n) +{ + if (n < 0) + throw invalid_argument("negative num_threads"); + return n > 0 ? static_cast(n) : default_thread_count(); +} + +// Map the C-side pixpat_rec / pixpat_range enums (defined in +// pixpat.h with explicit values 0/1/2 for rec, 0/1 for range) onto +// the internal pixpat::Rec / pixpat::Range. Out-of-range values fall +// back to BT.601 / Limited — matching the zero-initialised opts +// struct and kDefaultColorSpec. +[[maybe_unused]] static ColorSpec parse_spec(int rec_in, int range_in) noexcept +{ + Rec rec; + switch (rec_in) { + case PIXPAT_REC_BT709: rec = Rec::BT709; break; + case PIXPAT_REC_BT2020: rec = Rec::BT2020; break; + default: rec = Rec::BT601; break; + } + Range range = (range_in == PIXPAT_RANGE_FULL) ? Range::Full : Range::Limited; + return ColorSpec{ rec, range }; +} + +} // namespace pixpat + +// Marks the C entry points as part of the public ABI: restores default +// visibility against the build-wide -fvisibility=hidden, so they are +// exported from libpixpat.so. +#define PIXPAT_API __attribute__((visibility("default"))) + +extern "C" { + +PIXPAT_API int pixpat_draw_pattern(const pixpat_buffer* dst, + const char* pattern, + const pixpat_pattern_opts* opts) +{ + if constexpr (pixpat::kFeaturePattern) { + try { + pixpat::validate_buffer(dst); + auto id = pixpat::validate_format(dst->format); + pixpat::require_writable(id); + const auto& di = pixpat::s_format_info[size_t(id)]; + if (dst->width % di.snk_block_w != 0 || + dst->height % di.snk_block_h != 0) + throw pixpat::invalid_argument( + "dimensions not aligned to format block"); + const unsigned n_threads = opts + ? pixpat::validate_thread_count(opts->num_threads) + : pixpat::default_thread_count(); + const pixpat::ColorSpec spec = opts + ? pixpat::parse_spec(opts->rec, opts->range) + : pixpat::kDefaultColorSpec; + const pixpat::Params params(opts ? opts->params : nullptr); + if (!params.ok()) + throw pixpat::invalid_argument("malformed opts->params"); + + pixpat::run_stripes(dst->height, di.snk_block_h, n_threads, + [&](size_t y0, size_t y1) { + pixpat::dispatch_draw_pattern( + id, pattern, params, dst, + dst->width, dst->height, y0, y1, spec); + }); + return 0; + } catch (const std::exception&) { + return -1; + } + } else { + (void)dst; + (void)pattern; + (void)opts; + return -1; + } +} + +PIXPAT_API int pixpat_convert(const pixpat_buffer* dst, + const pixpat_buffer* src, + const pixpat_convert_opts* opts) +{ + if constexpr (pixpat::kFeatureConvert) { + try { + pixpat::validate_buffer(dst); + pixpat::validate_buffer(src); + if (src->width != dst->width || src->height != dst->height) + throw pixpat::invalid_argument("src/dst dimensions differ"); + + auto src_id = pixpat::validate_format(src->format); + auto dst_id = pixpat::validate_format(dst->format); + pixpat::require_readable(src_id); + pixpat::require_writable(dst_id); + + const auto& si = pixpat::s_format_info[size_t(src_id)]; + const auto& di = pixpat::s_format_info[size_t(dst_id)]; + // Each constraint must hold independently — checking only + // max() would miss e.g. h_sub=2 vs snk_block_w=3 with W=3. + if (src->width % si.h_sub != 0 || src->height % si.v_sub != 0 || + src->width % di.h_sub != 0 || src->height % di.v_sub != 0 || + src->width % di.snk_block_w != 0 || src->height % di.snk_block_h != 0) + throw pixpat::invalid_argument( + "dimensions not aligned to format subsampling"); + // run_stripes only needs the v dimension. Stripes must align + // to si.v_sub (source reads) and di.snk_block_h (sink block + // loop); for pixpat's catalog these are powers-of-two and + // max == LCM. + const unsigned vs = std::max({ unsigned(si.v_sub), + unsigned(di.v_sub), + unsigned(di.snk_block_h) }); + const unsigned n_threads = opts + ? pixpat::validate_thread_count(opts->num_threads) + : pixpat::default_thread_count(); + const pixpat::ColorSpec spec = opts + ? pixpat::parse_spec(opts->rec, opts->range) + : pixpat::kDefaultColorSpec; + + pixpat::run_stripes(src->height, vs, n_threads, + [&](size_t y0, size_t y1) { + pixpat::dispatch_convert(src_id, dst_id, src, dst, + src->width, src->height, + y0, y1, spec); + }); + return 0; + } catch (const std::exception&) { + return -1; + } + } else { + (void)dst; + (void)src; + (void)opts; + return -1; + } +} + +PIXPAT_API int pixpat_format_supported(const char* format) +{ + auto id = pixpat::lookup_format(format); + if (id == pixpat::FormatId::Unknown) + return 0; + return pixpat::s_format_caps[size_t(id)].enabled() ? 1 : 0; +} + +PIXPAT_API size_t pixpat_format_count(void) +{ + size_t n = 0; + for (const auto& c : pixpat::s_format_caps) + if (c.enabled()) + ++n; + return n; +} + +PIXPAT_API const char* pixpat_format_name(size_t idx) +{ + size_t n = 0; + for (size_t i = 0; i < pixpat::s_format_catalog_count; ++i) { + if (!pixpat::s_format_caps[i].enabled()) + continue; + if (n++ == idx) + return pixpat::s_format_table[i].name; + } + return nullptr; +} + +} // extern "C" -- cgit v1.2.3